FEMA矩阵配置完成,FEMA插件静态页面完成

This commit is contained in:
2026-09-14 14:59:27 +08:00
parent a7cd8f1901
commit 9bd865cb2c
7 changed files with 1974 additions and 119 deletions
+11 -3
View File
@@ -95,13 +95,13 @@ export function setApproverApi(id:number,data:any) {
}
// 获取风险矩阵配置
export function matrixConfigApi(bizType:string) {
export function matrixConfigApi() {
return request({
url: `/api/risk-matrix/${bizType}`,
url: `/api/matrix`,
method: 'get',
});
}
// 保存风险矩阵配置
// 保存风险矩阵配置(HAZOP)
export function saveMatrixConfigApi(bizType:string,data:any) {
return request({
url: `/api/risk-matrix/${bizType}`,
@@ -109,5 +109,13 @@ export function saveMatrixConfigApi(bizType:string,data:any) {
data,
});
}
// 保存风险矩阵配置(FMEA)
export function saveFmeaMatrixConfigApi(data:any) {
return request({
url: `/api/fmea-matrix`,
method: 'put',
data,
});
}
@@ -99,9 +99,9 @@ const PURPOSE_OPTIONS = [
const UPDATE_DOC_OPTIONS = ['PID图纸', '操作规程', '总图', '设备台账', '工艺卡片', '联锁台账', '应急预案'];
// 风险分析选项
const RISK_TOOLS_FORM = [
{ key: "RISK", label: "风险预分析" },
{ key: "HAZOP", label: "HAZOP" },
{ key: "JSA", label: "JSA" },
{ key: "SCL", label: "设备SCL" },
{ key: "FMEA", label: "FMEA" },
{ key: "RISK_CHECK", label: "风险检查表" },
];
const questions = ref<any>([
File diff suppressed because it is too large Load Diff
@@ -737,8 +737,8 @@ const getMenuOptions = (type: string) => {
{ label: '删除分析对象', key: 'delObj' },
]
const devOptions = [
{ label: '编辑离', key: 'editDev' },
{ label: '删除离', key: 'delDev' },
{ label: '编辑离', key: 'editDev' },
{ label: '删除离', key: 'delDev' },
]
const map:any = {
node: nodeOptions,
@@ -1105,7 +1105,7 @@ onBeforeUnmount(() => {
</thead>
<tbody>
<tr v-if="!currentDev || currentDev?.rows?.length === 0">
<td colspan="8" class="px-3 py-8 text-center text-xs text-slate-400">
<td colspan="9" class="px-3 py-8 text-center text-xs text-slate-400">
当前偏离暂无分析记录点击右上角AI 分析当前偏离自动生成新增行手动填写
</td>
</tr>
@@ -1389,9 +1389,9 @@ onBeforeUnmount(() => {
<div
class="mr-[5px] cursor-grab"
:draggable="true"
@dragstart="onDeviceDragStart($event, objIndex)"
@dragstart="onDeviceDragStart($event, Number(objIndex))"
@dragover.prevent
@drop="onDeviceDrop($event, objIndex)"
@drop="onDeviceDrop($event, Number(objIndex))"
>
<Icon icon="gravity-ui:grip" class="size-12px" />
</div>
@@ -1428,9 +1428,9 @@ onBeforeUnmount(() => {
<div
class="mr-[5px] cursor-grab"
:draggable="true"
@dragstart="onDeviateDragStart($event, objIndex, devIndex)"
@dragstart="onDeviateDragStart($event, Number(objIndex), Number(devIndex))"
@dragover.prevent
@drop="onDeviateDrop($event, objIndex, devIndex)"
@drop="onDeviateDrop($event, Number(objIndex), Number(devIndex))"
>
<Icon icon="gravity-ui:grip" class="size-12px" />
</div>
@@ -6,7 +6,7 @@ import { useThemeStore } from '@/store/modules/theme';
import RiskPre from "./riskPre.vue";
import Hazop from "./hazop.vue";
import Jsa from "./jsa.vue";
import Scl from "./scl.vue";
import Fmea from "./fmea.vue";
import RiskCheck from "./riskCheck.vue";
import Footer from './footer.vue'
import { matrixConfigApi } from "@/service/api/system";
@@ -19,12 +19,13 @@ const emit = defineEmits(['save','submit','confirmTab']);
watch(() => props.currentForm, (newVal:any) => {
const omit = (obj:any, fields: string[]) => Object.fromEntries(Object.entries(obj).filter(([k]) => !fields.includes(k)));
const result = newVal?.records.map((item:any) => omit(item, [
'id', 'created_at', 'updated_at', 'change_id', 'source_row_id'
'created_at', 'updated_at', 'change_id', 'source_row_id'
]));
riskRecords.value = result || [];
})
const hazopRiskMatrixConfig = ref<any>(null);
const fmeaRiskMatrixConfig = ref<any>(null);
const jsaRiskMatrixConfig = ref<any>(null);
const pluginDone = computed(() =>
riskRecords.value.reduce((acc:any, item:any) => {
@@ -33,7 +34,7 @@ const pluginDone = computed(() =>
return acc;
}, {} as Record<string, number>)
);
const tools = ref<string[]>(["RISK","HAZOP","JSA", "RISK_CHECK"]);
const tools = ref<string[]>(["RISK","HAZOP","FMEA", "RISK_CHECK"]);
const riskRecords = ref<any>([]);
const pluginDrawer = ref<boolean>(false);
const currentPlugin = ref<string>('');
@@ -42,7 +43,7 @@ const currentPlugin = ref<string>('');
const TOOL_CARDS = [
{ key: "RISK", name: "风险预分析" },
{ key: "HAZOP", name: "HAZOP 分析" },
{ key: "JSA", name: "JSA 分析" },
{ key: "FMEA", name: "FMEA 分析" },
{ key: "RISK_CHECK", name: "风险检查表" },
];
@@ -157,6 +158,10 @@ return result;
}
// 整理入表
const exportAll = (type: string, items: any[]) => {
// console.log(items)
// console.log(riskRecords.value)
// console.log([...riskRecords.value, ...items])
// 如果有id且id!=0,就用id去重,否则根据对象内容去重
riskRecords.value = uniqueById([...riskRecords.value, ...items]);
if (type === "all") {
pluginDrawer.value = false;
@@ -173,10 +178,7 @@ const usePlugin = (key: string) => {
pluginDrawer.value = true;
}
// 关闭插件弹框
const closePlugin = (type: string = "", count: any = undefined) => {
if(type){
pluginDone.value[type] = count;
}
const closePlugin = () => {
pluginDrawer.value = false;
}
@@ -193,26 +195,20 @@ const confirmTab = (tab:string, val:boolean) => {
emit('confirmTab', tab, val);
}
// 获取HAZOP风险矩阵配置
const getHazopRiskMatrixConfig = async () => {
const {data,error} = await matrixConfigApi("HAZOP");
if(!error){
hazopRiskMatrixConfig.value = data;
}
}
// 获取JSA风险矩阵配置
const getJsaRiskMatrixConfig = async () => {
const {data,error} = await matrixConfigApi("JSA");
const getRiskMatrixConfig = async () => {
const {data,error} = await matrixConfigApi();
if(!error){
jsaRiskMatrixConfig.value = data;
hazopRiskMatrixConfig.value = data?.hazop || null;
jsaRiskMatrixConfig.value = data?.jsa || null;
fmeaRiskMatrixConfig.value = data?.fmea || null;
}
}
defineExpose({riskRecords})
onMounted(() => {
getHazopRiskMatrixConfig();
getJsaRiskMatrixConfig();
getRiskMatrixConfig();
})
</script>
@@ -229,7 +225,7 @@ onMounted(() => {
{{ formInfo?.change_level === 2 ? '重要变更' : '一般变更' }}
</NTag>
<span class="text-xs text-slate-400">
{{ formInfo?.change_level === 2 ? '规则:HAZOP 必做,同时开展 JSA' : '规则:JSA涉及工艺安全边界的应升级 HAZOP' }}
{{ formInfo?.change_level === 2 ? '规则:HAZOP 必做' : '规则:涉及工艺安全边界的应升级 HAZOP' }}
</span>
</div>
<div class="grid gap-2 grid-cols-4 mt-3">
@@ -355,6 +351,16 @@ onMounted(() => {
{{jsaRiskLevel(rec.l_value, rec.s_value)?.name}}
</NTag>
</div>
<div v-if="rec.source==='RISK_CHECK'">
<NTag
class="text-[11px]"
size="small"
>
<span>
{{rec.inherent_level?rec.inherent_level:'-'}}
</span>
</NTag>
</div>
</td>
<td class="border px-1.5 py-1">
<NInput type="textarea" size="small" v-model:value="rec.safeguards" :autosize="{minRows:1}" class="text-xs" />
@@ -421,9 +427,19 @@ onMounted(() => {
</span>
</NTag>
</div>
<div v-if="rec.source==='RISK_CHECK'">
<NTag
class="text-[11px]"
size="small"
>
<span>
{{rec.residual_level?rec.residual_level:'-'}}
</span>
</NTag>
</div>
</td>
<td class="border px-1.5 py-1 text-center align-top">
<NTag size="small" type="info" class="text-11px">{{ rec.source==='RISK'?'风险预分析':rec.source }}</NTag>
<NTag size="small" type="info" class="text-11px">{{ rec.source==='RISK'?'风险预分析':rec.source==='RISK_CHECK'?'风险检查': rec.source }}</NTag>
</td>
<td class="border px-1.5 py-1 text-center align-top">
<NTag v-if="rec.to_pssr" size="small" :color="{ color: '#f5f3ff', textColor: '#7c3aed', borderColor: '#f5f3ff' }">已转 PSSR</NTag>
@@ -470,14 +486,11 @@ onMounted(() => {
<template v-if="currentPlugin === 'HAZOP'">
<Hazop :isAiOpen="props.isAiOpen" :currentId="props.currentId" :hazopRiskMatrixConfig="hazopRiskMatrixConfig" @close="closePlugin" @toTable="exportAll" />
</template>
<template v-if="currentPlugin === 'JSA'">
<Jsa :isAiOpen="props.isAiOpen" :currentId="props.currentId" :jsaRiskMatrixConfig="jsaRiskMatrixConfig" @close="closePlugin" @toTable="exportAll" />
</template>
<template v-if="currentPlugin === 'SCL'">
<Scl @close="closePlugin" @toTable="exportAll" />
<template v-if="currentPlugin === 'FMEA'">
<Fmea :isAiOpen="props.isAiOpen" :currentId="props.currentId" :fmeaRiskMatrixConfig="fmeaRiskMatrixConfig" @close="closePlugin" @toTable="exportAll" />
</template>
<template v-if="currentPlugin === 'RISK_CHECK'">
<RiskCheck :isAiOpen="props.isAiOpen" :currentId="props.currentId" @close="closePlugin" />
<RiskCheck :isAiOpen="props.isAiOpen" :currentId="props.currentId" @close="closePlugin" @toTable="exportAll" />
</template>
</NDrawerContent>
</NDrawer>
@@ -4,7 +4,7 @@ import { Icon } from '@iconify/vue'
import { riskCheckDetailApi, riskCheckSaveApi } from '@/service/api/plugin'
const props = defineProps(['isAiOpen','currentId'])
const emit = defineEmits(['close']);
const emit = defineEmits(['close','toTable']);
// answer 1 是
// answer 0 否
@@ -16,35 +16,35 @@ const RISK_CHECKS_INIT = [
group_label: "安全健康",
items: [
{
question_id: 1,
question_id: 'RISK_CHECK'+1,
answer: null,
question: "是否存在任何可能导致安全问题的化学反应?检查化学反应禁忌表。",
risk_desc: "",
control_measure: "",
},
{
question_id: 2,
question_id: 'RISK_CHECK'+2,
answer: null,
question: "是否存在任何易燃易爆化学物质或粉尘?",
risk_desc: "",
control_measure: "",
},
{
question_id: 3,
question_id: 'RISK_CHECK'+3,
answer: null,
question: "是否存在任何有危险的原材料流速、成分或温度条件变更?",
risk_desc: "",
control_measure: "",
},
{
question_id: 4,
question_id: 'RISK_CHECK'+4,
answer: null,
question: "是否会因加热、冷却或精确的温度控制失控而导致安全问题?",
risk_desc: "",
control_measure: "",
},
{
question_id: 5,
question_id: 'RISK_CHECK'+5,
answer: null,
question: "是否具有为安全操作所必需的仪表、控制、紧急制动、开关或报警?",
risk_desc: "",
@@ -57,14 +57,14 @@ const RISK_CHECKS_INIT = [
group_label: "工艺与操作",
items: [
{
question_id: 6,
question_id: 'RISK_CHECK'+6,
answer: null,
question: "变更后是否需要对操作人员进行专门培训?",
risk_desc: "",
control_measure: "",
},
{
question_id: 7,
question_id: 'RISK_CHECK'+7,
answer: null,
question: "是否涉及应急预案或消防设施的调整?",
risk_desc: "",
@@ -73,11 +73,10 @@ const RISK_CHECKS_INIT = [
],
}
];
// 是否是ai生成
const isAiGenerated = ref<boolean>(false);
const loading = ref<boolean>(false);
const riskGroups = ref<any[]>(RISK_CHECKS_INIT);
const riskFilter = ref<number>(-1);
const analyzing = ref<boolean>(false);
// 过滤风险项
const filterChange = (v: number) => {
@@ -92,11 +91,6 @@ const filterChange = (v: number) => {
riskGroups.value = result;
}
};
// 返回工具列表
const onBack = () => {
const count = riskGroups.value.flatMap((group: any) =>group.items.filter((item: any) => item.answer !== null)).length;
emit('close','RISK_CHECK',count===0?undefined:count);
}
// 保存
const save = async () => {
loading.value = true;
@@ -108,6 +102,51 @@ const save = async () => {
}
loading.value = false;
};
// AI整理如表、入表
const enterToTable = (type: string, groupIndex: number = -1, questionIndex: number = -1) => {
if(type==='single'){
const row = riskGroups.value[groupIndex]?.items[questionIndex];
const newRow = {
scene: row.risk_desc || '',
source: 'RISK_CHECK',
item: row.question || '',
inherent_level: '',
safeguards: row?.control_measure || '',
suggestions: '',
residual_level: '',
to_pssr: 0,
}
emit('toTable',type,newRow?[newRow]:[]);
}
if(type==='all'){
const enrichedRows:any[] = [];
riskGroups.value.forEach((group:any) => {
group.items.forEach((row:any) => {
enrichedRows.push({
scene: row.risk_desc || '',
source: 'RISK_CHECK',
item: row.question || '',
inherent_level: '',
safeguards: row?.control_measure || '',
suggestions: '',
residual_level: '',
to_pssr: 0,
});
})
})
emit('toTable',type,enrichedRows);
}
}
// AI 自动分析工序
const runAi = async () => {
analyzing.value = true;
setTimeout(() => {
analyzing.value = false;
}, 1000);
};
// 获取风险检查表详情
const getRiskCheckDetail = async () => {
loading.value = true;
@@ -130,7 +169,7 @@ onMounted(() => {
<NSpin :show="loading" size="small">
<div class="space-y-4">
<div class="flex flex-wrap items-center gap-3">
<NButton size="small" ghost type="primary" class="text-xs" @click="onBack">
<NButton size="small" ghost type="primary" class="text-xs" @click="emit('close')">
<Icon icon="basil:arrow-left-outline" class="size-18px" /> 返回工具列表
</NButton>
<NButton size="small" ghost type="primary" class="text-xs" @click="save">保存</NButton>
@@ -142,27 +181,37 @@ onMounted(() => {
<NRadio :value=1>仅看</NRadio>
</NRadioGroup>
</div>
<span v-if="isAiGenerated" class="ml-auto inline-flex items-center gap-1 rounded-full border border-violet-200 bg-violet-100 px-2 py-0.5 text-xs font-medium text-violet-700">
AI 生成 · 需人工确认
</span>
<div class="ml-auto flex items-center gap-1.5">
<NButton size="small" ghost type="primary" class="text-xs" @click="enterToTable('all')">
<Icon icon="carbon:document-import" class="size-14px mr-1" /> 全部整理入表
</NButton>
<NButton size="small" type="primary" class="text-xs" :disabled="analyzing" @click="runAi">
<Icon v-if="analyzing" icon="ri:loader-4-fill" class="size-14px animate-spin" />
<Icon v-else icon="lucide:sparkles" class="size-12px" />
AI 自动分析
</NButton>
</div>
</div>
<p class="text-[11px] text-slate-400">逐项判定 / / NA判定为的检查项输出风险描述与管控措施完成后点返回工具列表回显完成项数</p>
<div class="space-y-4 scroll">
<div v-for="group in riskGroups" :key="group.group_id" class="rounded-lg border">
<div v-for="(group,groupIndex) in riskGroups" :key="group.group_id" class="rounded-lg border">
<div class="bg-slate-100 px-3 py-2 text-sm font-medium text-slate-700">{{ group.group_label }}</div>
<div v-for="item in group.items" :key="item.question_id"
<div v-for="(item,questionIndex) in group.items" :key="item.question_id"
class="grid gap-3 border-t px-3 py-3 grid-cols-[1.1fr_1fr_1fr] min-h-77px">
<div>
<div class="text-sm text-slate-700">{{ item.question }}</div>
<div class="mt-2 flex items-center gap-4">
<NRadioGroup v-model:value="item.answer" name="radiogroup">
<div class="flex gap-2">
<NRadio :value="1"></NRadio>
<NRadio :value=0></NRadio>
<NRadio :value=2>NA</NRadio>
<div class="flex items-start gap-2">
<NButton text type="primary" class="text-xs mt-2px" @click="enterToTable('single',Number(groupIndex),Number(questionIndex))">入表</NButton>
<div>
<div class="text-sm text-slate-700">{{ item.question }}</div>
<div class="mt-2">
<NRadioGroup v-model:value="item.answer" name="radiogroup">
<div class="flex gap-2">
<NRadio :value="1"></NRadio>
<NRadio :value=0></NRadio>
<NRadio :value=2>NA</NRadio>
</div>
</NRadioGroup>
</div>
</NRadioGroup>
</div>
</div>
</div>
<template v-if="item.answer === 1">
<NInput v-model:value="item.risk_desc" type="textarea" size="small" :autosize="{minRows:2}" placeholder="风险描述" />
+341 -48
View File
@@ -11,7 +11,8 @@ import {
deleteSystemConfigApi,
updateConfigApi,
matrixConfigApi,
saveMatrixConfigApi
saveMatrixConfigApi,
saveFmeaMatrixConfigApi
} from '@/service/api/system';
import { localStg } from '@/utils/storage';
@@ -311,12 +312,13 @@ const dimensionModal = ref<boolean>(false);
const dimForm = ref<{s:{key:string, label:string}[], l:{key:string, label:string}[]}>({ s: [], l: [] });
// 当前选中的风险矩阵索引
const activeMatrix = ref('HAZOP');
// 当前选中的风险矩阵
const curMatrix = ref<any>(null);
// 风险矩阵配置
const hazopMatrix = ref<any>(null);
const fmeaMatrix = ref<any>(null);
// 查找匹配的等级配置
function findMatchingConfig(num:number) {
for (let i = 0; i < curMatrix.value?.risk_grades.length; i++) {
const item = curMatrix.value?.risk_grades[i];
for (let i = 0; i < hazopMatrix.value?.risk_grades.length; i++) {
const item = hazopMatrix.value?.risk_grades[i];
if (num >= item.min && num <= item.max) {
return item; // 返回整条数据
}
@@ -335,12 +337,11 @@ const cellLevel = (s:number, l:number) => {
// 风险矩阵tabs切换
const handleTabChange = (v: string) => {
activeMatrix.value = v;
matrixConfig();
}
// 打开风险矩阵编辑弹窗
const openDimEdit = () => {
dimForm.value = { s: [...curMatrix.value?.consequence], l: [...curMatrix.value?.probability] };
dimForm.value = { s: [...hazopMatrix.value?.consequence], l: [...hazopMatrix.value?.probability] };
dimensionModal.value = true;
}
// 添加后果严重性 S
@@ -361,11 +362,11 @@ const deleteL = (i:number) => {
}
// 添加等级配置
const addLevelConfig = () => {
curMatrix.value?.risk_grades.push({name:null,bg_color:'#333333',font_color:'#ffffff', min:null, max:null});
hazopMatrix.value?.risk_grades.push({name:null,bg_color:'#333333',font_color:'#ffffff', min:null, max:null});
}
// 删除等级配置
const delLevelConfig = (index:number) => {
curMatrix.value?.risk_grades.splice(index, 1);
hazopMatrix.value?.risk_grades.splice(index, 1);
}
// 矩阵维度分级定义保存
const dimensionSave = () => {
@@ -377,31 +378,109 @@ const dimensionSave = () => {
if(hasEmptyLabelL){
return window.$message?.warning("请完善发生可能性L信息");
}
curMatrix.value.consequence = [...dimForm.value.s];
curMatrix.value.probability = [...dimForm.value.l];
hazopMatrix.value.consequence = [...dimForm.value.s];
hazopMatrix.value.probability = [...dimForm.value.l];
dimensionModal.value = false;
}
// *************** 风险矩阵设置(FMEA ***************//
const fmeaModal = ref<boolean>(false);
const fmeaType = ref<string>('S');
// 添加RPN区间
const addRPN = () => {
fmeaMatrix.value?.risk_grades.push({name:'',bg_color:'#333333',font_color:'#ffffff', min_rpn:null, max_rpn:null, control_measure:''});
}
// 删除RPN区间
const delRPN = (index:number) => {
fmeaMatrix.value?.risk_grades.splice(index, 1);
}
// 新增取值
const addGrade = () => {
fmeaMatrix.value?.s_grades.push({value:null, name:'', describe:''});
}
// 删除取值
const delGrade = (index:number) => {
fmeaMatrix.value?.s_grades.splice(index, 1);
}
// 保存FMEA分级表信息
const saveFmeaGrade = () => {
let hasNull = false;
if(fmeaType.value === 'S'){
if(fmeaMatrix.value.s_grades.length === 0){
return window.$message?.warning("请添加S分级表信息");
}
hasNull = fmeaMatrix.value.s_grades.some((item:any) =>
item.score === null || item.label.trim() === ''
);
}
if(fmeaType.value === 'P'){
if(fmeaMatrix.value.p_grades.length === 0){
return window.$message?.warning("请添加P分级表信息");
}
hasNull = fmeaMatrix.value.p_grades.some((item:any) =>
item.score === null || item.label.trim() === ''
);
}
if(fmeaType.value === 'D'){
if(fmeaMatrix.value.d_grades.length === 0){
return window.$message?.warning("请添加D分级表信息");
}
hasNull = fmeaMatrix.value.d_grades.some((item:any) =>
item.score === null || item.label.trim() === ''
);
}
if(hasNull){
return window.$message?.warning("请完善取值、等级信息");
}
fmeaModal.value = false;
}
// 保存风险矩阵
const saveMatrix = async () => {
if(curMatrix.value.risk_grades.length === 0){
return window.$message?.warning("请添加按分值区间定级信息");
// HAZOP矩阵
if(activeMatrix.value === 'HAZOP'){
if(hazopMatrix.value.risk_grades.length === 0){
return window.$message?.warning("请添加按分值区间定级信息");
}
const hasNull = hazopMatrix.value.risk_grades.some((item:any) =>
item.name === null || item.min === null || item.max === null
);
if(hasNull){
return window.$message?.warning("请完善按分值区间定级信息");
}
matrixLoading.value = true;
const {error} = await saveMatrixConfigApi(activeMatrix.value,{
consequence:hazopMatrix.value.consequence,
probability:hazopMatrix.value.probability,
risk_grades:hazopMatrix.value.risk_grades,
});
if(!error){
window.$message?.success("保存成功");
}
matrixLoading.value = false;
}
const hasNull = curMatrix.value.risk_grades.some((item:any) =>
item.name === null || item.min === null || item.max === null
);
if(hasNull){
return window.$message?.warning("请完善按分值区间定级信息");
// FMEA矩阵
if(activeMatrix.value === 'FMEA'){
if(fmeaMatrix.value.risk_grades.length === 0){
return window.$message?.warning("请添加FMEA分析矩阵信息");
}
const hasNull = fmeaMatrix.value.risk_grades.some((item:any) =>
item.name === null || item.min_rpn === null || item.max_rpn === null
);
if(hasNull){
return window.$message?.warning("请完善等级、RPN区间等信息");
}
matrixLoading.value = true;
const {error} = await saveFmeaMatrixConfigApi({
risk_grades:fmeaMatrix.value.risk_grades,
s_grades:fmeaMatrix.value.s_grades,
p_grades:fmeaMatrix.value.p_grades,
d_grades:fmeaMatrix.value.d_grades,
});
if(!error){
window.$message?.success("保存成功");
}
matrixLoading.value = false;
}
matrixLoading.value = true;
const {error} = await saveMatrixConfigApi(activeMatrix.value,{
consequence:curMatrix.value.consequence,
probability:curMatrix.value.probability,
risk_grades:curMatrix.value.risk_grades,
});
if(!error){
window.$message?.success("保存成功");
}
matrixLoading.value = false;
}
// 获取配置
@@ -441,11 +520,10 @@ const getConfig = async () => {
// 获取风险矩阵配置
const matrixConfig = async () => {
const {data,error} = await matrixConfigApi(activeMatrix.value);
const {data,error} = await matrixConfigApi();
if(!error){
curMatrix.value = data;
}else{
curMatrix.value = null;
hazopMatrix.value = data?.hazop || null;
fmeaMatrix.value = data?.fmea || null;
}
}
@@ -744,7 +822,7 @@ onMounted(() => {
<Icon icon="icon-park-outline:all-application" class="size-16px mr-1" />
风险矩阵设置按分析方法
</div>
<NButton size="small" class="text-xs" @click="openDimEdit">
<NButton v-if="activeMatrix==='HAZOP'" size="small" class="text-xs" @click="openDimEdit">
<Icon icon="akar-icons:edit" class="size-14px mr-1" />
维度分级定义
</NButton>
@@ -755,32 +833,32 @@ onMounted(() => {
<div class="py-3 px-4">
<NTabs type="segment" animated :on-update:value="handleTabChange">
<NTabPane name="HAZOP" tab="HAZOP分析矩阵">
<div v-if="curMatrix" class="flex flex-wrap items-start gap-6">
<div v-if="hazopMatrix" class="flex flex-wrap items-start gap-6">
<div class="shrink-0">
<table class="border-collapse text-center text-[11px]">
<thead>
<tr>
<th class="px-1"></th>
<th v-for="(lb, li) in curMatrix?.probability" :key="li" class="px-0.5 pb-1 font-normal leading-tight text-slate-500">
<th v-for="(lb, li) in hazopMatrix?.probability" :key="li" class="px-0.5 pb-1 font-normal leading-tight text-slate-500">
{{ lb.key }}<br />{{ lb.label }}
</th>
</tr>
</thead>
<tbody>
<tr v-for="(row,rowIndex) in curMatrix?.consequence" :key="row.key">
<tr v-for="(row,rowIndex) in hazopMatrix?.consequence" :key="row.key">
<td class="whitespace-nowrap pr-1.5 text-right leading-tight text-slate-500">
{{ curMatrix?.consequence[curMatrix?.consequence.length - (Number(rowIndex)+1)]?.key }}
{{ curMatrix?.consequence[curMatrix?.consequence.length - (Number(rowIndex)+1)]?.label }}
{{ hazopMatrix?.consequence[hazopMatrix?.consequence.length - (Number(rowIndex)+1)]?.key }}
{{ hazopMatrix?.consequence[hazopMatrix?.consequence.length - (Number(rowIndex)+1)]?.label }}
</td>
<td v-for="(col,colIndex) in curMatrix?.probability" :key="col.key" class="p-0.5">
<td v-for="(col,colIndex) in hazopMatrix?.probability" :key="col.key" class="p-0.5">
<div
class="flex h-9 w-12 items-center justify-center rounded font-semibold"
:style="{
backgroundColor:cellLevel(curMatrix?.consequence.length - Number(rowIndex), (Number(colIndex)+1))?.bg_color,
color:cellLevel(curMatrix?.consequence.length - Number(rowIndex), (Number(colIndex)+1))?.font_color,
backgroundColor:cellLevel(hazopMatrix?.consequence.length - Number(rowIndex), (Number(colIndex)+1))?.bg_color,
color:cellLevel(hazopMatrix?.consequence.length - Number(rowIndex), (Number(colIndex)+1))?.font_color,
}"
>
{{ cellScore(curMatrix?.consequence.length - Number(rowIndex), (Number(colIndex)+1)) }}
{{ cellScore(hazopMatrix?.consequence.length - Number(rowIndex), (Number(colIndex)+1)) }}
</div>
</td>
</tr>
@@ -801,7 +879,7 @@ onMounted(() => {
风险值 = 严重性 S × 可能性 L按分值区间定级
</div>
<div class="space-y-2">
<div v-for="(lv,index) in curMatrix?.risk_grades" :key="lv.name" class="flex items-center gap-2 rounded-lg border px-2.5 py-2">
<div v-for="(lv,index) in hazopMatrix?.risk_grades" :key="lv.name" class="flex items-center gap-2 rounded-lg border px-2.5 py-2">
<span class="text-xs">背景</span>
<NColorPicker v-model:value="lv.bg_color">
<template #trigger="{ value, onClick, ref: triggerRef }">
@@ -865,7 +943,112 @@ onMounted(() => {
</div>
<div v-else class="py-5 text-center">暂无矩阵配置</div>
</NTabPane>
<NTabPane name="JSA" tab="JSA分析矩阵">
<NTabPane name="FMEA" tab="FMEA分析矩阵">
<div v-if="fmeaMatrix">
<div class="flex items-center justify-between">
<div class="flex items-center">
<p class="font-600">风险分级区间RPN = S × P × D</p>
<NButton size="tiny" ghost type="primary" @click="addRPN">
<Icon icon="ic:round-plus" class="size-14px" />新增区间
</NButton>
</div>
<div class="flex items-center gap-5">
<p class="font-600">S / P / D 分级表</p>
<div class="flex items-center gap-2">
<NButton size="tiny" ghost @click="fmeaType = 'S'; fmeaModal = true">S分级表</NButton>
<NButton size="tiny" ghost @click="fmeaType = 'P'; fmeaModal = true">P分级表</NButton>
<NButton size="tiny" ghost @click="fmeaType = 'D'; fmeaModal = true">D分级表</NButton>
</div>
</div>
</div>
<NTable size="small" :single-line="false" class="mt-3">
<thead>
<tr>
<th class="w-160px text-center">等级</th>
<th class="w-80px !text-center">背景</th>
<th class="w-80px !text-center">字体</th>
<th class="w-200px">RPN区间</th>
<th>管控要求</th>
<th class="w-80px !text-center">操作</th>
</tr>
</thead>
<tbody>
<tr v-if="fmeaMatrix?.risk_grades.length===0">
<td colspan="6" class="!py-2.5 text-center !text-slate-400">暂无数据</td>
</tr>
<tr v-for="(item, index) in fmeaMatrix?.risk_grades" :key="index">
<td><NInput size="small" v-model:value="item.name" /></td>
<td>
<div class="flex items-center justify-center">
<NColorPicker v-model:value="item.bg_color" :modes="['hex']">
<template #trigger="{ value, onClick, ref: triggerRef }">
<div
:ref="triggerRef"
:style="{
width: '40px',
height: '26px',
borderRadius: '8px',
backgroundColor: value || '#333',
cursor: 'pointer',
border: '2px solid #fff',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)',
}"
@click="onClick"
></div>
</template>
</NColorPicker>
</div>
</td>
<td>
<div class="flex items-center justify-center">
<NColorPicker v-model:value="item.font_color" :modes="['hex']">
<template #trigger="{ value, onClick, ref: triggerRef }">
<div
:ref="triggerRef"
:style="{
width: '40px',
height: '26px',
borderRadius: '8px',
backgroundColor: value || '#fff',
cursor: 'pointer',
border: '2px solid #fff',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)',
}"
@click="onClick"
></div>
</template>
</NColorPicker>
</div>
</td>
<td>
<div class="flex items-center justify-center gap-2">
<NInputNumber v-model:value="item.min_rpn" size="small" :min="0" :show-button="false" class="text-center" />
<span>~</span>
<NInputNumber v-model:value="item.max_rpn" size="small" :min="0" :show-button="false" class="text-center" />
</div>
</td>
<td>
<NInput v-model:value="item.control_measure" size="small" type="textarea" :autosize="{ minRows: 1 }" />
</td>
<td class="text-center">
<NPopconfirm
positive-text="确定"
:positiveButtonProps="{ size: 'tiny' }"
:negativeButtonProps="{ size: 'tiny' }"
@positive-click="delRPN(Number(index))"
>
<template #trigger>
<NButton text type="error"><Icon icon="ep:delete" class="size-14px" /></NButton>
</template>
确定删除吗
</NPopconfirm>
</td>
</tr>
</tbody>
</NTable>
</div>
</NTabPane>
<!-- <NTabPane name="JSA" tab="JSA分析矩阵">
<div v-if="curMatrix" class="flex flex-wrap items-start gap-6">
<div class="shrink-0">
<table class="border-collapse text-center text-[11px]">
@@ -902,8 +1085,6 @@ onMounted(() => {
<span>横轴发生可能性 L</span>
</div>
</div>
<!-- 等级分值区间 -->
<div class="min-w-72 flex-1">
<div class="mb-2 text-xs text-slate-500 flex items-center gap-2">
<NButton size="tiny" ghost type="primary" @click="addLevelConfig">
@@ -965,7 +1146,7 @@ onMounted(() => {
</div>
</div>
<div v-else class="py-5 text-center">暂无矩阵配置</div>
</NTabPane>
</NTabPane> -->
</NTabs>
</div>
</NCard>
@@ -1136,7 +1317,7 @@ onMounted(() => {
<NModal
v-model:show="dimensionModal"
preset="card"
:title="curMatrix?.tool+'矩阵维度分级定义'"
:title="hazopMatrix?.biz_type+'矩阵维度分级定义'"
:auto-focus="false"
:style="{ width: '500px', height: 'auto' }"
:segmented="{ content: true, footer: true }"
@@ -1210,6 +1391,118 @@ onMounted(() => {
</div>
</template>
</NModal>
<!-- 风险矩阵设置FMEA -->
<NModal
v-model:show="fmeaModal"
preset="card"
:title="fmeaType+'分级表'"
:auto-focus="false"
:style="{ width: '760px', height: 'auto' }"
:segmented="{ content: true, footer: true }"
>
<div>
<NButton size="tiny" ghost type="primary" @click="addGrade"><Icon icon="ic:round-plus" class="size-14px" />新增取值</NButton>
<NTable size="small" :single-line="false" class="mt-2">
<thead>
<tr>
<th class="w-80px !text-center">取值</th>
<th class="w-160px">等级</th>
<th>说明</th>
<th class="w-80px !text-center">操作</th>
</tr>
</thead>
<tbody>
<template v-if="fmeaType==='S'">
<tr v-if="fmeaMatrix?.s_grades?.length===0">
<td colspan="4" class="!py-2.5 text-center !text-slate-400">暂无数据</td>
</tr>
<tr v-for="(item, index) in fmeaMatrix?.s_grades" :key="index">
<td><NInputNumber size="small" v-model:value="item.score" :min="0" :show-button="false" class="text-center" /></td>
<td>
<NInput v-model:value="item.label" size="small" />
</td>
<td>
<NInput v-model:value="item.description" size="small" type="textarea" :autosize="{ minRows: 1 }" />
</td>
<td class="text-center">
<NPopconfirm
positive-text="确定"
:positiveButtonProps="{ size: 'tiny' }"
:negativeButtonProps="{ size: 'tiny' }"
@positive-click="delGrade(Number(index))"
>
<template #trigger>
<NButton text type="error"><Icon icon="ep:delete" class="size-14px" /></NButton>
</template>
确定删除吗
</NPopconfirm>
</td>
</tr>
</template>
<template v-if="fmeaType==='P'">
<tr v-if="fmeaMatrix?.p_grades?.length===0">
<td colspan="4" class="!py-2.5 text-center !text-slate-400">暂无数据</td>
</tr>
<tr v-for="(item, index) in fmeaMatrix?.p_grades" :key="index">
<td><NInputNumber size="small" v-model:value="item.score" :min="0" :show-button="false" class="text-center" /></td>
<td>
<NInput v-model:value="item.label" size="small" />
</td>
<td>
<NInput v-model:value="item.description" size="small" type="textarea" :autosize="{ minRows: 1 }" />
</td>
<td class="text-center">
<NPopconfirm
positive-text="确定"
:positiveButtonProps="{ size: 'tiny' }"
:negativeButtonProps="{ size: 'tiny' }"
@positive-click="delGrade(Number(index))"
>
<template #trigger>
<NButton text type="error"><Icon icon="ep:delete" class="size-14px" /></NButton>
</template>
确定删除吗
</NPopconfirm>
</td>
</tr>
</template>
<template v-if="fmeaType==='D'">
<tr v-if="fmeaMatrix?.d_grades?.length===0">
<td colspan="4" class="!py-2.5 text-center !text-slate-400">暂无数据</td>
</tr>
<tr v-for="(item, index) in fmeaMatrix?.d_grades" :key="index">
<td><NInputNumber size="small" v-model:value="item.score" :min="0" :show-button="false" class="text-center" /></td>
<td>
<NInput v-model:value="item.label" size="small" />
</td>
<td>
<NInput v-model:value="item.description" size="small" type="textarea" :autosize="{ minRows: 1 }" />
</td>
<td class="text-center">
<NPopconfirm
positive-text="确定"
:positiveButtonProps="{ size: 'tiny' }"
:negativeButtonProps="{ size: 'tiny' }"
@positive-click="delGrade(Number(index))"
>
<template #trigger>
<NButton text type="error"><Icon icon="ep:delete" class="size-14px" /></NButton>
</template>
确定删除吗
</NPopconfirm>
</td>
</tr>
</template>
</tbody>
</NTable>
</div>
<template #footer>
<div class="flex justify-end gap-[10px]">
<NButton size="small" @click="fmeaModal = false">取消</NButton>
<NButton size="small" type="primary" @click="saveFmeaGrade">确定</NButton>
</div>
</template>
</NModal>
</div>
</template>