hazop插件完成
This commit is contained in:
@@ -87,8 +87,8 @@ export function hazopRowAddApi(dev_id: number, data: {
|
|||||||
l_value: number,
|
l_value: number,
|
||||||
s_value: number,
|
s_value: number,
|
||||||
risk_value:string,
|
risk_value:string,
|
||||||
safeguards:string,
|
safeguards:[],
|
||||||
suggestions:string
|
suggestions:[]
|
||||||
}) {
|
}) {
|
||||||
return request({
|
return request({
|
||||||
url: `/api/hazop/deviations/${dev_id}/rows`,
|
url: `/api/hazop/deviations/${dev_id}/rows`,
|
||||||
@@ -104,8 +104,8 @@ export function hazopRowEditApi(row_id: number, data: {
|
|||||||
l_value: number,
|
l_value: number,
|
||||||
s_value: number,
|
s_value: number,
|
||||||
risk_value:string,
|
risk_value:string,
|
||||||
safeguards:string,
|
safeguards:[],
|
||||||
suggestions:string
|
suggestions:[]
|
||||||
}) {
|
}) {
|
||||||
return request({
|
return request({
|
||||||
url: `/api/hazop/rows/${row_id}`,
|
url: `/api/hazop/rows/${row_id}`,
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, ref } from "vue";
|
import { computed, onBeforeUnmount, onMounted, ref } from "vue";
|
||||||
import { useMessage } from "naive-ui";
|
|
||||||
import { Icon } from '@iconify/vue'
|
import { Icon } from '@iconify/vue'
|
||||||
import { useThemeStore } from '@/store/modules/theme';
|
import { useThemeStore } from '@/store/modules/theme';
|
||||||
import { AiError, aiHazopDeviation } from '@/service/api/ai';
|
import { peopleListApi } from '@/service/api/system';
|
||||||
import { matrixConfigApi } from "@/service/api/system";
|
|
||||||
import {
|
import {
|
||||||
hazopNodesApi,
|
hazopNodesApi,
|
||||||
hazopNodeAddApi,
|
hazopNodeAddApi,
|
||||||
@@ -22,13 +20,13 @@ import {
|
|||||||
} from "@/service/api/plugin";
|
} from "@/service/api/plugin";
|
||||||
|
|
||||||
const themeStore = useThemeStore();
|
const themeStore = useThemeStore();
|
||||||
const message = useMessage();
|
|
||||||
|
|
||||||
const props = defineProps(['isAiOpen','currentId'])
|
const props = defineProps(['isAiOpen','currentId','hazopRiskMatrixConfig'])
|
||||||
const emit = defineEmits(['close','export']);
|
const emit = defineEmits(['close','toTable']);
|
||||||
|
|
||||||
const loading = ref<boolean>(false);
|
const loading = ref<boolean>(false);
|
||||||
const treeLoading = ref<boolean>(false);
|
const treeLoading = ref<boolean>(false);
|
||||||
|
const analyzing = ref(false);
|
||||||
// 删除确认弹窗
|
// 删除确认弹窗
|
||||||
const delModal = ref<boolean>(false);
|
const delModal = ref<boolean>(false);
|
||||||
// 删除确定状态
|
// 删除确定状态
|
||||||
@@ -44,17 +42,28 @@ const currentModalObj = ref<string>('');
|
|||||||
// 弹窗保存状态
|
// 弹窗保存状态
|
||||||
const modalSaving = ref<boolean>(false);
|
const modalSaving = ref<boolean>(false);
|
||||||
const name = ref<string>('');
|
const name = ref<string>('');
|
||||||
// 风险矩阵配置
|
|
||||||
const riskMatrixConfig = ref<any>(null);
|
const selectOptions = computed(() => {
|
||||||
|
const l = props.hazopRiskMatrixConfig?.probability?.map((item:{key:string,label:string}, index:number) => ({
|
||||||
|
label: 'L'+'_'+Number(index + 1),
|
||||||
|
value: 'L'+'_'+Number(index + 1)
|
||||||
|
}));
|
||||||
|
const s = props.hazopRiskMatrixConfig?.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 l_options = computed(() => {
|
||||||
const newArr = riskMatrixConfig.value?.consequence?.map((item:{key:string,label:string}, index:number) => ({
|
const newArr = props.hazopRiskMatrixConfig?.probability?.map((item:{key:string,label:string}, index:number) => ({
|
||||||
...item,
|
...item,
|
||||||
key: Number(index + 1)
|
key: Number(index + 1)
|
||||||
}));
|
}));
|
||||||
return newArr;
|
return newArr;
|
||||||
});
|
});
|
||||||
const s_options = computed(() => {
|
const s_options = computed(() => {
|
||||||
const newArr = riskMatrixConfig.value?.probability?.map((item:{key:string,label:string}, index:number) => ({
|
const newArr = props.hazopRiskMatrixConfig?.consequence?.map((item:{key:string,label:string}, index:number) => ({
|
||||||
...item,
|
...item,
|
||||||
key: Number(index + 1)
|
key: Number(index + 1)
|
||||||
}));
|
}));
|
||||||
@@ -133,97 +142,19 @@ const recTotal = computed(() => {
|
|||||||
return totalRows;
|
return totalRows;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const workers = ref<{id:number,name:string,org:string}[]>([]);
|
||||||
const HAZOP_AI:any = {
|
// 选中的人员
|
||||||
"搅拌失效": [
|
const selectedWorkers = ref<number[]>([]);
|
||||||
{ 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 sel = ref({ n: 0, d: 0 });
|
|
||||||
const analyzing = ref(false);
|
|
||||||
|
|
||||||
|
|
||||||
const toRec = (obj: string, dev: string, row: any): any => ({
|
|
||||||
item: `${dev}(${obj})`,
|
|
||||||
scene: `${row.cause};可能导致:${row.cons}`,
|
|
||||||
inherent: inherentOf(row.l, row.s),
|
|
||||||
existing: row.safe,
|
|
||||||
suggest: row.sug,
|
|
||||||
residual: lowerOf(inherentOf(row.l, row.s)),
|
|
||||||
source: "HAZOP",
|
|
||||||
});
|
|
||||||
|
|
||||||
const runAi = async () => {
|
|
||||||
if (currentDev.value.rows.length) return message.info("当前偏离已有分析记录,可手动新增行补充");
|
|
||||||
analyzing.value = true;
|
|
||||||
try {
|
|
||||||
const node = nodes.value[sel.value.n];
|
|
||||||
const res = await aiHazopDeviation({
|
|
||||||
node_name: node.name,
|
|
||||||
deviation: currentDev.value.name,
|
|
||||||
object_name: node.obj,
|
|
||||||
change_context: {
|
|
||||||
change: "缩合反应温度及时间优化",
|
|
||||||
content: "R-101 缩合反应釜反应温度由 80℃ 提升至 90℃、保温时间由 8h 缩短至 4h,DCS 温度报警 / 联锁值随之上调",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const rows = (res?.rows ?? []).map((r) => ({
|
|
||||||
cause: r.cause ?? "", cons: r.consequence ?? "", l: r.l ?? 2, s: r.s ?? 3,
|
|
||||||
safe: r.safeguards ?? "", sug: r.suggestions ?? "",
|
|
||||||
}));
|
|
||||||
if (!rows.length) throw new AiError(0, "AI 未返回分析结果");
|
|
||||||
currentDev.value.rows = rows;
|
|
||||||
message.success(`AI 已完成「${currentDev.value.name}」偏离分析,生成 ${rows.length} 条记录(支持手动修改)`);
|
|
||||||
} catch (e: any) {
|
|
||||||
const msg = e instanceof AiError ? e.message : "AI 服务异常";
|
|
||||||
window.$message?.warning(`AI 分析失败:${msg},可手动填写`);
|
|
||||||
// 静态示例数据作 fallback,不阻塞页面
|
|
||||||
const gen = HAZOP_AI[currentDev.value.name] ?? [
|
|
||||||
{ cause: "(AI 生成)偏离场景原因分析", cons: "(AI 生成)可能后果描述", l: 2, s: 3, safe: "现有保护层说明", sug: "建议补充的管控措施" },
|
|
||||||
];
|
|
||||||
currentDev.value.rows = gen.map((x: any) => ({ ...x }));
|
|
||||||
} finally {
|
|
||||||
analyzing.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
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 sels = ref<string[]>(["张工艺", "王仪表"]);
|
|
||||||
|
|
||||||
// 选择分析人员弹窗
|
// 选择分析人员弹窗
|
||||||
const openSelectWorker = ref(false);
|
const openSelectWorker = ref(false);
|
||||||
// 搜索分析人员
|
// 搜索分析人员
|
||||||
const searchWorker = ref("");
|
const searchWorker = ref("");
|
||||||
// 分析人员列表
|
// 分析人员列表
|
||||||
const list = computed(() => ANALYSIS_POOL.filter((x) => !searchWorker.value || x.name.includes(searchWorker.value)));
|
const workerList = computed(() => workers.value.filter((x:{id:number,name:string,org:string}) => !searchWorker.value || x.name.includes(searchWorker.value)));
|
||||||
|
|
||||||
// 选择分析人员
|
// 选择分析人员
|
||||||
const selectWorker = (n: string) => {
|
const selectWorker = (n: number) => {
|
||||||
sels.value = sels.value.includes(n) ? sels.value.filter((x) => x !== n) : [...sels.value, n];
|
selectedWorkers.value = selectedWorkers.value.includes(n) ? selectedWorkers.value.filter((x:number) => x !== n) : [...selectedWorkers.value, n];
|
||||||
};
|
};
|
||||||
|
|
||||||
// 保存
|
// 保存
|
||||||
@@ -231,27 +162,146 @@ const save = () => {
|
|||||||
window.$message?.success("已手动保存,分析记录数已同步至工具卡片(内容修改实时自动保存,退出不丢失)");
|
window.$message?.success("已手动保存,分析记录数已同步至工具卡片(内容修改实时自动保存,退出不丢失)");
|
||||||
};
|
};
|
||||||
|
|
||||||
// AI整理如表
|
/**
|
||||||
const exportAll = () => {
|
* 根据行 ID 查找并返回带有父级名称的行对象
|
||||||
emit("export", nodes.value.flatMap((n) => n.devs.flatMap((d) => d.rows.map((row) => toRec(n.obj, d.name, row)))), "ai");
|
* @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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null; // 未找到
|
||||||
|
}
|
||||||
|
// AI整理如表、入表
|
||||||
|
const enterToTable = (type: string, row: any) => {
|
||||||
|
if(type==='single'){
|
||||||
|
const enrichedRow = findRowWithParents(nodes.value, row.id);
|
||||||
|
emit('toTable',type,enrichedRow?[enrichedRow]:[]);
|
||||||
|
}
|
||||||
|
if(type==='all'){
|
||||||
|
const enrichedRows:any[] = [];
|
||||||
|
nodes.value.forEach((node:any) => {
|
||||||
|
const node_ame = node.node_name;
|
||||||
|
node.objects.forEach((obj:any) => {
|
||||||
|
const obj_ame = obj.obj_name;
|
||||||
|
obj.deviations.forEach((dev:any) => {
|
||||||
|
const deviation = dev.deviation;
|
||||||
|
if (dev.rows && dev.rows.length > 0) {
|
||||||
|
dev.rows.forEach((row:any) => {
|
||||||
|
// 创建新对象,保留原 row 全部属性,并添加父级信息
|
||||||
|
enrichedRows.push({
|
||||||
|
...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
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
emit('toTable',type,enrichedRows);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 导出 Word
|
// 导出 Word
|
||||||
const exportWord = () => {
|
const exportWord = () => {
|
||||||
window.$message?.success('HAZOP 分析报告已导出(Word)')
|
window.$message?.info('还差接口')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 根据l和s获取风险等级
|
// AI 分析当前偏离
|
||||||
|
const runAi = async () => {
|
||||||
|
if(!currentDev.value){
|
||||||
|
return window.$message?.warning("请先在左侧选择偏离")
|
||||||
|
}
|
||||||
|
if(currentDev.value.rows.length){
|
||||||
|
return window.$message?.info("当前偏离已有分析记录,可手动新增行补充");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 根据l和s计算RR风险等级
|
||||||
const riskLevel = (l: number, s: number) => {
|
const riskLevel = (l: number, s: number) => {
|
||||||
const num = l * s;
|
const num = l * s;
|
||||||
for (let i = 0; i < riskMatrixConfig.value?.risk_grades.length; i++) {
|
for (let i = 0; i < props.hazopRiskMatrixConfig?.risk_grades.length; i++) {
|
||||||
const item = riskMatrixConfig.value?.risk_grades[i];
|
const item = props.hazopRiskMatrixConfig?.risk_grades[i];
|
||||||
if (num >= item.min && num <= item.max) {
|
if (num >= item.min && num <= item.max) {
|
||||||
return item; // 返回整条数据
|
return item; // 返回整条数据
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
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 addSafeguard = (index: number) => {
|
||||||
|
if(currentDev.value.rows[index].safeguards){
|
||||||
|
currentDev.value.rows[index].safeguards.push({name:'',value:''});
|
||||||
|
}else{
|
||||||
|
currentDev.value.rows[index].safeguards = [{name:'',value:''}];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 删除安全措施
|
||||||
|
const delSafeguard = (index: number, safeguardIndex: number, row: any) => {
|
||||||
|
currentDev.value.rows[index].safeguards.splice(safeguardIndex, 1);
|
||||||
|
editRow(row)
|
||||||
|
}
|
||||||
|
// 新增建议措施
|
||||||
|
const addSuggestion = (index: number) => {
|
||||||
|
if(currentDev.value.rows[index].suggestions){
|
||||||
|
currentDev.value.rows[index].suggestions.push({name:'',value:''});
|
||||||
|
}else{
|
||||||
|
currentDev.value.rows[index].suggestions = [{name:'',value:''}];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 删除建议措施
|
||||||
|
const delSuggestion = (index: number, suggestionIndex: number, row: any) => {
|
||||||
|
currentDev.value.rows[index].suggestions.splice(suggestionIndex, 1);
|
||||||
|
editRow(row)
|
||||||
|
}
|
||||||
|
|
||||||
// 新增行
|
// 新增行
|
||||||
const addRow = async () => {
|
const addRow = async () => {
|
||||||
@@ -265,12 +315,11 @@ const addRow = async () => {
|
|||||||
l_value: l_options.value[0]?.key || 1,
|
l_value: l_options.value[0]?.key || 1,
|
||||||
s_value: s_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 || '',
|
risk_value: riskLevel(l_options.value[0]?.key || 1, s_options.value[0]?.key || 1)?.name || '',
|
||||||
safeguards: "",
|
safeguards: [],
|
||||||
suggestions: "",
|
suggestions: [],
|
||||||
})
|
})
|
||||||
if(!error){
|
if(!error){
|
||||||
currentDev.value.rows.push(data);
|
currentDev.value.rows.push(data);
|
||||||
window.$message?.success("新增行成功")
|
|
||||||
}
|
}
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
};
|
};
|
||||||
@@ -459,21 +508,9 @@ const handleContextMenu = (event: MouseEvent, type: string, node: any, obj = nul
|
|||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
// 记录目标数据
|
// 记录目标数据
|
||||||
contextTarget.value = { type, node, obj, dev }
|
contextTarget.value = { type, node, obj, dev }
|
||||||
// 计算菜单位置(防止超出视口)
|
// 计算菜单位置
|
||||||
const menuWidth = 200 // 预估菜单宽度
|
contextMenuX.value = Math.max(0, event.clientX)
|
||||||
const menuHeight = 300 // 预估菜单高度
|
contextMenuY.value = Math.max(0, event.clientY)
|
||||||
let x = event.clientX
|
|
||||||
let y = event.clientY
|
|
||||||
// 如果菜单会超出右边界,向左偏移
|
|
||||||
if (x + menuWidth > window.innerWidth) {
|
|
||||||
x = window.innerWidth - menuWidth - 10
|
|
||||||
}
|
|
||||||
// 如果菜单会超出下边界,向上偏移
|
|
||||||
if (y + menuHeight > window.innerHeight) {
|
|
||||||
y = window.innerHeight - menuHeight - 10
|
|
||||||
}
|
|
||||||
contextMenuX.value = x
|
|
||||||
contextMenuY.value = y
|
|
||||||
contextMenuShow.value = true
|
contextMenuShow.value = true
|
||||||
}
|
}
|
||||||
// 菜单选中回调
|
// 菜单选中回调
|
||||||
@@ -491,7 +528,6 @@ const handleMenuSelect = (key: string) => {
|
|||||||
openModal('edit','node','编辑节点',node?.node_name);
|
openModal('edit','node','编辑节点',node?.node_name);
|
||||||
break
|
break
|
||||||
case 'delNode':
|
case 'delNode':
|
||||||
console.log('[删除节点] 节点:', node?.node_name)
|
|
||||||
// 删除
|
// 删除
|
||||||
openDelModal('node');
|
openDelModal('node');
|
||||||
break
|
break
|
||||||
@@ -538,31 +574,35 @@ if (typeof window !== 'undefined') {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取风险矩阵配置
|
|
||||||
const getRiskMatrixConfig = async () => {
|
|
||||||
treeLoading.value = true;
|
|
||||||
const {data,error} = await matrixConfigApi("HAZOP");
|
|
||||||
if(!error){
|
|
||||||
console.log(data)
|
|
||||||
riskMatrixConfig.value = data;
|
|
||||||
getHazopNodes();
|
|
||||||
}else{
|
|
||||||
treeLoading.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 获取HAZOP节点列表
|
// 获取HAZOP节点列表
|
||||||
const getHazopNodes = async () => {
|
const getHazopNodes = async () => {
|
||||||
treeLoading.value = true;
|
treeLoading.value = true;
|
||||||
const {data,error} = await hazopNodesApi(props.currentId);
|
const {data,error} = await hazopNodesApi(props.currentId);
|
||||||
if(!error){
|
if(!error){
|
||||||
console.log(data)
|
|
||||||
nodes.value = data;
|
nodes.value = data;
|
||||||
}
|
}
|
||||||
treeLoading.value = false;
|
treeLoading.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取人员列表
|
||||||
|
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(';')
|
||||||
|
}));
|
||||||
|
workers.value = transformed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
getRiskMatrixConfig();
|
getWorkers();
|
||||||
|
getHazopNodes();
|
||||||
|
})
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
document.removeEventListener('click', closeMenu)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -582,29 +622,28 @@ onMounted(() => {
|
|||||||
<NButton size="small" ghost type="primary" class="text-xs" @click="save">保存</NButton>
|
<NButton size="small" ghost type="primary" class="text-xs" @click="save">保存</NButton>
|
||||||
<span class="text-sm font-bold text-[var(--theme-color)]">HAZOP 分析插件</span>
|
<span class="text-sm font-bold text-[var(--theme-color)]">HAZOP 分析插件</span>
|
||||||
<NTag size="small" type="info">插件</NTag>
|
<NTag size="small" type="info">插件</NTag>
|
||||||
<NTag size="small">MOC 变更:缩合反应温度及时间优化(演示)</NTag>
|
|
||||||
<span class="text-[11px] text-slate-500">{{ `分析进度 ${devDone}/${devTotal} 个偏离 · ${recTotal} 条记录` }}</span>
|
<span class="text-[11px] text-slate-500">{{ `分析进度 ${devDone}/${devTotal} 个偏离 · ${recTotal} 条记录` }}</span>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<NButton size="small" ghost type="primary" class="text-[11px]" @click="openSelectWorker = !openSelectWorker">
|
<NButton size="small" ghost type="primary" class="text-[11px]" @click="openSelectWorker = !openSelectWorker">
|
||||||
<Icon icon="lucide:users" class="size-12px" /> 分析组成员 · 已选 {{ sels.length }} 人
|
<Icon icon="lucide:users" class="size-12px" /> 分析组成员 · 已选 {{ selectedWorkers.length }} 人
|
||||||
</NButton>
|
</NButton>
|
||||||
<template v-if="openSelectWorker">
|
<template v-if="openSelectWorker">
|
||||||
<div class="fixed inset-0 z-10" @click="openSelectWorker = false"></div>
|
<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="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">
|
<div class="mb-1.5 flex items-center justify-between">
|
||||||
<span class="text-xs font-semibold text-slate-700">选择分析组成员(岗位人员库)</span>
|
<span class="text-xs font-semibold text-slate-700">选择分析组成员(岗位人员库)</span>
|
||||||
<span class="text-[11px] text-slate-400">已选 {{ sels.length }} 人</span>
|
<span class="text-[11px] text-slate-400">已选 {{ selectedWorkers.length }} 人</span>
|
||||||
</div>
|
</div>
|
||||||
<NInput v-model:value="searchWorker" size="small" placeholder="搜索人名…" class="mb-1.5 text-xs" />
|
<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 class="max-h-48 space-y-0.5 overflow-y-auto">
|
||||||
<div v-for="x in list" :key="x.name" @click="selectWorker(x.name)"
|
<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="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'">
|
: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]"
|
<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>
|
: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="font-medium text-slate-800">{{ x.name }}</span>
|
||||||
<span class="text-[11px] text-slate-400">{{ x.org }} · {{ x.post }}</span>
|
<span class="text-[11px] text-slate-400">{{ x.org }}</span>
|
||||||
<NTag v-if="sels[0] === x.name" size="small" type="primary" class="ml-auto">组长</NTag>
|
<NTag v-if="selectedWorkers[0] === x.id" size="small" type="primary" class="ml-auto">组长</NTag>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-1.5 border-t pt-1.5 text-[11px] leading-4 text-slate-400">
|
<div class="mt-1.5 border-t pt-1.5 text-[11px] leading-4 text-slate-400">
|
||||||
@@ -614,7 +653,7 @@ onMounted(() => {
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div class="ml-auto flex items-center gap-1.5">
|
<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="exportAll">
|
<NButton v-if="isAiOpen" size="small" ghost type="primary" class="text-xs" :disabled="recTotal === 0" @click="enterToTable('all',null)">
|
||||||
<Icon icon="lucide:sparkles" class="size-12px mr-1" /> AI 整理入表
|
<Icon icon="lucide:sparkles" class="size-12px mr-1" /> AI 整理入表
|
||||||
</NButton>
|
</NButton>
|
||||||
<NButton size="small" ghost type="primary" class="text-xs" @click="exportWord">
|
<NButton size="small" ghost type="primary" class="text-xs" @click="exportWord">
|
||||||
@@ -701,82 +740,144 @@ onMounted(() => {
|
|||||||
</NButton>
|
</NButton>
|
||||||
<span class="text-[11px] text-slate-400">单元格点击即可修改;风险等级按风险矩阵L / S 取值实时重算;「入表」将本条选入风险分析记录表</span>
|
<span class="text-[11px] text-slate-400">单元格点击即可修改;风险等级按风险矩阵L / S 取值实时重算;「入表」将本条选入风险分析记录表</span>
|
||||||
</div>
|
</div>
|
||||||
<table class="w-full min-w-[860px] border-collapse text-xs">
|
<div class="scroll">
|
||||||
<thead>
|
<table class="w-full min-w-[860px] border-collapse text-xs">
|
||||||
<tr>
|
<thead>
|
||||||
<th :class="TH" class="w-16">操作</th>
|
<tr>
|
||||||
<th :class="TH" class="w-60">原因</th>
|
<th :class="TH" class="w-16">操作</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-60">后果</th>
|
||||||
<th :class="TH" class="w-6">S</th>
|
<th :class="TH" class="w-6">L</th>
|
||||||
<th :class="TH" class="w-20">RR</th>
|
<th :class="TH" class="w-6">S</th>
|
||||||
<th :class="TH" class="w-60">安全措施</th>
|
<th :class="TH" class="w-20">RR</th>
|
||||||
<th :class="TH" class="w-60">建议措施</th>
|
<th :class="TH" class="w-60">安全措施</th>
|
||||||
</tr>
|
<th :class="TH" class="w-60">建议措施</th>
|
||||||
</thead>
|
<th :class="TH" class="w-20">RR1</th>
|
||||||
<tbody>
|
</tr>
|
||||||
<tr v-if="!currentDev || currentDev?.rows?.length === 0">
|
</thead>
|
||||||
<td colspan="8" class="px-3 py-8 text-center text-xs text-slate-400">
|
<tbody>
|
||||||
当前偏离暂无分析记录,点击右上角「AI 分析当前偏离」自动生成,或「新增行」手动填写
|
<tr v-if="!currentDev || currentDev?.rows?.length === 0">
|
||||||
</td>
|
<td colspan="8" class="px-3 py-8 text-center text-xs text-slate-400">
|
||||||
</tr>
|
当前偏离暂无分析记录,点击右上角「AI 分析当前偏离」自动生成,或「新增行」手动填写
|
||||||
<tr v-for="(row, ri) in currentDev?.rows" :key="ri">
|
</td>
|
||||||
<td :class="TD">
|
</tr>
|
||||||
<div class="flex items-center justify-between px-1 mt-1">
|
<tr v-for="(row, ri) in currentDev?.rows" :key="ri">
|
||||||
<NButton text type="primary" class="mt-1px text-[11px] hover:underline" title="选入风险分析记录表"
|
<td :class="TD">
|
||||||
@click="message.success('已选入 1 条风险记录,可继续挑选或返回查看记录表')">入表
|
<div class="flex items-center justify-between px-1 mt-1">
|
||||||
</NButton>
|
<NButton text type="primary" class="mt-1px text-[11px] hover:underline" title="选入风险分析记录表"
|
||||||
|
@click="enterToTable('single',row)">入表
|
||||||
<NPopconfirm
|
</NButton>
|
||||||
positive-text="确定"
|
<NPopconfirm
|
||||||
:positiveButtonProps="{ size: 'tiny' }"
|
positive-text="确定"
|
||||||
:negativeButtonProps="{ size: 'tiny' }"
|
:positiveButtonProps="{ size: 'tiny' }"
|
||||||
@positive-click="delRow(Number(ri), row.id)"
|
:negativeButtonProps="{ size: 'tiny' }"
|
||||||
>
|
@positive-click="delRow(Number(ri), row.id)"
|
||||||
<template #trigger>
|
>
|
||||||
<NButton text type="error" title="删除行">
|
<template #trigger>
|
||||||
<Icon icon="ep:delete" class="size-14px" />
|
<NButton text type="error" title="删除行">
|
||||||
</NButton>
|
<Icon icon="ep:delete" class="size-14px" />
|
||||||
</template>
|
</NButton>
|
||||||
确定删除吗?
|
</template>
|
||||||
</NPopconfirm>
|
确定删除吗?
|
||||||
</div>
|
</NPopconfirm>
|
||||||
</td>
|
</div>
|
||||||
<td :class="TD">
|
</td>
|
||||||
<NInput type="textarea" size="small" v-model:value="row.cause" :autosize="{minRows:1}" class="text-xs" @blur="editRow(row)" />
|
<td :class="TD">
|
||||||
</td>
|
<NInput type="textarea" size="small" v-model:value="row.cause" :autosize="{minRows:1}" class="text-xs" @blur="editRow(row)" />
|
||||||
<td :class="TD">
|
</td>
|
||||||
<NInput type="textarea" size="small" v-model:value="row.consequence" :autosize="{minRows:1}" class="text-xs" @blur="editRow(row)" />
|
<td :class="TD">
|
||||||
</td>
|
<NInput type="textarea" size="small" v-model:value="row.consequence" :autosize="{minRows:1}" class="text-xs" @blur="editRow(row)" />
|
||||||
<td :class="TD" class="text-center">
|
</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" @blur="editRow(row)" />
|
<td :class="TD" class="text-center">
|
||||||
</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" @blur="editRow(row)" />
|
||||||
<td :class="TD" class="text-center">
|
</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" @blur="editRow(row)" />
|
<td :class="TD" class="text-center">
|
||||||
</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" @blur="editRow(row)" />
|
||||||
<td :class="TD" class="text-center">
|
</td>
|
||||||
<NTag
|
<td :class="TD" class="text-center">
|
||||||
v-if="row.l_value>0 && row.s_value>0"
|
<NTag
|
||||||
class="text-[11px]"
|
v-if="row.l_value>0 && row.s_value>0"
|
||||||
size="small"
|
class="text-[11px]"
|
||||||
:color="{
|
size="small"
|
||||||
color: riskLevel(row.l_value, row.s_value)?.bg_color,
|
:color="{
|
||||||
textColor: riskLevel(row.l_value, row.s_value)?.font_color,
|
color: riskLevel(row.l_value, row.s_value)?.bg_color,
|
||||||
borderColor: 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>
|
{{riskLevel(row.l_value, row.s_value)?.name}}({{row.l_value * row.s_value}})
|
||||||
</td>
|
</NTag>
|
||||||
<td :class="TD">
|
</td>
|
||||||
<NInput type="textarea" size="small" v-model:value="row.safeguards" :autosize="{minRows:1}" class="text-xs" @blur="editRow(row)" />
|
<td :class="TD">
|
||||||
</td>
|
<div v-if="row.safeguards && row.safeguards.length" class="flex flex-col gap-2 mb-1">
|
||||||
<td :class="TD">
|
<div v-for="(sItem, si) in row.safeguards" :key="si" class="border rounded-md relative">
|
||||||
<NInput type="textarea" size="small" v-model:value="row.suggestions" :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="editRow(row)" />
|
||||||
</td>
|
<div class="flex justify-end px-2 pb-1">
|
||||||
</tr>
|
<NSelect v-model:value="sItem.value" size="tiny" class="!w-70px" :options="selectOptions" placeholder="" @blur="editRow(row)"></NSelect>
|
||||||
</tbody>
|
</div>
|
||||||
</table>
|
<NPopconfirm
|
||||||
|
positive-text="确定"
|
||||||
|
:positiveButtonProps="{ size: 'tiny' }"
|
||||||
|
:negativeButtonProps="{ size: 'tiny' }"
|
||||||
|
@positive-click="delSafeguard(Number(ri), Number(si),row)"
|
||||||
|
>
|
||||||
|
<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(ri))">
|
||||||
|
<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="(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 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>
|
||||||
|
</div>
|
||||||
|
<NPopconfirm
|
||||||
|
positive-text="确定"
|
||||||
|
:positiveButtonProps="{ size: 'tiny' }"
|
||||||
|
:negativeButtonProps="{ size: 'tiny' }"
|
||||||
|
@positive-click="delSuggestion(Number(ri), Number(si),row)"
|
||||||
|
>
|
||||||
|
<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(ri))">
|
||||||
|
<Icon icon="ic:round-plus" class="size-12px" />增加
|
||||||
|
</NButton>
|
||||||
|
</td>
|
||||||
|
<td :class="TD" class="text-center">
|
||||||
|
<NTag
|
||||||
|
v-if="row.l_value>0 && row.s_value>0"
|
||||||
|
class="text-[11px]"
|
||||||
|
size="small"
|
||||||
|
:color="{
|
||||||
|
color: countRiskLevel(row)?.bg_color,
|
||||||
|
textColor: countRiskLevel(row)?.font_color,
|
||||||
|
borderColor: countRiskLevel(row)?.bg_color,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{countRiskLevel(row)?.name}}
|
||||||
|
</NTag>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -791,13 +892,15 @@ onMounted(() => {
|
|||||||
placement="bottom-start"
|
placement="bottom-start"
|
||||||
trigger="manual"
|
trigger="manual"
|
||||||
:mask="false"
|
:mask="false"
|
||||||
|
to="body"
|
||||||
|
flip
|
||||||
/>
|
/>
|
||||||
<!-- 新增/编辑弹窗 -->
|
<!-- 新增/编辑弹窗 -->
|
||||||
<NModal
|
<NModal
|
||||||
v-model:show="editModal"
|
v-model:show="editModal"
|
||||||
preset="card"
|
preset="card"
|
||||||
:title="currentModalTitle"
|
:title="currentModalTitle"
|
||||||
:auto-focus="false"
|
:auto-focus="true"
|
||||||
:style="{ width: '450px', height: 'auto' }"
|
:style="{ width: '450px', height: 'auto' }"
|
||||||
:segmented="{ content: true, footer: true }"
|
:segmented="{ content: true, footer: true }"
|
||||||
>
|
>
|
||||||
@@ -834,7 +937,7 @@ onMounted(() => {
|
|||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.scroll {
|
.scroll {
|
||||||
height: calc(100vh - 200px);
|
height: calc(100vh - 160px);
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,171 +1,152 @@
|
|||||||
<!-- 风险分析记录表 -->
|
<!-- 风险分析记录表 -->
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue';
|
import { onMounted, ref } from 'vue';
|
||||||
import { Icon } from '@iconify/vue'
|
import { Icon } from '@iconify/vue'
|
||||||
import { useThemeStore } from '@/store/modules/theme';
|
import { useThemeStore } from '@/store/modules/theme';
|
||||||
import { AiError, aiRiskCheck } from "@/service/api/ai";
|
|
||||||
import Ed from "./Ed.vue";
|
|
||||||
import Hazop from "./hazop.vue";
|
import Hazop from "./hazop.vue";
|
||||||
import Jsa from "./jsa.vue";
|
import Jsa from "./jsa.vue";
|
||||||
import Scl from "./scl.vue";
|
import Scl from "./scl.vue";
|
||||||
import RiskCheck from "./riskCheck.vue";
|
import RiskCheck from "./riskCheck.vue";
|
||||||
import Footer from './footer.vue'
|
import Footer from './footer.vue'
|
||||||
|
import { matrixConfigApi } from "@/service/api/system";
|
||||||
|
|
||||||
const themeStore = useThemeStore();
|
const themeStore = useThemeStore();
|
||||||
|
|
||||||
const props = defineProps(['type','isAiOpen','title','currentId','formInfo','preInfo'])
|
const props = defineProps(['type','isAiOpen','title','currentId','formInfo'])
|
||||||
const emit = defineEmits(['save','submit','confirmTab']);
|
const emit = defineEmits(['save','submit','confirmTab']);
|
||||||
|
|
||||||
const footerRef = ref<any>(null);
|
|
||||||
|
|
||||||
const aiFail = (e: any) => {
|
|
||||||
if (e instanceof AiError) window.$message?.warning(`AI 分析失败:${e.message},可手动填写`);
|
|
||||||
else window.$message?.warning("AI 分析失败,可手动填写");
|
|
||||||
};
|
|
||||||
|
|
||||||
const form = ref<{ content: string }>({
|
const form = ref<{ content: string }>({
|
||||||
content: ''
|
content: ''
|
||||||
});
|
});
|
||||||
|
const hazopRiskMatrixConfig = ref<any>(null);
|
||||||
|
const jsaRiskMatrixConfig = ref<any>(null);
|
||||||
const pluginDone = ref<Record<string, number>>({});
|
const pluginDone = ref<Record<string, number>>({});
|
||||||
const tools = ref<string[]>(["HAZOP","JSA", "SCL", "RISK_CHECK"]);
|
const tools = ref<string[]>(["HAZOP","JSA", "SCL", "RISK_CHECK"]);
|
||||||
const riskRecords = ref<any>([]);
|
const riskRecords = ref<any>([]);
|
||||||
// 风险检查表检查项(数据提升到本组件:AI 预填/重新生成随时可写,插件抽屉打开即渲染)
|
|
||||||
const riskChecks = ref<any[]>([]);
|
|
||||||
const riskCheckLoading = ref<boolean>(false);
|
|
||||||
const pluginDrawer = ref<boolean>(false);
|
const pluginDrawer = ref<boolean>(false);
|
||||||
const currentPlugin = ref<string>('');
|
const currentPlugin = ref<string>('');
|
||||||
const lvlNext = (v: string) => (v === "高" ? "中" : v === "中" ? "低" : "高");
|
|
||||||
|
|
||||||
// AI 生成/重新生成风险检查表(footer 派发;整表生成约 1~3 分钟,42901 闸门满自动重试一次)
|
|
||||||
const generateRiskCheck = async () => {
|
|
||||||
const info = props.preInfo;
|
|
||||||
if (!info?.description) {
|
|
||||||
return window.$message?.warning("缺少变更描述,请先在 [变更预识别] 里面输入变更描述");
|
|
||||||
}
|
|
||||||
if (riskCheckLoading.value) return;
|
|
||||||
riskCheckLoading.value = true;
|
|
||||||
footerRef.value?.setRegenerateLoading(true);
|
|
||||||
try {
|
|
||||||
for (let attempt = 0; attempt < 2; attempt++) {
|
|
||||||
try {
|
|
||||||
const res = await aiRiskCheck(info.description, (info.compare_rows ?? []).map((x: any) => ({ item: x.item, before_text: x.before_text, after_text: x.after_text })));
|
|
||||||
const items = (res?.items ?? []).map((x: any, i: number) => ({
|
|
||||||
id: i + 1,
|
|
||||||
group: x.group || '安全健康',
|
|
||||||
q: x.q ?? '',
|
|
||||||
ans: x.ans ?? 'NA',
|
|
||||||
risk: x.risk ?? '',
|
|
||||||
measure: x.measure ?? '',
|
|
||||||
}));
|
|
||||||
if (!items.length) {
|
|
||||||
window.$message?.warning("AI 未生成检查项,可在风险检查表插件中手动维护");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
riskChecks.value = items;
|
|
||||||
window.$message?.success(`风险检查表已由 AI 生成(${items.length} 项),打开「风险检查表」工具卡逐项确认`);
|
|
||||||
return;
|
|
||||||
} catch (e: any) {
|
|
||||||
if (e instanceof AiError && e.code === 42901 && attempt === 0) {
|
|
||||||
const waitSec = (e.data?.retry_after as number) ?? 30;
|
|
||||||
window.$message?.info(`当前 AI 生成任务较多,${waitSec} 秒后自动重试一次…`);
|
|
||||||
await new Promise((r) => setTimeout(r, waitSec * 1000));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
aiFail(e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
riskCheckLoading.value = false;
|
|
||||||
footerRef.value?.setRegenerateLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const onRegenerate = generateRiskCheck;
|
|
||||||
|
|
||||||
// 风险识别分析记录表
|
// 风险识别分析记录表
|
||||||
const TOOL_CARDS = [
|
const TOOL_CARDS = [
|
||||||
{ key: "HAZOP" as const, name: "HAZOP 分析", desc: "节点-偏离结构化分析:原因 / 后果 / LS 风险矩阵 / 安全措施" },
|
{ key: "HAZOP", name: "HAZOP 分析" },
|
||||||
{ key: "JSA" as const, name: "JSA 分析", desc: "作业步骤分解:危害因素识别与 LS 风险分级管控" },
|
{ key: "JSA", name: "JSA 分析" },
|
||||||
{ key: "SCL" as const, name: "设备 SCL", desc: "设备 / 管理检查项目对照标准逐项核查" },
|
{ key: "SCL", name: "设备 SCL" },
|
||||||
{ key: "RISK_CHECK" as const, name: "风险检查表", desc: "变更通用检查项:是 / 否 / 不涉及逐项判定,输出风险描述与管控措施" },
|
{ key: "RISK_CHECK", name: "风险检查表" },
|
||||||
];
|
];
|
||||||
const pssrGroups = ref<{ g: string; items: any[] }[]>([
|
|
||||||
{ g: "一、风险分析行动项落实", items: [
|
|
||||||
{ c: "联锁跳车电流设定值校核并留存记录", p: "投用前" },
|
|
||||||
{ c: "电缆与开关容量校核升级完成", p: "投用前" },
|
|
||||||
{ c: "操作人员培训完成并考核合格", p: "投用前" },
|
|
||||||
] },
|
|
||||||
{ g: "二、现场设备安装检查确认", items: [
|
|
||||||
{ c: "电机安装就位,联轴器对中合格", p: "投用前" },
|
|
||||||
{ c: "地脚螺栓紧固,减振垫安装到位", p: "投用前" },
|
|
||||||
{ c: "电缆接线、防爆格兰头密封良好", p: "投用前" },
|
|
||||||
] },
|
|
||||||
{ g: "三、保护措施确认", items: [
|
|
||||||
{ c: "联锁跳车设定值与新电机额定电流匹配", p: "投用前" },
|
|
||||||
{ c: "电机外壳接地、防护罩完好", p: "投用前" },
|
|
||||||
{ c: "紧急停机按钮功能试验正常", p: "投用前" },
|
|
||||||
] },
|
|
||||||
{ g: "四、资料归档确认", items: [
|
|
||||||
{ c: "P&ID(图号 PID-R201-03)已更新并受控", p: "关闭前" },
|
|
||||||
{ c: "设备台账、备件清单已更新", p: "关闭前" },
|
|
||||||
{ c: "培训签到表、考核记录已归档", p: "关闭前" },
|
|
||||||
] },
|
|
||||||
]);
|
|
||||||
|
|
||||||
const toPssrAll = () => {
|
// 根据l和s获取风险等级
|
||||||
const rest = riskRecords.value.map((x:any, i:any) => (x.pssr ? -1 : i)).filter((i:any) => i >= 0);
|
const hazopRiskLevel = (l: number, s: number) => {
|
||||||
if (!rest.length) { window.$message?.info("所有建议措施均已转为 PSSR 行动项"); return; }
|
const num = l * s;
|
||||||
const items = rest.map((i:any) => ({ c: `${riskRecords.value[i].suggest}(来源:风险分析记录表 · ${riskRecords.value[i].item})`, p: "投用前" }));
|
for (let i = 0; i < hazopRiskMatrixConfig.value?.risk_grades.length; i++) {
|
||||||
pssrGroups.value = pssrGroups.value.map((g:any, gi:any) => gi === 0 ? { ...g, items: [...g.items, ...items] } : g);
|
const item = hazopRiskMatrixConfig.value?.risk_grades[i];
|
||||||
riskRecords.value = riskRecords.value.map((x:any) => ({ ...x, pssr: true }));
|
if (num >= item.min && num <= item.max) {
|
||||||
window.$message?.success(`已将 ${rest.length} 条建议措施全部转为 PSSR 行动项(见「PSSR 检查内容」)`);
|
return item; // 返回整条数据
|
||||||
};
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const jsaRiskLevel = (l: number, s: number) => {
|
||||||
|
const num = l * s;
|
||||||
|
for (let i = 0; i < jsaRiskMatrixConfig.value?.risk_grades.length; i++) {
|
||||||
|
const item = jsaRiskMatrixConfig.value?.risk_grades[i];
|
||||||
|
if (num >= item.min && num <= item.max) {
|
||||||
|
return item; // 返回整条数据
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// 计算RR1风险等级
|
||||||
|
const countHazopRiskLevel = (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 hazopRiskLevel(newL, newS);
|
||||||
|
}
|
||||||
|
const countJsaRiskLevel = (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 jsaRiskLevel(newL, newS);
|
||||||
|
}
|
||||||
|
|
||||||
const lsLevel = (l: number | null, s: number | null) => {
|
|
||||||
const rr = (l ?? 0) * (s ?? 0);
|
|
||||||
return rr >= 10 ? "高" : rr >= 5 ? "中" : "低";
|
|
||||||
};
|
|
||||||
|
|
||||||
const aiOrganize = async () => {
|
// 删除记录
|
||||||
// let form = changePreRef.value?.form;
|
const delRec = (i: number) => {
|
||||||
// analyzing.value = true;
|
riskRecords.value.splice(i, 1);
|
||||||
// try {
|
|
||||||
// const res = await aiRiskOrganize(form.description, (form.compare_rows ?? []).map((x: any) => ({ item: x.item, before_text: x.before_text, after_text: x.after_text })));
|
|
||||||
// const gen = (res?.records ?? []).map((r) => ({
|
|
||||||
// item: r.item,
|
|
||||||
// scene: r.scene,
|
|
||||||
// inherent: lsLevel(r.l, r.s),
|
|
||||||
// existing: r.existing,
|
|
||||||
// suggest: r.suggest,
|
|
||||||
// residual: lsLevel(r.residual_l, r.residual_s),
|
|
||||||
// source: "AI 整理",
|
|
||||||
// }));
|
|
||||||
// riskRecords.value = [...riskRecords.value, ...gen];
|
|
||||||
// window.$message?.success(`AI 已根据变更内容自动整理形成风险分析记录表(${gen.length} 项),支持手动修改`);
|
|
||||||
// } catch (e: any) {
|
|
||||||
// aiFail(e);
|
|
||||||
// } finally {
|
|
||||||
// analyzing.value = false;
|
|
||||||
// }
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 单个转换为 PSSR 行动项
|
||||||
const toPssr = (i: number) => {
|
const toPssr = (i: number) => {
|
||||||
const rec = riskRecords.value[i];
|
const rec = riskRecords.value[i];
|
||||||
if (!rec || rec.pssr) return;
|
if (!rec || rec.to_pssr) return;
|
||||||
pssrGroups.value = pssrGroups.value.map((g:any, gi:any) => gi === 0
|
riskRecords.value[i] = { ...rec, to_pssr: true };
|
||||||
? { ...g, items: [...g.items, { c: `${rec.suggest}(来源:风险分析记录表 · ${rec.item})`, p: "投用前" }] }
|
|
||||||
: g);
|
|
||||||
updRec(i, { pssr: true });
|
|
||||||
window.$message?.success("建议措施已转为 PSSR 行动项(见「PSSR 检查内容」· 一、风险分析行动项落实)");
|
window.$message?.success("建议措施已转为 PSSR 行动项(见「PSSR 检查内容」· 一、风险分析行动项落实)");
|
||||||
};
|
};
|
||||||
|
|
||||||
const updRec = (i: number, patch: Partial<any>) => {
|
// 全部转为 PSSR 行动项
|
||||||
riskRecords.value = riskRecords.value.map((x, j) => (j === i ? { ...x, ...patch } : x));
|
const toPssrAll = () => {
|
||||||
|
const result = riskRecords.value.filter((item:any) => item.to_pssr === 1);
|
||||||
|
if (!result.length) {
|
||||||
|
return window.$message?.info("所有建议措施均已转为 PSSR 行动项");
|
||||||
|
}
|
||||||
|
riskRecords.value = riskRecords.value.map((x:any) => ({ ...x, to_pssr: true }));
|
||||||
|
window.$message?.success(`已将建议措施全部转为 PSSR 行动项(见「PSSR 检查内容」)`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const exportAll = (items: any[], mode: "manual" | "ai") => {
|
// AI 自动整理
|
||||||
riskRecords.value = [...riskRecords.value, ...items];
|
const aiOrganize = async () => {
|
||||||
if (mode === "ai") {
|
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据 id 字段对数组去重(保留首次出现)
|
||||||
|
* @param {Array} arr - 包含 id 字段的对象数组
|
||||||
|
* @returns {Array} 去重后的新数组
|
||||||
|
*/
|
||||||
|
function uniqueById(arr: any[]) {
|
||||||
|
const seen = new Set();
|
||||||
|
const result = [];
|
||||||
|
for (let i = arr.length - 1; i >= 0; i--) {
|
||||||
|
const item = arr[i];
|
||||||
|
if (!seen.has(item.id)) {
|
||||||
|
seen.add(item.id);
|
||||||
|
result.unshift(item); // 插入到头部,保持原顺序
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
// 整理入表
|
||||||
|
const exportAll = (type: string, items: any[]) => {
|
||||||
|
riskRecords.value = uniqueById([...riskRecords.value, ...items]);
|
||||||
|
if (type === "all") {
|
||||||
pluginDrawer.value = false;
|
pluginDrawer.value = false;
|
||||||
currentPlugin.value = '';
|
currentPlugin.value = '';
|
||||||
window.$message?.success(`AI 已自动整理形成风险分析记录表(新增 ${items.length} 项),支持手动修改`);
|
window.$message?.success(`AI 已自动整理形成风险分析记录表(新增 ${items.length} 项),支持手动修改`);
|
||||||
@@ -197,7 +178,27 @@ const confirmTab = (tab:string, val:boolean) => {
|
|||||||
emit('confirmTab', tab, val);
|
emit('confirmTab', tab, val);
|
||||||
}
|
}
|
||||||
|
|
||||||
defineExpose({form, generateRiskCheck})
|
// 获取HAZOP风险矩阵配置
|
||||||
|
const getHazopRiskMatrixConfig = async () => {
|
||||||
|
const {data,error} = await matrixConfigApi("HAZOP");
|
||||||
|
if(!error){
|
||||||
|
hazopRiskMatrixConfig.value = data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 获取JSA风险矩阵配置
|
||||||
|
const getJsaRiskMatrixConfig = async () => {
|
||||||
|
const {data,error} = await matrixConfigApi("JSA");
|
||||||
|
if(!error){
|
||||||
|
jsaRiskMatrixConfig.value = data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({form})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getHazopRiskMatrixConfig();
|
||||||
|
getJsaRiskMatrixConfig();
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -217,7 +218,7 @@ defineExpose({form, generateRiskCheck})
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid gap-2 grid-cols-4 mt-3">
|
<div class="grid gap-2 grid-cols-4 mt-3">
|
||||||
<div v-for="t in TOOL_CARDS" :key="t.key" :title="t.desc"
|
<div v-for="t in TOOL_CARDS" :key="t.key"
|
||||||
class="flex items-center gap-2 rounded-lg border px-3 py-2 transition"
|
class="flex items-center gap-2 rounded-lg border px-3 py-2 transition"
|
||||||
:class="pluginDone[t.key] !== undefined ? 'border-[var(--theme-color)] bg-[var(--theme-color)]' : tools.includes(t.key) ? 'border-[var(--theme-color)] bg-white' : 'border-slate-200 bg-white'"
|
:class="pluginDone[t.key] !== undefined ? 'border-[var(--theme-color)] bg-[var(--theme-color)]' : tools.includes(t.key) ? 'border-[var(--theme-color)] bg-white' : 'border-slate-200 bg-white'"
|
||||||
:style="{'--theme-color': themeStore.themeColor}"
|
:style="{'--theme-color': themeStore.themeColor}"
|
||||||
@@ -266,47 +267,113 @@ defineExpose({form, generateRiskCheck})
|
|||||||
<table class="w-full min-w-[960px] border-collapse text-xs">
|
<table class="w-full min-w-[960px] border-collapse text-xs">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th v-for="h in ['风险项', '场景描述', '固有风险', '现有措施', '建议措施', '剩余风险', '来源', '操作']" :key="h"
|
<th class="w-40 border bg-slate-50 px-2 py-1.5 text-[11px] font-medium text-slate-500">风险项</th>
|
||||||
class="border bg-slate-50 px-2 py-1.5 text-[11px] font-medium text-slate-500">{{ h }}</th>
|
<th class="w-80 border bg-slate-50 px-2 py-1.5 text-[11px] font-medium text-slate-500">场景描述</th>
|
||||||
|
<th class="w-16 border bg-slate-50 px-2 py-1.5 text-[11px] font-medium text-slate-500">固有风险</th>
|
||||||
|
<th class="w-60 border bg-slate-50 px-2 py-1.5 text-[11px] font-medium text-slate-500">现有措施</th>
|
||||||
|
<th class="w-60 border bg-slate-50 px-2 py-1.5 text-[11px] font-medium text-slate-500">建议措施</th>
|
||||||
|
<th class="w-16 border bg-slate-50 px-2 py-1.5 text-[11px] font-medium text-slate-500">剩余风险</th>
|
||||||
|
<th class="w-16 border bg-slate-50 px-2 py-1.5 text-[11px] font-medium text-slate-500">来源</th>
|
||||||
|
<th class="w-15 border bg-slate-50 px-2 py-1.5 text-[11px] font-medium text-slate-500">操作</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="(rec, i) in riskRecords" :key="i">
|
<tr v-for="(rec, i) in riskRecords" :key="i">
|
||||||
<td class="border px-1.5 py-1 align-top font-medium text-slate-700">
|
<td class="border px-1.5 py-1 align-top font-medium text-slate-700">
|
||||||
<Ed :v="rec.item" @update="(x) => updRec(i, { item: x })" />
|
<NInput type="textarea" size="small" v-model:value="rec.risk_item" :autosize="{minRows:1}" class="text-xs" />
|
||||||
</td>
|
</td>
|
||||||
<td class="border px-1.5 py-1 align-top"><Ed :v="rec.scene" @update="(x) => updRec(i, { scene: x })" /></td>
|
<td class="border px-1.5 py-1 align-top">
|
||||||
<td class="border px-1.5 py-1 text-center align-top">
|
<NInput type="textarea" size="small" v-model:value="rec.scene" :autosize="{minRows:1}" class="text-xs" />
|
||||||
<NTag v-if="rec.inherent==='高'" type="error" size="small" class="cursor-pointer" title="点击切换等级" @click="updRec(i, { inherent: lvlNext(rec.inherent) as '高' | '中' | '低' })">
|
|
||||||
{{ rec.inherent }}
|
|
||||||
</NTag>
|
|
||||||
<NTag v-if="rec.inherent==='中'" type="warning" size="small" class="cursor-pointer" title="点击切换等级" @click="updRec(i, { inherent: lvlNext(rec.inherent) as '高' | '中' | '低' })">
|
|
||||||
{{ rec.inherent }}
|
|
||||||
</NTag>
|
|
||||||
<NTag v-if="rec.inherent==='低'" type="success" size="small" class="cursor-pointer" title="点击切换等级" @click="updRec(i, { inherent: lvlNext(rec.inherent) as '高' | '中' | '低' })">
|
|
||||||
{{ rec.inherent }}
|
|
||||||
</NTag>
|
|
||||||
</td>
|
|
||||||
<td class="border px-1.5 py-1 align-top"><Ed :v="rec.existing" @update="(x) => updRec(i, { existing: x })" /></td>
|
|
||||||
<td class="border px-1.5 py-1 align-top"><Ed :v="rec.suggest" @update="(x) => updRec(i, { suggest: x })" /></td>
|
|
||||||
<td class="border px-1.5 py-1 text-center align-top">
|
|
||||||
<NTag v-if="rec.residual==='高'" type="error" size="small" class="cursor-pointer" title="点击切换等级" @click="updRec(i, { residual: lvlNext(rec.residual) as '高' | '中' | '低' })">
|
|
||||||
{{ rec.residual }}
|
|
||||||
</NTag>
|
|
||||||
<NTag v-if="rec.residual==='中'" type="warning" size="small" class="cursor-pointer" title="点击切换等级" @click="updRec(i, { residual: lvlNext(rec.residual) as '高' | '中' | '低' })">
|
|
||||||
{{ rec.residual }}
|
|
||||||
</NTag>
|
|
||||||
<NTag v-if="rec.residual==='低'" type="success" size="small" class="cursor-pointer" title="点击切换等级" @click="updRec(i, { residual: lvlNext(rec.residual) as '高' | '中' | '低' })">
|
|
||||||
{{ rec.residual }}
|
|
||||||
</NTag>
|
|
||||||
</td>
|
</td>
|
||||||
<td class="border px-1.5 py-1 text-center align-top">
|
<td class="border px-1.5 py-1 text-center align-top">
|
||||||
<NTag size="small" type="info">{{ rec.source }}</NTag>
|
<div v-if="rec.source==='HAZOP'">
|
||||||
|
<NTag
|
||||||
|
v-if="rec.l_value>0 && rec.s_value>0"
|
||||||
|
class="text-[11px]"
|
||||||
|
size="small"
|
||||||
|
:color="{
|
||||||
|
color: hazopRiskLevel(rec.l_value, rec.s_value)?.bg_color,
|
||||||
|
textColor: hazopRiskLevel(rec.l_value, rec.s_value)?.font_color,
|
||||||
|
borderColor: hazopRiskLevel(rec.l_value, rec.s_value)?.bg_color,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{hazopRiskLevel(rec.l_value, rec.s_value)?.name}}
|
||||||
|
</NTag>
|
||||||
|
</div>
|
||||||
|
<div v-if="rec.source==='JSA'">
|
||||||
|
<NTag
|
||||||
|
v-if="rec.l_value>0 && rec.s_value>0"
|
||||||
|
class="text-[11px]"
|
||||||
|
size="small"
|
||||||
|
:color="{
|
||||||
|
color: jsaRiskLevel(rec.l_value, rec.s_value)?.bg_color,
|
||||||
|
textColor: jsaRiskLevel(rec.l_value, rec.s_value)?.font_color,
|
||||||
|
borderColor: jsaRiskLevel(rec.l_value, rec.s_value)?.bg_color,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{jsaRiskLevel(rec.l_value, rec.s_value)?.name}}
|
||||||
|
</NTag>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="border px-1.5 py-1 align-top">
|
||||||
|
<NInput type="textarea" size="small" v-model:value="rec.safe" :autosize="{minRows:1}" class="text-xs" />
|
||||||
|
</td>
|
||||||
|
<td class="border px-1.5 py-1 align-top">
|
||||||
|
<NInput type="textarea" size="small" v-model:value="rec.suggestion" :autosize="{minRows:1}" class="text-xs" />
|
||||||
</td>
|
</td>
|
||||||
<td class="border px-1.5 py-1 text-center align-top">
|
<td class="border px-1.5 py-1 text-center align-top">
|
||||||
<NTag v-if="rec.pssr" size="small" :color="{ color: '#f5f3ff', textColor: '#7c3aed', borderColor: '#f5f3ff' }">已转 PSSR</NTag>
|
<div v-if="rec.source==='HAZOP'">
|
||||||
<NButton text size="tiny" color="#7c3aed" v-else class="hover:underline" @click="toPssr(i)">转 PSSR</NButton>
|
<NTag
|
||||||
<NButton text type="error" size="tiny" class="mt-2" @click="riskRecords = riskRecords.filter((_, j) => j !== i)">删除</NButton>
|
v-if="rec.l_value>0 && rec.s_value>0"
|
||||||
|
class="text-[11px]"
|
||||||
|
size="small"
|
||||||
|
:color="{
|
||||||
|
color: countHazopRiskLevel(rec)?.bg_color,
|
||||||
|
textColor: countHazopRiskLevel(rec)?.font_color,
|
||||||
|
borderColor: countHazopRiskLevel(rec)?.bg_color,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
{{countHazopRiskLevel(rec)?.name}}
|
||||||
|
</span>
|
||||||
|
</NTag>
|
||||||
|
</div>
|
||||||
|
<div v-if="rec.source==='JSA'">
|
||||||
|
<NTag
|
||||||
|
v-if="rec.l_value>0 && rec.s_value>0"
|
||||||
|
class="text-[11px]"
|
||||||
|
size="small"
|
||||||
|
:color="{
|
||||||
|
color: countJsaRiskLevel(rec)?.bg_color,
|
||||||
|
textColor: countJsaRiskLevel(rec)?.font_color,
|
||||||
|
borderColor: countJsaRiskLevel(rec)?.bg_color,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
{{countJsaRiskLevel(rec)?.name}}
|
||||||
|
</span>
|
||||||
|
</NTag>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="border px-1.5 py-1 text-center align-top">
|
||||||
|
<NTag size="small" type="info" class="text-11px">{{ rec.source }}</NTag>
|
||||||
|
</td>
|
||||||
|
<td class="border px-1.5 py-1 text-center align-top">
|
||||||
|
<NTag v-if="rec.to_pssr" size="small" :color="{ color: '#f5f3ff', textColor: '#7c3aed', borderColor: '#f5f3ff' }">已转 PSSR</NTag>
|
||||||
|
<NButton text size="tiny" color="#7c3aed" v-else class="hover:underline" @click="toPssr(Number(i))">转 PSSR</NButton>
|
||||||
|
<div>
|
||||||
|
<NPopconfirm
|
||||||
|
positive-text="确定"
|
||||||
|
:positiveButtonProps="{ size: 'tiny' }"
|
||||||
|
:negativeButtonProps="{ size: 'tiny' }"
|
||||||
|
@positive-click="delRec(Number(i))"
|
||||||
|
>
|
||||||
|
<template #trigger>
|
||||||
|
<NButton text type="error" size="tiny" class="mt-1 hover:underline">删除</NButton>
|
||||||
|
</template>
|
||||||
|
确定删除吗?
|
||||||
|
</NPopconfirm>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -325,22 +392,21 @@ defineExpose({form, generateRiskCheck})
|
|||||||
@save="saveDraft"
|
@save="saveDraft"
|
||||||
@submit="submitConfirm"
|
@submit="submitConfirm"
|
||||||
@confirmTab="confirmTab"
|
@confirmTab="confirmTab"
|
||||||
@regenerate="onRegenerate"
|
|
||||||
/>
|
/>
|
||||||
<!-- 插件抽屉 -->
|
<!-- 插件抽屉 -->
|
||||||
<NDrawer v-model:show="pluginDrawer" :width="1200" placement="right">
|
<NDrawer v-model:show="pluginDrawer" width="85%" placement="right">
|
||||||
<NDrawerContent>
|
<NDrawerContent>
|
||||||
<template v-if="currentPlugin === 'HAZOP'">
|
<template v-if="currentPlugin === 'HAZOP'">
|
||||||
<Hazop :isAiOpen="props.isAiOpen" :currentId="props.currentId" @close="closePlugin" @export="exportAll" />
|
<Hazop :isAiOpen="props.isAiOpen" :currentId="props.currentId" :hazopRiskMatrixConfig="hazopRiskMatrixConfig" @close="closePlugin" @toTable="exportAll" />
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="currentPlugin === 'JSA'">
|
<template v-else-if="currentPlugin === 'JSA'">
|
||||||
<Jsa @close="closePlugin" @export="exportAll" />
|
<Jsa @close="closePlugin" @toTable="exportAll" />
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="currentPlugin === 'SCL'">
|
<template v-else-if="currentPlugin === 'SCL'">
|
||||||
<Scl @close="closePlugin" @export="exportAll" />
|
<Scl @close="closePlugin" @toTable="exportAll" />
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="currentPlugin === 'RISK_CHECK'">
|
<template v-else-if="currentPlugin === 'RISK_CHECK'">
|
||||||
<RiskCheck v-model="riskChecks" :loading="riskCheckLoading" @close="closePlugin" @back="closePlugin" />
|
<RiskCheck @close="closePlugin" />
|
||||||
</template>
|
</template>
|
||||||
</NDrawerContent>
|
</NDrawerContent>
|
||||||
</NDrawer>
|
</NDrawer>
|
||||||
|
|||||||
Reference in New Issue
Block a user