hazop插件和jsa插件修改完成
This commit is contained in:
+15
-103
@@ -9,115 +9,27 @@ export function hazopNodesApi(id: number) {
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
|
||||
// 新增HAZOP节点
|
||||
export function hazopNodeAddApi(id: number, data: { node_name: string }) {
|
||||
// HAZOP保存
|
||||
export function hazopSaveApi(change_id: number, data: any) {
|
||||
return request({
|
||||
url: `/api/changes/${id}/hazop/nodes`,
|
||||
method: 'post',
|
||||
data,
|
||||
});
|
||||
}
|
||||
// 编辑HAZOP节点
|
||||
export function hazopNodeEditApi(node_id: number, data: { node_name: string }) {
|
||||
return request({
|
||||
url: `/api/hazop/nodes/${node_id}`,
|
||||
method: 'put',
|
||||
data,
|
||||
});
|
||||
}
|
||||
// 删除HAZOP节点
|
||||
export function hazopNodeDelApi(node_id: number) {
|
||||
return request({
|
||||
url: `/api/hazop/nodes/${node_id}`,
|
||||
method: 'delete',
|
||||
});
|
||||
}
|
||||
// 新增分析对象
|
||||
export function hazopObjAddApi(node_id: number, data: { obj_name: string }) {
|
||||
return request({
|
||||
url: `/api/hazop/nodes/${node_id}/objects`,
|
||||
method: 'post',
|
||||
data,
|
||||
});
|
||||
}
|
||||
// 编辑分析对象
|
||||
export function hazopObjEditApi(obj_id: number, data: { obj_name: string }) {
|
||||
return request({
|
||||
url: `/api/hazop/objects/${obj_id}`,
|
||||
method: 'put',
|
||||
data,
|
||||
});
|
||||
}
|
||||
// 删除分析对象
|
||||
export function hazopObjDelApi(obj_id: number) {
|
||||
return request({
|
||||
url: `/api/hazop/objects/${obj_id}`,
|
||||
method: 'delete',
|
||||
});
|
||||
}
|
||||
// 新增偏离
|
||||
export function hazopDevAddApi(obj_id: number, data: { deviation: string }) {
|
||||
return request({
|
||||
url: `/api/hazop/objects/${obj_id}/deviations`,
|
||||
method: 'post',
|
||||
data,
|
||||
});
|
||||
}
|
||||
// 编辑偏离
|
||||
export function hazopDevEditApi(dev_id: number, data: { deviation: string }) {
|
||||
return request({
|
||||
url: `/api/hazop/deviations/${dev_id}`,
|
||||
method: 'put',
|
||||
data,
|
||||
});
|
||||
}
|
||||
// 删除偏离
|
||||
export function hazopDevDelApi(dev_id: number) {
|
||||
return request({
|
||||
url: `/api/hazop/deviations/${dev_id}`,
|
||||
method: 'delete',
|
||||
});
|
||||
}
|
||||
|
||||
// 新增偏离分析行
|
||||
export function hazopRowAddApi(dev_id: number, data: {
|
||||
cause: string,
|
||||
consequence: string,
|
||||
l_value: number,
|
||||
s_value: number,
|
||||
risk_value:string,
|
||||
safeguards:[],
|
||||
suggestions:[]
|
||||
}) {
|
||||
return request({
|
||||
url: `/api/hazop/deviations/${dev_id}/rows`,
|
||||
method: 'post',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
// 编辑偏离分析行
|
||||
export function hazopRowEditApi(row_id: number, data: {
|
||||
cause: string,
|
||||
consequence: string,
|
||||
l_value: number,
|
||||
s_value: number,
|
||||
risk_value:string,
|
||||
safeguards:[],
|
||||
suggestions:[]
|
||||
}) {
|
||||
return request({
|
||||
url: `/api/hazop/rows/${row_id}`,
|
||||
url: `/api/changes/${change_id}/hazop`,
|
||||
method: 'put',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
// 删除偏离分析行
|
||||
export function hazopRowDelApi(row_id: number) {
|
||||
// 获取JSA详情
|
||||
export function jsaDetailApi(change_id: number) {
|
||||
return request({
|
||||
url: `/api/hazop/rows/${row_id}`,
|
||||
method: 'delete',
|
||||
url: `/api/changes/${change_id}/jsa`,
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
// JSA保存
|
||||
export function jsaSaveApi(change_id: number, data: any) {
|
||||
return request({
|
||||
url: `/api/changes/${change_id}/jsa`,
|
||||
method: 'put',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ export const themeSettings: App.Theme.ThemeSetting = {
|
||||
colourWeakness: false,
|
||||
recommendColor: false,
|
||||
themeColor: '#2080f0',
|
||||
themeRadius: 6,
|
||||
themeRadius: 4,
|
||||
otherColor: {
|
||||
info: '#2080f0',
|
||||
success: '#52c41a',
|
||||
|
||||
@@ -176,19 +176,20 @@ const getOrgList = async () => {
|
||||
const getAllFormData = () => {
|
||||
let changePreForm = changePreRef.value?.form;
|
||||
let changeApplyForm = changeApplyRef.value?.form;
|
||||
let changeRiskForm = riskAnalysisRef.value?.form;
|
||||
let changeRiskForm = riskAnalysisRef.value?.riskRecords;
|
||||
let changeTrainingForm = changeTrainingRef.value?.form;
|
||||
let pssrForm = pssrRef.value?.list;
|
||||
let evaluationForm = evaluationRef.value?.list;
|
||||
// let closeConfirmForm = closeConfirmRef.value?.form;
|
||||
return {
|
||||
title: title.value,
|
||||
...changePreForm,
|
||||
...changeApplyForm,
|
||||
...changeRiskForm,
|
||||
...changeTrainingForm,
|
||||
...pssrForm,
|
||||
...evaluationForm,
|
||||
precheck:{...changePreForm},
|
||||
apply_form:{...changeApplyForm},
|
||||
approval_flow: {},
|
||||
risk_records:changeRiskForm,
|
||||
train:{...changeTrainingForm},
|
||||
pssr:{items:pssrForm},
|
||||
accept_form:{...evaluationForm},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,303 +1,612 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from "vue";
|
||||
import { useMessage } from "naive-ui";
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { Icon } from '@iconify/vue'
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import { AiError, aiJsaAnalyze } from '@/service/api/ai';
|
||||
import Ed from "./Ed.vue";
|
||||
import { peopleListApi } from '@/service/api/system';
|
||||
import { jsaDetailApi, jsaSaveApi } from '@/service/api/plugin';
|
||||
|
||||
const themeStore = useThemeStore();
|
||||
const message = useMessage();
|
||||
const emit = defineEmits<{
|
||||
back: [];
|
||||
done: [records: number];
|
||||
export: [items: any[], mode: "manual" | "ai"];
|
||||
save: [];
|
||||
}>();
|
||||
|
||||
const props = defineProps(['isAiOpen','currentId','jsaRiskMatrixConfig'])
|
||||
const emit = defineEmits(['close','toTable']);
|
||||
// 表格样式
|
||||
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 HAZOP_INIT = [
|
||||
{ name: "节点1:缩合反应釜 R-101", obj: "R-101 缩合反应釜", devs: [
|
||||
{ dev: "温度过高", rows: [
|
||||
{ cause: "温控回路失效或冷却水中断;90℃ 新工艺窗口更接近溶剂回流温度,超温溜温概率上升", cons: "反应溜温冲料,釜压骤升,物料喷溅造成灼烫与环境污染", l: 3, s: 4, safe: "DCS 温度高报 95℃、高高报及联锁 100℃(随变更上调并重新整定验证);紧急冷却有效", sug: "投用前完成联锁测试并留存记录;投用首周每班复核温度趋势" },
|
||||
{ cause: "未知杂质含量升高(0.05%→0.12%),热稳定性尚未定性,长期累积加速釜壁结垢", cons: "副反应加剧,可能引发二次分解放热,搅拌负荷异常", l: 2, s: 4, safe: "小试热稳定性筛查;安全阀泄放能力不受本次调整影响,仍然有效", sug: "将杂质含量纳入日常分析计划(每周 1 次,连续 8 周)" },
|
||||
] },
|
||||
{ dev: "温度过低", rows: [
|
||||
{ cause: "保温时间缩短至 4h,反应终点判断窗口变窄,人为误判", cons: "转化率不足 98%,批次报废或返工,增加处置风险", l: 3, s: 2, safe: "DCS 批次记录与转化率化验联动判定", sug: "明确反应终点取样判定规程,首 5 批加密化验" },
|
||||
] },
|
||||
{ dev: "搅拌失效", rows: [] },
|
||||
{ dev: "冷却水中断", rows: [] },
|
||||
] },
|
||||
{ name: "节点2:DCS 温控与联锁", obj: "DCS 温控回路及联锁", devs: [
|
||||
{ dev: "报警 / 联锁值设置不当", rows: [
|
||||
{ cause: "报警 / 联锁值上调后(高报 85→95℃、联锁 90→100℃),原有报警裕度改变", cons: "误报、漏报导致操作人员对真实超温失去预警", l: 2, s: 4, safe: "参数修改执行申请-复核双人确认;修改清单与变更单逐项核对", sug: "联锁逻辑图与台账同步更新,纳入变更资料归档" },
|
||||
] },
|
||||
{ dev: "联锁拒动 / 误动", rows: [] },
|
||||
] },
|
||||
];
|
||||
const HAZOP_AI:any = {
|
||||
"搅拌失效": [
|
||||
{ cause: "搅拌器机械故障或变频器跳闸,釜内物料传热不均", cons: "局部过热引发暴沸冲料;温度测量失真误导操作", l: 2, s: 4, safe: "搅拌电流 DCS 在线监视与低电流报警;跳闸联锁停进料", sug: "变更后复核搅拌电流裕量,纳入交接班巡检" },
|
||||
],
|
||||
"冷却水中断": [
|
||||
{ cause: "冷却水总管压力波动或阀门误关", cons: "撤热失效,反应温度快速上升溜温", l: 2, s: 5, safe: "冷却水流量低报警;紧急切断进料联锁", sug: "核实紧急冷却旁路可用性,写入超温应急处置卡" },
|
||||
],
|
||||
"联锁拒动 / 误动": [
|
||||
{ cause: "联锁设定值修改后未重新整定验证,或逻辑修改错误", cons: "超温时联锁拒动失去最后保护层;误动造成非计划停车", l: 1, s: 5, safe: "投用前 100% 联锁传动测试,动作正确率确认", sug: "联锁测试记录归档至变更资料;修改执行双人复核" },
|
||||
],
|
||||
};
|
||||
const inherentOf = (l: number, s: number): "高" | "中" | "低" => {
|
||||
const v = l * s;
|
||||
return v >= 10 ? "高" : v >= 5 ? "中" : "低";
|
||||
}
|
||||
const lowerOf = (lv: "高" | "中" | "低"): "高" | "中" | "低" => {
|
||||
return lv === "高" ? "中" : "低";
|
||||
}
|
||||
|
||||
const nodes = ref<any>(JSON.parse(JSON.stringify(HAZOP_INIT)));
|
||||
const sel = ref({ n: 0, d: 0 });
|
||||
const analyzing = ref(false);
|
||||
// 加载状态
|
||||
const loading = ref(false);
|
||||
const taskId = ref<number>(0);
|
||||
// 作业名称
|
||||
const workName = ref<string>("");
|
||||
// 作业信息
|
||||
const workInfo = ref<string>("");
|
||||
// 危害因素总数
|
||||
const recTotal = computed(() => steps.value.reduce((a:number, s:any) => a + s.risks.length, 0));
|
||||
// 作业步骤
|
||||
const steps = ref<{id:number,step_name:string,step_desc:string,risks:any[]}[]>([]);
|
||||
|
||||
const recTotal = computed(() => steps.value.reduce((a, s) => a + s.rows.length, 0));
|
||||
|
||||
const updRow = (si: number, ri: number, patch: Partial<any>) => {
|
||||
const row = steps.value[si]?.rows[ri];
|
||||
if (row) Object.assign(row, patch);
|
||||
};
|
||||
const clampLS = (v: string) => Math.max(1, Math.min(5, +v || 1));
|
||||
|
||||
const runAi = async () => {
|
||||
analyzing.value = true;
|
||||
try {
|
||||
const hasRows = steps.value.some((s) => s.rows.length > 0);
|
||||
const res = await aiJsaAnalyze({
|
||||
job_info: { job_name: "变更实施作业", job_location: "一车间 R-101 区域" },
|
||||
steps: steps.value.map((s) => ({ step: s.step, desc: s.desc })),
|
||||
mode: hasRows ? "supplement" : "full",
|
||||
change_context: { content: info.value },
|
||||
});
|
||||
let added = 0;
|
||||
(res?.steps ?? []).forEach((st) => {
|
||||
const rows = (st.rows ?? []).map((r) => ({
|
||||
hazard: r.hazard ?? "", cons: r.consequence ?? "", l: r.l ?? 2, s: r.s ?? 3,
|
||||
safe: r.safeguards ?? "", sug: r.suggestions ?? "",
|
||||
}));
|
||||
if (!rows.length) return;
|
||||
// 按步骤名匹配合并,匹配不到则追加为新步骤
|
||||
const target = steps.value.find((s) => st.step && (s.step.includes(st.step) || st.step.includes(s.step)));
|
||||
if (target) target.rows.push(...rows);
|
||||
else steps.value.push({ step: st.step || `步骤${steps.value.length + 1}`, desc: st.desc ?? "", rows });
|
||||
added += rows.length;
|
||||
});
|
||||
if (!added) throw new AiError(0, "AI 未返回分析结果");
|
||||
message.success(`AI 危害因素分析完成:补充 ${added} 条记录(支持手动修改)`);
|
||||
} catch (e: any) {
|
||||
const msg = e instanceof AiError ? e.message : "AI 服务异常";
|
||||
window.$message?.warning(`AI 分析失败:${msg},可手动填写`);
|
||||
} finally {
|
||||
analyzing.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const addRow = () => steps.value[steps.value.length - 1].rows.push({ hazard: "", cons: "", l: 2, s: 3, safe: "", sug: "" });
|
||||
const delRow = (si: number, ri: number) => steps.value[si].rows.splice(ri, 1);
|
||||
const exportAll = () => emit("export", steps.value.flatMap((s) => s.rows.map((row) => toRec(s.step, row))), "ai");
|
||||
const save = () => {
|
||||
message.success("已手动保存,分析记录数已同步至工具卡片(内容修改实时自动保存,退出不丢失)");
|
||||
};
|
||||
|
||||
const ANALYSIS_POOL = [
|
||||
{ name: "张工艺", org: "一车间", post: "工艺工程师" },
|
||||
{ name: "王仪表", org: "技术部", post: "仪表工程师" },
|
||||
{ name: "周设备", org: "设备科", post: "设备工程师" },
|
||||
{ name: "刘文静", org: "技术部", post: "工艺员" },
|
||||
{ name: "王海燕", org: "安全环保部", post: "安全工程师" },
|
||||
{ name: "李主任", org: "一车间", post: "车间主任" },
|
||||
{ name: "吴海涛", org: "设备科", post: "维修班长" },
|
||||
{ name: "陈国栋", org: "一车间", post: "班长" },
|
||||
];
|
||||
const open = ref(false);
|
||||
const q = ref("");
|
||||
const sels = ref<string[]>(["张工艺", "王仪表"]);
|
||||
const list = computed(() => ANALYSIS_POOL.filter((x) => !q.value || x.name.includes(q.value)));
|
||||
const toggle = (n: string) => {
|
||||
sels.value = sels.value.includes(n) ? sels.value.filter((x) => x !== n) : [...sels.value, n];
|
||||
};
|
||||
const rrOf = (l: number, s: number) => {
|
||||
const v = l * s;
|
||||
if (v >= 15) return { t: `重大 (${v})`, c: "bg-red-500 text-white" };
|
||||
if (v >= 10) return { t: `较大 (${v})`, c: "bg-[#F5A623] text-white" };
|
||||
if (v >= 5) return { t: `一般 (${v})`, c: "bg-amber-300 text-amber-900" };
|
||||
return { t: `低 (${v})`, c: "bg-emerald-500 text-white" };
|
||||
}
|
||||
|
||||
const steps = ref([
|
||||
{ step: "步骤1:作业准备与票证办理", desc: "作业方案交底、作业许可办理、能量状态确认", rows: [
|
||||
{ hazard: "高温介质 / 蒸汽意外释放", cons: "隔离确认不到位导致人员灼烫", l: 2, s: 4, safe: "作业前确认釜体温度 ≤40℃、管线泄压排空;办理作业票证并现场交底", sug: "隔离点挂牌上锁(LOTO),作业前双人确认" },
|
||||
] },
|
||||
{ step: "步骤2:旧保温拆除与新保温安装", desc: "脚手架登高作业,保温棉拆除与更换", rows: [
|
||||
{ hazard: "登高坠落", cons: "脚手架作业人员坠落伤害", l: 3, s: 3, safe: "脚手架验收合格挂牌;全程系挂双钩安全带", sug: "下方设警戒区,禁止交叉作业" },
|
||||
{ hazard: "保温纤维粉尘吸入与皮肤刺激", cons: "硅酸铝纤维引起呼吸道与皮肤职业健康危害", l: 3, s: 2, safe: "佩戴防尘口罩、护目镜与防护服;湿法拆除降尘", sug: "作业后更衣清洗,废料袋装化处置" },
|
||||
] },
|
||||
{ step: "步骤3:DCS 参数修改与联锁测试", desc: "报警 / 联锁值上调,联锁传动试验", rows: [
|
||||
{ hazard: "参数误改导致报警 / 联锁失效", cons: "运行装置失去超温保护,可能导致超温失控", l: 2, s: 5, safe: "参数修改执行申请-复核双人确认;修改清单与变更单逐项核对", sug: "联锁测试全覆盖并留存记录;修改期间工艺加强监控" },
|
||||
] },
|
||||
{ step: "步骤4:升温投用与验证", desc: "按新版操作规程升温至 90℃ 投用", rows: [
|
||||
{ hazard: "升温过快导致冲料", cons: "釜压骤升,法兰泄漏或安全阀起跳", l: 2, s: 4, safe: "按 5℃/10min 梯度升温;首批投用工艺、仪表、设备三方现场值守", sug: "首 3 批加密温度 / 压力巡检,异常立即降回 80℃ 运行" },
|
||||
] },
|
||||
]);
|
||||
|
||||
const info = ref("R-101 缩合反应釜反应温度由 80℃ 提升至 90℃、保温时间由 8h 缩短至 4h。实施作业包括:釜体保温更换(硅酸铝纤维毡)、DCS 温度报警 / 联锁值上调(高报 85→95℃、联锁 90→100℃)与联锁测试、操作规程修订与培训。作业期间装置保持运行,涉及登高、保温纤维、DCS 参数修改等作业内容。");
|
||||
const toRec = (step: string, row: any): any => ({
|
||||
item: row.hazard || "(未命名危害因素)",
|
||||
scene: `${step}:${row.hazard},可能导致:${row.cons}`,
|
||||
inherent: inherentOf(row.l, row.s),
|
||||
existing: row.safe,
|
||||
suggest: row.sug,
|
||||
residual: lowerOf(inherentOf(row.l, row.s)),
|
||||
source: "JSA",
|
||||
const selectOptions = computed(() => {
|
||||
const l = props.jsaRiskMatrixConfig?.probability?.map((item:{key:string,label:string}, index:number) => ({
|
||||
label: 'L'+'_'+Number(index + 1),
|
||||
value: 'L'+'_'+Number(index + 1)
|
||||
}));
|
||||
const s = props.jsaRiskMatrixConfig?.consequence?.map((item:{key:string,label:string}, index:number) => ({
|
||||
label: 'S'+'_'+Number(index + 1),
|
||||
value: 'S'+'_'+Number(index + 1)
|
||||
}));
|
||||
return [...l, ...s];
|
||||
});
|
||||
const l_options = computed(() => {
|
||||
const newArr = props.jsaRiskMatrixConfig?.probability?.map((item:{key:string,label:string}, index:number) => ({
|
||||
...item,
|
||||
key: Number(index + 1)
|
||||
}));
|
||||
return newArr;
|
||||
});
|
||||
const s_options = computed(() => {
|
||||
const newArr = props.jsaRiskMatrixConfig?.consequence?.map((item:{key:string,label:string}, index:number) => ({
|
||||
...item,
|
||||
key: Number(index + 1)
|
||||
}));
|
||||
return newArr;
|
||||
});
|
||||
|
||||
// 人员列表源数据
|
||||
const workers = ref<{user_id:number,user_name:string,org_name?:string}[]>([]);
|
||||
// 选中的人员
|
||||
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:{user_id:number,user_name:string,org_name?:string}) => !searchWorker.value || x.user_name.includes(searchWorker.value)));
|
||||
// 选择分析人员
|
||||
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);
|
||||
};
|
||||
|
||||
// 运行 AI 分析危害原因
|
||||
const runAi = async () => {
|
||||
if(workInfo.value?.trim()===''){
|
||||
return window.$message?.warning("请先输入作业信息");
|
||||
}
|
||||
};
|
||||
// 运行 AI 识别作业步骤
|
||||
const runAiStep = async () => {
|
||||
if(workInfo.value?.trim()===''){
|
||||
return window.$message?.warning("请先输入作业信息");
|
||||
}
|
||||
}
|
||||
// 生成报告
|
||||
const generateReport = async () => {
|
||||
window.$message?.warning("还差接口");
|
||||
}
|
||||
|
||||
// 保存
|
||||
const save = async () => {
|
||||
if(workName.value?.trim()===''){
|
||||
return window.$message?.warning("作业名称不能为空");
|
||||
}
|
||||
if(workInfo.value?.trim()===''){
|
||||
return window.$message?.warning("作业信息不能为空");
|
||||
}
|
||||
loading.value = true;
|
||||
// 处理选中的人员,移除 org_name 字段
|
||||
const workers = (selectedWorkers.value ?? []).map((item: { org_name?:string; user_id: number; user_name: string }) => {
|
||||
const { org_name, ...rest } = item;
|
||||
return rest;
|
||||
});
|
||||
// 处理作业步骤,计算风险等级
|
||||
steps.value.forEach((step:any) => {
|
||||
step.risks.forEach((risk:any) => {
|
||||
const result = riskLevel(risk.l_value, risk.s_value);
|
||||
risk.risk_value = result ? result.name : '';
|
||||
});
|
||||
});
|
||||
const {error} = await jsaSaveApi(props.currentId, {
|
||||
task: {
|
||||
id: taskId.value,
|
||||
job_name: workName.value,
|
||||
job_info: workInfo.value,
|
||||
steps: steps.value,
|
||||
},
|
||||
users: workers,
|
||||
});
|
||||
if(!error){
|
||||
window.$message?.success("保存成功");
|
||||
}
|
||||
loading.value = false;
|
||||
};
|
||||
|
||||
// AI整理如表、入表
|
||||
const enterToTable = (type: string, row: any, stepIdx: number) => {
|
||||
if(type==='single'){
|
||||
const step = steps.value[stepIdx];
|
||||
const enrichedRow:any[] = [];
|
||||
enrichedRow.push({
|
||||
...row,
|
||||
scene: `${row.cause?row.cause+';':''}${row.consequence?'可能导致:'+row.consequence:''}`,
|
||||
source: 'JSA',
|
||||
risk_item: `${step.step_name?step.step_name:''}`,
|
||||
safe: row?.safeguards?.map((item:any) => item.name).join(';') || '',
|
||||
suggestion: row?.suggestions?.map((item:any) => item.name).join(';') || '',
|
||||
to_pssr: 0
|
||||
});
|
||||
emit('toTable',type,enrichedRow);
|
||||
}
|
||||
if(type==='all'){
|
||||
const enrichedRows:any[] = [];
|
||||
steps.value.forEach((step:any) => {
|
||||
step?.risks?.forEach((risk:any) => {
|
||||
enrichedRows.push({
|
||||
...risk,
|
||||
scene: `${risk.cause?risk.cause+';':''}${risk.consequence?'可能导致:'+risk.consequence:''}`,
|
||||
source: 'JSA',
|
||||
risk_item: `${step.step_name}`,
|
||||
safe: risk?.safeguards?.map((item:any) => item.name).join(';') || '',
|
||||
suggestion: risk?.suggestions?.map((item:any) => item.name).join(';') || '',
|
||||
to_pssr: 0
|
||||
});
|
||||
})
|
||||
})
|
||||
emit('toTable',type,enrichedRows);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 行级拖拽 ----------
|
||||
let dragStepIdx: number | null = null
|
||||
const dragOverIdx = ref<number | string | null>(null)
|
||||
const onDragStart = (e: DragEvent, stepIdx: number) => {
|
||||
dragStepIdx = stepIdx
|
||||
if (e.dataTransfer) e.dataTransfer.effectAllowed = 'move'
|
||||
}
|
||||
const onDrop = (e: DragEvent, stepIdx: number) => {
|
||||
e.preventDefault()
|
||||
if (dragStepIdx !== null && dragStepIdx !== stepIdx) {
|
||||
// 1. 移除源步骤
|
||||
const [nodeItem] = steps.value.splice(dragStepIdx, 1)
|
||||
// 2. 【核心修正】直接使用目标索引进行插入。
|
||||
// 如果是从上往下拖拽,会自动插入到目标元素的后面(沉底/落在下面)
|
||||
// 如果是从下往上拖拽,会自动插入到目标元素的前面(冒泡/落在上面)
|
||||
steps.value.splice(stepIdx, 0, nodeItem)
|
||||
}
|
||||
onDragEnd()
|
||||
}
|
||||
const onDragEnd = () => {
|
||||
dragStepIdx = null
|
||||
dragOverIdx.value = null
|
||||
}
|
||||
|
||||
// 根据l和s计算RR风险等级
|
||||
const riskLevel = (l: number, s: number) => {
|
||||
const num = l * s;
|
||||
for (let i = 0; i < props.jsaRiskMatrixConfig?.risk_grades.length; i++) {
|
||||
const item = props.jsaRiskMatrixConfig?.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 addStep = () => {
|
||||
if(workName.value?.trim()===''){
|
||||
return window.$message?.warning("请先输入作业名称");
|
||||
}
|
||||
if(workInfo.value?.trim()===''){
|
||||
return window.$message?.warning("请先输入作业信息");
|
||||
}
|
||||
steps.value.push({
|
||||
id: 0,
|
||||
step_name: '',
|
||||
step_desc: '',
|
||||
risks: [{
|
||||
id: 0,
|
||||
cause: '',
|
||||
consequence: '',
|
||||
l_value: l_options.value[0]?.key || 1,
|
||||
s_value: s_options.value[0]?.key || 1,
|
||||
risk_value: riskLevel(l_options.value[0]?.key || 1, s_options.value[0]?.key || 1)?.name || '',
|
||||
safeguards: [],
|
||||
suggestions: []
|
||||
}]
|
||||
})
|
||||
}
|
||||
// 新增行
|
||||
const addRow = (stepIdx: number) => {
|
||||
steps.value[stepIdx]?.risks?.push({
|
||||
id: 0,
|
||||
step_id: steps.value[stepIdx]?.id || 0,
|
||||
cause: '',
|
||||
consequence: '',
|
||||
l_value: l_options.value[0]?.key || 1,
|
||||
s_value: s_options.value[0]?.key || 1,
|
||||
risk_value: riskLevel(l_options.value[0]?.key || 1, s_options.value[0]?.key || 1)?.name || '',
|
||||
safeguards: [],
|
||||
suggestions: []
|
||||
});
|
||||
}
|
||||
// 删除行
|
||||
const delRow = (stepIdx: number, rowIndex: number) => {
|
||||
steps.value[stepIdx]?.risks?.splice(rowIndex, 1);
|
||||
}
|
||||
// 清空行
|
||||
const clearRow = (stepIdx: number, rowIndex: number) => {
|
||||
const step = steps.value[stepIdx];
|
||||
if(!step) return;
|
||||
const row = steps.value[stepIdx]?.risks[rowIndex];
|
||||
if(!row) return;
|
||||
const clearedRow = {
|
||||
id: row.id,
|
||||
step_id: step?.id || 0,
|
||||
cause: '',
|
||||
consequence: '',
|
||||
l_value: l_options.value[0]?.key || 1,
|
||||
s_value: s_options.value[0]?.key || 1,
|
||||
risk_value: riskLevel(l_options.value[0]?.key || 1, s_options.value[0]?.key || 1)?.name || '',
|
||||
safeguards: [],
|
||||
suggestions: []
|
||||
};
|
||||
step.risks[rowIndex] = clearedRow;
|
||||
}
|
||||
|
||||
// 新增安全措施
|
||||
const addSafeguard = (stepIndex: number, rowIndex: number) => {
|
||||
steps.value[stepIndex]?.risks[rowIndex]?.safeguards?.push({
|
||||
name:'',
|
||||
value:''
|
||||
});
|
||||
}
|
||||
// 删除安全措施
|
||||
const delSafeguard = (stepIndex: number, rowIndex: number, safeguardIndex: number) => {
|
||||
steps.value[stepIndex]?.risks[rowIndex]?.safeguards?.splice(safeguardIndex, 1);
|
||||
}
|
||||
// 新增建议措施
|
||||
const addSuggestion = (stepIndex: number, rowIndex: number) => {
|
||||
steps.value[stepIndex]?.risks[rowIndex]?.suggestions?.push({
|
||||
name:'',
|
||||
value:''
|
||||
});
|
||||
}
|
||||
// 删除建议措施
|
||||
const delSuggestion = (stepIndex: number, rowIndex: number, suggestionIndex: number) => {
|
||||
steps.value[stepIndex]?.risks[rowIndex]?.suggestions?.splice(suggestionIndex, 1);
|
||||
}
|
||||
|
||||
// 获取人员列表
|
||||
const getWorkers = async () => {
|
||||
const {data,error} = await peopleListApi();
|
||||
if(!error){
|
||||
const transformed = data.map((item:any) => ({
|
||||
user_id: item.id,
|
||||
user_name: item.realname,
|
||||
org_name: item.user_departments.map((d:any) => d.label).join(';')
|
||||
}));
|
||||
workers.value = transformed;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取JSA详情
|
||||
const getJsaDetail = async () => {
|
||||
loading.value = true;
|
||||
const {data,error} = await jsaDetailApi(props.currentId);
|
||||
if(!error){
|
||||
steps.value = data?.task?.steps || [];
|
||||
workName.value = data?.task?.job_name || '';
|
||||
workInfo.value = data?.task?.job_info || '';
|
||||
taskId.value = data?.task?.id || 0;
|
||||
selectedWorkers.value = data?.users || [];
|
||||
}
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getWorkers();
|
||||
getJsaDetail();
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NSpin :show="loading" size="small">
|
||||
<div class="space-y-3">
|
||||
<div
|
||||
class="flex flex-wrap items-center gap-2 rounded-lg border border-[#ACC6E5] bg-[#EAF0F9] px-3 py-2"
|
||||
:style="{
|
||||
'--theme-color': themeStore.themeColor
|
||||
}"
|
||||
class="flex flex-wrap items-center gap-2 rounded-lg border border-[#ACC6E5] bg-[#EAF0F9] px-3 py-2"
|
||||
:style="{
|
||||
'--theme-color': themeStore.themeColor
|
||||
}"
|
||||
>
|
||||
<NButton size="small" ghost type="primary" class="text-xs" @click="emit('back')">
|
||||
<Icon icon="basil:arrow-left-outline" class="size-18px" />
|
||||
返回工具列表
|
||||
<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>
|
||||
<span class="text-sm font-bold text-[var(--theme-color)]">JSA 分析插件</span>
|
||||
<NTag size="small" type="info">插件</NTag>
|
||||
<span class="text-[11px] text-slate-500">{{`LS 矩阵 · ${steps.length} 个作业步骤 · ${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 }} 人
|
||||
</NButton>
|
||||
<NButton size="small" ghost type="primary" class="text-xs" @click="save">保存</NButton>
|
||||
<span class="text-sm font-bold text-[var(--theme-color)]">JSA 分析插件</span>
|
||||
<NTag size="small" type="info">插件</NTag>
|
||||
<NTag size="small">MOC 变更:缩合反应温度及时间优化(演示)</NTag>
|
||||
<span class="text-[11px] text-slate-500">{{`LS 矩阵 · ${steps.length} 个作业步骤 · ${recTotal} 条危害因素记录`}}</span>
|
||||
<div class="relative">
|
||||
<NButton size="small" ghost type="primary" class="text-[11px]" @click="open = !open">
|
||||
<Icon icon="lucide:users" class="size-12px" /> 分析组成员 · 已选 {{ sels.length }} 人
|
||||
</NButton>
|
||||
<template v-if="open">
|
||||
<div class="fixed inset-0 z-10" @click="open = false"></div>
|
||||
<div class="absolute right-0 top-8 z-20 w-72 rounded-lg border bg-white p-2 shadow-xl">
|
||||
<div class="mb-1.5 flex items-center justify-between">
|
||||
<span class="text-xs font-semibold text-slate-700">选择分析组成员(岗位人员库)</span>
|
||||
<span class="text-[11px] text-slate-400">已选 {{ sels.length }} 人</span>
|
||||
</div>
|
||||
<NInput v-model:value="q" 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 list" :key="x.name" @click="toggle(x.name)"
|
||||
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="sels.includes(x.name) ? '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="sels.includes(x.name) ? 'border-[var(--theme-color)] bg-[var(--theme-color)] text-white' : 'border-slate-300'">{{ sels.includes(x.name) ? "✓" : "" }}</span>
|
||||
<span class="font-medium text-slate-800">{{ x.name }}</span>
|
||||
<span class="text-[11px] text-slate-400">{{ x.org }} · {{ x.post }}</span>
|
||||
<NTag v-if="sels[0] === x.name" 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">
|
||||
首位选择者默认为组长;成员为选择式(非手写),参与记录自动归属到人,用于「参与风险分析次数」统计采集。
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="ml-auto flex items-center gap-1.5">
|
||||
<NButton size="small" ghost type="primary" class="text-xs" :disabled="recTotal === 0" @click="exportAll">
|
||||
<Icon icon="lucide:sparkles" class="size-12px mr-1" /> AI 整理入表
|
||||
</NButton>
|
||||
<NButton size="small" ghost type="primary" class="text-xs" @click="message.success('JSA 分析报告已生成(Word/PDF)')">
|
||||
<Icon icon="material-symbols:download" class="size-14px" /> 生成报告
|
||||
</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>
|
||||
<template v-if="openSelectWorker">
|
||||
<div class="fixed inset-0 z-10" @click="openSelectWorker = false"></div>
|
||||
<div class="absolute right-0 top-8 z-20 w-72 rounded-lg border bg-white p-2 shadow-xl">
|
||||
<div class="mb-1.5 flex items-center justify-between">
|
||||
<span class="text-xs font-semibold text-slate-700">选择分析组成员(岗位人员库)</span>
|
||||
<span class="text-[11px] text-slate-400">已选 {{ selectedWorkers.length }} 人</span>
|
||||
</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.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">
|
||||
首位选择者默认为组长;成员为选择式(非手写),参与记录自动归属到人,用于「参与风险分析次数」统计采集。
|
||||
</div>
|
||||
</div>
|
||||
</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,-1)">
|
||||
<Icon icon="lucide:sparkles" class="size-12px mr-1" /> AI 整理入表
|
||||
</NButton>
|
||||
<NButton size="small" ghost type="primary" class="text-xs" @click="generateReport">
|
||||
<Icon icon="material-symbols:download" class="size-14px" /> 生成报告
|
||||
</NButton>
|
||||
<NButton v-if="isAiOpen" size="small" type="primary" class="text-xs" :disabled="loading" @click="runAi">
|
||||
<Icon v-if="loading" icon="ri:loader-4-fill" class="size-14px animate-spin" />
|
||||
<Icon v-else icon="lucide:sparkles" class="size-12px" />
|
||||
AI 分析危害原因
|
||||
</NButton>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="space-y-3"
|
||||
:style="{
|
||||
'--theme-color': themeStore.themeColor,
|
||||
}"
|
||||
>
|
||||
<!-- 作业信息 -->
|
||||
<div class="rounded-lg border bg-white p-3">
|
||||
<div class="mb-1.5 flex items-center gap-2 text-xs font-semibold text-slate-600">
|
||||
<span class="flex h-4 w-4 items-center justify-center rounded-full bg-[var(--theme-color)] text-[10px] text-white">1</span>
|
||||
作业信息
|
||||
<span class="ml-auto text-[11px] font-normal text-slate-400">作业名称:变更实施作业(自动生成,可修改)</span>
|
||||
</div>
|
||||
<p class="rounded-lg bg-slate-50 p-2.5 text-xs leading-relaxed text-slate-600">{{ info }}</p>
|
||||
<div class="space-y-3" :style="{'--theme-color': themeStore.themeColor}">
|
||||
<!-- 作业信息 -->
|
||||
<div class="rounded-lg border bg-white p-3">
|
||||
<div class="mb-1.5 flex items-center gap-2 text-xs font-semibold text-slate-600">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="flex h-4 w-4 items-center justify-center rounded-full bg-[var(--theme-color)] text-[10px] text-white">1</span>
|
||||
作业信息
|
||||
<NButton v-if="isAiOpen" size="tiny" type="primary" class="text-xs" :disabled="loading" @click="runAiStep">
|
||||
<Icon v-if="loading" icon="ri:loader-4-fill" class="size-14px animate-spin" />
|
||||
<Icon v-else icon="lucide:sparkles" class="size-12px" />
|
||||
AI 识别作业步骤
|
||||
</NButton>
|
||||
</div>
|
||||
<div class="ml-auto text-slate-400 flex items-center">
|
||||
<p class="w-90px">作业名称:</p><NInput v-model:value="workName" size="small" placeholder="自动生成,可修改" class="text-xs" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- 作业活动分析记录 -->
|
||||
<div class="bg-white">
|
||||
<div class="flex items-center gap-2 px-3 py-2 text-xs font-semibold text-slate-600 border border-b-0 rounded-tl-lg rounded-tr-lg">
|
||||
<span class="flex h-4 w-4 items-center justify-center rounded-full bg-[var(--theme-color)] text-[10px] text-white">2</span>
|
||||
作业活动分析记录
|
||||
<NButton size="small" ghost type="primary" class="text-xs ml-2" @click="addRow">
|
||||
<Icon icon="ic:round-plus" class="size-14px" /> 新增记录(末步骤)
|
||||
</NButton>
|
||||
<span class="ml-auto text-[11px] font-normal text-slate-400">单元格点击即可修改;L / S 取值 1-5,RR 实时重算;「入表」将本条选入风险分析记录表</span>
|
||||
</div>
|
||||
<table class="w-full min-w-[900px] border-collapse text-xs">
|
||||
<thead>
|
||||
<tr>
|
||||
<th :class="TH" class="w-40">作业步骤</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-20">RR</th>
|
||||
<th :class="TH">安全措施</th>
|
||||
<th :class="TH">建议措施</th>
|
||||
<th :class="TH" class="w-16">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template v-for="(s, si) in steps" :key="si">
|
||||
<tr v-for="(row, ri) in s.rows" :key="`${si}-${ri}`">
|
||||
<td v-if="ri === 0" :class="TD" class="bg-slate-50" :rowspan="s.rows.length">
|
||||
<div class="px-1 text-xs font-semibold text-slate-700">{{ s.step }}</div>
|
||||
<div class="mt-1 px-1 text-[11px] leading-relaxed text-slate-400">{{ s.desc }}</div>
|
||||
</td>
|
||||
<td :class="TD"><Ed :v="row.hazard" @update="(x) => updRow(si, ri, { hazard: x })" /></td>
|
||||
<td :class="TD"><Ed :v="row.cons" @update="(x) => updRow(si, ri, { cons: x })" /></td>
|
||||
<td :class="TD" class="text-center">
|
||||
<input :value="row.l" @change="(e) => updRow(si, ri, { l: clampLS((e.target as HTMLInputElement).value) })"
|
||||
class="w-7 rounded border border-transparent text-center hover:border-slate-200" />
|
||||
</td>
|
||||
<td :class="TD" class="text-center">
|
||||
<input :value="row.s" @change="(e) => updRow(si, ri, { s: clampLS((e.target as HTMLInputElement).value) })"
|
||||
class="w-7 rounded border border-transparent text-center hover:border-slate-200" />
|
||||
</td>
|
||||
<td :class="TD" class="text-center">
|
||||
<span class="inline-block rounded px-1.5 py-0.5 text-[11px] font-medium" :class="rrOf(row.l, row.s).c">{{ rrOf(row.l, row.s).t }}</span>
|
||||
</td>
|
||||
<td :class="TD"><Ed :v="row.safe" @update="(x) => updRow(si, ri, { safe: x })" /></td>
|
||||
<td :class="TD"><Ed :v="row.sug" @update="(x) => updRow(si, ri, { sug: x })" /></td>
|
||||
<td :class="TD" class="text-center">
|
||||
<div class="flex items-center justify-between px-1 mt-1">
|
||||
<NButton text type="primary" class="mt-1px text-[11px] hover:underline" title="选入风险分析记录表"
|
||||
@click="emit('export', [toRec(s.step, row)], 'manual')">入表
|
||||
</NButton>
|
||||
<NButton text type="error" title="删除行" @click="delRow(si, ri)">
|
||||
<Icon icon="ep:delete" class="size-14px" />
|
||||
</NButton>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
<NInput v-model:value="workInfo" type="textarea" size="small" placeholder="请先输入作业信息,AI将据此识别作业步骤并生成作业名称" class="w-full" />
|
||||
</div>
|
||||
<!-- 作业活动分析记录 -->
|
||||
<div class="bg-white">
|
||||
<div class="flex items-center gap-2 px-3 py-2 text-xs font-semibold text-slate-600 border border-b-0 rounded-tl-lg rounded-tr-lg">
|
||||
<span class="flex h-4 w-4 items-center justify-center rounded-full bg-[var(--theme-color)] text-[10px] text-white">2</span>
|
||||
作业活动分析记录
|
||||
<NButton size="small" ghost type="primary" class="text-xs ml-2" @click="addStep">
|
||||
<Icon icon="ic:round-plus" class="size-14px" /> 新增步骤
|
||||
</NButton>
|
||||
<span class="ml-auto text-[11px] font-normal text-slate-400">风险等级按风险矩阵L / S 取值实时重算;「入表」将本条选入风险分析记录表</span>
|
||||
</div>
|
||||
<p v-if="steps.length===0" class="border py-5 text-center text-xs text-slate-400">暂无作业步骤</p>
|
||||
<div v-else class="scroll">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th :class="TH" class="w-96px">操作</th>
|
||||
<th :class="TH" class="w-170px">作业步骤</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" class="w-60">安全措施</th>
|
||||
<th :class="TH" class="w-60">建议措施</th>
|
||||
<th :class="TH" class="w-20">RR1</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template v-for="(step, stepIdx) in steps" :key="stepIdx">
|
||||
<tr v-for="(row, rowIndex) in step.risks" :key="rowIndex">
|
||||
<td :class="TD">
|
||||
<div class="flex flex-col items-center gap-2 mt-1">
|
||||
<n-button text type="primary" size="tiny" @click="enterToTable('single',row,Number(stepIdx))">
|
||||
入表
|
||||
</n-button>
|
||||
<n-button text type="primary" size="tiny" @click="addRow(Number(stepIdx))">
|
||||
新增行
|
||||
</n-button>
|
||||
<NPopconfirm
|
||||
positive-text="确定"
|
||||
:positiveButtonProps="{ size: 'tiny' }"
|
||||
:negativeButtonProps="{ size: 'tiny' }"
|
||||
@positive-click="delRow(Number(stepIdx), Number(rowIndex))"
|
||||
>
|
||||
<template #trigger>
|
||||
<n-button text type="primary" size="tiny">
|
||||
删除行
|
||||
</n-button>
|
||||
</template>
|
||||
确定删除此行吗?
|
||||
</NPopconfirm>
|
||||
<NPopconfirm
|
||||
positive-text="确定"
|
||||
:positiveButtonProps="{ size: 'tiny' }"
|
||||
:negativeButtonProps="{ size: 'tiny' }"
|
||||
@positive-click="clearRow(Number(stepIdx), Number(rowIndex))"
|
||||
>
|
||||
<template #trigger>
|
||||
<n-button text type="primary" size="tiny">
|
||||
清空行
|
||||
</n-button>
|
||||
</template>
|
||||
确定清空此行吗?
|
||||
</NPopconfirm>
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
v-if="rowIndex === 0"
|
||||
:rowspan="step.risks.length"
|
||||
draggable="true"
|
||||
@dragstart="onDragStart($event, Number(stepIdx))"
|
||||
@dragover.prevent="dragOverIdx = Number(stepIdx)"
|
||||
@dragleave="dragOverIdx === stepIdx && (dragOverIdx = null)"
|
||||
@drop="onDrop($event, Number(stepIdx))"
|
||||
@dragend="onDragEnd"
|
||||
:class="[TD,'align-top',dragOverIdx === stepIdx ? 'outline outline-2 outline-dashed outline-blue-400' : '']"
|
||||
>
|
||||
<div
|
||||
class="flex cursor-grab text-xs items-center gap-1 text-slate-400 hover:text-slate-600 active:cursor-grabbing"
|
||||
title="按住拖拽可调整行顺序"
|
||||
>
|
||||
<Icon icon="gravity-ui:grip" class="size-12px" />
|
||||
拖拽排序
|
||||
</div>
|
||||
<div class="flex items-start mt-1">
|
||||
<span class="pt-3px font-600 whitespace-nowrap text-xs">步骤{{ Number(stepIdx) + 1 }}:</span>
|
||||
<NInput v-model:value="step.step_name" type="textarea" :autosize="{minRows:1}" size="small" placeholder="步骤名称" class="font-600 text-xs" />
|
||||
</div>
|
||||
<NInput v-model:value="step.step_desc" type="textarea" :autosize="{minRows:1}" size="small" placeholder="步骤描述" class="mt-[6px] text-xs" />
|
||||
</td>
|
||||
<td :class="TD">
|
||||
<NInput type="textarea" size="small" v-model:value="row.cause" :autosize="{minRows:2}" class="text-xs" />
|
||||
</td>
|
||||
<td :class="TD">
|
||||
<NInput type="textarea" size="small" v-model:value="row.consequence" :autosize="{minRows:2}" class="text-xs" />
|
||||
</td>
|
||||
<td :class="TD">
|
||||
<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" />
|
||||
</td>
|
||||
<td :class="TD">
|
||||
<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" />
|
||||
</td>
|
||||
<td :class="TD">
|
||||
<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">
|
||||
<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" />
|
||||
<div class="flex justify-end px-2 pb-1">
|
||||
<NSelect v-model:value="sItem.value" size="tiny" class="!w-70px" :options="selectOptions" placeholder=""></NSelect>
|
||||
</div>
|
||||
<NPopconfirm
|
||||
positive-text="确定"
|
||||
:positiveButtonProps="{ size: 'tiny' }"
|
||||
:negativeButtonProps="{ size: 'tiny' }"
|
||||
@positive-click="delSafeguard(Number(stepIdx),Number(rowIndex), Number(si))"
|
||||
>
|
||||
<template #trigger>
|
||||
<NButton text type="error" class="absolute top-[-5px] right-[-5px] bg-white">
|
||||
<Icon icon="ant-design:close-circle-outlined" class="size-16px" />
|
||||
</NButton>
|
||||
</template>
|
||||
确定删除吗?
|
||||
</NPopconfirm>
|
||||
</div>
|
||||
</div>
|
||||
<NButton size="tiny" dashed type="primary" @click="addSafeguard(Number(stepIdx),Number(rowIndex))">
|
||||
<Icon icon="ic:round-plus" class="size-12px" />增加
|
||||
</NButton>
|
||||
</td>
|
||||
<td :class="TD">
|
||||
<div v-if="row.suggestions && row.suggestions.length" class="flex flex-col gap-2 mb-1">
|
||||
<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" />
|
||||
<div class="flex justify-end px-2 pb-1">
|
||||
<NSelect v-model:value="sItem2.value" size="tiny" class="!w-70px" :options="selectOptions" placeholder=""></NSelect>
|
||||
</div>
|
||||
<NPopconfirm
|
||||
positive-text="确定"
|
||||
:positiveButtonProps="{ size: 'tiny' }"
|
||||
:negativeButtonProps="{ size: 'tiny' }"
|
||||
@positive-click="delSuggestion(Number(stepIdx),Number(rowIndex), Number(si2))"
|
||||
>
|
||||
<template #trigger>
|
||||
<NButton text type="error" class="absolute top-[-5px] right-[-5px] bg-white">
|
||||
<Icon icon="ant-design:close-circle-outlined" class="size-16px" />
|
||||
</NButton>
|
||||
</template>
|
||||
确定删除吗?
|
||||
</NPopconfirm>
|
||||
</div>
|
||||
</div>
|
||||
<NButton size="tiny" dashed type="primary" @click="addSuggestion(Number(stepIdx),Number(rowIndex))">
|
||||
<Icon icon="ic:round-plus" class="size-12px" />增加
|
||||
</NButton>
|
||||
</td>
|
||||
<td :class="TD">
|
||||
<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>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</NSpin>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.scroll{
|
||||
height: calc(100vh - 285px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<!-- 风险分析记录表 -->
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { Icon } from '@iconify/vue'
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import Hazop from "./hazop.vue";
|
||||
@@ -15,12 +15,15 @@ const themeStore = useThemeStore();
|
||||
const props = defineProps(['type','isAiOpen','title','currentId','formInfo'])
|
||||
const emit = defineEmits(['save','submit','confirmTab']);
|
||||
|
||||
const form = ref<{ content: string }>({
|
||||
content: ''
|
||||
});
|
||||
const hazopRiskMatrixConfig = ref<any>(null);
|
||||
const jsaRiskMatrixConfig = ref<any>(null);
|
||||
const pluginDone = ref<Record<string, number>>({});
|
||||
const pluginDone = computed(() =>
|
||||
riskRecords.value.reduce((acc:any, item:any) => {
|
||||
const s = item.source;
|
||||
if (s) acc[s] = (acc[s] || 0) + 1;
|
||||
return acc;
|
||||
}, {} as Record<string, number>)
|
||||
);
|
||||
const tools = ref<string[]>(["HAZOP","JSA", "SCL", "RISK_CHECK"]);
|
||||
const riskRecords = ref<any>([]);
|
||||
const pluginDrawer = ref<boolean>(false);
|
||||
@@ -157,6 +160,7 @@ const exportAll = (type: string, items: any[]) => {
|
||||
|
||||
// 使用插件弹框
|
||||
const usePlugin = (key: string) => {
|
||||
console.log(riskRecords.value)
|
||||
currentPlugin.value = key;
|
||||
pluginDrawer.value = true;
|
||||
}
|
||||
@@ -193,7 +197,7 @@ const getJsaRiskMatrixConfig = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({form})
|
||||
defineExpose({riskRecords})
|
||||
|
||||
onMounted(() => {
|
||||
getHazopRiskMatrixConfig();
|
||||
@@ -232,11 +236,11 @@ onMounted(() => {
|
||||
<Icon icon="fluent:wand-24-regular" class="size-16px" />
|
||||
{{ t.name }}
|
||||
</div>
|
||||
<div class="mt-0.5 text-[10px]" :class="pluginDone[t.key] !== undefined ? 'text-white/75' : 'text-slate-400'">
|
||||
<div class="mt-0.5 text-[10px]" :class="pluginDone[t.key] !== undefined ? 'text-white' : 'text-slate-400'">
|
||||
{{ pluginDone[t.key] !== undefined ? `已使用并分析了 ${pluginDone[t.key]} 条内容` : '未使用此工具分析' }}
|
||||
</div>
|
||||
</div>
|
||||
<NButton size="small" class="text-xs" type="primary" @click="usePlugin(t.key)">
|
||||
<NButton size="small" class="text-xs !text-white" :tertiary="pluginDone[t.key] !== undefined" type="primary" @click="usePlugin(t.key)">
|
||||
使用
|
||||
</NButton>
|
||||
</div>
|
||||
@@ -388,7 +392,7 @@ onMounted(() => {
|
||||
:currentId="props.currentId"
|
||||
:title="props.title"
|
||||
:isAiOpen="props.isAiOpen"
|
||||
:currentForm="form"
|
||||
:currentForm="formInfo"
|
||||
@save="saveDraft"
|
||||
@submit="submitConfirm"
|
||||
@confirmTab="confirmTab"
|
||||
@@ -400,7 +404,7 @@ onMounted(() => {
|
||||
<Hazop :isAiOpen="props.isAiOpen" :currentId="props.currentId" :hazopRiskMatrixConfig="hazopRiskMatrixConfig" @close="closePlugin" @toTable="exportAll" />
|
||||
</template>
|
||||
<template v-else-if="currentPlugin === 'JSA'">
|
||||
<Jsa @close="closePlugin" @toTable="exportAll" />
|
||||
<Jsa :isAiOpen="props.isAiOpen" :currentId="props.currentId" :jsaRiskMatrixConfig="jsaRiskMatrixConfig" @close="closePlugin" @toTable="exportAll" />
|
||||
</template>
|
||||
<template v-else-if="currentPlugin === 'SCL'">
|
||||
<Scl @close="closePlugin" @toTable="exportAll" />
|
||||
|
||||
Reference in New Issue
Block a user