hazop插件和jsa插件修改完成
This commit is contained in:
@@ -2,35 +2,18 @@
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from "vue";
|
||||
import { Icon } from '@iconify/vue'
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import { useDialog } from 'naive-ui';
|
||||
import { peopleListApi } from '@/service/api/system';
|
||||
import {
|
||||
hazopNodesApi,
|
||||
hazopNodeAddApi,
|
||||
hazopObjAddApi,
|
||||
hazopDevAddApi,
|
||||
hazopNodeEditApi,
|
||||
hazopObjEditApi,
|
||||
hazopDevEditApi,
|
||||
hazopNodeDelApi,
|
||||
hazopObjDelApi,
|
||||
hazopDevDelApi,
|
||||
hazopRowAddApi,
|
||||
hazopRowEditApi,
|
||||
hazopRowDelApi,
|
||||
} from "@/service/api/plugin";
|
||||
import { hazopNodesApi, hazopSaveApi } from "@/service/api/plugin";
|
||||
|
||||
const themeStore = useThemeStore();
|
||||
const dialog = useDialog();
|
||||
|
||||
const props = defineProps(['isAiOpen','currentId','hazopRiskMatrixConfig'])
|
||||
const emit = defineEmits(['close','toTable']);
|
||||
|
||||
const loading = ref<boolean>(false);
|
||||
const treeLoading = ref<boolean>(false);
|
||||
const analyzing = ref(false);
|
||||
// 删除确认弹窗
|
||||
const delModal = ref<boolean>(false);
|
||||
// 删除确定状态
|
||||
const delLoading = ref<boolean>(false);
|
||||
// 新增/编辑弹窗
|
||||
const editModal = ref<boolean>(false);
|
||||
// 当前弹框标题
|
||||
@@ -39,8 +22,6 @@ const currentModalTitle = ref<string>('');
|
||||
const currentModalType = ref<string>('');
|
||||
// 当前弹框对象
|
||||
const currentModalObj = ref<string>('');
|
||||
// 弹窗保存状态
|
||||
const modalSaving = ref<boolean>(false);
|
||||
const name = ref<string>('');
|
||||
|
||||
const selectOptions = computed(() => {
|
||||
@@ -74,11 +55,14 @@ const contextMenuShow = ref<boolean>(false)
|
||||
const contextMenuX = ref<number>(0)
|
||||
const contextMenuY = ref<number>(0)
|
||||
// 存储当前右键点击的目标数据
|
||||
const contextTarget = ref<{type: string, node: any, obj: any, dev: any}>({
|
||||
const contextTarget = ref<{type: string, node: any, obj: any, dev: any, nodeIndex: number, objIndex: number, devIndex: number}>({
|
||||
type: '', // 'node' | 'obj' | 'dev'
|
||||
node: null,
|
||||
obj: null,
|
||||
dev: null,
|
||||
nodeIndex: -1,
|
||||
objIndex: -1,
|
||||
devIndex: -1,
|
||||
})
|
||||
// 当前节点
|
||||
const currentNode = computed(() => {
|
||||
@@ -142,61 +126,66 @@ const recTotal = computed(() => {
|
||||
return totalRows;
|
||||
});
|
||||
|
||||
const workers = ref<{id:number,name:string,org:string}[]>([]);
|
||||
// 人员列表源数据
|
||||
const workers = ref<{user_id:number,user_name:string,org_name?:string}[]>([]);
|
||||
// 选中的人员
|
||||
const selectedWorkers = ref<number[]>([]);
|
||||
const selectedWorkers = ref<{user_id:number,user_name:string,org_name?:string}[]>([]);
|
||||
// 选择分析人员弹窗
|
||||
const openSelectWorker = ref(false);
|
||||
// 搜索分析人员
|
||||
const searchWorker = ref("");
|
||||
// 分析人员列表
|
||||
const workerList = computed(() => workers.value.filter((x:{id:number,name:string,org:string}) => !searchWorker.value || x.name.includes(searchWorker.value)));
|
||||
|
||||
const workerList = computed(() => workers.value.filter((x:{user_id:number,user_name:string,org_name?:string}) => !searchWorker.value || x.user_name.includes(searchWorker.value)));
|
||||
// 选择分析人员
|
||||
const selectWorker = (n: number) => {
|
||||
selectedWorkers.value = selectedWorkers.value.includes(n) ? selectedWorkers.value.filter((x:number) => x !== n) : [...selectedWorkers.value, n];
|
||||
const selectWorker = (n: {user_id:number,user_name:string,org_name?:string}) => {
|
||||
selectedWorkers.value = selectedWorkers.value.includes(n) ? selectedWorkers.value.filter((x:any) => x.user_id !== n.user_id) : [...selectedWorkers.value, n];
|
||||
};
|
||||
// 匹配是否有人员
|
||||
const hasWorker = (id:number) => {
|
||||
return selectedWorkers.value.some((item:{user_id:number,user_name:string,org_name?:string}) => item.user_id === id);
|
||||
};
|
||||
|
||||
// 保存
|
||||
const save = () => {
|
||||
window.$message?.success("已手动保存,分析记录数已同步至工具卡片(内容修改实时自动保存,退出不丢失)");
|
||||
const save = async (isClick: boolean = false) => {
|
||||
loading.value = isClick;
|
||||
const workers = (selectedWorkers.value ?? []).map((item: { org_name?:string; user_id: number; user_name: string }) => {
|
||||
const { org_name, ...rest } = item;
|
||||
return rest;
|
||||
});
|
||||
const {error} = await hazopSaveApi(props.currentId, {
|
||||
nodes: nodes.value,
|
||||
users: workers,
|
||||
});
|
||||
if(!error){
|
||||
if(isClick){
|
||||
window.$message?.success("保存成功");
|
||||
}
|
||||
}
|
||||
loading.value = false;
|
||||
};
|
||||
|
||||
/**
|
||||
* 根据行 ID 查找并返回带有父级名称的行对象
|
||||
* @param {Array} data - 原始 JSON 数据数组
|
||||
* @param {Number|String} rowId - 要查找的行 ID
|
||||
* @returns {Object|null} - 增强后的行对象,或 null(未找到)
|
||||
* 根据行下标 查找并返回带有父级名称的行对象
|
||||
*/
|
||||
const findRowWithParents = (data:any, rowId:any) => {
|
||||
for (const node of data) {
|
||||
const node_ame = node.node_name;
|
||||
for (const obj of node.objects) {
|
||||
const obj_ame = obj.obj_name;
|
||||
for (const dev of obj.deviations) {
|
||||
const deviation = dev.deviation;
|
||||
for (const row of dev.rows) {
|
||||
if (row.id === rowId) {
|
||||
return {
|
||||
...row,
|
||||
scene: `${row.cause?row.cause+';':''}${deviation?deviation+';':''}${row.consequence?'可能导致:'+row.consequence:''}`,
|
||||
source: 'HAZOP',
|
||||
risk_item: `${deviation}${node_ame?`(${node_ame}${obj_ame?`-${obj_ame}`:''})`:''}`,
|
||||
safe: row?.safeguards?.map((item:any) => item.name).join(';') || '',
|
||||
suggestion: row?.suggestions?.map((item:any) => item.name).join(';') || '',
|
||||
to_pssr: 0
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const findRowWithParents = (rowIndex: number = -1) => {
|
||||
const row = currentDev.value.rows[rowIndex];
|
||||
if(row){
|
||||
return {
|
||||
...row,
|
||||
scene: `${row.cause?row.cause+';':''}${currentDev.value?.deviation+';'}${row.consequence?'可能导致:'+row.consequence:''}`,
|
||||
source: 'HAZOP',
|
||||
risk_item: `${currentDev.value?.deviation}${currentNode.value?.node_name?`(${currentNode.value?.node_name}${currentObj.value?.obj_name?`-${currentObj.value?.obj_name}`:''})`:''}`,
|
||||
safe: row?.safeguards?.map((item:any) => item.name).join(';') || '',
|
||||
suggestion: row?.suggestions?.map((item:any) => item.name).join(';') || '',
|
||||
to_pssr: 0
|
||||
};
|
||||
}
|
||||
return null; // 未找到
|
||||
return null;
|
||||
}
|
||||
// AI整理如表、入表
|
||||
const enterToTable = (type: string, row: any) => {
|
||||
const enterToTable = (type: string, rowIndex: number = -1) => {
|
||||
if(type==='single'){
|
||||
const enrichedRow = findRowWithParents(nodes.value, row.id);
|
||||
const enrichedRow = findRowWithParents(rowIndex);
|
||||
emit('toTable',type,enrichedRow?[enrichedRow]:[]);
|
||||
}
|
||||
if(type==='all'){
|
||||
@@ -283,11 +272,12 @@ const addSafeguard = (index: number) => {
|
||||
}else{
|
||||
currentDev.value.rows[index].safeguards = [{name:'',value:''}];
|
||||
}
|
||||
save()
|
||||
}
|
||||
// 删除安全措施
|
||||
const delSafeguard = (index: number, safeguardIndex: number, row: any) => {
|
||||
const delSafeguard = (index: number, safeguardIndex: number) => {
|
||||
currentDev.value.rows[index].safeguards.splice(safeguardIndex, 1);
|
||||
editRow(row)
|
||||
save()
|
||||
}
|
||||
// 新增建议措施
|
||||
const addSuggestion = (index: number) => {
|
||||
@@ -296,11 +286,12 @@ const addSuggestion = (index: number) => {
|
||||
}else{
|
||||
currentDev.value.rows[index].suggestions = [{name:'',value:''}];
|
||||
}
|
||||
save()
|
||||
}
|
||||
// 删除建议措施
|
||||
const delSuggestion = (index: number, suggestionIndex: number, row: any) => {
|
||||
const delSuggestion = (index: number, suggestionIndex: number) => {
|
||||
currentDev.value.rows[index].suggestions.splice(suggestionIndex, 1);
|
||||
editRow(row)
|
||||
save()
|
||||
}
|
||||
|
||||
// 新增行
|
||||
@@ -308,8 +299,7 @@ const addRow = async () => {
|
||||
if(!currentDev.value){
|
||||
return window.$message?.warning("请先在左侧选择偏离")
|
||||
}
|
||||
loading.value = true;
|
||||
const {data,error} = await hazopRowAddApi(currentDev.value?.id,{
|
||||
currentDev.value.rows.push({
|
||||
cause: "",
|
||||
consequence: "",
|
||||
l_value: l_options.value[0]?.key || 1,
|
||||
@@ -317,33 +307,12 @@ const addRow = async () => {
|
||||
risk_value: riskLevel(l_options.value[0]?.key || 1, s_options.value[0]?.key || 1)?.name || '',
|
||||
safeguards: [],
|
||||
suggestions: [],
|
||||
})
|
||||
if(!error){
|
||||
currentDev.value.rows.push(data);
|
||||
}
|
||||
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 = 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 delRow = async (index: number) => {
|
||||
currentDev.value.rows.splice(index, 1);
|
||||
save()
|
||||
}
|
||||
|
||||
// 展开收起
|
||||
@@ -369,101 +338,102 @@ const modalSave = async () => {
|
||||
if(name.value===''){
|
||||
return window.$message?.warning("名称不能为空")
|
||||
}
|
||||
modalSaving.value = true;
|
||||
// 新增节点
|
||||
if(currentModalType.value==='add' && currentModalObj.value==='node'){
|
||||
const {error} = await hazopNodeAddApi(props.currentId,{node_name: name.value});
|
||||
if(!error){
|
||||
getHazopNodes();
|
||||
window.$message?.success("新增节点成功")
|
||||
editModal.value = false;
|
||||
}
|
||||
nodes.value.push({id:0,node_name:name.value,change_id:props.currentId,objects:[]});
|
||||
window.$message?.success("新增节点成功")
|
||||
editModal.value = false;
|
||||
}
|
||||
// 编辑节点
|
||||
if(currentModalType.value==='edit' && currentModalObj.value==='node'){
|
||||
const {error} = await hazopNodeEditApi(contextTarget.value?.node?.id,{node_name: name.value});
|
||||
if(!error){
|
||||
getHazopNodes();
|
||||
window.$message?.success("编辑节点成功")
|
||||
editModal.value = false;
|
||||
}
|
||||
nodes.value[contextTarget.value?.nodeIndex].node_name = name.value;
|
||||
window.$message?.success("编辑节点成功")
|
||||
editModal.value = false;
|
||||
}
|
||||
// 新增分析对象
|
||||
if(currentModalType.value==='add' && currentModalObj.value==='obj'){
|
||||
const {error} = await hazopObjAddApi(contextTarget.value?.node?.id,{obj_name: name.value});
|
||||
if(!error){
|
||||
getHazopNodes();
|
||||
window.$message?.success("新增分析对象成功")
|
||||
editModal.value = false;
|
||||
let obj = {
|
||||
id: 0,
|
||||
obj_name: name.value,
|
||||
node_id: nodes.value[contextTarget.value?.nodeIndex]?.id,
|
||||
deviations: []
|
||||
}
|
||||
if(nodes.value[contextTarget.value?.nodeIndex].objects){
|
||||
nodes.value[contextTarget.value?.nodeIndex].objects.push(obj);
|
||||
}else{
|
||||
nodes.value[contextTarget.value?.nodeIndex].objects = [obj];
|
||||
}
|
||||
window.$message?.success("新增分析对象成功")
|
||||
editModal.value = false;
|
||||
}
|
||||
// 编辑分析对象
|
||||
if(currentModalType.value==='edit' && currentModalObj.value==='obj'){
|
||||
const {error} = await hazopObjEditApi(contextTarget.value?.obj?.id,{obj_name: name.value});
|
||||
if(!error){
|
||||
getHazopNodes();
|
||||
window.$message?.success("编辑分析对象成功")
|
||||
editModal.value = false;
|
||||
}
|
||||
nodes.value[contextTarget.value?.nodeIndex].objects[contextTarget.value?.objIndex].obj_name = name.value;
|
||||
window.$message?.success("编辑分析对象成功")
|
||||
editModal.value = false;
|
||||
}
|
||||
// 新增偏离
|
||||
if(currentModalType.value==='add' && currentModalObj.value==='dev'){
|
||||
const {error} = await hazopDevAddApi(contextTarget.value?.obj?.id,{deviation: name.value});
|
||||
if(!error){
|
||||
getHazopNodes();
|
||||
window.$message?.success("新增偏离成功")
|
||||
editModal.value = false;
|
||||
let dev = {
|
||||
id: 0,
|
||||
object_id: nodes.value[contextTarget.value?.nodeIndex].objects[contextTarget.value?.objIndex].id,
|
||||
deviation: name.value,
|
||||
rows: []
|
||||
}
|
||||
if(nodes.value[contextTarget.value?.nodeIndex].objects[contextTarget.value?.objIndex].deviations){
|
||||
nodes.value[contextTarget.value?.nodeIndex].objects[contextTarget.value?.objIndex].deviations.push(dev);
|
||||
}else {
|
||||
nodes.value[contextTarget.value?.nodeIndex].objects[contextTarget.value?.objIndex].deviations = [dev];
|
||||
}
|
||||
window.$message?.success("新增偏离成功")
|
||||
editModal.value = false;
|
||||
}
|
||||
// 编辑偏离
|
||||
if(currentModalType.value==='edit' && currentModalObj.value==='dev'){
|
||||
const {error} = await hazopDevEditApi(contextTarget.value?.dev?.id,{deviation: name.value});
|
||||
if(!error){
|
||||
getHazopNodes();
|
||||
window.$message?.success("编辑偏离成功")
|
||||
editModal.value = false;
|
||||
}
|
||||
nodes.value[contextTarget.value?.nodeIndex].objects[contextTarget.value?.objIndex].deviations[contextTarget.value?.devIndex].deviation = name.value;
|
||||
window.$message?.success("编辑偏离成功")
|
||||
editModal.value = false;
|
||||
}
|
||||
modalSaving.value = false;
|
||||
}
|
||||
|
||||
// 打开删除弹框
|
||||
const openDelModal = (type: string) => {
|
||||
currentModalType.value = type;
|
||||
delModal.value = true;
|
||||
}
|
||||
|
||||
// 删除确认
|
||||
const delConfirm = async () => {
|
||||
delLoading.value = true;
|
||||
// 删除节点
|
||||
if(currentModalType.value==='node'){
|
||||
const {error} = await hazopNodeDelApi(contextTarget.value?.node?.id);
|
||||
if(!error){
|
||||
getHazopNodes();
|
||||
window.$message?.success("删除节点成功")
|
||||
delModal.value = false;
|
||||
}
|
||||
let txt = ''
|
||||
if(type==='node'){
|
||||
txt = `确定删除节点「${contextTarget.value?.node?.node_name}」吗?`
|
||||
}
|
||||
// 删除分析对象
|
||||
if(currentModalType.value==='obj'){
|
||||
const {error} = await hazopObjDelApi(contextTarget.value?.obj?.id);
|
||||
if(!error){
|
||||
getHazopNodes();
|
||||
window.$message?.success("删除分析对象成功")
|
||||
delModal.value = false;
|
||||
}
|
||||
if(type==='obj'){
|
||||
txt = `确定删除分析对象「${contextTarget.value?.obj?.obj_name}」吗?`
|
||||
}
|
||||
// 删除偏离
|
||||
if(currentModalType.value==='dev'){
|
||||
const {error} = await hazopDevDelApi(contextTarget.value?.dev?.id);
|
||||
if(!error){
|
||||
getHazopNodes();
|
||||
window.$message?.success("删除偏离成功")
|
||||
delModal.value = false;
|
||||
}
|
||||
if(type==='dev'){
|
||||
txt = `确定删除偏离「${contextTarget.value?.dev?.deviation}」吗?`
|
||||
}
|
||||
delLoading.value = false;
|
||||
dialog.info({
|
||||
title: '删除',
|
||||
content: txt,
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: () => {
|
||||
// 删除节点
|
||||
if(type==='node'){
|
||||
nodes.value.splice(contextTarget.value?.nodeIndex, 1);
|
||||
window.$message?.success("删除节点成功")
|
||||
}
|
||||
// 删除分析对象
|
||||
if(type==='obj'){
|
||||
nodes.value[contextTarget.value?.nodeIndex].objects.splice(contextTarget.value?.objIndex, 1);
|
||||
window.$message?.success("删除分析对象成功")
|
||||
}
|
||||
// 删除偏离
|
||||
if(type==='dev'){
|
||||
nodes.value[contextTarget.value?.nodeIndex].objects[contextTarget.value?.objIndex].deviations.splice(contextTarget.value?.devIndex, 1);
|
||||
window.$message?.success("删除偏离成功")
|
||||
}
|
||||
},
|
||||
onNegativeClick: () => {
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 菜单选项配置(根据不同层级返回不同菜单)
|
||||
@@ -473,18 +443,15 @@ const getMenuOptions = (type: string) => {
|
||||
{ label: '编辑节点', key: 'editNode' },
|
||||
{ label: '删除节点', key: 'delNode' },
|
||||
]
|
||||
|
||||
const objOptions = [
|
||||
{ label: '新增偏离', key: 'addDev' },
|
||||
{ label: '编辑分析对象', key: 'editObj' },
|
||||
{ label: '删除分析对象', key: 'delObj' },
|
||||
]
|
||||
|
||||
const devOptions = [
|
||||
{ label: '编辑分离', key: 'editDev' },
|
||||
{ label: '删除分离', key: 'delDev' },
|
||||
]
|
||||
|
||||
const map:any = {
|
||||
node: nodeOptions,
|
||||
obj: objOptions,
|
||||
@@ -503,11 +470,11 @@ const closeMenu = () => {
|
||||
}
|
||||
|
||||
// 右键菜单事件处理
|
||||
const handleContextMenu = (event: MouseEvent, type: string, node: any, obj = null, dev = null) => {
|
||||
const handleContextMenu = (event: MouseEvent, type: string, node: any, obj: any, dev: any, nodeIndex: number, objIndex: number, devIndex: number) => {
|
||||
// 阻止浏览器默认右键菜单
|
||||
event.preventDefault()
|
||||
// 记录目标数据
|
||||
contextTarget.value = { type, node, obj, dev }
|
||||
contextTarget.value = { type, node, obj, dev, nodeIndex, objIndex, devIndex }
|
||||
// 计算菜单位置
|
||||
contextMenuX.value = Math.max(0, event.clientX)
|
||||
contextMenuY.value = Math.max(0, event.clientY)
|
||||
@@ -576,12 +543,14 @@ if (typeof window !== 'undefined') {
|
||||
|
||||
// 获取HAZOP节点列表
|
||||
const getHazopNodes = async () => {
|
||||
treeLoading.value = true;
|
||||
loading.value = true;
|
||||
const {data,error} = await hazopNodesApi(props.currentId);
|
||||
if(!error){
|
||||
nodes.value = data;
|
||||
nodes.value = data.nodes;
|
||||
selectedWorkers.value = data.users;
|
||||
currentDev.value = nodes.value[0]?.objects[0]?.deviations?.[0] || null;
|
||||
}
|
||||
treeLoading.value = false;
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
// 获取人员列表
|
||||
@@ -589,9 +558,9 @@ const getWorkers = async () => {
|
||||
const {data,error} = await peopleListApi();
|
||||
if(!error){
|
||||
const transformed = data.map((item:any) => ({
|
||||
id: item.id,
|
||||
name: item.realname,
|
||||
org: item.user_departments.map((d:any) => d.label).join(';')
|
||||
user_id: item.id,
|
||||
user_name: item.realname,
|
||||
org_name: item.user_departments.map((d:any) => d.label).join(';')
|
||||
}));
|
||||
workers.value = transformed;
|
||||
}
|
||||
@@ -619,13 +588,13 @@ onBeforeUnmount(() => {
|
||||
<Icon icon="basil:arrow-left-outline" class="size-18px" />
|
||||
返回工具列表
|
||||
</NButton>
|
||||
<NButton size="small" ghost type="primary" class="text-xs" @click="save">保存</NButton>
|
||||
<NButton size="small" ghost type="primary" class="text-xs" @click="save(true)">保存</NButton>
|
||||
<span class="text-sm font-bold text-[var(--theme-color)]">HAZOP 分析插件</span>
|
||||
<NTag size="small" type="info">插件</NTag>
|
||||
<span class="text-[11px] text-slate-500">{{ `分析进度 ${devDone}/${devTotal} 个偏离 · ${recTotal} 条记录` }}</span>
|
||||
<div class="relative">
|
||||
<NButton size="small" ghost type="primary" class="text-[11px]" @click="openSelectWorker = !openSelectWorker">
|
||||
<Icon icon="lucide:users" class="size-12px" /> 分析组成员 · 已选 {{ selectedWorkers.length }} 人
|
||||
<Icon icon="lucide:users" class="size-12px" /> 分析组成员 · 已选 {{ selectedWorkers.length }} 人
|
||||
</NButton>
|
||||
<template v-if="openSelectWorker">
|
||||
<div class="fixed inset-0 z-10" @click="openSelectWorker = false"></div>
|
||||
@@ -636,15 +605,15 @@ onBeforeUnmount(() => {
|
||||
</div>
|
||||
<NInput v-model:value="searchWorker" size="small" placeholder="搜索人名…" class="mb-1.5 text-xs" />
|
||||
<div class="max-h-48 space-y-0.5 overflow-y-auto">
|
||||
<div v-for="x in workerList" :key="x.id" @click="selectWorker(x.id)"
|
||||
class="cursor-pointer flex w-full items-center min-h-34px gap-2 rounded-md px-2 py-1.5 text-left text-xs"
|
||||
:class="selectedWorkers.includes(x.id) ? 'bg-[#DFE9F6]' : 'hover:bg-slate-50'">
|
||||
<span class="flex h-3.5 w-3.5 shrink-0 items-center justify-center rounded border text-[10px]"
|
||||
:class="selectedWorkers.includes(x.id) ? 'border-[var(--theme-color)] bg-[var(--theme-color)] text-white' : 'border-slate-300'">{{ selectedWorkers.includes(x.id) ? "✓" : "" }}</span>
|
||||
<span class="font-medium text-slate-800">{{ x.name }}</span>
|
||||
<span class="text-[11px] text-slate-400">{{ x.org }}</span>
|
||||
<NTag v-if="selectedWorkers[0] === x.id" size="small" type="primary" class="ml-auto">组长</NTag>
|
||||
</div>
|
||||
<div v-for="x in workerList" :key="x.user_id" @click="selectWorker(x)"
|
||||
class="cursor-pointer flex w-full items-center min-h-34px gap-2 rounded-md px-2 py-1.5 text-left text-xs"
|
||||
:class="hasWorker(x.user_id) ? 'bg-[#DFE9F6]' : 'hover:bg-slate-50'">
|
||||
<span class="flex h-3.5 w-3.5 shrink-0 items-center justify-center rounded border text-[10px]"
|
||||
:class="hasWorker(x.user_id) ? 'border-[var(--theme-color)] bg-[var(--theme-color)] text-white' : 'border-slate-300'">{{ hasWorker(x.user_id) ? "✓" : "" }}</span>
|
||||
<span class="font-medium text-slate-800">{{ x.user_name }}</span>
|
||||
<span class="text-[11px] text-slate-400">{{ x.org_name }}</span>
|
||||
<NTag v-if="selectedWorkers[0]?.user_id === x.user_id" size="small" type="primary" class="ml-auto">组长</NTag>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-1.5 border-t pt-1.5 text-[11px] leading-4 text-slate-400">
|
||||
首位选择者默认为组长;成员为选择式(非手写),参与记录自动归属到人,用于「参与风险分析次数」统计采集。
|
||||
@@ -653,7 +622,7 @@ onBeforeUnmount(() => {
|
||||
</template>
|
||||
</div>
|
||||
<div class="ml-auto flex items-center gap-1.5">
|
||||
<NButton v-if="isAiOpen" size="small" ghost type="primary" class="text-xs" :disabled="recTotal === 0" @click="enterToTable('all',null)">
|
||||
<NButton v-if="isAiOpen" size="small" ghost type="primary" class="text-xs" :disabled="recTotal === 0" @click="enterToTable('all')">
|
||||
<Icon icon="lucide:sparkles" class="size-12px mr-1" /> AI 整理入表
|
||||
</NButton>
|
||||
<NButton size="small" ghost type="primary" class="text-xs" @click="exportWord">
|
||||
@@ -685,52 +654,50 @@ onBeforeUnmount(() => {
|
||||
>
|
||||
<!-- 左:节点 / 偏离树 -->
|
||||
<div class="w-240px">
|
||||
<NSpin :show="treeLoading" size="small">
|
||||
<NButton size="small" type="primary" ghost block class="mb-2 text-xs" @click="openModal('add','node','新增节点')">
|
||||
<Icon icon="ic:round-plus" class="size-14px" />新增节点
|
||||
</NButton>
|
||||
<div class="shrink-0 rounded-md border p-2 scroll">
|
||||
<div v-for="(node, nodeIndex) in nodes" :key="nodeIndex">
|
||||
<div
|
||||
class="cursor-pointer flex items-center select-none"
|
||||
@click="leftToggle(node.id)"
|
||||
@contextmenu.prevent="handleContextMenu($event, 'node', node)"
|
||||
>
|
||||
<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="(obj, objIndex) in node.objects" :key="objIndex" class="mb-1 ml-3">
|
||||
<div
|
||||
class="cursor-pointer flex items-center select-none"
|
||||
@click="leftToggle(node.id+'_'+obj.id)"
|
||||
@contextmenu.prevent="handleContextMenu($event, 'obj', node, obj)"
|
||||
>
|
||||
<Icon v-if="collapsed[node.id+'_'+obj.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">{{ obj.obj_name }}</p>
|
||||
</div>
|
||||
<template v-if="!collapsed[node.id+'_'+obj.id]">
|
||||
<div v-for="(dev, devIndex) in obj.deviations" :key="devIndex"
|
||||
@click="currentDev = dev"
|
||||
@contextmenu.prevent="handleContextMenu($event, 'dev', node, obj, dev)"
|
||||
class="cursor-pointer flex w-full items-center ml-2 gap-1.5 rounded-md px-3 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>
|
||||
<Icon v-if="dev.analyzing" icon="ri:loader-4-fill" class="size-14px animate-spin text-blue-500" />
|
||||
<span v-else-if="dev.rows.length" class="rounded-full bg-emerald-100 px-1.5 text-[10px] text-emerald-600">
|
||||
{{ dev.rows.length }}条
|
||||
</span>
|
||||
<span v-else class="rounded-full bg-slate-100 px-1.5 text-[10px] text-slate-400">待分析</span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<NButton size="small" type="primary" ghost block class="mb-2 text-xs" @click="openModal('add','node','新增节点')">
|
||||
<Icon icon="ic:round-plus" class="size-14px" />新增节点
|
||||
</NButton>
|
||||
<div class="shrink-0 rounded-md border p-2 scroll">
|
||||
<div v-for="(node, nodeIndex) in nodes" :key="nodeIndex">
|
||||
<div
|
||||
class="cursor-pointer flex items-center select-none"
|
||||
@click="leftToggle(node.id)"
|
||||
@contextmenu.prevent="handleContextMenu($event, 'node', node, null, null, Number(nodeIndex), -1, -1)"
|
||||
>
|
||||
<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="(obj, objIndex) in node.objects" :key="objIndex" class="mb-1 ml-3">
|
||||
<div
|
||||
class="cursor-pointer flex items-center select-none"
|
||||
@click="leftToggle(node.id+'_'+obj.id)"
|
||||
@contextmenu.prevent="handleContextMenu($event, 'obj', node, obj, null, Number(nodeIndex), Number(objIndex), -1)"
|
||||
>
|
||||
<Icon v-if="collapsed[node.id+'_'+obj.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">{{ obj.obj_name }}</p>
|
||||
</div>
|
||||
<template v-if="!collapsed[node.id+'_'+obj.id]">
|
||||
<div v-for="(dev, devIndex) in obj.deviations" :key="devIndex"
|
||||
@click="currentDev = dev"
|
||||
@contextmenu.prevent="handleContextMenu($event, 'dev', node, obj, dev, Number(nodeIndex), Number(objIndex), Number(devIndex))"
|
||||
class="cursor-pointer flex w-full items-center ml-2 gap-1.5 rounded-md px-3 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>
|
||||
<Icon v-if="dev.analyzing" icon="ri:loader-4-fill" class="size-14px animate-spin text-blue-500" />
|
||||
<span v-else-if="dev.rows.length" class="rounded-full bg-emerald-100 px-1.5 text-[10px] text-emerald-600">
|
||||
{{ dev.rows.length }}条
|
||||
</span>
|
||||
<span v-else class="rounded-full bg-slate-100 px-1.5 text-[10px] text-slate-400">待分析</span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</NSpin>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 右:HAZOP 记录表 -->
|
||||
<div class="min-w-0 flex-1 bg-white">
|
||||
@@ -741,7 +708,7 @@ onBeforeUnmount(() => {
|
||||
<span class="text-[11px] text-slate-400">单元格点击即可修改;风险等级按风险矩阵L / S 取值实时重算;「入表」将本条选入风险分析记录表</span>
|
||||
</div>
|
||||
<div class="scroll">
|
||||
<table class="w-full min-w-[860px] border-collapse text-xs">
|
||||
<table class="w-full border-collapse text-xs">
|
||||
<thead>
|
||||
<tr>
|
||||
<th :class="TH" class="w-16">操作</th>
|
||||
@@ -765,13 +732,13 @@ onBeforeUnmount(() => {
|
||||
<td :class="TD">
|
||||
<div class="flex items-center justify-between px-1 mt-1">
|
||||
<NButton text type="primary" class="mt-1px text-[11px] hover:underline" title="选入风险分析记录表"
|
||||
@click="enterToTable('single',row)">入表
|
||||
@click="enterToTable('single', Number(ri))">入表
|
||||
</NButton>
|
||||
<NPopconfirm
|
||||
positive-text="确定"
|
||||
:positiveButtonProps="{ size: 'tiny' }"
|
||||
:negativeButtonProps="{ size: 'tiny' }"
|
||||
@positive-click="delRow(Number(ri), row.id)"
|
||||
@positive-click="delRow(Number(ri))"
|
||||
>
|
||||
<template #trigger>
|
||||
<NButton text type="error" title="删除行">
|
||||
@@ -783,16 +750,16 @@ onBeforeUnmount(() => {
|
||||
</div>
|
||||
</td>
|
||||
<td :class="TD">
|
||||
<NInput type="textarea" size="small" v-model:value="row.cause" :autosize="{minRows:1}" class="text-xs" @blur="editRow(row)" />
|
||||
<NInput type="textarea" size="small" v-model:value="row.cause" :autosize="{minRows:1}" class="text-xs" @blur="save()" />
|
||||
</td>
|
||||
<td :class="TD">
|
||||
<NInput type="textarea" size="small" v-model:value="row.consequence" :autosize="{minRows:1}" class="text-xs" @blur="editRow(row)" />
|
||||
<NInput type="textarea" size="small" v-model:value="row.consequence" :autosize="{minRows:1}" class="text-xs" @blur="save()" />
|
||||
</td>
|
||||
<td :class="TD" class="text-center">
|
||||
<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)" />
|
||||
<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="save()" />
|
||||
</td>
|
||||
<td :class="TD" class="text-center">
|
||||
<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)" />
|
||||
<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="save()" />
|
||||
</td>
|
||||
<td :class="TD" class="text-center">
|
||||
<NTag
|
||||
@@ -811,15 +778,15 @@ onBeforeUnmount(() => {
|
||||
<td :class="TD">
|
||||
<div v-if="row.safeguards && row.safeguards.length" class="flex flex-col gap-2 mb-1">
|
||||
<div v-for="(sItem, si) in row.safeguards" :key="si" class="border rounded-md relative">
|
||||
<NInput type="textarea" size="small" :bordered="false" v-model:value="sItem.name" :autosize="{minRows:1}" class="text-xs" @blur="editRow(row)" />
|
||||
<NInput type="textarea" size="small" :bordered="false" v-model:value="sItem.name" :autosize="{minRows:1}" class="text-xs" @blur="save()" />
|
||||
<div class="flex justify-end px-2 pb-1">
|
||||
<NSelect v-model:value="sItem.value" size="tiny" class="!w-70px" :options="selectOptions" placeholder="" @blur="editRow(row)"></NSelect>
|
||||
<NSelect v-model:value="sItem.value" size="tiny" class="!w-70px" :options="selectOptions" placeholder="" @blur="save()"></NSelect>
|
||||
</div>
|
||||
<NPopconfirm
|
||||
positive-text="确定"
|
||||
:positiveButtonProps="{ size: 'tiny' }"
|
||||
:negativeButtonProps="{ size: 'tiny' }"
|
||||
@positive-click="delSafeguard(Number(ri), Number(si),row)"
|
||||
@positive-click="delSafeguard(Number(ri), Number(si))"
|
||||
>
|
||||
<template #trigger>
|
||||
<NButton text type="error" class="absolute top-[-5px] right-[-5px] bg-white">
|
||||
@@ -836,16 +803,16 @@ onBeforeUnmount(() => {
|
||||
</td>
|
||||
<td :class="TD">
|
||||
<div v-if="row.suggestions && row.suggestions.length" class="flex flex-col gap-2 mb-1">
|
||||
<div v-for="(sItem, si) in row.suggestions" :key="si" class="border rounded-md relative">
|
||||
<NInput type="textarea" size="small" :bordered="false" v-model:value="sItem.name" :autosize="{minRows:1}" class="text-xs" @blur="editRow(row)" />
|
||||
<div v-for="(sItem2, si2) in row.suggestions" :key="si2" class="border rounded-md relative">
|
||||
<NInput type="textarea" size="small" :bordered="false" v-model:value="sItem2.name" :autosize="{minRows:1}" class="text-xs" @blur="save()" />
|
||||
<div class="flex justify-end px-2 pb-1">
|
||||
<NSelect v-model:value="sItem.value" size="tiny" class="!w-70px" :options="selectOptions" placeholder="" @blur="editRow(row)"></NSelect>
|
||||
<NSelect v-model:value="sItem2.value" size="tiny" class="!w-70px" :options="selectOptions" placeholder="" @blur="save()"></NSelect>
|
||||
</div>
|
||||
<NPopconfirm
|
||||
positive-text="确定"
|
||||
:positiveButtonProps="{ size: 'tiny' }"
|
||||
:negativeButtonProps="{ size: 'tiny' }"
|
||||
@positive-click="delSuggestion(Number(ri), Number(si),row)"
|
||||
@positive-click="delSuggestion(Number(ri), Number(si2))"
|
||||
>
|
||||
<template #trigger>
|
||||
<NButton text type="error" class="absolute top-[-5px] right-[-5px] bg-white">
|
||||
@@ -910,26 +877,7 @@ onBeforeUnmount(() => {
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-[10px]">
|
||||
<NButton size="small" @click="editModal = false">取消</NButton>
|
||||
<NButton size="small" type="primary" :loading="modalSaving" @click="modalSave">确定</NButton>
|
||||
</div>
|
||||
</template>
|
||||
</NModal>
|
||||
<!-- 删除弹窗 -->
|
||||
<NModal
|
||||
v-model:show="delModal"
|
||||
preset="card"
|
||||
title="删除"
|
||||
:auto-focus="false"
|
||||
:style="{ width: '450px', height: 'auto' }"
|
||||
:segmented="{ content: true, footer: true }"
|
||||
>
|
||||
<p v-if="currentModalType === 'node'">节点:{{contextTarget?.node?.node_name}},确定删除吗?</p>
|
||||
<p v-else-if="currentModalType === 'obj'">分析对象:{{contextTarget?.obj?.obj_name}},确定删除吗?</p>
|
||||
<p v-else-if="currentModalType === 'dev'">偏离:{{contextTarget?.dev?.deviation}},确定删除吗?</p>
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-[10px]">
|
||||
<NButton size="small" @click="delModal = false">取消</NButton>
|
||||
<NButton size="small" type="primary" :loading="delLoading" @click="delConfirm">确定</NButton>
|
||||
<NButton size="small" type="primary" @click="modalSave">确定</NButton>
|
||||
</div>
|
||||
</template>
|
||||
</NModal>
|
||||
|
||||
Reference in New Issue
Block a user