hazop插件部分接口完成
This commit is contained in:
@@ -1,41 +1,106 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from "vue";
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { useMessage } from "naive-ui";
|
||||
import { Icon } from '@iconify/vue'
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import { AiError, aiHazopDeviation } from '@/service/api/ai';
|
||||
import Ed from "./Ed.vue";
|
||||
import { matrixConfigApi } from "@/service/api/system";
|
||||
import {
|
||||
hazopNodesApi,
|
||||
hazopNodeAddApi,
|
||||
hazopObjAddApi,
|
||||
hazopDevAddApi,
|
||||
hazopNodeEditApi,
|
||||
hazopObjEditApi,
|
||||
hazopDevEditApi,
|
||||
hazopNodeDelApi,
|
||||
hazopObjDelApi,
|
||||
hazopDevDelApi,
|
||||
hazopRowAddApi
|
||||
} 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'])
|
||||
const emit = defineEmits(['close','export']);
|
||||
|
||||
const loading = ref<boolean>(false);
|
||||
const treeLoading = ref<boolean>(false);
|
||||
// 删除确认弹窗
|
||||
const delModal = ref<boolean>(false);
|
||||
// 删除确定状态
|
||||
const delLoading = ref<boolean>(false);
|
||||
// 新增/编辑弹窗
|
||||
const editModal = ref<boolean>(false);
|
||||
// 当前弹框标题
|
||||
const currentModalTitle = ref<string>('');
|
||||
// 当前弹框类型
|
||||
const currentModalType = ref<string>('');
|
||||
// 当前弹框对象
|
||||
const currentModalObj = ref<string>('');
|
||||
// 弹窗保存状态
|
||||
const modalSaving = ref<boolean>(false);
|
||||
const name = ref<string>('');
|
||||
// 风险矩阵配置
|
||||
const riskMatrixConfig = ref<any>(null);
|
||||
const l_options = computed(() => {
|
||||
const newArr = riskMatrixConfig.value?.consequence?.map((item:{key:string,label:string}, index:number) => ({
|
||||
...item,
|
||||
key: Number(index + 1)
|
||||
}));
|
||||
return newArr;
|
||||
});
|
||||
const s_options = computed(() => {
|
||||
const newArr = riskMatrixConfig.value?.probability?.map((item:{key:string,label:string}, index:number) => ({
|
||||
...item,
|
||||
key: Number(index + 1)
|
||||
}));
|
||||
return newArr;
|
||||
});
|
||||
// 右键菜单状态
|
||||
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}>({
|
||||
type: '', // 'node' | 'obj' | 'dev'
|
||||
node: null,
|
||||
obj: null,
|
||||
dev: null,
|
||||
})
|
||||
// 当前节点
|
||||
const currentNode = computed(() => {
|
||||
const targetId = currentDev.value?.id;
|
||||
if (!targetId) return null;
|
||||
for (const top of nodes.value) {
|
||||
const matchedObj = top.objects?.find((obj:any) =>obj.deviations?.some((dev:any) => dev.id === targetId));
|
||||
if (matchedObj) return top;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
// 当前对象
|
||||
const currentObj = computed(() => {
|
||||
const targetId = currentDev.value?.id;
|
||||
if (!targetId) return null;
|
||||
for (const top of nodes.value) {
|
||||
const matchedObj = top.objects?.find((obj:any) =>obj.deviations?.some((dev:any) => dev.id === targetId));
|
||||
if (matchedObj) return matchedObj;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
// 当前偏离
|
||||
const currentDev = ref<any>(null);
|
||||
// 展开收起
|
||||
const collapsed = ref<Record<string, boolean>>({})
|
||||
// 节点列表
|
||||
const nodes = ref<any>([]);
|
||||
// 表格样式
|
||||
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: "变更后复核搅拌电流裕量,纳入交接班巡检" },
|
||||
@@ -55,14 +120,13 @@ 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 cur = computed(() => nodes.value[sel.value.n].devs[sel.value.d]);
|
||||
const devTotal = computed(() => nodes.value.reduce((a, n) => a + n.devs.length, 0));
|
||||
const devDone = computed(() => nodes.value.reduce((a, n) => a + n.devs.filter((d) => d.rows.length > 0).length, 0));
|
||||
const recTotal = computed(() => nodes.value.reduce((a, n) => a + n.devs.reduce((b, d) => b + d.rows.length, 0), 0));
|
||||
const devTotal = computed(() => 0);
|
||||
const devDone = computed(() => 0);
|
||||
const recTotal = computed(() => 0);
|
||||
|
||||
const toRec = (obj: string, dev: string, row: any): any => ({
|
||||
item: `${dev}(${obj})`,
|
||||
@@ -75,19 +139,19 @@ const toRec = (obj: string, dev: string, row: any): any => ({
|
||||
});
|
||||
|
||||
const updRow = (ri: number, patch: Partial<any>) => {
|
||||
const row = cur.value.rows[ri];
|
||||
const row = currentDev.value.rows[ri];
|
||||
if (row) Object.assign(row, patch);
|
||||
};
|
||||
const clampLS = (v: string) => Math.max(1, Math.min(5, +v || 1));
|
||||
|
||||
const runAi = async () => {
|
||||
if (cur.value.rows.length) return message.info("当前偏离已有分析记录,可手动新增行补充");
|
||||
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: cur.value.dev,
|
||||
deviation: currentDev.value.name,
|
||||
object_name: node.obj,
|
||||
change_context: {
|
||||
change: "缩合反应温度及时间优化",
|
||||
@@ -99,30 +163,21 @@ const runAi = async () => {
|
||||
safe: r.safeguards ?? "", sug: r.suggestions ?? "",
|
||||
}));
|
||||
if (!rows.length) throw new AiError(0, "AI 未返回分析结果");
|
||||
cur.value.rows = rows;
|
||||
message.success(`AI 已完成「${cur.value.dev}」偏离分析,生成 ${rows.length} 条记录(支持手动修改)`);
|
||||
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[cur.value.dev] ?? [
|
||||
const gen = HAZOP_AI[currentDev.value.name] ?? [
|
||||
{ cause: "(AI 生成)偏离场景原因分析", cons: "(AI 生成)可能后果描述", l: 2, s: 3, safe: "现有保护层说明", sug: "建议补充的管控措施" },
|
||||
];
|
||||
cur.value.rows = gen.map((x: any) => ({ ...x }));
|
||||
currentDev.value.rows = gen.map((x: any) => ({ ...x }));
|
||||
} finally {
|
||||
analyzing.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const addRow = () => cur.value.rows.push({ cause: "", cons: "", l: 2, s: 3, safe: "", sug: "" });
|
||||
const delRow = (ri: number) => cur.value.rows.splice(ri, 1);
|
||||
const exportAll = () => {
|
||||
emit("export", nodes.value.flatMap((n) => n.devs.flatMap((d) => d.rows.map((row) => toRec(n.obj, d.dev, row)))), "ai");
|
||||
}
|
||||
const save = () => {
|
||||
message.success("已手动保存,分析记录数已同步至工具卡片(内容修改实时自动保存,退出不丢失)");
|
||||
};
|
||||
|
||||
const ANALYSIS_POOL = [
|
||||
{ name: "张工艺", org: "一车间", post: "工艺工程师" },
|
||||
{ name: "王仪表", org: "技术部", post: "仪表工程师" },
|
||||
@@ -133,173 +188,588 @@ const ANALYSIS_POOL = [
|
||||
{ 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) => {
|
||||
|
||||
// 选择分析人员弹窗
|
||||
const openSelectWorker = ref(false);
|
||||
// 搜索分析人员
|
||||
const searchWorker = ref("");
|
||||
// 分析人员列表
|
||||
const list = computed(() => ANALYSIS_POOL.filter((x) => !searchWorker.value || x.name.includes(searchWorker.value)));
|
||||
|
||||
// 选择分析人员
|
||||
const selectWorker = (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" };
|
||||
|
||||
// 根据l和s获取风险等级
|
||||
const riskLevel = (l: number, s: number) => {
|
||||
const num = l * s;
|
||||
for (let i = 0; i < riskMatrixConfig.value?.risk_grades.length; i++) {
|
||||
const item = riskMatrixConfig.value?.risk_grades[i];
|
||||
if (num >= item.min && num <= item.max) {
|
||||
return item; // 返回整条数据
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// 新增行
|
||||
const addRow = async () => {
|
||||
console.log(l_options.value)
|
||||
console.log(s_options.value)
|
||||
loading.value = true;
|
||||
const {data,error} = await hazopRowAddApi(currentDev.value?.id,{
|
||||
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: "",
|
||||
})
|
||||
if(!error){
|
||||
console.log(data)
|
||||
// getHazopNodes();
|
||||
window.$message?.success("新增行成功")
|
||||
}
|
||||
loading.value = false;
|
||||
};
|
||||
// 删除行
|
||||
const delRow = (index: number) => currentDev.value.rows.splice(index, 1);
|
||||
|
||||
// 展开收起
|
||||
const leftToggle = (id: string) => {
|
||||
collapsed.value = { ...collapsed.value, [id]: !collapsed.value[id] }
|
||||
}
|
||||
|
||||
// 保存
|
||||
const save = () => {
|
||||
window.$message?.success("已手动保存,分析记录数已同步至工具卡片(内容修改实时自动保存,退出不丢失)");
|
||||
};
|
||||
|
||||
// AI整理如表
|
||||
const exportAll = () => {
|
||||
emit("export", nodes.value.flatMap((n) => n.devs.flatMap((d) => d.rows.map((row) => toRec(n.obj, d.name, row)))), "ai");
|
||||
}
|
||||
|
||||
// 导出 Word
|
||||
const exportWord = () => {
|
||||
window.$message?.success('HAZOP 分析报告已导出(Word)')
|
||||
}
|
||||
|
||||
// 打开弹窗
|
||||
const openModal = (type: string,obj: string, title: string,editName: string = '') => {
|
||||
if(type==='add'){
|
||||
name.value = '';
|
||||
}else{
|
||||
name.value = editName;
|
||||
}
|
||||
currentModalType.value = type;
|
||||
currentModalObj.value = obj;
|
||||
currentModalTitle.value = title;
|
||||
editModal.value = true;
|
||||
}
|
||||
|
||||
// 新增/编辑保存
|
||||
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;
|
||||
}
|
||||
}
|
||||
// 编辑节点
|
||||
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;
|
||||
}
|
||||
}
|
||||
// 新增分析对象
|
||||
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;
|
||||
}
|
||||
}
|
||||
// 编辑分析对象
|
||||
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;
|
||||
}
|
||||
}
|
||||
// 新增偏离
|
||||
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;
|
||||
}
|
||||
}
|
||||
// 编辑偏离
|
||||
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;
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
// 删除分析对象
|
||||
if(currentModalType.value==='obj'){
|
||||
const {error} = await hazopObjDelApi(contextTarget.value?.obj?.id);
|
||||
if(!error){
|
||||
getHazopNodes();
|
||||
window.$message?.success("删除分析对象成功")
|
||||
delModal.value = false;
|
||||
}
|
||||
}
|
||||
// 删除偏离
|
||||
if(currentModalType.value==='dev'){
|
||||
const {error} = await hazopDevDelApi(contextTarget.value?.dev?.id);
|
||||
if(!error){
|
||||
getHazopNodes();
|
||||
window.$message?.success("删除偏离成功")
|
||||
delModal.value = false;
|
||||
}
|
||||
}
|
||||
delLoading.value = false;
|
||||
}
|
||||
|
||||
// 菜单选项配置(根据不同层级返回不同菜单)
|
||||
const getMenuOptions = (type: string) => {
|
||||
const nodeOptions = [
|
||||
{ label: '新增分析对象', key: 'addObj' },
|
||||
{ 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,
|
||||
dev: devOptions,
|
||||
}
|
||||
return map[type] || []
|
||||
}
|
||||
|
||||
// 计算当前菜单选项
|
||||
const contextMenuOptions = computed(() => {
|
||||
return getMenuOptions(contextTarget.value.type)
|
||||
})
|
||||
// 点击页面其他区域关闭菜单(可选)
|
||||
const closeMenu = () => {
|
||||
contextMenuShow.value = false
|
||||
}
|
||||
|
||||
// 右键菜单事件处理
|
||||
const handleContextMenu = (event: MouseEvent, type: string, node: any, obj = null, dev = null) => {
|
||||
// 阻止浏览器默认右键菜单
|
||||
event.preventDefault()
|
||||
// 记录目标数据
|
||||
contextTarget.value = { type, node, obj, dev }
|
||||
// 计算菜单位置(防止超出视口)
|
||||
const menuWidth = 200 // 预估菜单宽度
|
||||
const menuHeight = 300 // 预估菜单高度
|
||||
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
|
||||
}
|
||||
// 菜单选中回调
|
||||
// ============================================================
|
||||
const handleMenuSelect = (key: string) => {
|
||||
const { type, node, obj, dev } = contextTarget.value
|
||||
switch (key) {
|
||||
// ---------- 节点操作 ----------
|
||||
case 'addObj':
|
||||
// 新增分析对象
|
||||
openModal('add','obj','新增分析对象');
|
||||
break
|
||||
case 'editNode':
|
||||
// 编辑节点
|
||||
openModal('edit','node','编辑节点',node?.node_name);
|
||||
break
|
||||
case 'delNode':
|
||||
console.log('[删除节点] 节点:', node?.node_name)
|
||||
// 删除
|
||||
openDelModal('node');
|
||||
break
|
||||
// ---------- 对象操作 ----------
|
||||
case 'addDev':
|
||||
// 新增偏离
|
||||
openModal('add','dev','新增偏离');
|
||||
break
|
||||
case 'editObj':
|
||||
// 编辑分析对象
|
||||
openModal('edit','obj','编辑分析对象',obj?.obj_name);
|
||||
break
|
||||
case 'delObj':
|
||||
// 删除
|
||||
openDelModal('obj');
|
||||
break
|
||||
// ---------- 偏离操作 ----------
|
||||
case 'editDev':
|
||||
// 编辑偏离
|
||||
openModal('edit','dev','编辑偏离',dev?.deviation);
|
||||
break
|
||||
case 'delDev':
|
||||
// 删除
|
||||
openDelModal('dev');
|
||||
break
|
||||
default:
|
||||
console.warn('未知菜单项:', key)
|
||||
}
|
||||
// 关闭菜单
|
||||
closeMenu()
|
||||
}
|
||||
|
||||
// 监听全局点击关闭菜单
|
||||
// 注意:这里需要排除菜单自身,使用 Naive UI 的 mask 或手动处理
|
||||
// 简单起见,我们利用 n-dropdown 的 mask: false,并在外部点击时关闭
|
||||
// 但更好的方式是监听 document 的 click 事件
|
||||
if (typeof window !== 'undefined') {
|
||||
document.addEventListener('click', (e) => {
|
||||
// 如果点击的不是菜单内部,关闭菜单
|
||||
const menuEl = document.querySelector('.n-dropdown')
|
||||
if (menuEl && !menuEl.contains(e.target as Node)) {
|
||||
closeMenu()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 获取风险矩阵配置
|
||||
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节点列表
|
||||
const getHazopNodes = async () => {
|
||||
treeLoading.value = true;
|
||||
const {data,error} = await hazopNodesApi(props.currentId);
|
||||
if(!error){
|
||||
nodes.value = data;
|
||||
}
|
||||
treeLoading.value = false;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getRiskMatrixConfig();
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<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
|
||||
}"
|
||||
>
|
||||
<NButton size="small" ghost type="primary" class="text-xs" @click="emit('back')">
|
||||
<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)]">HAZOP 分析插件</span>
|
||||
<NTag size="small" type="info">插件</NTag>
|
||||
<NTag size="small">MOC 变更:缩合反应温度及时间优化(演示)</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="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('HAZOP 分析报告已导出(Word)')">
|
||||
<Icon icon="material-symbols:download" class="size-14px" /> 导出 Word
|
||||
</NButton>
|
||||
<NButton size="small" type="primary" class="text-xs" :disabled="analyzing" @click="runAi">
|
||||
<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
|
||||
}"
|
||||
>
|
||||
<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)]">HAZOP 分析插件</span>
|
||||
<NTag size="small" type="info">插件</NTag>
|
||||
<NTag size="small">MOC 变更:缩合反应温度及时间优化(演示)</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" /> 分析组成员 · 已选 {{ sels.length }} 人
|
||||
</NButton>
|
||||
<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">已选 {{ sels.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 list" :key="x.name" @click="selectWorker(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 v-if="isAiOpen" 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="exportWord">
|
||||
<Icon icon="material-symbols:download" class="size-14px" /> 导出 Word
|
||||
</NButton>
|
||||
<NButton v-if="isAiOpen" size="small" type="primary" class="text-xs" :disabled="analyzing" @click="runAi">
|
||||
<Icon v-if="analyzing" icon="ri:loader-4-fill" class="size-14px animate-spin" />
|
||||
<Icon v-else icon="lucide:sparkles" class="size-12px" />
|
||||
AI 分析当前偏离
|
||||
</NButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 text-xs text-slate-500">
|
||||
<span>当前位置:</span>
|
||||
<NTag size="small">{{ nodes[sel.n].name }}</NTag>
|
||||
<span>›</span>
|
||||
<NTag size="small" type="success">分析对象:{{ nodes[sel.n].obj }}</NTag>
|
||||
<span>›</span>
|
||||
<NTag size="small" type="warning">偏离:{{ cur.dev }}</NTag>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex gap-3"
|
||||
:style="{
|
||||
'--theme-color': themeStore.themeColor,
|
||||
'--success-color': themeStore.otherColor.success
|
||||
}"
|
||||
>
|
||||
<!-- 左:节点 / 偏离树 -->
|
||||
<div class="shrink-0 rounded-lg border bg-white p-2">
|
||||
<div v-for="(n, i) in nodes" :key="n.name" class="mb-1">
|
||||
<div class="px-2 py-1.5 text-xs font-semibold text-slate-700">{{ n.name }}</div>
|
||||
<div v-for="(d, j) in n.devs" :key="d.dev" @click="sel = { n: i, d: j }"
|
||||
class="cursor-pointer flex w-full items-center gap-1.5 rounded-md px-3 py-1.5 text-left text-xs transition"
|
||||
:class="sel.n === i && sel.d === j ? 'bg-[var(--theme-color)] text-white' : 'text-slate-600 hover:bg-slate-100'">
|
||||
<span class="flex-1">{{ d.dev }}</span>
|
||||
<span class="rounded-full px-1.5 text-[10px]"
|
||||
:class="d.rows.length
|
||||
? (sel.n === i && sel.d === j ? 'bg-white/25 text-white' : 'bg-emerald-100 text-[var(--success-color)]')
|
||||
: (sel.n === i && sel.d === j ? 'bg-white/30 text-white' : 'bg-slate-100 text-slate-400')">
|
||||
{{ d.rows.length ? `${d.rows.length} 条` : "待分析" }}
|
||||
</span>
|
||||
</NButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 右:HAZOP 记录表 -->
|
||||
<div class="min-w-0 flex-1 bg-white">
|
||||
<div class="flex items-center gap-2 pb-2">
|
||||
<NButton size="small" ghost type="primary" class="text-xs" @click="addRow">
|
||||
<Icon icon="ic:round-plus" class="size-14px" /> 新增行
|
||||
</NButton>
|
||||
<span class="text-[11px] text-slate-400">单元格点击即可修改;L / S 取值 1-5,风险等级按风险矩阵实时重算;「入表」将本条选入风险分析记录表</span>
|
||||
</div>
|
||||
<table class="w-full min-w-[860px] border-collapse text-xs">
|
||||
<thead>
|
||||
<tr>
|
||||
<th :class="TH" class="w-16">操作</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>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-if="cur.rows.length === 0">
|
||||
<td colspan="8" class="px-3 py-8 text-center text-xs text-slate-400">
|
||||
当前偏离暂无分析记录,点击右上角「AI 分析当前偏离」自动生成,或「新增行」手动填写
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-for="(row, ri) in cur.rows" :key="ri">
|
||||
<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="message.success('已选入 1 条风险记录,可继续挑选或返回查看记录表')">入表
|
||||
</NButton>
|
||||
<NButton text type="error" title="删除行" @click="delRow(ri)">
|
||||
<Icon icon="ep:delete" class="size-14px" />
|
||||
</NButton>
|
||||
|
||||
<div class="flex items-center gap-2 text-xs text-slate-500">
|
||||
<span>当前位置:</span>
|
||||
<NTag size="small">节点:{{ currentNode?.node_name ?? '未选择' }}</NTag>
|
||||
<span>›</span>
|
||||
<NTag size="small" type="success">分析对象:{{ currentObj?.obj_name ?? '未选择' }}</NTag>
|
||||
<span>›</span>
|
||||
<NTag size="small" type="warning">偏离:{{ currentDev?.deviation ?? '未选择' }}</NTag>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex gap-3"
|
||||
:style="{
|
||||
'--theme-color': themeStore.themeColor,
|
||||
'--success-color': themeStore.otherColor.success
|
||||
}"
|
||||
>
|
||||
<!-- 左:节点 / 偏离树 -->
|
||||
<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>
|
||||
</td>
|
||||
<td :class="TD"><Ed :v="row.cause" @update="(x) => updRow(ri, { cause: x })" /></td>
|
||||
<td :class="TD"><Ed :v="row.cons" @update="(x) => updRow(ri, { cons: x })" /></td>
|
||||
<td :class="TD" class="text-center">
|
||||
<input :value="row.l" @change="(e) => updRow(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(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(ri, { safe: x })" /></td>
|
||||
<td :class="TD"><Ed :v="row.sug" @update="(x) => updRow(ri, { sug: x })" /></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</NSpin>
|
||||
</div>
|
||||
<!-- 右:HAZOP 记录表 -->
|
||||
<div class="min-w-0 flex-1 bg-white">
|
||||
<div class="flex items-center gap-2 pb-2">
|
||||
<NButton size="small" ghost type="primary" class="text-xs" @click="addRow">
|
||||
<Icon icon="ic:round-plus" class="size-14px" /> 新增行
|
||||
</NButton>
|
||||
<span class="text-[11px] text-slate-400">单元格点击即可修改;风险等级按风险矩阵L / S 取值实时重算;「入表」将本条选入风险分析记录表</span>
|
||||
</div>
|
||||
<table class="w-full min-w-[860px] border-collapse text-xs">
|
||||
<thead>
|
||||
<tr>
|
||||
<th :class="TH" class="w-16">操作</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>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-if="currentDev?.rows?.length === 0">
|
||||
<td colspan="8" class="px-3 py-8 text-center text-xs text-slate-400">
|
||||
当前偏离暂无分析记录,点击右上角「AI 分析当前偏离」自动生成,或「新增行」手动填写
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-for="(row, ri) in currentDev?.rows" :key="ri">
|
||||
<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="message.success('已选入 1 条风险记录,可继续挑选或返回查看记录表')">入表
|
||||
</NButton>
|
||||
<NButton text type="error" title="删除行" @click="delRow(Number(ri))">
|
||||
<Icon icon="ep:delete" class="size-14px" />
|
||||
</NButton>
|
||||
</div>
|
||||
</td>
|
||||
<td :class="TD">
|
||||
<Ed :v="row.cause" @update="(x) => updRow(Number(ri), { cause: x })" />
|
||||
</td>
|
||||
<td :class="TD">
|
||||
<Ed :v="row.consequence" @update="(x) => updRow(Number(ri), { consequence: x })" />
|
||||
</td>
|
||||
<td :class="TD" class="text-center">
|
||||
<NInputNumber v-model:value="row.lvalue" size="tiny" :min="l_options[0].key" :max="l_options[l_options.length-1].key" :show-button="false" class="text-center w-10" />
|
||||
</td>
|
||||
<td :class="TD" class="text-center">
|
||||
<NInputNumber v-model:value="row.svalue" size="tiny" :min="s_options[0].key" :max="l_options[s_options.length-1].key" :show-button="false" class="text-center w-10" />
|
||||
</td>
|
||||
<td :class="TD" class="text-center">
|
||||
<span class="inline-block rounded px-1.5 py-0.5 text-[11px] font-medium">
|
||||
{{ row.risk_value }}
|
||||
</span>
|
||||
</td>
|
||||
<td :class="TD">
|
||||
<Ed :v="row.safeguards" @update="(x) => updRow(Number(ri), { safeguards: x })" />
|
||||
</td>
|
||||
<td :class="TD">
|
||||
<Ed :v="row.suggestions" @update="(x) => updRow(Number(ri), { suggestions: x })" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</NSpin>
|
||||
<!-- ========== 右键菜单 ========== -->
|
||||
<NDropdown
|
||||
v-model:show="contextMenuShow"
|
||||
:x="contextMenuX"
|
||||
:y="contextMenuY"
|
||||
:options="contextMenuOptions"
|
||||
@select="handleMenuSelect"
|
||||
placement="bottom-start"
|
||||
trigger="manual"
|
||||
:mask="false"
|
||||
/>
|
||||
<!-- 新增/编辑弹窗 -->
|
||||
<NModal
|
||||
v-model:show="editModal"
|
||||
preset="card"
|
||||
:title="currentModalTitle"
|
||||
:auto-focus="false"
|
||||
:style="{ width: '450px', height: 'auto' }"
|
||||
:segmented="{ content: true, footer: true }"
|
||||
>
|
||||
<div>
|
||||
<NInput v-model:value="name" placeholder="请输入名称" />
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
</template>
|
||||
</NModal>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.scroll {
|
||||
height: calc(100vh - 200px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -12,7 +12,7 @@ import Footer from './footer.vue'
|
||||
|
||||
const themeStore = useThemeStore();
|
||||
|
||||
const props = defineProps(['type','isAiOpen','title','currentId'])
|
||||
const props = defineProps(['type','isAiOpen','title','currentId','formInfo'])
|
||||
const emit = defineEmits(['save','submit','confirmTab']);
|
||||
|
||||
const form = ref<{ content: string }>({
|
||||
@@ -105,9 +105,7 @@ const toPssr = (i: number) => {
|
||||
const updRec = (i: number, patch: Partial<any>) => {
|
||||
riskRecords.value = riskRecords.value.map((x, j) => (j === i ? { ...x, ...patch } : x));
|
||||
};
|
||||
const onBack = () => {
|
||||
pluginDrawer.value = false;
|
||||
}
|
||||
|
||||
const exportAll = (items: any[], mode: "manual" | "ai") => {
|
||||
riskRecords.value = [...riskRecords.value, ...items];
|
||||
if (mode === "ai") {
|
||||
@@ -119,6 +117,16 @@ const exportAll = (items: any[], mode: "manual" | "ai") => {
|
||||
}
|
||||
}
|
||||
|
||||
// 使用插件弹框
|
||||
const usePlugin = (key: string) => {
|
||||
currentPlugin.value = key;
|
||||
pluginDrawer.value = true;
|
||||
}
|
||||
// 关闭插件弹框
|
||||
const closePlugin = () => {
|
||||
pluginDrawer.value = false;
|
||||
}
|
||||
|
||||
// 保存草稿
|
||||
const saveDraft = () => {
|
||||
emit('save');
|
||||
@@ -144,12 +152,12 @@ defineExpose({form})
|
||||
<Icon icon="lucide:shield-alert" class="size-16px" />
|
||||
风险识别工具(按变更等级匹配)
|
||||
</span>
|
||||
<!-- <NTag size="small" :type="form?.level === '重要' ? 'error' : 'default'">
|
||||
{{ form?.level === '重要' ? '重要变更' : '一般变更' }}
|
||||
<NTag size="small" :type="formInfo?.change_level === 2 ? 'error' : 'default'">
|
||||
{{ formInfo?.change_level === 2 ? '重要变更' : '一般变更' }}
|
||||
</NTag>
|
||||
<span class="text-xs text-slate-400">
|
||||
{{ form?.level === '重要' ? '规则:HAZOP 必做,同时开展 JSA 与检查表法(SCL)' : '规则:JSA + 检查表法(SCL),涉及工艺安全边界的应升级 HAZOP' }};可在申请表「风险分析」栏手动重选
|
||||
</span> -->
|
||||
{{ formInfo?.change_level === 2 ? '规则:HAZOP 必做,同时开展 JSA 与检查表法(SCL)' : '规则:JSA + 检查表法(SCL),涉及工艺安全边界的应升级 HAZOP' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="grid gap-2 grid-cols-4 mt-3">
|
||||
<div v-for="t in TOOL_CARDS" :key="t.key" :title="t.desc"
|
||||
@@ -170,8 +178,7 @@ defineExpose({form})
|
||||
{{ pluginDone[t.key] !== undefined ? `已使用并分析了 ${pluginDone[t.key]} 条内容` : '未使用此工具分析' }}
|
||||
</div>
|
||||
</div>
|
||||
<NButton size="small" class="text-xs" :type="pluginDone[t.key] !== undefined ? 'default' : tools.includes(t.key) ? 'primary' : 'default'"
|
||||
>
|
||||
<NButton size="small" class="text-xs" type="primary" @click="usePlugin(t.key)">
|
||||
使用
|
||||
</NButton>
|
||||
</div>
|
||||
@@ -190,7 +197,7 @@ defineExpose({form})
|
||||
<Icon icon="tabler:clipboard-check" class="size-14px" />
|
||||
全部转 PSSR 行动项
|
||||
</NButton>
|
||||
<NButton ghost size="small" type="primary" class="text-xs" @click="aiOrganize">
|
||||
<NButton v-if="isAiOpen" ghost size="small" type="primary" class="text-xs" @click="aiOrganize">
|
||||
<Icon icon="lucide:sparkles" class="size-12px" />
|
||||
AI 自动整理
|
||||
</NButton>
|
||||
@@ -266,16 +273,16 @@ defineExpose({form})
|
||||
<NDrawer v-model:show="pluginDrawer" :width="1200" placement="right">
|
||||
<NDrawerContent>
|
||||
<template v-if="currentPlugin === 'HAZOP'">
|
||||
<Hazop @back="onBack" @export="exportAll" />
|
||||
<Hazop :isAiOpen="props.isAiOpen" :currentId="props.currentId" @close="closePlugin" @export="exportAll" />
|
||||
</template>
|
||||
<template v-else-if="currentPlugin === 'JSA'">
|
||||
<Jsa @back="onBack" @export="exportAll" />
|
||||
<Jsa @close="closePlugin" @export="exportAll" />
|
||||
</template>
|
||||
<template v-else-if="currentPlugin === 'SCL'">
|
||||
<Scl @back="onBack" @export="exportAll" />
|
||||
<Scl @close="closePlugin" @export="exportAll" />
|
||||
</template>
|
||||
<template v-else-if="currentPlugin === 'RISK_CHECK'">
|
||||
<RiskCheck @back="onBack" />
|
||||
<RiskCheck @close="closePlugin" />
|
||||
</template>
|
||||
</NDrawerContent>
|
||||
</NDrawer>
|
||||
|
||||
Reference in New Issue
Block a user