335 lines
17 KiB
Vue
335 lines
17 KiB
Vue
<!-- 风险分析记录表 -->
|
||
<script setup lang="ts">
|
||
import { ref } from 'vue';
|
||
import { Icon } from '@iconify/vue'
|
||
import { useThemeStore } from '@/store/modules/theme';
|
||
import Ed from "./Ed.vue";
|
||
import Hazop from "./hazop.vue";
|
||
import Jsa from "./jsa.vue";
|
||
import Scl from "./scl.vue";
|
||
import RiskCheck from "./riskCheck.vue";
|
||
import ApprovalDrawer from './approvalDrawer.vue'
|
||
|
||
const themeStore = useThemeStore();
|
||
// 抽屉类型
|
||
const approvalDrawerShow = ref<boolean>(false);
|
||
const trainText = ref<string>('');
|
||
const btnDisabled = ref<boolean>(false);
|
||
const saveLoading = ref<boolean>(false);
|
||
const downloadLoading = ref<boolean>(false);
|
||
const regenerateLoading = ref<boolean>(false);
|
||
const submitLoading = ref<boolean>(false);
|
||
const confirmed = ref<boolean>(false);
|
||
const approvers = ref<Record<string, string>>({
|
||
部门审核: "李主任(一车间主任)",
|
||
最终批准: "李主任(部门负责人 · 固定)",
|
||
});
|
||
const signers = ref<Record<string, string>>({});
|
||
const pluginDone = ref<Record<string, number>>({});
|
||
const tools = ref<string[]>(["HAZOP","JSA", "SCL", "RISK_CHECK"]);
|
||
const riskRecords = ref<any>([]);
|
||
const pluginDrawer = ref<boolean>(false);
|
||
const currentPlugin = ref<string>('');
|
||
const lvlNext = (v: string) => (v === "高" ? "中" : v === "中" ? "低" : "高");
|
||
|
||
// 风险识别分析记录表
|
||
const TOOL_CARDS = [
|
||
{ key: "HAZOP" as const, name: "HAZOP 分析", desc: "节点-偏离结构化分析:原因 / 后果 / LS 风险矩阵 / 安全措施" },
|
||
{ key: "JSA" as const, name: "JSA 分析", desc: "作业步骤分解:危害因素识别与 LS 风险分级管控" },
|
||
{ key: "SCL" as const, name: "设备 SCL", desc: "设备 / 管理检查项目对照标准逐项核查" },
|
||
{ key: "RISK_CHECK" as const, name: "风险检查表", desc: "变更通用检查项:是 / 否 / 不涉及逐项判定,输出风险描述与管控措施" },
|
||
];
|
||
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 = () => {
|
||
const rest = riskRecords.value.map((x:any, i:any) => (x.pssr ? -1 : i)).filter((i:any) => i >= 0);
|
||
if (!rest.length) { window.$message?.info("所有建议措施均已转为 PSSR 行动项"); return; }
|
||
const items = rest.map((i:any) => ({ c: `${riskRecords.value[i].suggest}(来源:风险分析记录表 · ${riskRecords.value[i].item})`, p: "投用前" }));
|
||
pssrGroups.value = pssrGroups.value.map((g:any, gi:any) => gi === 0 ? { ...g, items: [...g.items, ...items] } : g);
|
||
riskRecords.value = riskRecords.value.map((x:any) => ({ ...x, pssr: true }));
|
||
window.$message?.success(`已将 ${rest.length} 条建议措施全部转为 PSSR 行动项(见「PSSR 检查内容」)`);
|
||
};
|
||
|
||
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;
|
||
// analyzing.value = true;
|
||
// 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;
|
||
// }
|
||
};
|
||
|
||
const toPssr = (i: number) => {
|
||
const rec = riskRecords.value[i];
|
||
if (!rec || rec.pssr) return;
|
||
pssrGroups.value = pssrGroups.value.map((g:any, gi:any) => gi === 0
|
||
? { ...g, items: [...g.items, { c: `${rec.suggest}(来源:风险分析记录表 · ${rec.item})`, p: "投用前" }] }
|
||
: g);
|
||
updRec(i, { pssr: true });
|
||
window.$message?.success("建议措施已转为 PSSR 行动项(见「PSSR 检查内容」· 一、风险分析行动项落实)");
|
||
};
|
||
|
||
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") {
|
||
pluginDrawer.value = false;
|
||
currentPlugin.value = '';
|
||
window.$message?.success(`AI 已自动整理形成风险分析记录表(新增 ${items.length} 项),支持手动修改`);
|
||
} else {
|
||
window.$message?.success(`已选入 ${items.length} 条风险记录,可继续挑选或返回查看记录表`);
|
||
}
|
||
}
|
||
|
||
const regenerate = () => window.$message?.success(`演示:已根据变更描述重新生成变更申请表(本页内容已刷新)`);
|
||
const download = () => window.$message?.success(`演示:变更申请表已下载(Word/PDF)`);
|
||
|
||
// 保存草稿
|
||
const saveDraft = async () => {
|
||
// if(changeApplyRules()){
|
||
// return
|
||
// }
|
||
// saveLoading.value = true;
|
||
// const {error} = await applicationSaveApi(props.currentId, form.value);
|
||
// if(!error){
|
||
// window.$message?.success("已保存为草稿,可在左侧「我的草稿」中继续编辑");
|
||
// }
|
||
// saveLoading.value = false;
|
||
}
|
||
// ---------- 提交 ----------
|
||
const submitApply = () => {
|
||
|
||
}
|
||
const confirmTab = () => {
|
||
confirmed.value = !confirmed.value;
|
||
}
|
||
|
||
defineExpose({trainText})
|
||
</script>
|
||
|
||
<template>
|
||
<div class="p-4">
|
||
<div class="space-y-4 scroll">
|
||
<div class="border p-3" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
|
||
<div class="flex flex-wrap items-center gap-2">
|
||
<span class="flex items-center gap-1.5 text-sm font-semibold text-slate-700">
|
||
<Icon icon="lucide:shield-alert" class="size-16px" />
|
||
风险识别工具(按变更等级匹配)
|
||
</span>
|
||
<!-- <NTag size="small" :type="form?.level === '重要' ? 'error' : 'default'">
|
||
{{ form?.level === '重要' ? '重要变更' : '一般变更' }}
|
||
</NTag>
|
||
<span class="text-xs text-slate-400">
|
||
{{ form?.level === '重要' ? '规则: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"
|
||
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'"
|
||
:style="{'--theme-color': themeStore.themeColor}"
|
||
>
|
||
<div class="min-w-0 flex-1">
|
||
<div
|
||
class="flex items-center gap-1.5 text-xs font-semibold"
|
||
:class="pluginDone[t.key] !== undefined ? 'text-white' : tools.includes(t.key) ? 'text-[var(--theme-color)]' : 'text-slate-500'"
|
||
:style="{'--theme-color': themeStore.themeColor}"
|
||
>
|
||
<Icon icon="fluent:wand-24-regular" class="size-16px" />
|
||
{{ t.name }}
|
||
</div>
|
||
<div class="mt-0.5 text-[10px]" :class="pluginDone[t.key] !== undefined ? 'text-white/75' : 'text-slate-400'">
|
||
{{ 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>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="border" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
|
||
<div class="flex flex-wrap items-center gap-2 bg-slate-100 px-3 py-2">
|
||
<span class="flex items-center gap-1.5 text-sm font-medium text-slate-700">
|
||
<Icon icon="proicons:alert-triangle" class="size-16px text-amber-500" />
|
||
风险分析记录表
|
||
</span>
|
||
<span class="text-[11px] text-slate-400">共 {{ riskRecords.length }} 项</span>
|
||
<span class="ml-auto hidden text-[11px] text-slate-400 lg:block">插件结果「入表」或 AI 自动整理形成;单元格点击修改,风险等级点击切换</span>
|
||
<NButton ghost size="small" color="#7c3aed" class="text-xs" @click="toPssrAll">
|
||
<Icon icon="tabler:clipboard-check" class="size-14px" />
|
||
全部转 PSSR 行动项
|
||
</NButton>
|
||
<NButton ghost size="small" type="primary" class="text-xs" @click="aiOrganize">
|
||
<Icon icon="lucide:sparkles" class="size-12px" />
|
||
AI 自动整理
|
||
</NButton>
|
||
</div>
|
||
<div v-if="riskRecords.length === 0" class="px-3 py-6 text-center text-xs text-slate-400">
|
||
尚无风险记录:调用上方插件完成分析后逐条「入表」,或点击「AI 自动整理」形成记录表
|
||
</div>
|
||
<div v-else class="overflow-x-auto p-3">
|
||
<table class="w-full min-w-[960px] border-collapse text-xs">
|
||
<thead>
|
||
<tr>
|
||
<th v-for="h in ['风险项', '场景描述', '固有风险', '现有措施', '建议措施', '剩余风险', '来源', '操作']" :key="h"
|
||
class="border bg-slate-50 px-2 py-1.5 text-[11px] font-medium text-slate-500">{{ h }}</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="(rec, i) in riskRecords" :key="i">
|
||
<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 })" />
|
||
</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 text-center align-top">
|
||
<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 class="border px-1.5 py-1 text-center align-top">
|
||
<NTag size="small" type="info">{{ rec.source }}</NTag>
|
||
</td>
|
||
<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>
|
||
<NButton text size="tiny" color="#7c3aed" v-else class="hover:underline" @click="toPssr(i)">转 PSSR</NButton>
|
||
<NButton text type="error" size="tiny" class="mt-2" @click="riskRecords = riskRecords.filter((_, j) => j !== i)">删除</NButton>
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="flex flex-wrap items-center justify-between gap-2 border-t p-4">
|
||
<div class="flex flex-wrap gap-2">
|
||
<NButton ghost type="success" :disabled="btnDisabled" :loading="downloadLoading" @click="download">
|
||
<Icon icon="material-symbols:download" class="size-16px" /> 下载风险分析记录表
|
||
</NButton>
|
||
<NButton ghost type="warning" :disabled="btnDisabled" :loading="regenerateLoading" @click="regenerate">
|
||
<Icon icon="tdesign:refresh" class="size-14px" /> 重新生成风险分析记录表
|
||
</NButton>
|
||
<NButton ghost :type="confirmed ? 'success' : 'primary'" :disabled="btnDisabled" @click="confirmTab">
|
||
<Icon icon="ix:success" class="size-16px" /> {{ confirmed ? '已确认完毕' : '本标签内容确认完毕' }}
|
||
</NButton>
|
||
</div>
|
||
</div>
|
||
<div class="flex flex-wrap items-center gap-x-4 gap-y-2 bg-[#EAF0F9] px-4 py-2.5 shadow-[0_-2px_8px_rgba(0,0,0,0.05)]">
|
||
<div class="flex min-w-0 flex-1 flex-wrap items-center gap-x-2 gap-y-1 text-xs text-slate-500">
|
||
<Icon icon="lucide:users" class="size-14px" />
|
||
<span class="min-w-0 truncate">
|
||
部门审核:{{ approvers.部门审核 }} → 专业会签:{{ Object.keys(signers).length ? Object.values(signers).join('、') : '未涉及专业 · 免会签' }} → 最终批准:{{ approvers.最终批准 }}
|
||
</span>
|
||
<NButton ghost size="small" type="primary" @click="approvalDrawerShow = true">
|
||
<Icon icon="octicon:sliders-24" class="size-14px" />
|
||
审批流程设置
|
||
</NButton>
|
||
</div>
|
||
<div class="ml-auto flex shrink-0 items-center gap-2">
|
||
<NButton size="small" type="primary" :disabled="btnDisabled" :loading="saveLoading" @click="saveDraft">保存</NButton>
|
||
<!-- <span v-if="!form" class="flex items-center text-xs text-amber-600">请先在「变更预识别」页生成变更申请单</span> -->
|
||
<NButton size="small" type="primary" @click="submitApply" :disabled="btnDisabled" :loading="submitLoading">
|
||
提交 <Icon icon="formkit:right" class="size-14px" />
|
||
</NButton>
|
||
</div>
|
||
</div>
|
||
<!-- 插件抽屉 -->
|
||
<NDrawer v-model:show="pluginDrawer" :width="1200" placement="right">
|
||
<NDrawerContent>
|
||
<template v-if="currentPlugin === 'HAZOP'">
|
||
<Hazop @back="onBack" @export="exportAll" />
|
||
</template>
|
||
<template v-else-if="currentPlugin === 'JSA'">
|
||
<Jsa @back="onBack" @export="exportAll" />
|
||
</template>
|
||
<template v-else-if="currentPlugin === 'SCL'">
|
||
<Scl @back="onBack" @export="exportAll" />
|
||
</template>
|
||
<template v-else-if="currentPlugin === 'RISK_CHECK'">
|
||
<RiskCheck @back="onBack" />
|
||
</template>
|
||
</NDrawerContent>
|
||
</NDrawer>
|
||
<!-- 审批流程设置 -->
|
||
<NDrawer v-model:show="approvalDrawerShow" placement="right" width="28rem">
|
||
<NDrawerContent>
|
||
<template #header>
|
||
<div class="text-base font-semibold text-slate-800">审批流程设置</div>
|
||
</template>
|
||
<ApprovalDrawer />
|
||
</NDrawerContent>
|
||
</NDrawer>
|
||
</template>
|
||
<style scoped lang="scss">
|
||
.scroll {
|
||
height: calc(100vh - 393px);
|
||
overflow-y: auto;
|
||
}
|
||
</style> |