jsa插件完成

This commit is contained in:
2026-08-26 15:21:15 +08:00
parent 80cc999687
commit b0144d190b
3 changed files with 287 additions and 1 deletions
+4
View File
@@ -9,6 +9,7 @@ import { getColorWithOpacity } from "@/utils/common";
import HighlightText from "./modules/HighlightText.vue"; import HighlightText from "./modules/HighlightText.vue";
import Ed from "./modules/Ed.vue"; import Ed from "./modules/Ed.vue";
import Hazop from "./modules/hazop.vue"; import Hazop from "./modules/hazop.vue";
import Jsa from "./modules/jsa.vue";
const themeStore = useThemeStore(); const themeStore = useThemeStore();
@@ -1649,6 +1650,9 @@ const onBack = () => {
<template v-if="currentPlugin === 'HAZOP'"> <template v-if="currentPlugin === 'HAZOP'">
<Hazop @back="onBack" @export="exportAll" /> <Hazop @back="onBack" @export="exportAll" />
</template> </template>
<template v-else-if="currentPlugin === 'JSA'">
<Jsa @back="onBack" @export="exportAll" />
</template>
</NDrawerContent> </NDrawerContent>
</NDrawer> </NDrawer>
</NCard> </NCard>
@@ -177,7 +177,7 @@ const rrOf = (l: number, s: number) => {
<NButton size="small" ghost type="primary" class="text-xs" :disabled="recTotal === 0" @click="exportAll"> <NButton size="small" ghost type="primary" class="text-xs" :disabled="recTotal === 0" @click="exportAll">
<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="message.success('演示:HAZOP 分析报告已导出(Word')"> <NButton size="small" ghost type="primary" class="text-xs" @click="message.success('HAZOP 分析报告已导出(Word')">
<Icon icon="material-symbols:download" class="size-14px" /> 导出 Word <Icon icon="material-symbols:download" class="size-14px" /> 导出 Word
</NButton> </NButton>
<NButton size="small" type="primary" class="text-xs" :disabled="analyzing" @click="runAi"> <NButton size="small" type="primary" class="text-xs" :disabled="analyzing" @click="runAi">
+282
View File
@@ -0,0 +1,282 @@
<script setup lang="ts">
import { computed, ref } from "vue";
import { useMessage } from "naive-ui";
import { Icon } from '@iconify/vue'
import { useThemeStore } from '@/store/modules/theme';
import Ed from "./Ed.vue";
const themeStore = useThemeStore();
const message = useMessage();
const emit = defineEmits<{
back: [];
done: [records: number];
export: [items: any[], mode: "manual" | "ai"];
save: [];
}>();
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: "节点2DCS 温控与联锁", obj: "DCS 温控回路及联锁", devs: [
{ dev: "报警 / 联锁值设置不当", rows: [
{ cause: "报警 / 联锁值上调后(高报 85→95℃、联锁 90→100℃),原有报警裕度改变", cons: "误报、漏报导致操作人员对真实超温失去预警", l: 2, s: 4, safe: "参数修改执行申请-复核双人确认;修改清单与变更单逐项核对", sug: "联锁逻辑图与台账同步更新,纳入变更资料归档" },
] },
{ dev: "联锁拒动 / 误动", rows: [] },
] },
];
const HAZOP_AI:any = {
"搅拌失效": [
{ cause: "搅拌器机械故障或变频器跳闸,釜内物料传热不均", cons: "局部过热引发暴沸冲料;温度测量失真误导操作", l: 2, s: 4, safe: "搅拌电流 DCS 在线监视与低电流报警;跳闸联锁停进料", sug: "变更后复核搅拌电流裕量,纳入交接班巡检" },
],
"冷却水中断": [
{ cause: "冷却水总管压力波动或阀门误关", cons: "撤热失效,反应温度快速上升溜温", l: 2, s: 5, safe: "冷却水流量低报警;紧急切断进料联锁", sug: "核实紧急冷却旁路可用性,写入超温应急处置卡" },
],
"联锁拒动 / 误动": [
{ cause: "联锁设定值修改后未重新整定验证,或逻辑修改错误", cons: "超温时联锁拒动失去最后保护层;误动造成非计划停车", l: 1, s: 5, safe: "投用前 100% 联锁传动测试,动作正确率确认", sug: "联锁测试记录归档至变更资料;修改执行双人复核" },
],
};
const inherentOf = (l: number, s: number): "高" | "中" | "低" => {
const v = l * s;
return v >= 10 ? "高" : v >= 5 ? "中" : "低";
}
const lowerOf = (lv: "高" | "中" | "低"): "高" | "中" | "低" => {
return lv === "高" ? "中" : "低";
}
const nodes = ref<any>(JSON.parse(JSON.stringify(HAZOP_INIT)));
const sel = ref({ n: 0, d: 0 });
const analyzing = ref(false);
const cur = computed(() => nodes.value[sel.value.n].devs[sel.value.d]);
const recTotal = computed(() => nodes.value.reduce((a, n) => a + n.devs.reduce((b, d) => b + d.rows.length, 0), 0));
const updRow = (ri: number, patch: Partial<any>) => {
const row = cur.value.rows[ri];
if (row) Object.assign(row, patch);
};
const clampLS = (v: string) => Math.max(1, Math.min(5, +v || 1));
const runAi = () => {
analyzing.value = true;
setTimeout(() => {
steps.value[steps.value.length - 1].rows.push({
hazard: "新版操作规程执行不熟练", cons: "误操作导致温度 / 时间偏离工艺窗口", l: 2, s: 3,
safe: "培训考核合格后上岗;首批执行班长旁站确认", sug: "投用首周班长复核关键操作并记录",
});
analyzing.value = false;
message.success("AI 危害因素分析完成:补充 1 条记录(支持手动修改)");
}, 1100);
};
const addRow = () => steps.value[steps.value.length - 1].rows.push({ hazard: "", cons: "", l: 2, s: 3, safe: "", sug: "" });
const delRow = (si: number, ri: number) => steps.value[si].rows.splice(ri, 1);
const exportAll = () => emit("export", steps.value.flatMap((s) => s.rows.map((row) => toRec(s.step, row))), "ai");
const save = () => {
message.success("已手动保存,分析记录数已同步至工具卡片(内容修改实时自动保存,退出不丢失)");
};
const ANALYSIS_POOL = [
{ name: "张工艺", org: "一车间", post: "工艺工程师" },
{ name: "王仪表", org: "技术部", post: "仪表工程师" },
{ name: "周设备", org: "设备科", post: "设备工程师" },
{ name: "刘文静", org: "技术部", post: "工艺员" },
{ name: "王海燕", org: "安全环保部", post: "安全工程师" },
{ name: "李主任", org: "一车间", post: "车间主任" },
{ name: "吴海涛", org: "设备科", post: "维修班长" },
{ name: "陈国栋", org: "一车间", post: "班长" },
];
const open = ref(false);
const q = ref("");
const sels = ref<string[]>(["张工艺", "王仪表"]);
const list = computed(() => ANALYSIS_POOL.filter((x) => !q.value || x.name.includes(q.value)));
const toggle = (n: string) => {
sels.value = sels.value.includes(n) ? sels.value.filter((x) => x !== n) : [...sels.value, n];
};
const rrOf = (l: number, s: number) => {
const v = l * s;
if (v >= 15) return { t: `重大 (${v})`, c: "bg-red-500 text-white" };
if (v >= 10) return { t: `较大 (${v})`, c: "bg-[#F5A623] text-white" };
if (v >= 5) return { t: `一般 (${v})`, c: "bg-amber-300 text-amber-900" };
return { t: `低 (${v})`, c: "bg-emerald-500 text-white" };
}
const steps = ref([
{ step: "步骤1:作业准备与票证办理", desc: "作业方案交底、作业许可办理、能量状态确认", rows: [
{ hazard: "高温介质 / 蒸汽意外释放", cons: "隔离确认不到位导致人员灼烫", l: 2, s: 4, safe: "作业前确认釜体温度 ≤40℃、管线泄压排空;办理作业票证并现场交底", sug: "隔离点挂牌上锁(LOTO),作业前双人确认" },
] },
{ step: "步骤2:旧保温拆除与新保温安装", desc: "脚手架登高作业,保温棉拆除与更换", rows: [
{ hazard: "登高坠落", cons: "脚手架作业人员坠落伤害", l: 3, s: 3, safe: "脚手架验收合格挂牌;全程系挂双钩安全带", sug: "下方设警戒区,禁止交叉作业" },
{ hazard: "保温纤维粉尘吸入与皮肤刺激", cons: "硅酸铝纤维引起呼吸道与皮肤职业健康危害", l: 3, s: 2, safe: "佩戴防尘口罩、护目镜与防护服;湿法拆除降尘", sug: "作业后更衣清洗,废料袋装化处置" },
] },
{ step: "步骤3DCS 参数修改与联锁测试", desc: "报警 / 联锁值上调,联锁传动试验", rows: [
{ hazard: "参数误改导致报警 / 联锁失效", cons: "运行装置失去超温保护,可能导致超温失控", l: 2, s: 5, safe: "参数修改执行申请-复核双人确认;修改清单与变更单逐项核对", sug: "联锁测试全覆盖并留存记录;修改期间工艺加强监控" },
] },
{ step: "步骤4:升温投用与验证", desc: "按新版操作规程升温至 90℃ 投用", rows: [
{ hazard: "升温过快导致冲料", cons: "釜压骤升,法兰泄漏或安全阀起跳", l: 2, s: 4, safe: "按 5℃/10min 梯度升温;首批投用工艺、仪表、设备三方现场值守", sug: "首 3 批加密温度 / 压力巡检,异常立即降回 80℃ 运行" },
] },
]);
const info = ref("R-101 缩合反应釜反应温度由 80℃ 提升至 90℃、保温时间由 8h 缩短至 4h。实施作业包括:釜体保温更换(硅酸铝纤维毡)、DCS 温度报警 / 联锁值上调(高报 85→95℃、联锁 90→100℃)与联锁测试、操作规程修订与培训。作业期间装置保持运行,涉及登高、保温纤维、DCS 参数修改等作业内容。");
const toRec = (step: string, row: any): any => ({
item: row.hazard || "(未命名危害因素)",
scene: `${step}${row.hazard},可能导致:${row.cons}`,
inherent: inherentOf(row.l, row.s),
existing: row.safe,
suggest: row.sug,
residual: lowerOf(inherentOf(row.l, row.s)),
source: "JSA",
});
</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)]">JSA 分析插件</span>
<NTag size="small" type="primary">插件</NTag>
<NTag size="small">MOC 变更缩合反应温度及时间优化演示</NTag>
<span class="text-[11px] text-slate-500">{{`LS 矩阵 · ${steps.length} 个作业步骤 · ${recTotal} 条危害因素记录`}}</span>
<div class="relative">
<NButton size="small" ghost type="primary" class="text-[11px]" @click="open = !open">
<Icon icon="lucide:users" class="size-12px" /> 分析组成员 · 已选 {{ sels.length }}
</NButton>
<template v-if="open">
<div class="fixed inset-0 z-10" @click="open = false"></div>
<div class="absolute right-0 top-8 z-20 w-72 rounded-lg border bg-white p-2 shadow-xl">
<div class="mb-1.5 flex items-center justify-between">
<span class="text-xs font-semibold text-slate-700">选择分析组成员岗位人员库</span>
<span class="text-[11px] text-slate-400">已选 {{ sels.length }} </span>
</div>
<NInput v-model:value="q" size="small" placeholder="搜索人名…" class="mb-1.5 text-xs" />
<div class="max-h-48 space-y-0.5 overflow-y-auto">
<div v-for="x in list" :key="x.name" @click="toggle(x.name)"
class="cursor-pointer flex w-full items-center min-h-34px gap-2 rounded-md px-2 py-1.5 text-left text-xs"
:class="sels.includes(x.name) ? 'bg-[#DFE9F6]' : 'hover:bg-slate-50'">
<span class="flex h-3.5 w-3.5 shrink-0 items-center justify-center rounded border text-[10px]"
:class="sels.includes(x.name) ? 'border-[var(--theme-color)] bg-[var(--theme-color)] text-white' : 'border-slate-300'">{{ sels.includes(x.name) ? "✓" : "" }}</span>
<span class="font-medium text-slate-800">{{ x.name }}</span>
<span class="text-[11px] text-slate-400">{{ x.org }} · {{ x.post }}</span>
<NTag v-if="sels[0] === x.name" size="small" type="primary" class="ml-auto">组长</NTag>
</div>
</div>
<div class="mt-1.5 border-t pt-1.5 text-[11px] leading-4 text-slate-400">
首位选择者默认为组长成员为选择式非手写参与记录自动归属到人用于参与风险分析次数统计采集
</div>
</div>
</template>
</div>
<div class="ml-auto flex items-center gap-1.5">
<NButton size="small" ghost type="primary" class="text-xs" :disabled="recTotal === 0" @click="exportAll">
<Icon icon="lucide:sparkles" class="size-12px mr-1" /> AI 整理入表
</NButton>
<NButton size="small" ghost type="primary" class="text-xs" @click="message.success('JSA 分析报告已生成(Word/PDF')">
<Icon icon="material-symbols:download" class="size-14px" /> 生成报告
</NButton>
<NButton size="small" type="primary" class="text-xs" :disabled="analyzing" @click="runAi">
<Icon v-if="analyzing" icon="ri:loader-4-fill" class="size-14px animate-spin" />
<Icon v-else icon="lucide:sparkles" class="size-12px" />
AI 分析危害原因
</NButton>
</div>
</div>
<div
class="space-y-3"
:style="{
'--theme-color': themeStore.themeColor,
}"
>
<!-- 作业信息 -->
<div class="rounded-lg border bg-white p-3">
<div class="mb-1.5 flex items-center gap-2 text-xs font-semibold text-slate-600">
<span class="flex h-4 w-4 items-center justify-center rounded-full bg-[var(--theme-color)] text-[10px] text-white">1</span>
作业信息
<span class="ml-auto text-[11px] font-normal text-slate-400">作业名称变更实施作业自动生成可修改</span>
</div>
<p class="rounded-lg bg-slate-50 p-2.5 text-xs leading-relaxed text-slate-600">{{ info }}</p>
</div>
<!-- 作业活动分析记录 -->
<div class="bg-white">
<div class="flex items-center gap-2 px-3 py-2 text-xs font-semibold text-slate-600 border border-b-0 rounded-tl-lg rounded-tr-lg">
<span class="flex h-4 w-4 items-center justify-center rounded-full bg-[var(--theme-color)] text-[10px] text-white">2</span>
作业活动分析记录
<NButton size="small" ghost type="primary" class="text-xs ml-2" @click="addRow">
<Icon icon="ic:round-plus" class="size-14px" /> 新增记录末步骤
</NButton>
<span class="ml-auto text-[11px] font-normal text-slate-400">单元格点击即可修改L / S 取值 1-5RR 实时重算入表将本条选入风险分析记录表</span>
</div>
<table class="w-full min-w-[900px] border-collapse text-xs">
<thead>
<tr>
<th :class="TH" class="w-40">作业步骤</th>
<th :class="TH">危害因素</th>
<th :class="TH">后果</th>
<th :class="TH" class="w-8">L</th>
<th :class="TH" class="w-8">S</th>
<th :class="TH" class="w-20">RR</th>
<th :class="TH">安全措施</th>
<th :class="TH">建议措施</th>
<th :class="TH" class="w-16">操作</th>
</tr>
</thead>
<tbody>
<template v-for="(s, si) in steps" :key="si">
<tr v-for="(row, ri) in s.rows" :key="`${si}-${ri}`">
<td v-if="ri === 0" :class="TD" class="bg-slate-50" :rowspan="s.rows.length">
<div class="px-1 text-xs font-semibold text-slate-700">{{ s.step }}</div>
<div class="mt-1 px-1 text-[11px] leading-relaxed text-slate-400">{{ s.desc }}</div>
</td>
<td :class="TD"><Ed :v="row.hazard" @update="(x) => updRow(si, ri, { hazard: x })" /></td>
<td :class="TD"><Ed :v="row.cons" @update="(x) => updRow(si, ri, { cons: x })" /></td>
<td :class="TD" class="text-center">
<input :value="row.l" @change="(e) => updRow(si, ri, { l: clampLS((e.target as HTMLInputElement).value) })"
class="w-7 rounded border border-transparent text-center hover:border-slate-200" />
</td>
<td :class="TD" class="text-center">
<input :value="row.s" @change="(e) => updRow(si, ri, { s: clampLS((e.target as HTMLInputElement).value) })"
class="w-7 rounded border border-transparent text-center hover:border-slate-200" />
</td>
<td :class="TD" class="text-center">
<span class="inline-block rounded px-1.5 py-0.5 text-[11px] font-medium" :class="rrOf(row.l, row.s).c">{{ rrOf(row.l, row.s).t }}</span>
</td>
<td :class="TD"><Ed :v="row.safe" @update="(x) => updRow(si, ri, { safe: x })" /></td>
<td :class="TD"><Ed :v="row.sug" @update="(x) => updRow(si, ri, { sug: x })" /></td>
<td :class="TD" class="text-center">
<div class="flex items-center justify-between px-1 mt-1">
<NButton text type="primary" class="mt-1px text-[11px] hover:underline" title="选入风险分析记录表"
@click="emit('export', [toRec(s.step, row)], 'manual')">入表
</NButton>
<NButton text type="error" title="删除行" @click="delRow(si, ri)">
<Icon icon="ep:delete" class="size-14px" />
</NButton>
</div>
</td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
</div>
</template>