Compare commits
5
Commits
2908a10ee5
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
26192167f3 | ||
|
|
b51de262b3 | ||
|
|
b0144d190b | ||
|
|
80cc999687 | ||
|
|
26f33d8219 |
Vendored
+2
@@ -65,6 +65,7 @@ declare module 'vue' {
|
|||||||
NMessageProvider: typeof import('naive-ui')['NMessageProvider']
|
NMessageProvider: typeof import('naive-ui')['NMessageProvider']
|
||||||
NModal: typeof import('naive-ui')['NModal']
|
NModal: typeof import('naive-ui')['NModal']
|
||||||
NNotificationProvider: typeof import('naive-ui')['NNotificationProvider']
|
NNotificationProvider: typeof import('naive-ui')['NNotificationProvider']
|
||||||
|
NPopconfirm: typeof import('naive-ui')['NPopconfirm']
|
||||||
NProgress: typeof import('naive-ui')['NProgress']
|
NProgress: typeof import('naive-ui')['NProgress']
|
||||||
NRadio: typeof import('naive-ui')['NRadio']
|
NRadio: typeof import('naive-ui')['NRadio']
|
||||||
NRadioGroup: typeof import('naive-ui')['NRadioGroup']
|
NRadioGroup: typeof import('naive-ui')['NRadioGroup']
|
||||||
@@ -152,6 +153,7 @@ declare global {
|
|||||||
const NMessageProvider: typeof import('naive-ui')['NMessageProvider']
|
const NMessageProvider: typeof import('naive-ui')['NMessageProvider']
|
||||||
const NModal: typeof import('naive-ui')['NModal']
|
const NModal: typeof import('naive-ui')['NModal']
|
||||||
const NNotificationProvider: typeof import('naive-ui')['NNotificationProvider']
|
const NNotificationProvider: typeof import('naive-ui')['NNotificationProvider']
|
||||||
|
const NPopconfirm: typeof import('naive-ui')['NPopconfirm']
|
||||||
const NProgress: typeof import('naive-ui')['NProgress']
|
const NProgress: typeof import('naive-ui')['NProgress']
|
||||||
const NRadio: typeof import('naive-ui')['NRadio']
|
const NRadio: typeof import('naive-ui')['NRadio']
|
||||||
const NRadioGroup: typeof import('naive-ui')['NRadioGroup']
|
const NRadioGroup: typeof import('naive-ui')['NRadioGroup']
|
||||||
|
|||||||
@@ -3,13 +3,15 @@ import { computed, 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 {
|
import {
|
||||||
CHANGES, DOC_TODO, LEVEL_COLOR, STATUS_COLOR, TEMP_WATCH, URGE_RECORDS,
|
CHANGES, type ChangeLevel, type ChangeOrder, type ChangeStatus, type ChangeType,
|
||||||
countOpenStatus, type ChangeLevel, type ChangeOrder, type ChangeStatus, type ChangeType,
|
|
||||||
} from "@/typings/model";
|
} from "@/typings/model";
|
||||||
import { getColorWithOpacity } from "@/utils/common";
|
import { getColorWithOpacity } from "@/utils/common";
|
||||||
import HighlightText from "@/views/my/modules/HighlightText.vue";
|
import HighlightText from "./modules/HighlightText.vue";
|
||||||
import Ed from "@/views/my/modules/Ed.vue";
|
import Ed from "./modules/Ed.vue";
|
||||||
|
import Hazop from "./modules/hazop.vue";
|
||||||
|
import Jsa from "./modules/jsa.vue";
|
||||||
|
import Scl from "./modules/scl.vue";
|
||||||
|
import RiskCheck from "./modules/riskCheck.vue";
|
||||||
|
|
||||||
const themeStore = useThemeStore();
|
const themeStore = useThemeStore();
|
||||||
|
|
||||||
@@ -108,8 +110,7 @@ const formGen = ref<string>("idle");
|
|||||||
const materialsText = ref("");
|
const materialsText = ref("");
|
||||||
const updateDocs = ref<string[]>([]);
|
const updateDocs = ref<string[]>([]);
|
||||||
const disciplines = ref<string[]>([]);
|
const disciplines = ref<string[]>([]);
|
||||||
const tools = ref<string[]>(["JSA", "检查表法", "通用检查表"]);
|
const tools = ref<string[]>(["HAZOP","JSA", "SCL", "RISK_CHECK"]);
|
||||||
const plugin = ref<"HAZOP" | "JSA" | "检查表法" | "通用检查表" | null>(null);
|
|
||||||
const genForm = () => {
|
const genForm = () => {
|
||||||
if (formGen.value === "generating") return;
|
if (formGen.value === "generating") return;
|
||||||
formGen.value = "generating";
|
formGen.value = "generating";
|
||||||
@@ -136,8 +137,8 @@ const genForm = () => {
|
|||||||
updateDocs.value = ["PID图纸", "操作规程", "总图", "设备台账"];
|
updateDocs.value = ["PID图纸", "操作规程", "总图", "设备台账"];
|
||||||
disciplines.value = ["技术", "仪表"];
|
disciplines.value = ["技术", "仪表"];
|
||||||
signers.value = { 技术: "王仪表(技术部)", 仪表: "王仪表(技术部 · 仪表)" };
|
signers.value = { 技术: "王仪表(技术部)", 仪表: "王仪表(技术部 · 仪表)" };
|
||||||
tools.value = ["JSA", "检查表法", "通用检查表"];
|
tools.value = ["HAZOP","JSA", "SCL", "通用检查表"];
|
||||||
plugin.value = null;
|
currentPlugin.value = "";
|
||||||
formGen.value = "done";
|
formGen.value = "done";
|
||||||
tab.value = "form";
|
tab.value = "form";
|
||||||
window.$message?.success("变更申请单已生成:AI 自动总结名称、预填内容,均可手动修改");
|
window.$message?.success("变更申请单已生成:AI 自动总结名称、预填内容,均可手动修改");
|
||||||
@@ -293,17 +294,14 @@ const levelResult = computed(() => (totalScore.value >= 20 ? "重要变更" : "
|
|||||||
const RISK_TOOLS_FORM = [
|
const RISK_TOOLS_FORM = [
|
||||||
{ key: "HAZOP", label: "HAZOP" },
|
{ key: "HAZOP", label: "HAZOP" },
|
||||||
{ key: "JSA", label: "JSA" },
|
{ key: "JSA", label: "JSA" },
|
||||||
{ key: "检查表法", label: "设备SCL" },
|
{ key: "SCL", label: "设备SCL" },
|
||||||
{ key: "通用检查表", label: "风险检查表" },
|
{ key: "RISK_CHECK", label: "风险检查表" },
|
||||||
];
|
];
|
||||||
const recommendTools = () => {
|
const recommendTools = () => {
|
||||||
const lv = form.value?.level === "重要" ? "重要" : "一般";
|
const lv = form.value?.level === "重要" ? "重要" : "一般";
|
||||||
tools.value = lv === "重要" ? ["HAZOP", "JSA", "检查表法", "通用检查表"] : ["JSA", "检查表法", "通用检查表"];
|
tools.value = lv === "重要" ? ["HAZOP", "JSA", "检查表法", "通用检查表"] : ["JSA", "检查表法", "通用检查表"];
|
||||||
window.$message?.success(`已按「${lv}变更」规则匹配风险分析工具,可继续手动重选`);
|
window.$message?.success(`已按「${lv}变更」规则匹配风险分析工具,可继续手动重选`);
|
||||||
};
|
};
|
||||||
const toggleTool = (t: string) => {
|
|
||||||
tools.value = tools.value.includes(t) ? tools.value.filter((x) => x !== t) : [...tools.value, t];
|
|
||||||
};
|
|
||||||
|
|
||||||
const files = ref<string[]>(["小试实验报告.pdf", "杂质初步分析.docx"]);
|
const files = ref<string[]>(["小试实验报告.pdf", "杂质初步分析.docx"]);
|
||||||
const previewFile = ref<string | null>(null);
|
const previewFile = ref<string | null>(null);
|
||||||
@@ -396,23 +394,14 @@ const finishApprover = () => {
|
|||||||
const TOOL_CARDS = [
|
const TOOL_CARDS = [
|
||||||
{ key: "HAZOP" as const, name: "HAZOP 分析", desc: "节点-偏离结构化分析:原因 / 后果 / LS 风险矩阵 / 安全措施" },
|
{ key: "HAZOP" as const, name: "HAZOP 分析", desc: "节点-偏离结构化分析:原因 / 后果 / LS 风险矩阵 / 安全措施" },
|
||||||
{ key: "JSA" as const, name: "JSA 分析", desc: "作业步骤分解:危害因素识别与 LS 风险分级管控" },
|
{ key: "JSA" as const, name: "JSA 分析", desc: "作业步骤分解:危害因素识别与 LS 风险分级管控" },
|
||||||
{ key: "检查表法" as const, name: "设备 SCL", desc: "设备 / 管理检查项目对照标准逐项核查" },
|
{ key: "SCL" as const, name: "设备 SCL", desc: "设备 / 管理检查项目对照标准逐项核查" },
|
||||||
{ key: "通用检查表" as const, name: "风险检查表", desc: "变更通用检查项:是 / 否 / 不涉及逐项判定,输出风险描述与管控措施" },
|
{ key: "RISK_CHECK" as const, name: "风险检查表", desc: "变更通用检查项:是 / 否 / 不涉及逐项判定,输出风险描述与管控措施" },
|
||||||
];
|
];
|
||||||
const pluginDone = ref<Record<string, number>>({});
|
const pluginDone = ref<Record<string, number>>({});
|
||||||
const riskRecords = ref<any>([]);
|
const riskRecords = ref<any>([]);
|
||||||
const updRec = (i: number, patch: Partial<any>) => {
|
const updRec = (i: number, patch: Partial<any>) => {
|
||||||
riskRecords.value = riskRecords.value.map((x, j) => (j === i ? { ...x, ...patch } : x));
|
riskRecords.value = riskRecords.value.map((x, j) => (j === i ? { ...x, ...patch } : x));
|
||||||
};
|
};
|
||||||
const addRiskRecords = (items: any[], mode: "manual" | "ai") => {
|
|
||||||
riskRecords.value = [...riskRecords.value, ...items];
|
|
||||||
if (mode === "ai") {
|
|
||||||
plugin.value = null;
|
|
||||||
window.$message?.success(`AI 已自动整理形成风险分析记录表(新增 ${items.length} 项),支持手动修改`);
|
|
||||||
} else {
|
|
||||||
window.$message?.success(`已选入 ${items.length} 条风险记录,可继续挑选或返回查看记录表`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const lvlNext = (v: string) => (v === "高" ? "中" : v === "中" ? "低" : "高");
|
const lvlNext = (v: string) => (v === "高" ? "中" : v === "中" ? "低" : "高");
|
||||||
const pssrGroups = ref<{ g: string; items: any[] }[]>([
|
const pssrGroups = ref<{ g: string; items: any[] }[]>([
|
||||||
{ g: "一、风险分析行动项落实", items: [
|
{ g: "一、风险分析行动项落实", items: [
|
||||||
@@ -561,6 +550,30 @@ const uploadConfirm = () => {
|
|||||||
uploadDoc.value = null;
|
uploadDoc.value = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const pluginDrawer = ref<boolean>(false);
|
||||||
|
const currentPlugin = ref<string>("");
|
||||||
|
// 插件选择
|
||||||
|
const toggleTool = (t: any) => {
|
||||||
|
if (!tools.value.includes(t.key))
|
||||||
|
tools.value = tools.value.includes(t.key) ? tools.value.filter((x) => x !== t.key) : [...tools.value, t.key];
|
||||||
|
currentPlugin.value = t.key;
|
||||||
|
pluginDrawer.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
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 onBack = () => {
|
||||||
|
pluginDrawer.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
@@ -927,7 +940,7 @@ const uploadConfirm = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
<!-- ===== 风险记录表 ===== -->
|
<!-- ===== 风险分析记录表 ===== -->
|
||||||
<template v-if="tab === 'risk'">
|
<template v-if="tab === 'risk'">
|
||||||
<div class="border p-3" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
|
<div class="border p-3" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
@@ -962,7 +975,7 @@ const uploadConfirm = () => {
|
|||||||
</div>
|
</div>
|
||||||
</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="pluginDone[t.key] !== undefined ? 'default' : tools.includes(t.key) ? 'primary' : 'default'"
|
||||||
@click="if (!tools.includes(t.key)) toggleTool(t.key); plugin = t.key;">
|
@click="toggleTool(t)">
|
||||||
使用
|
使用
|
||||||
</NButton>
|
</NButton>
|
||||||
</div>
|
</div>
|
||||||
@@ -1632,6 +1645,23 @@ const uploadConfirm = () => {
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</NModal>
|
</NModal>
|
||||||
|
<!-- 插件抽屉 -->
|
||||||
|
<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>
|
||||||
</NCard>
|
</NCard>
|
||||||
</NSpace>
|
</NSpace>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -0,0 +1,283 @@
|
|||||||
|
<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: "节点2:DCS 温控与联锁", obj: "DCS 温控回路及联锁", devs: [
|
||||||
|
{ dev: "报警 / 联锁值设置不当", rows: [
|
||||||
|
{ cause: "报警 / 联锁值上调后(高报 85→95℃、联锁 90→100℃),原有报警裕度改变", cons: "误报、漏报导致操作人员对真实超温失去预警", l: 2, s: 4, safe: "参数修改执行申请-复核双人确认;修改清单与变更单逐项核对", sug: "联锁逻辑图与台账同步更新,纳入变更资料归档" },
|
||||||
|
] },
|
||||||
|
{ dev: "联锁拒动 / 误动", rows: [] },
|
||||||
|
] },
|
||||||
|
];
|
||||||
|
const HAZOP_AI:any = {
|
||||||
|
"搅拌失效": [
|
||||||
|
{ cause: "搅拌器机械故障或变频器跳闸,釜内物料传热不均", cons: "局部过热引发暴沸冲料;温度测量失真误导操作", l: 2, s: 4, safe: "搅拌电流 DCS 在线监视与低电流报警;跳闸联锁停进料", sug: "变更后复核搅拌电流裕量,纳入交接班巡检" },
|
||||||
|
],
|
||||||
|
"冷却水中断": [
|
||||||
|
{ cause: "冷却水总管压力波动或阀门误关", cons: "撤热失效,反应温度快速上升溜温", l: 2, s: 5, safe: "冷却水流量低报警;紧急切断进料联锁", sug: "核实紧急冷却旁路可用性,写入超温应急处置卡" },
|
||||||
|
],
|
||||||
|
"联锁拒动 / 误动": [
|
||||||
|
{ cause: "联锁设定值修改后未重新整定验证,或逻辑修改错误", cons: "超温时联锁拒动失去最后保护层;误动造成非计划停车", l: 1, s: 5, safe: "投用前 100% 联锁传动测试,动作正确率确认", sug: "联锁测试记录归档至变更资料;修改执行双人复核" },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const inherentOf = (l: number, s: number): "高" | "中" | "低" => {
|
||||||
|
const v = l * s;
|
||||||
|
return v >= 10 ? "高" : v >= 5 ? "中" : "低";
|
||||||
|
}
|
||||||
|
const lowerOf = (lv: "高" | "中" | "低"): "高" | "中" | "低" => {
|
||||||
|
return lv === "高" ? "中" : "低";
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodes = ref<any>(JSON.parse(JSON.stringify(HAZOP_INIT)));
|
||||||
|
const sel = ref({ n: 0, d: 0 });
|
||||||
|
const analyzing = ref(false);
|
||||||
|
|
||||||
|
const 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 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 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 = () => {
|
||||||
|
if (cur.value.rows.length) return message.info("当前偏离已有分析记录,可手动新增行补充");
|
||||||
|
analyzing.value = true;
|
||||||
|
setTimeout(() => {
|
||||||
|
const gen = HAZOP_AI[cur.value.dev] ?? [
|
||||||
|
{ cause: "(AI 生成)偏离场景原因分析", cons: "(AI 生成)可能后果描述", l: 2, s: 3, safe: "现有保护层说明", sug: "建议补充的管控措施" },
|
||||||
|
];
|
||||||
|
cur.value.rows = gen.map((x) => ({ ...x }));
|
||||||
|
analyzing.value = false;
|
||||||
|
message.success(`AI 已完成「${cur.value.dev}」偏离分析,生成 ${gen.length} 条记录(支持手动修改)`);
|
||||||
|
}, 1100);
|
||||||
|
};
|
||||||
|
|
||||||
|
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: "仪表工程师" },
|
||||||
|
{ 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" };
|
||||||
|
}
|
||||||
|
</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">
|
||||||
|
<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>
|
||||||
|
</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>
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,281 @@
|
|||||||
|
<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: "节点2:DCS 温控与联锁", obj: "DCS 温控回路及联锁", devs: [
|
||||||
|
{ dev: "报警 / 联锁值设置不当", rows: [
|
||||||
|
{ cause: "报警 / 联锁值上调后(高报 85→95℃、联锁 90→100℃),原有报警裕度改变", cons: "误报、漏报导致操作人员对真实超温失去预警", l: 2, s: 4, safe: "参数修改执行申请-复核双人确认;修改清单与变更单逐项核对", sug: "联锁逻辑图与台账同步更新,纳入变更资料归档" },
|
||||||
|
] },
|
||||||
|
{ dev: "联锁拒动 / 误动", rows: [] },
|
||||||
|
] },
|
||||||
|
];
|
||||||
|
const HAZOP_AI:any = {
|
||||||
|
"搅拌失效": [
|
||||||
|
{ cause: "搅拌器机械故障或变频器跳闸,釜内物料传热不均", cons: "局部过热引发暴沸冲料;温度测量失真误导操作", l: 2, s: 4, safe: "搅拌电流 DCS 在线监视与低电流报警;跳闸联锁停进料", sug: "变更后复核搅拌电流裕量,纳入交接班巡检" },
|
||||||
|
],
|
||||||
|
"冷却水中断": [
|
||||||
|
{ cause: "冷却水总管压力波动或阀门误关", cons: "撤热失效,反应温度快速上升溜温", l: 2, s: 5, safe: "冷却水流量低报警;紧急切断进料联锁", sug: "核实紧急冷却旁路可用性,写入超温应急处置卡" },
|
||||||
|
],
|
||||||
|
"联锁拒动 / 误动": [
|
||||||
|
{ cause: "联锁设定值修改后未重新整定验证,或逻辑修改错误", cons: "超温时联锁拒动失去最后保护层;误动造成非计划停车", l: 1, s: 5, safe: "投用前 100% 联锁传动测试,动作正确率确认", sug: "联锁测试记录归档至变更资料;修改执行双人复核" },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const inherentOf = (l: number, s: number): "高" | "中" | "低" => {
|
||||||
|
const v = l * s;
|
||||||
|
return v >= 10 ? "高" : v >= 5 ? "中" : "低";
|
||||||
|
}
|
||||||
|
const lowerOf = (lv: "高" | "中" | "低"): "高" | "中" | "低" => {
|
||||||
|
return lv === "高" ? "中" : "低";
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodes = ref<any>(JSON.parse(JSON.stringify(HAZOP_INIT)));
|
||||||
|
const sel = ref({ n: 0, d: 0 });
|
||||||
|
const analyzing = ref(false);
|
||||||
|
|
||||||
|
const recTotal = computed(() => steps.value.reduce((a, s) => a + s.rows.length, 0));
|
||||||
|
|
||||||
|
const updRow = (si: number, ri: number, patch: Partial<any>) => {
|
||||||
|
const row = steps.value[si]?.rows[ri];
|
||||||
|
if (row) Object.assign(row, patch);
|
||||||
|
};
|
||||||
|
const clampLS = (v: string) => Math.max(1, Math.min(5, +v || 1));
|
||||||
|
|
||||||
|
const runAi = () => {
|
||||||
|
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: "步骤3:DCS 参数修改与联锁测试", desc: "报警 / 联锁值上调,联锁传动试验", rows: [
|
||||||
|
{ hazard: "参数误改导致报警 / 联锁失效", cons: "运行装置失去超温保护,可能导致超温失控", l: 2, s: 5, safe: "参数修改执行申请-复核双人确认;修改清单与变更单逐项核对", sug: "联锁测试全覆盖并留存记录;修改期间工艺加强监控" },
|
||||||
|
] },
|
||||||
|
{ step: "步骤4:升温投用与验证", desc: "按新版操作规程升温至 90℃ 投用", rows: [
|
||||||
|
{ hazard: "升温过快导致冲料", cons: "釜压骤升,法兰泄漏或安全阀起跳", l: 2, s: 4, safe: "按 5℃/10min 梯度升温;首批投用工艺、仪表、设备三方现场值守", sug: "首 3 批加密温度 / 压力巡检,异常立即降回 80℃ 运行" },
|
||||||
|
] },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const info = ref("R-101 缩合反应釜反应温度由 80℃ 提升至 90℃、保温时间由 8h 缩短至 4h。实施作业包括:釜体保温更换(硅酸铝纤维毡)、DCS 温度报警 / 联锁值上调(高报 85→95℃、联锁 90→100℃)与联锁测试、操作规程修订与培训。作业期间装置保持运行,涉及登高、保温纤维、DCS 参数修改等作业内容。");
|
||||||
|
const toRec = (step: string, row: any): any => ({
|
||||||
|
item: row.hazard || "(未命名危害因素)",
|
||||||
|
scene: `${step}:${row.hazard},可能导致:${row.cons}`,
|
||||||
|
inherent: inherentOf(row.l, row.s),
|
||||||
|
existing: row.safe,
|
||||||
|
suggest: row.sug,
|
||||||
|
residual: lowerOf(inherentOf(row.l, row.s)),
|
||||||
|
source: "JSA",
|
||||||
|
});
|
||||||
|
|
||||||
|
</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="info">插件</NTag>
|
||||||
|
<NTag size="small">MOC 变更:缩合反应温度及时间优化(演示)</NTag>
|
||||||
|
<span class="text-[11px] text-slate-500">{{`LS 矩阵 · ${steps.length} 个作业步骤 · ${recTotal} 条危害因素记录`}}</span>
|
||||||
|
<div class="relative">
|
||||||
|
<NButton size="small" ghost type="primary" class="text-[11px]" @click="open = !open">
|
||||||
|
<Icon icon="lucide:users" class="size-12px" /> 分析组成员 · 已选 {{ sels.length }} 人
|
||||||
|
</NButton>
|
||||||
|
<template v-if="open">
|
||||||
|
<div class="fixed inset-0 z-10" @click="open = false"></div>
|
||||||
|
<div class="absolute right-0 top-8 z-20 w-72 rounded-lg border bg-white p-2 shadow-xl">
|
||||||
|
<div class="mb-1.5 flex items-center justify-between">
|
||||||
|
<span class="text-xs font-semibold text-slate-700">选择分析组成员(岗位人员库)</span>
|
||||||
|
<span class="text-[11px] text-slate-400">已选 {{ sels.length }} 人</span>
|
||||||
|
</div>
|
||||||
|
<NInput v-model:value="q" size="small" placeholder="搜索人名…" class="mb-1.5 text-xs" />
|
||||||
|
<div class="max-h-48 space-y-0.5 overflow-y-auto">
|
||||||
|
<div v-for="x in list" :key="x.name" @click="toggle(x.name)"
|
||||||
|
class="cursor-pointer flex w-full items-center min-h-34px gap-2 rounded-md px-2 py-1.5 text-left text-xs"
|
||||||
|
:class="sels.includes(x.name) ? 'bg-[#DFE9F6]' : 'hover:bg-slate-50'">
|
||||||
|
<span class="flex h-3.5 w-3.5 shrink-0 items-center justify-center rounded border text-[10px]"
|
||||||
|
:class="sels.includes(x.name) ? 'border-[var(--theme-color)] bg-[var(--theme-color)] text-white' : 'border-slate-300'">{{ sels.includes(x.name) ? "✓" : "" }}</span>
|
||||||
|
<span class="font-medium text-slate-800">{{ x.name }}</span>
|
||||||
|
<span class="text-[11px] text-slate-400">{{ x.org }} · {{ x.post }}</span>
|
||||||
|
<NTag v-if="sels[0] === x.name" size="small" type="primary" class="ml-auto">组长</NTag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-1.5 border-t pt-1.5 text-[11px] leading-4 text-slate-400">
|
||||||
|
首位选择者默认为组长;成员为选择式(非手写),参与记录自动归属到人,用于「参与风险分析次数」统计采集。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class="ml-auto flex items-center gap-1.5">
|
||||||
|
<NButton size="small" ghost type="primary" class="text-xs" :disabled="recTotal === 0" @click="exportAll">
|
||||||
|
<Icon icon="lucide:sparkles" class="size-12px mr-1" /> AI 整理入表
|
||||||
|
</NButton>
|
||||||
|
<NButton size="small" ghost type="primary" class="text-xs" @click="message.success('JSA 分析报告已生成(Word/PDF)')">
|
||||||
|
<Icon icon="material-symbols:download" class="size-14px" /> 生成报告
|
||||||
|
</NButton>
|
||||||
|
<NButton size="small" type="primary" class="text-xs" :disabled="analyzing" @click="runAi">
|
||||||
|
<Icon v-if="analyzing" icon="ri:loader-4-fill" class="size-14px animate-spin" />
|
||||||
|
<Icon v-else icon="lucide:sparkles" class="size-12px" />
|
||||||
|
AI 分析危害原因
|
||||||
|
</NButton>
|
||||||
|
</div>
|
||||||
|
</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-5,RR 实时重算;「入表」将本条选入风险分析记录表</span>
|
||||||
|
</div>
|
||||||
|
<table class="w-full min-w-[900px] border-collapse text-xs">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th :class="TH" class="w-40">作业步骤</th>
|
||||||
|
<th :class="TH">危害因素</th>
|
||||||
|
<th :class="TH">后果</th>
|
||||||
|
<th :class="TH" class="w-8">L</th>
|
||||||
|
<th :class="TH" class="w-8">S</th>
|
||||||
|
<th :class="TH" class="w-20">RR</th>
|
||||||
|
<th :class="TH">安全措施</th>
|
||||||
|
<th :class="TH">建议措施</th>
|
||||||
|
<th :class="TH" class="w-16">操作</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<template v-for="(s, si) in steps" :key="si">
|
||||||
|
<tr v-for="(row, ri) in s.rows" :key="`${si}-${ri}`">
|
||||||
|
<td v-if="ri === 0" :class="TD" class="bg-slate-50" :rowspan="s.rows.length">
|
||||||
|
<div class="px-1 text-xs font-semibold text-slate-700">{{ s.step }}</div>
|
||||||
|
<div class="mt-1 px-1 text-[11px] leading-relaxed text-slate-400">{{ s.desc }}</div>
|
||||||
|
</td>
|
||||||
|
<td :class="TD"><Ed :v="row.hazard" @update="(x) => updRow(si, ri, { hazard: x })" /></td>
|
||||||
|
<td :class="TD"><Ed :v="row.cons" @update="(x) => updRow(si, ri, { cons: x })" /></td>
|
||||||
|
<td :class="TD" class="text-center">
|
||||||
|
<input :value="row.l" @change="(e) => updRow(si, ri, { l: clampLS((e.target as HTMLInputElement).value) })"
|
||||||
|
class="w-7 rounded border border-transparent text-center hover:border-slate-200" />
|
||||||
|
</td>
|
||||||
|
<td :class="TD" class="text-center">
|
||||||
|
<input :value="row.s" @change="(e) => updRow(si, ri, { s: clampLS((e.target as HTMLInputElement).value) })"
|
||||||
|
class="w-7 rounded border border-transparent text-center hover:border-slate-200" />
|
||||||
|
</td>
|
||||||
|
<td :class="TD" class="text-center">
|
||||||
|
<span class="inline-block rounded px-1.5 py-0.5 text-[11px] font-medium" :class="rrOf(row.l, row.s).c">{{ rrOf(row.l, row.s).t }}</span>
|
||||||
|
</td>
|
||||||
|
<td :class="TD"><Ed :v="row.safe" @update="(x) => updRow(si, ri, { safe: x })" /></td>
|
||||||
|
<td :class="TD"><Ed :v="row.sug" @update="(x) => updRow(si, ri, { sug: x })" /></td>
|
||||||
|
<td :class="TD" class="text-center">
|
||||||
|
<div class="flex items-center justify-between px-1 mt-1">
|
||||||
|
<NButton text type="primary" class="mt-1px text-[11px] hover:underline" title="选入风险分析记录表"
|
||||||
|
@click="emit('export', [toRec(s.step, row)], 'manual')">入表
|
||||||
|
</NButton>
|
||||||
|
<NButton text type="error" title="删除行" @click="delRow(si, ri)">
|
||||||
|
<Icon icon="ep:delete" class="size-14px" />
|
||||||
|
</NButton>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from "vue";
|
||||||
|
import { useMessage } from "naive-ui";
|
||||||
|
import { Icon } from '@iconify/vue'
|
||||||
|
|
||||||
|
const message = useMessage();
|
||||||
|
const emit = defineEmits<{
|
||||||
|
back: [];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const RISK_CHECKS_INIT = [
|
||||||
|
{
|
||||||
|
id: 1, group: "安全健康", ans: "是",
|
||||||
|
q: "是否存在任何可能导致安全问题的化学反应?检查化学反应禁忌表。",
|
||||||
|
risk: "反应温度从80℃升至90℃,反应速率加快,放热更集中,可能引发反应失控或副反应,导致超温超压。",
|
||||||
|
measure: "1. 进行反应量热试验或查阅反应热数据,确认90℃下的放热速率和绝热温升;2. 评估现有冷却系统能力是否满足最大放热需求;3. 更新化学反应风险评估报告;4. 验证反应釜搅拌、冷却系统完好。",
|
||||||
|
},
|
||||||
|
{ id: 2, group: "安全健康", ans: "否", q: "是否存在任何易燃易爆化学物质或粉尘?" },
|
||||||
|
{
|
||||||
|
id: 3, group: "安全健康", ans: "是",
|
||||||
|
q: "是否存在任何有危险的原材料流速、成分或温度条件变更?",
|
||||||
|
risk: "反应温度条件从80±2℃变更至90±2℃,接近设备设计温度上限100℃,可能造成副反应增加或杂质生成。",
|
||||||
|
measure: "1. 通过小试实验验证90℃下的副反应种类及含量,确保产品杂质合格;2. 确认原料A和B在此温度下的稳定性;3. 将温度控制精度要求写入操作参数表。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4, group: "安全健康", ans: "是",
|
||||||
|
q: "是否会因加热、冷却或精确的温度控制失控而导致安全问题?",
|
||||||
|
risk: "温度控制失效(如加热阀门卡死、冷却中断)可能使实际温度超过100℃,导致设备超温、压力升高或反应失控。",
|
||||||
|
measure: "1. 调试并确认DCS温度高报警(95℃)和高高报警联锁(100℃)功能正常;2. 执行联锁回路功能测试(温度变送器、DCS卡件、电磁阀、切断阀);3. 确认加热介质切断阀为故障安全型。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5, group: "安全健康", ans: "是",
|
||||||
|
q: "是否具有为安全操作所必需的仪表、控制、紧急制动、开关或报警?",
|
||||||
|
risk: "变更后温度报警值上调,现有温度仪表量程可能不足或精度不够,影响安全操作。",
|
||||||
|
measure: "1. 检查温度变送器量程是否覆盖0~100℃,精度是否优于0.5级;2. 确认DCS报警值设定正确且已组态;3. 在DCS画面中明显标注新报警值。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6, group: "工艺与操作", ans: "是",
|
||||||
|
q: "变更后是否需要对操作人员进行专门培训?",
|
||||||
|
risk: "新工况(90℃ / 4小时)与原操作习惯差异较大,误操作可能引发超温或产品质量波动。",
|
||||||
|
measure: "1. 修订操作规程并组织专项培训考核;2. 投用首周实行双人复核操作;3. 关键参数调整权限上收至班长。",
|
||||||
|
},
|
||||||
|
{ id: 7, group: "工艺与操作", ans: "否", q: "是否涉及应急预案或消防设施的调整?" },
|
||||||
|
];
|
||||||
|
const pluginDone = ref<Record<string, number>>({});
|
||||||
|
const riskChecks = ref<any[]>(RISK_CHECKS_INIT.map((x) => ({ ...x })));
|
||||||
|
const riskGroups = computed(() => [...new Set(riskChecks.value.map((x) => x.group))]);
|
||||||
|
const riskFilter = ref<string>("全部");
|
||||||
|
const onBack = () => {
|
||||||
|
pluginDone.value = { ...pluginDone.value, RISK_CHECK: riskChecks.value.filter((x) => x.ans === '是').length };
|
||||||
|
emit("back");
|
||||||
|
}
|
||||||
|
const save = () => {
|
||||||
|
pluginDone.value = { ...pluginDone.value, RISK_CHECK: riskChecks.value.filter((x) => x.ans === '是').length };
|
||||||
|
message.success("已手动保存,分析记录数已同步至工具卡片(内容修改实时自动保存,退出不丢失)");
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="flex flex-wrap items-center gap-3">
|
||||||
|
<NButton size="small" ghost type="primary" class="text-xs" @click="onBack">
|
||||||
|
<Icon icon="basil:arrow-left-outline" class="size-18px" /> 返回工具列表
|
||||||
|
</NButton>
|
||||||
|
<NButton size="small" ghost type="primary" class="text-xs" @click="save">保存</NButton>
|
||||||
|
<span class="flex items-center gap-1.5 text-sm font-semibold text-slate-700">变更风险检查表(通用)</span>
|
||||||
|
<NTag size="small" type="info">插件</NTag>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<NRadioGroup v-model:value="riskFilter" name="radiogroup">
|
||||||
|
<NRadio v-for="song in [{label:'全部',value:'全部'},{label:'仅看「是」',value:'是'}]" :key="song.value" :value="song.value">
|
||||||
|
{{ song.label }}
|
||||||
|
</NRadio>
|
||||||
|
</NRadioGroup>
|
||||||
|
</div>
|
||||||
|
<span class="ml-auto inline-flex items-center gap-1 rounded-full border border-violet-200 bg-violet-100 px-2 py-0.5 text-xs font-medium text-violet-700">
|
||||||
|
✨ AI 生成 · 需人工确认
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-[11px] text-slate-400">逐项判定「是 / 否 / 不涉及」;判定为「是」的检查项输出风险描述与管控措施,完成后点「返回工具列表」回显完成项数</p>
|
||||||
|
<div v-for="g in riskGroups" :key="g" class="overflow-hidden rounded-lg border">
|
||||||
|
<div class="bg-slate-100 px-3 py-2 text-sm font-medium text-slate-700">{{ g }}</div>
|
||||||
|
<div v-for="item in riskChecks.filter((x) => x.group === g && (riskFilter === '全部' || x.ans === '是'))" :key="item.id"
|
||||||
|
class="grid gap-3 border-t px-3 py-3 grid-cols-[1.1fr_1fr_1fr]">
|
||||||
|
<div>
|
||||||
|
<div class="text-sm text-slate-700">{{ item.q }}</div>
|
||||||
|
<div class="mt-2 flex items-center gap-4">
|
||||||
|
<span v-for="a in ['是', '否', 'NA']" :key="a">
|
||||||
|
<NRadio
|
||||||
|
:checked="item.ans === a"
|
||||||
|
@change="riskChecks = riskChecks.map((x) => (x.id === item.id ? { ...x, ans: a } : x))"
|
||||||
|
>
|
||||||
|
{{ a }}
|
||||||
|
</NRadio>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template v-if="item.ans === '是'">
|
||||||
|
<div class="rounded-lg bg-slate-50 p-2.5 text-xs leading-relaxed text-slate-600">
|
||||||
|
<span class="font-medium text-slate-500">风险描述:</span>{{ item.risk }}
|
||||||
|
</div>
|
||||||
|
<div class="rounded-lg bg-slate-50 p-2.5 text-xs leading-relaxed text-slate-600">
|
||||||
|
<span class="font-medium text-slate-500">管控措施:</span>{{ item.measure }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div v-else class="text-xs text-slate-300 self-center col-span-2">(回答为「{{ item.ans }}」,无风险描述)</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,237 @@
|
|||||||
|
<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 lowerOf = (lv: "高" | "中" | "低"): "高" | "中" | "低" => {
|
||||||
|
return lv === "高" ? "中" : "低";
|
||||||
|
}
|
||||||
|
const inherentOf = (l: number, s: number): "高" | "中" | "低" => {
|
||||||
|
const v = l * s;
|
||||||
|
return v >= 10 ? "高" : v >= 5 ? "中" : "低";
|
||||||
|
}
|
||||||
|
const toRec = (row: any): any => ({
|
||||||
|
item: row.item || "(未命名检查项)",
|
||||||
|
scene: `标准:${row.std};不符合可能导致:${row.cons}`,
|
||||||
|
inherent: inherentOf(row.l, row.s),
|
||||||
|
existing: row.ctrl,
|
||||||
|
suggest: row.sug,
|
||||||
|
residual: lowerOf(inherentOf(row.l, row.s)),
|
||||||
|
source: "SCL",
|
||||||
|
});
|
||||||
|
|
||||||
|
const gi = ref(0);
|
||||||
|
const cur = computed(() => groups.value[gi.value]);
|
||||||
|
const recTotal = computed(() => groups.value.reduce((a, g) => a + g.rows.length, 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 addRow = () => cur.value.rows.push({ item: "", std: "", cons: "", l: 2, s: 3, ctrl: "", sug: "" });
|
||||||
|
const delRow = (ri: number) => cur.value.rows.splice(ri, 1);
|
||||||
|
const exportAll = () => emit("export", groups.value.flatMap((g) => g.rows.map(toRec)), "ai");
|
||||||
|
|
||||||
|
const save = () => {
|
||||||
|
message.success("已手动保存,分析记录数已同步至工具卡片(内容修改实时自动保存,退出不丢失)");
|
||||||
|
};
|
||||||
|
const SCL_INFO = {
|
||||||
|
dev: "R-101 缩合反应釜",
|
||||||
|
mat: "缩合反应体系(有机溶剂 / 反应物料)",
|
||||||
|
param: "设计温度:-10/120 ℃,设计压力:0.6 MPa;变更后操作温度:90±2 ℃(原 80±2 ℃),常压",
|
||||||
|
};
|
||||||
|
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 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 groups = ref([
|
||||||
|
{ g: "本体", rows: [
|
||||||
|
{ item: "釜体及夹套壁厚", std: "测厚值 ≥ 设计最小壁厚,无超标腐蚀减薄", cons: "壁厚不足导致强度失效、泄漏", l: 1, s: 4, ctrl: "工程控制:定期测厚;管理措施:年度检验计划", sug: "变更后温度提升,复核腐蚀速率评估" },
|
||||||
|
{ item: "搅拌器运行状态", std: "电流、振动在正常范围,无异响", cons: "搅拌失效导致传热不均、局部过热", l: 2, s: 3, ctrl: "工程控制:电流在线监视;培训教育:异常处置规程", sug: "投用首周每班记录搅拌电流趋势" },
|
||||||
|
] },
|
||||||
|
{ g: "安全附件", rows: [
|
||||||
|
{ item: "安全阀", std: "校验在有效期内,起跳压力 0.5 MPa,铅封完好", cons: "超压时无法正常泄放", l: 1, s: 5, ctrl: "工程控制:定期校验;管理措施:校验台账管理", sug: "本次温度变更不影响泄放工况,维持原校验周期" },
|
||||||
|
{ item: "温度 / 压力仪表", std: "校验合格,现场示值与 DCS 一致", cons: "测量失真导致误判", l: 2, s: 3, ctrl: "管理措施:周期校验;现场与 DCS 比对核查", sug: "变更投用前完成一次示值比对" },
|
||||||
|
] },
|
||||||
|
{ g: "DCS 联锁与报警", rows: [
|
||||||
|
{ item: "温度高报 / 联锁设定值", std: "高报 95℃、高高报及联锁 100℃,与变更单一致", cons: "设定错误导致保护失效或误动作", l: 2, s: 5, ctrl: "管理措施:双人复核修改;工程控制:联锁测试", sug: "测试记录归档至变更资料" },
|
||||||
|
{ item: "联锁传动测试", std: "试验动作正确率 100%", cons: "联锁拒动 / 误动", l: 1, s: 5, ctrl: "管理措施:投用前强制测试;培训教育", sug: "纳入 PSSR 必查项" },
|
||||||
|
] },
|
||||||
|
{ g: "操作规程与应急", rows: [
|
||||||
|
{ item: "新版操作规程", std: "90℃ / 4h 工艺参数已修订发布并完成培训", cons: "沿用旧规程导致误操作", l: 2, s: 3, ctrl: "管理措施:规程受控发放;培训教育:签到考核", sug: "旧版规程现场收回,防止混用" },
|
||||||
|
{ item: "超温应急处置卡", std: "处置卡已更新并放置岗位", cons: "应急处置不当扩大事态", l: 1, s: 4, ctrl: "培训教育:应急演练;个体防护:应急物资就位", sug: "投用前组织一次超温处置桌面演练" },
|
||||||
|
] },
|
||||||
|
])
|
||||||
|
</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)]">SCL 安全检查表插件</span>
|
||||||
|
<NTag size="small" type="info">插件</NTag>
|
||||||
|
<NTag size="small">MOC 变更:缩合反应温度及时间优化(演示)</NTag>
|
||||||
|
<span class="text-[11px] text-slate-500">{{ `${groups.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('SCL 检查表已导出(Word)')">
|
||||||
|
<Icon icon="material-symbols:download" class="size-14px" /> 导出 Word
|
||||||
|
</NButton>
|
||||||
|
<NButton size="small" type="primary" class="text-xs" @click="message.success('演示:AI 辅助分析完成,已按变更内容核对检查标准(支持手动修改)')">
|
||||||
|
<Icon icon="lucide:sparkles" class="size-12px" />
|
||||||
|
辅助分析
|
||||||
|
</NButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 设备信息头 -->
|
||||||
|
<div class="grid gap-2 rounded-lg border bg-white p-3 text-xs sm:grid-cols-3">
|
||||||
|
<div><span class="text-slate-400">设备名称:</span><b class="text-slate-700">{{ SCL_INFO.dev }}</b></div>
|
||||||
|
<div><span class="text-slate-400">物料信息:</span><b class="text-slate-700">{{ SCL_INFO.mat }}</b></div>
|
||||||
|
<div class="sm:col-span-3"><span class="text-slate-400">运行参数:</span><span class="text-slate-600">{{ SCL_INFO.param }}</span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="flex gap-3"
|
||||||
|
:style="{'--theme-color':themeStore.themeColor}"
|
||||||
|
>
|
||||||
|
<!-- 左:检查分类 -->
|
||||||
|
<div class="shrink-0 rounded-lg border bg-white p-2 w-44">
|
||||||
|
<div v-for="(g, i) in groups" :key="g.g" @click="gi = i"
|
||||||
|
class="cursor-pointer flex w-full items-center rounded-md px-3 py-1.5 text-left text-xs transition"
|
||||||
|
:class="gi === i ? 'bg-[var(--theme-color)] text-white' : 'text-slate-600 hover:bg-slate-100'">
|
||||||
|
<span class="flex-1">{{ g.g }}</span>
|
||||||
|
<span class="rounded-full px-1.5 text-[10px]" :class="gi === i ? 'bg-white/25' : 'bg-slate-100 text-slate-400'">{{ g.rows.length }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 右:检查表 -->
|
||||||
|
<div class="min-w-0 flex-1 bg-white">
|
||||||
|
<div class="flex items-center gap-2 pb-2">
|
||||||
|
<NButton size="small" 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-[900px] border-collapse text-xs">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th :class="TH">检查项目</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>
|
||||||
|
<tr v-for="(row, ri) in cur.rows" :key="ri">
|
||||||
|
<td :class="TD"><Ed :v="row.item" @update="(x) => updRow(ri, { item: x })" /></td>
|
||||||
|
<td :class="TD"><Ed :v="row.std" @update="(x) => updRow(ri, { std: 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.ctrl" @update="(x) => updRow(ri, { ctrl: x })" /></td>
|
||||||
|
<td :class="TD"><Ed :v="row.sug" @update="(x) => updRow(ri, { sug: x })" /></td>
|
||||||
|
<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="emit('export', [toRec(row)], 'manual')">入表
|
||||||
|
</NButton>
|
||||||
|
<NButton text type="error" title="删除行" @click="delRow(ri)">
|
||||||
|
<Icon icon="ep:delete" class="size-14px" />
|
||||||
|
</NButton>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -9,29 +9,158 @@ const appStore = useAppStore();
|
|||||||
const themeStore = useThemeStore();
|
const themeStore = useThemeStore();
|
||||||
const gap = computed(() => (appStore.isMobile ? 0 : 16));
|
const gap = computed(() => (appStore.isMobile ? 0 : 16));
|
||||||
|
|
||||||
const aiEnabled = ref<boolean>(true);
|
|
||||||
// AI 辅助功能配置
|
// AI 辅助功能配置
|
||||||
|
const aiEnabled = ref<boolean>(true);
|
||||||
const aiConfig = ref<{address:string, mode:number|null, key:string,timeout:null|number}>({
|
const aiConfig = ref<{address:string, mode:number|null, key:string,timeout:null|number}>({
|
||||||
address:'',
|
address:'',
|
||||||
mode:null,
|
mode:null,
|
||||||
key:'',
|
key:'',
|
||||||
timeout:null,
|
timeout:null,
|
||||||
})
|
})
|
||||||
|
const aiTested = ref<string>("none");
|
||||||
const modeOptions = ref<{label:string, value:number}[]>([
|
const modeOptions = ref<{label:string, value:number}[]>([
|
||||||
{label:'kimi-k2(企业私有化部署)',value:1}
|
{label:'kimi-k2(企业私有化部署)',value:1}
|
||||||
])
|
])
|
||||||
|
const testing = ref<boolean>(false);
|
||||||
|
const showLog = ref<boolean>(false);
|
||||||
|
const AI_LOGS = [
|
||||||
|
{ scene: "AI-01 变更识别", user: "张工艺", change: "MOC-2026-R01-0041", time: "08-24 09:12", cost: "1.8s", adopted: "修改后采纳" },
|
||||||
|
{ scene: "AI-03 风险预分析", user: "张工艺", change: "MOC-2026-R01-0041", time: "08-24 09:15", cost: "2.4s", adopted: "采纳" },
|
||||||
|
{ scene: "AI-07 审批辅助", user: "李主任", change: "MOC-2026-R01-0039", time: "08-24 08:47", cost: "1.2s", adopted: "采纳" },
|
||||||
|
{ scene: "AI-06 PSSR 生成", user: "张工艺", change: "MOC-2026-R02-0052", time: "08-23 16:20", cost: "2.1s", adopted: "未采纳" },
|
||||||
|
{ scene: "AI-09 关闭辅助判定", user: "系统管理员", change: "MOC-2026-R01-0035", time: "08-23 15:02", cost: "1.5s", adopted: "采纳" },
|
||||||
|
];
|
||||||
|
|
||||||
// 变更类型
|
// 变更类型
|
||||||
|
const typeRef = ref<any>(null);
|
||||||
|
const typeModal = ref<boolean>(false);
|
||||||
|
const typeLoading = ref<boolean>(false);
|
||||||
|
const currentType = ref<string>('add');
|
||||||
|
const typeInfo = ref<{id:number, name:string, desc?:string}>({
|
||||||
|
id:0,
|
||||||
|
name:'',
|
||||||
|
desc:'',
|
||||||
|
})
|
||||||
|
const typeRules = {
|
||||||
|
name:{required:true, message:'请输入类型名称', trigger:'blur'},
|
||||||
|
}
|
||||||
const typeList = ref<{id:number, name:string, desc?:string}[]>([
|
const typeList = ref<{id:number, name:string, desc?:string}[]>([
|
||||||
{id:1,name:'工艺', desc:'工艺参数、配方、操作条件、操作规程等工艺技术类变更'},
|
{id:1,name:'工艺', desc:'工艺参数、配方、操作条件、操作规程等工艺技术类变更'},
|
||||||
{id:2,name:'设备', desc:'设备本体、附件、材质、规格、备件等硬件类变更'},
|
{id:2,name:'设备', desc:'设备本体、附件、材质、规格、备件等硬件类变更'},
|
||||||
{id:3,name:'管理', desc:'制度、职责、程序、组织与人员等管理类变更'},
|
{id:3,name:'管理', desc:'制度、职责、程序、组织与人员等管理类变更'},
|
||||||
])
|
])
|
||||||
|
// 新增类型
|
||||||
|
const addType = () => {
|
||||||
|
typeInfo.value = {
|
||||||
|
id:0,
|
||||||
|
name:'',
|
||||||
|
desc:'',
|
||||||
|
}
|
||||||
|
currentType.value = 'add';
|
||||||
|
typeModal.value = true;
|
||||||
|
}
|
||||||
|
// 编辑类型
|
||||||
|
const editType = (row:any) => {
|
||||||
|
typeInfo.value = {
|
||||||
|
id:row.id,
|
||||||
|
name:row.name,
|
||||||
|
desc:row.desc,
|
||||||
|
}
|
||||||
|
currentType.value = 'edit';
|
||||||
|
typeModal.value = true;
|
||||||
|
}
|
||||||
|
// 删除类型
|
||||||
|
const deleteType = (item:any) => {
|
||||||
|
typeList.value = typeList.value.filter((i:any) => i.id !== item.id);
|
||||||
|
}
|
||||||
|
// 保存类型
|
||||||
|
const typeSave = async (e:MouseEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
typeRef.value?.validate((errors:any) => {
|
||||||
|
if (!errors) {
|
||||||
|
typeLoading.value = true;
|
||||||
|
try {
|
||||||
|
// await addTypeApi(typeInfo.value);
|
||||||
|
window.$message?.success('操作成功')
|
||||||
|
typeLoading.value = false;
|
||||||
|
typeModal.value = false;
|
||||||
|
} finally {
|
||||||
|
typeLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
// 变更等级
|
// 变更等级
|
||||||
|
const levelRef = ref<any>(null);
|
||||||
|
const levelModal = ref<boolean>(false);
|
||||||
|
const levelLoading = ref<boolean>(false);
|
||||||
|
const currentLevel = ref<string>('add');
|
||||||
|
const toolsOptions = ref<{label:string, value:number}[]>([
|
||||||
|
{label:'AI预分析',value:1},
|
||||||
|
{label:'检查表法',value:2},
|
||||||
|
{label:'HAZOP',value:3},
|
||||||
|
{label:'JSA',value:4},
|
||||||
|
{label:'SCL',value:5},
|
||||||
|
])
|
||||||
|
const levelInfo = ref<{id:number, name:string, way:string,tools:number[]}>({
|
||||||
|
id:0,
|
||||||
|
name:'',
|
||||||
|
way:'',
|
||||||
|
tools:[],
|
||||||
|
})
|
||||||
|
const levelRules = {
|
||||||
|
name:{required:true, message:'请输入等级名称', trigger:'blur'},
|
||||||
|
way:{required:true, message:'请输入等级判定方式', trigger:'blur'},
|
||||||
|
tools:{type:'array', required:true, message:'请选择分析工具', trigger:['blur','change']},
|
||||||
|
}
|
||||||
const levelList = ref<{id:number, name:string, way:string,tools:{id:number, name:string}[]}[]>([
|
const levelList = ref<{id:number, name:string, way:string,tools:{id:number, name:string}[]}[]>([
|
||||||
{id:1,name:'一般', way:'等级评分< 20 分',tools:[{id:1,name:'AI预分析'},{id:2,name:'检查表法'}]},
|
{id:1,name:'一般', way:'等级评分< 20 分',tools:[{id:1,name:'AI预分析'},{id:2,name:'检查表法'}]},
|
||||||
{id:2,name:'重要', way:'等级评分 > 20 分',tools:[{id:1,name:'AI预分析'},{id:2,name:'HAZOP'},{id:3,name:'JSA'},{id:4,name:'SCL'}]},
|
{id:2,name:'重要', way:'等级评分 > 20 分',tools:[{id:1,name:'AI预分析'},{id:2,name:'HAZOP'},{id:3,name:'JSA'},{id:4,name:'SCL'}]},
|
||||||
])
|
])
|
||||||
|
// 新增等级
|
||||||
|
const addLevel = () => {
|
||||||
|
levelInfo.value = {
|
||||||
|
id:0,
|
||||||
|
name:'',
|
||||||
|
way:'',
|
||||||
|
tools:[],
|
||||||
|
}
|
||||||
|
currentLevel.value = 'add';
|
||||||
|
levelModal.value = true;
|
||||||
|
}
|
||||||
|
// 编辑类型
|
||||||
|
const editLevel = (row:any) => {
|
||||||
|
levelInfo.value = {
|
||||||
|
id:row.id,
|
||||||
|
name:row.name,
|
||||||
|
way:row.way,
|
||||||
|
tools:row.tools.map((item:any) => (item.id)),
|
||||||
|
}
|
||||||
|
currentLevel.value = 'edit';
|
||||||
|
levelModal.value = true;
|
||||||
|
}
|
||||||
|
// 删除类型
|
||||||
|
const deleteLevel = (item:any) => {
|
||||||
|
levelList.value = levelList.value.filter((i:any) => i.id !== item.id);
|
||||||
|
}
|
||||||
|
// 保存类型
|
||||||
|
const levelSave = async (e:MouseEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
levelRef.value?.validate((errors:any) => {
|
||||||
|
if (!errors) {
|
||||||
|
levelLoading.value = true;
|
||||||
|
try {
|
||||||
|
// await addLevelApi(levelInfo.value);
|
||||||
|
window.$message?.success('操作成功')
|
||||||
|
levelLoading.value = false;
|
||||||
|
levelModal.value = false;
|
||||||
|
} finally {
|
||||||
|
levelLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// 变更编号规则
|
// 变更编号规则
|
||||||
const ruleInfo = ref<{prefix:string, year:null|string, code:number|null, digit:number|null, rule:number|null}>({
|
const ruleInfo = ref<{prefix:string, year:null|string, code:number|null, digit:number|null, rule:number|null}>({
|
||||||
prefix:'MOC',
|
prefix:'MOC',
|
||||||
@@ -50,14 +179,91 @@ const ruleOptions = ref<{label:string, value:number}[]>([
|
|||||||
{label:'按年度+装置代码重置',value:1}
|
{label:'按年度+装置代码重置',value:1}
|
||||||
])
|
])
|
||||||
|
|
||||||
|
// ---------- 风险矩阵设置(按分析方法) ----------
|
||||||
|
const dimensionModal = ref<boolean>(false);
|
||||||
|
const dimensionLoading = ref<boolean>(false);
|
||||||
|
const dimForm = ref({ s: [], l: [] });
|
||||||
|
const LV_CLS = [
|
||||||
|
"border-emerald-300 bg-emerald-100 text-emerald-700",
|
||||||
|
"border-yellow-300 bg-yellow-100 text-yellow-700",
|
||||||
|
"border-orange-300 bg-orange-100 text-orange-700",
|
||||||
|
"border-red-300 bg-red-100 text-red-700",
|
||||||
|
];
|
||||||
|
const matrices = ref([
|
||||||
|
{
|
||||||
|
tool: "HAZOP",
|
||||||
|
sLabels: ["轻微", "一般", "较重", "严重", "灾难"],
|
||||||
|
lLabels: ["极少发生", "较少发生", "偶尔发生", "可能发生", "频繁发生"],
|
||||||
|
levels: [
|
||||||
|
{ name: "低", min: 1, max: 4, cls: LV_CLS[0] },
|
||||||
|
{ name: "一般", min: 5, max: 9, cls: LV_CLS[1] },
|
||||||
|
{ name: "较大", min: 10, max: 16, cls: LV_CLS[2] },
|
||||||
|
{ name: "重大", min: 17, max: 25, cls: LV_CLS[3] },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tool: "JSA",
|
||||||
|
sLabels: ["轻微伤害", "轻伤", "重伤", "单人死亡", "多人死亡"],
|
||||||
|
lLabels: ["极不可能", "不太可能", "有可能", "较可能", "很可能"],
|
||||||
|
levels: [
|
||||||
|
{ name: "低", min: 1, max: 6, cls: LV_CLS[0] },
|
||||||
|
{ name: "一般", min: 7, max: 12, cls: LV_CLS[1] },
|
||||||
|
{ name: "较大", min: 13, max: 19, cls: LV_CLS[2] },
|
||||||
|
{ name: "重大", min: 20, max: 25, cls: LV_CLS[3] },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tool: "SCL",
|
||||||
|
sLabels: ["轻微影响", "一般影响", "较大影响", "严重影响", "灾难性影响"],
|
||||||
|
lLabels: ["极少", "较少", "偶尔", "可能", "经常"],
|
||||||
|
levels: [
|
||||||
|
{ name: "低", min: 1, max: 2, cls: LV_CLS[0] },
|
||||||
|
{ name: "一般", min: 3, max: 6, cls: LV_CLS[1] },
|
||||||
|
{ name: "较大", min: 7, max: 12, cls: LV_CLS[2] },
|
||||||
|
{ name: "重大", min: 13, max: 25, cls: LV_CLS[3] },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const activeMatrix = ref(0);
|
||||||
|
const curMatrix = computed(() => matrices.value[activeMatrix.value]);
|
||||||
|
const cellScore = (s:number, l:number) => (s + 1) * (l + 1);
|
||||||
|
const cellLevel = (s:number, l:number) => {
|
||||||
|
const sc = cellScore(s, l);
|
||||||
|
return curMatrix.value.levels.find((lv:{min:number,max:number}) => sc >= lv.min && sc <= lv.max);
|
||||||
|
}
|
||||||
|
// AI 辅助功能切换
|
||||||
const toggle = (v: boolean) => {
|
const toggle = (v: boolean) => {
|
||||||
window.$message?.success(v ? "AI 辅助功能已启用" : "AI 辅助功能已关闭");
|
window.$message?.success(v ? "AI 辅助功能已启用" : "AI 辅助功能已关闭");
|
||||||
aiEnabled.value = v;
|
aiEnabled.value = v;
|
||||||
}
|
}
|
||||||
|
const testConn = () => {
|
||||||
|
testing.value = true;
|
||||||
|
setTimeout(() => {
|
||||||
|
testing.value = false;
|
||||||
|
aiTested.value = "ok";
|
||||||
|
window.$message?.success(`连接测试通过:${aiConfig.value.mode} 响应正常(耗时 0.8s)`);
|
||||||
|
}, 900);
|
||||||
|
}
|
||||||
|
const saveAiCfg = () => {
|
||||||
|
window.$message?.success("AI 接口配置已保存(配置变更留痕,立即生效)");
|
||||||
|
}
|
||||||
|
const dimensionSave = () => {
|
||||||
|
dimensionLoading.value = true;
|
||||||
|
setTimeout(() => {
|
||||||
|
window.$message?.success("操作成功");
|
||||||
|
dimensionLoading.value = false;
|
||||||
|
dimensionModal.value = false;
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
const openDimEdit = () => {
|
||||||
|
dimForm.value = { s: [...curMatrix.value.sLabels], l: [...curMatrix.value.lLabels] };
|
||||||
|
dimensionModal.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// tabs 切换
|
||||||
|
const handleTabChange = (v: number) => {
|
||||||
|
activeMatrix.value = v;
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -96,6 +302,11 @@ const toggle = (v: boolean) => {
|
|||||||
当前状态:{{ aiEnabled ? "已启用 —— 变更申请界面提供 AI 变更识别与申请表生成" : "已关闭 —— 变更申请界面为纯手动模式" }}
|
当前状态:{{ aiEnabled ? "已启用 —— 变更申请界面提供 AI 变更识别与申请表生成" : "已关闭 —— 变更申请界面为纯手动模式" }}
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-2 border rounded-md p-3 mt-3">
|
<div class="space-y-2 border rounded-md p-3 mt-3">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<Icon icon="bx:plug" class="size-14px text-slate-400 mr-1" />
|
||||||
|
AI 接口配置
|
||||||
|
<NTag v-if="aiTested === 'ok'" size="small" type="success" class="ml-auto">连接正常</NTag>
|
||||||
|
</div>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<p class="w-100px text-left">接口地址</p>
|
<p class="w-100px text-left">接口地址</p>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
@@ -121,9 +332,9 @@ const toggle = (v: boolean) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<NButton size="small" type="primary" class="text-xs">测试连接</NButton>
|
<NButton size="small" type="primary" class="text-xs" :loading="testing" @click="testConn">测试连接</NButton>
|
||||||
<NButton size="small" class="text-xs">保存配置</NButton>
|
<NButton size="small" class="text-xs" @click="saveAiCfg">保存配置</NButton>
|
||||||
<NButton size="small" class="text-xs">
|
<NButton size="small" class="text-xs" @click="showLog = true">
|
||||||
<Icon icon="akar-icons:file" class="size-14px mr-1" />
|
<Icon icon="akar-icons:file" class="size-14px mr-1" />
|
||||||
调用记录查询
|
调用记录查询
|
||||||
</NButton>
|
</NButton>
|
||||||
@@ -140,7 +351,7 @@ const toggle = (v: boolean) => {
|
|||||||
<Icon icon="ep:setting" class="size-16px mr-1" />
|
<Icon icon="ep:setting" class="size-16px mr-1" />
|
||||||
变更类型设置(支持自定义)
|
变更类型设置(支持自定义)
|
||||||
</div>
|
</div>
|
||||||
<NButton size="small" type="primary" class="text-xs">
|
<NButton size="small" type="primary" class="text-xs" @click="addType">
|
||||||
<Icon icon="ic:round-plus" class="size-16px" />新增类型
|
<Icon icon="ic:round-plus" class="size-16px" />新增类型
|
||||||
</NButton>
|
</NButton>
|
||||||
</div>
|
</div>
|
||||||
@@ -153,12 +364,22 @@ const toggle = (v: boolean) => {
|
|||||||
<p class="flex-1">{{item.desc}}</p>
|
<p class="flex-1">{{item.desc}}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<NButton text type="primary" class="text-xs">
|
<NButton text type="primary" class="text-xs" @click="editType(item)">
|
||||||
<Icon icon="akar-icons:edit" class="size-14px" />编辑
|
<Icon icon="akar-icons:edit" class="size-14px" />编辑
|
||||||
</NButton>
|
</NButton>
|
||||||
|
<NPopconfirm
|
||||||
|
positive-text="确定"
|
||||||
|
:positiveButtonProps="{ size: 'tiny' }"
|
||||||
|
:negativeButtonProps="{ size: 'tiny' }"
|
||||||
|
@positive-click="deleteType(item)"
|
||||||
|
>
|
||||||
|
<template #trigger>
|
||||||
<NButton text type="error" class="text-xs">
|
<NButton text type="error" class="text-xs">
|
||||||
<Icon icon="ep:delete" class="size-12px" />删除
|
<Icon icon="ep:delete" class="size-12px" />删除
|
||||||
</NButton>
|
</NButton>
|
||||||
|
</template>
|
||||||
|
确定删除吗?
|
||||||
|
</NPopconfirm>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -188,7 +409,7 @@ const toggle = (v: boolean) => {
|
|||||||
<Icon icon="ep:setting" class="size-16px mr-1" />
|
<Icon icon="ep:setting" class="size-16px mr-1" />
|
||||||
变更等级设定(支持自定义)
|
变更等级设定(支持自定义)
|
||||||
</div>
|
</div>
|
||||||
<NButton size="small" type="primary" class="text-xs">
|
<NButton size="small" type="primary" class="text-xs" @click="addLevel">
|
||||||
<Icon icon="ic:round-plus" class="size-16px" />新增等级
|
<Icon icon="ic:round-plus" class="size-16px" />新增等级
|
||||||
</NButton>
|
</NButton>
|
||||||
</div>
|
</div>
|
||||||
@@ -202,12 +423,22 @@ const toggle = (v: boolean) => {
|
|||||||
<p class="flex-1">判定方式:<b>{{item.way}}</b></p>
|
<p class="flex-1">判定方式:<b>{{item.way}}</b></p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<NButton text type="primary" class="text-xs">
|
<NButton text type="primary" class="text-xs" @click="editLevel(item)">
|
||||||
<Icon icon="akar-icons:edit" class="size-14px" />编辑
|
<Icon icon="akar-icons:edit" class="size-14px" />编辑
|
||||||
</NButton>
|
</NButton>
|
||||||
|
<NPopconfirm
|
||||||
|
positive-text="确定"
|
||||||
|
:positiveButtonProps="{ size: 'tiny' }"
|
||||||
|
:negativeButtonProps="{ size: 'tiny' }"
|
||||||
|
@positive-click="deleteLevel(item)"
|
||||||
|
>
|
||||||
|
<template #trigger>
|
||||||
<NButton text type="error" class="text-xs">
|
<NButton text type="error" class="text-xs">
|
||||||
<Icon icon="ep:delete" class="size-12px" />删除
|
<Icon icon="ep:delete" class="size-12px" />删除
|
||||||
</NButton>
|
</NButton>
|
||||||
|
</template>
|
||||||
|
确定删除吗?
|
||||||
|
</NPopconfirm>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-wrap mt-2">
|
<div class="flex flex-wrap mt-2">
|
||||||
@@ -302,7 +533,7 @@ const toggle = (v: boolean) => {
|
|||||||
<Icon icon="icon-park-outline:all-application" class="size-16px mr-1" />
|
<Icon icon="icon-park-outline:all-application" class="size-16px mr-1" />
|
||||||
风险矩阵设置(按分析方法)
|
风险矩阵设置(按分析方法)
|
||||||
</div>
|
</div>
|
||||||
<NButton size="small" class="text-xs">
|
<NButton size="small" class="text-xs" @click="openDimEdit">
|
||||||
<Icon icon="akar-icons:edit" class="size-14px mr-1" />
|
<Icon icon="akar-icons:edit" class="size-14px mr-1" />
|
||||||
维度分级定义
|
维度分级定义
|
||||||
</NButton>
|
</NButton>
|
||||||
@@ -311,21 +542,294 @@ const toggle = (v: boolean) => {
|
|||||||
</NButton>
|
</NButton>
|
||||||
</div>
|
</div>
|
||||||
<div class="py-3 px-4">
|
<div class="py-3 px-4">
|
||||||
<NTabs type="segment" animated>
|
<NTabs type="segment" animated :on-update:value="handleTabChange">
|
||||||
<NTabPane name="chap1" tab="HAZOP分析矩阵">
|
<NTabPane :name="0" tab="HAZOP分析矩阵">
|
||||||
HAZOP分析矩阵
|
<div class="flex flex-wrap items-start gap-6">
|
||||||
|
<div class="shrink-0">
|
||||||
|
<table class="border-collapse text-center text-[11px]">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="px-1"></th>
|
||||||
|
<th v-for="(lb, li) in curMatrix.lLabels" :key="li" class="px-0.5 pb-1 font-normal leading-tight text-slate-500">
|
||||||
|
L{{ li + 1 }}<br />{{ lb }}
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="row in 5" :key="row">
|
||||||
|
<td class="whitespace-nowrap pr-1.5 text-right leading-tight text-slate-500">S{{ 6 - row }} {{ curMatrix.sLabels[5 - row] }}</td>
|
||||||
|
<td v-for="col in 5" :key="col" class="p-0.5">
|
||||||
|
<div class="flex h-9 w-12 items-center justify-center rounded border font-semibold" :class="cellLevel(5 - row, col - 1)?.cls">
|
||||||
|
{{ cellScore(5 - row, col - 1) }}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div class="mt-1.5 flex justify-between text-[11px] text-slate-400">
|
||||||
|
<span>纵轴:后果严重性 S</span>
|
||||||
|
<span>横轴:发生可能性 L</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 等级分值区间 -->
|
||||||
|
<div class="min-w-72 flex-1">
|
||||||
|
<div class="mb-2 text-xs text-slate-500">风险值 = 严重性 S × 可能性 L(5×5),按分值区间定级:</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div v-for="lv in curMatrix.levels" :key="lv.name" class="flex items-center gap-2 rounded-lg border px-2.5 py-2">
|
||||||
|
<span class="h-3 w-3 shrink-0 rounded-sm border" :class="lv.cls"></span>
|
||||||
|
<NInput v-model:value="lv.name" size="small" class="!w-16 text-xs" />
|
||||||
|
<span class="shrink-0 text-xs text-slate-400">分值</span>
|
||||||
|
<NInputNumber v-model:value="lv.min" :min="1" :max="25" size="small" :show-button="false" class="!w-14 text-center text-xs" />
|
||||||
|
<span class="text-slate-400">–</span>
|
||||||
|
<NInputNumber v-model:value="lv.max" :min="1" :max="25" size="small" :show-button="false" class="!w-14 text-center text-xs" />
|
||||||
|
<NTag size="small" :class="lv.cls" class="ml-auto">{{ lv.name }}风险</NTag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="mt-2.5 text-[11px] text-slate-400">
|
||||||
|
各分析方法独立维护矩阵与维度分级;风险分析记录表按所选工具对应的矩阵自动定级。分值区间不得重叠、须落在 1–25 内;在途分析记录按原矩阵保留定级快照。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</NTabPane>
|
</NTabPane>
|
||||||
<NTabPane name="chap2" tab="JSA分析矩阵">
|
<NTabPane :name="1" tab="JSA分析矩阵">
|
||||||
JSA分析矩阵
|
<div class="flex flex-wrap items-start gap-6">
|
||||||
|
<div class="shrink-0">
|
||||||
|
<table class="border-collapse text-center text-[11px]">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="px-1"></th>
|
||||||
|
<th v-for="(lb, li) in curMatrix.lLabels" :key="li" class="px-0.5 pb-1 font-normal leading-tight text-slate-500">
|
||||||
|
L{{ li + 1 }}<br />{{ lb }}
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="row in 5" :key="row">
|
||||||
|
<td class="whitespace-nowrap pr-1.5 text-right leading-tight text-slate-500">S{{ 6 - row }} {{ curMatrix.sLabels[5 - row] }}</td>
|
||||||
|
<td v-for="col in 5" :key="col" class="p-0.5">
|
||||||
|
<div class="flex h-9 w-12 items-center justify-center rounded border font-semibold" :class="cellLevel(5 - row, col - 1)?.cls">
|
||||||
|
{{ cellScore(5 - row, col - 1) }}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div class="mt-1.5 flex justify-between text-[11px] text-slate-400">
|
||||||
|
<span>纵轴:后果严重性 S</span>
|
||||||
|
<span>横轴:发生可能性 L</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 等级分值区间 -->
|
||||||
|
<div class="min-w-72 flex-1">
|
||||||
|
<div class="mb-2 text-xs text-slate-500">风险值 = 严重性 S × 可能性 L(5×5),按分值区间定级:</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div v-for="lv in curMatrix.levels" :key="lv.name" class="flex items-center gap-2 rounded-lg border px-2.5 py-2">
|
||||||
|
<span class="h-3 w-3 shrink-0 rounded-sm border" :class="lv.cls"></span>
|
||||||
|
<NInput v-model:value="lv.name" size="small" class="!w-16 text-xs" />
|
||||||
|
<span class="shrink-0 text-xs text-slate-400">分值</span>
|
||||||
|
<NInputNumber v-model:value="lv.min" :min="1" :max="25" size="small" :show-button="false" class="!w-14 text-center text-xs" />
|
||||||
|
<span class="text-slate-400">–</span>
|
||||||
|
<NInputNumber v-model:value="lv.max" :min="1" :max="25" size="small" :show-button="false" class="!w-14 text-center text-xs" />
|
||||||
|
<NTag size="small" :class="lv.cls" class="ml-auto">{{ lv.name }}风险</NTag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="mt-2.5 text-[11px] text-slate-400">
|
||||||
|
各分析方法独立维护矩阵与维度分级;风险分析记录表按所选工具对应的矩阵自动定级。分值区间不得重叠、须落在 1–25 内;在途分析记录按原矩阵保留定级快照。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</NTabPane>
|
</NTabPane>
|
||||||
<NTabPane name="chap3" tab="SCL分析矩阵">
|
<NTabPane :name="2" tab="SCL分析矩阵">
|
||||||
SCL分析矩阵
|
<div class="flex flex-wrap items-start gap-6">
|
||||||
|
<div class="shrink-0">
|
||||||
|
<table class="border-collapse text-center text-[11px]">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="px-1"></th>
|
||||||
|
<th v-for="(lb, li) in curMatrix.lLabels" :key="li" class="px-0.5 pb-1 font-normal leading-tight text-slate-500">
|
||||||
|
L{{ li + 1 }}<br />{{ lb }}
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="row in 5" :key="row">
|
||||||
|
<td class="whitespace-nowrap pr-1.5 text-right leading-tight text-slate-500">S{{ 6 - row }} {{ curMatrix.sLabels[5 - row] }}</td>
|
||||||
|
<td v-for="col in 5" :key="col" class="p-0.5">
|
||||||
|
<div class="flex h-9 w-12 items-center justify-center rounded border font-semibold" :class="cellLevel(5 - row, col - 1)?.cls">
|
||||||
|
{{ cellScore(5 - row, col - 1) }}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div class="mt-1.5 flex justify-between text-[11px] text-slate-400">
|
||||||
|
<span>纵轴:后果严重性 S</span>
|
||||||
|
<span>横轴:发生可能性 L</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 等级分值区间 -->
|
||||||
|
<div class="min-w-72 flex-1">
|
||||||
|
<div class="mb-2 text-xs text-slate-500">风险值 = 严重性 S × 可能性 L(5×5),按分值区间定级:</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div v-for="lv in curMatrix.levels" :key="lv.name" class="flex items-center gap-2 rounded-lg border px-2.5 py-2">
|
||||||
|
<span class="h-3 w-3 shrink-0 rounded-sm border" :class="lv.cls"></span>
|
||||||
|
<NInput v-model:value="lv.name" size="small" class="!w-16 text-xs" />
|
||||||
|
<span class="shrink-0 text-xs text-slate-400">分值</span>
|
||||||
|
<NInputNumber v-model:value="lv.min" :min="1" :max="25" size="small" :show-button="false" class="!w-14 text-center text-xs" />
|
||||||
|
<span class="text-slate-400">–</span>
|
||||||
|
<NInputNumber v-model:value="lv.max" :min="1" :max="25" size="small" :show-button="false" class="!w-14 text-center text-xs" />
|
||||||
|
<NTag size="small" :class="lv.cls" class="ml-auto">{{ lv.name }}风险</NTag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="mt-2.5 text-[11px] text-slate-400">
|
||||||
|
各分析方法独立维护矩阵与维度分级;风险分析记录表按所选工具对应的矩阵自动定级。分值区间不得重叠、须落在 1–25 内;在途分析记录按原矩阵保留定级快照。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</NTabPane>
|
</NTabPane>
|
||||||
</NTabs>
|
</NTabs>
|
||||||
</div>
|
</div>
|
||||||
</NCard>
|
</NCard>
|
||||||
</NGi>
|
</NGi>
|
||||||
</NGrid>
|
</NGrid>
|
||||||
|
|
||||||
|
<!-- AI调用记录查询 -->
|
||||||
|
<NDrawer v-model:show="showLog" :width="700" placement="right">
|
||||||
|
<NDrawerContent title="调用记录查询" closable>
|
||||||
|
<table class="w-full text-xs">
|
||||||
|
<thead>
|
||||||
|
<tr class="border-b text-left text-slate-500">
|
||||||
|
<th class="py-2 font-semibold">AI 场景</th>
|
||||||
|
<th class="py-2 font-semibold">调用人</th>
|
||||||
|
<th class="py-2 font-semibold">关联变更</th>
|
||||||
|
<th class="py-2 font-semibold">时间</th>
|
||||||
|
<th class="py-2 text-right font-semibold">耗时</th>
|
||||||
|
<th class="py-2 text-right font-semibold">采纳结果</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="(r, i) in AI_LOGS" :key="i" class="border-b last:border-0">
|
||||||
|
<td class="py-2 text-slate-700">{{ r.scene }}</td>
|
||||||
|
<td class="py-2 text-slate-600">{{ r.user }}</td>
|
||||||
|
<td class="py-2 font-mono text-slate-500">{{ r.change }}</td>
|
||||||
|
<td class="py-2 text-slate-500">{{ r.time }}</td>
|
||||||
|
<td class="py-2 text-right text-slate-600">{{ r.cost }}</td>
|
||||||
|
<td class="py-2 text-right">
|
||||||
|
<NTag v-if="r.adopted === '采纳'" size="small" type="success">{{ r.adopted }}</NTag>
|
||||||
|
<NTag v-if="r.adopted === '修改后采纳'" size="small" type="warning">{{ r.adopted }}</NTag>
|
||||||
|
<NTag v-if="r.adopted === '未采纳'" size="small">{{ r.adopted }}</NTag>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<p class="mt-3 text-[11px] text-slate-400">调用记录含输入摘要(脱敏)、模型与提示词版本、耗时与采纳结果,支持采纳率统计;完整输出存对象存储。</p>
|
||||||
|
</NDrawerContent>
|
||||||
|
</NDrawer>
|
||||||
|
<!-- 新增/编辑类型 -->
|
||||||
|
<NModal
|
||||||
|
v-model:show="typeModal"
|
||||||
|
preset="card"
|
||||||
|
:title="currentType === 'add' ? '新增变更类型' : '编辑变更类型'"
|
||||||
|
:auto-focus="false"
|
||||||
|
:style="{ width: '450px', height: 'auto' }"
|
||||||
|
:segmented="{ content: true, footer: true }"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<NForm
|
||||||
|
ref="typeRef"
|
||||||
|
label-width="auto"
|
||||||
|
:model="typeInfo"
|
||||||
|
:rules="typeRules"
|
||||||
|
>
|
||||||
|
<NFormItem label="类型名称" path="name">
|
||||||
|
<NInput v-model:value="typeInfo.name" placeholder="如:仪表控制" />
|
||||||
|
</NFormItem>
|
||||||
|
<NFormItem label="类型定义描述" path="desc">
|
||||||
|
<NInput v-model:value="typeInfo.desc" type="textarea" placeholder="该类型覆盖的变更范围说明" />
|
||||||
|
</NFormItem>
|
||||||
|
</NForm>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<div class="flex justify-end gap-[10px]">
|
||||||
|
<NButton size="small" @click="typeModal = false">取消</NButton>
|
||||||
|
<NButton size="small" type="primary" :loading="typeLoading" @click="typeSave">确定</NButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</NModal>
|
||||||
|
<!-- 新增/编辑等级 -->
|
||||||
|
<NModal
|
||||||
|
v-model:show="levelModal"
|
||||||
|
preset="card"
|
||||||
|
:title="currentLevel === 'add' ? '新增变更等级' : '编辑变更等级'"
|
||||||
|
:auto-focus="false"
|
||||||
|
:style="{ width: '500px', height: 'auto' }"
|
||||||
|
:segmented="{ content: true, footer: true }"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<NForm
|
||||||
|
ref="levelRef"
|
||||||
|
label-width="auto"
|
||||||
|
:model="levelInfo"
|
||||||
|
:rules="levelRules"
|
||||||
|
>
|
||||||
|
<NFormItem label="等级名称" path="name">
|
||||||
|
<NInput v-model:value="levelInfo.name" placeholder="如:重大" />
|
||||||
|
</NFormItem>
|
||||||
|
<NFormItem label="等级判定方式" path="way">
|
||||||
|
<NInput v-model:value="levelInfo.way" placeholder="如:等级评分 ≥ 20 分" />
|
||||||
|
</NFormItem>
|
||||||
|
<NFormItem label="对应分析工具(多选)" path="tools">
|
||||||
|
<NSelect v-model:value="levelInfo.tools" multiple :options="toolsOptions" placeholder="请选择分析工具" />
|
||||||
|
</NFormItem>
|
||||||
|
</NForm>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<div class="flex justify-end gap-[10px]">
|
||||||
|
<NButton size="small" @click="levelModal = false">取消</NButton>
|
||||||
|
<NButton size="small" type="primary" :loading="levelLoading" @click="levelSave">确定</NButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</NModal>
|
||||||
|
<!-- 维度分级定义 -->
|
||||||
|
<NModal
|
||||||
|
v-model:show="dimensionModal"
|
||||||
|
preset="card"
|
||||||
|
:title="curMatrix.tool+'矩阵维度分级定义'"
|
||||||
|
:auto-focus="false"
|
||||||
|
:style="{ width: '500px', height: 'auto' }"
|
||||||
|
:segmented="{ content: true, footer: true }"
|
||||||
|
>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<div class="mb-1.5 text-xs font-semibold text-slate-600">后果严重性 S(S1 → S5 递增)</div>
|
||||||
|
<div class="grid grid-cols-5 gap-1.5">
|
||||||
|
<label v-for="(_, i) in dimForm.s" :key="'s' + i" class="text-center text-[10px] text-slate-400">
|
||||||
|
S{{ i + 1 }}
|
||||||
|
<NInput v-model:value="dimForm.s[i]" size="small" class="mt-0.5 text-center text-xs" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="mb-1.5 text-xs font-semibold text-slate-600">发生可能性 L(L1 → L5 递增)</div>
|
||||||
|
<div class="grid grid-cols-5 gap-1.5">
|
||||||
|
<label v-for="(_, i) in dimForm.l" :key="'l' + i" class="text-center text-[10px] text-slate-400">
|
||||||
|
L{{ i + 1 }}
|
||||||
|
<NInput v-model:value="dimForm.l[i]" size="small" class="mt-0.5 text-center text-xs" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<div class="flex justify-end gap-[10px]">
|
||||||
|
<NButton size="small" @click="dimensionModal = false">取消</NButton>
|
||||||
|
<NButton size="small" type="primary" :loading="dimensionLoading" @click="dimensionSave">确定</NButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</NModal>
|
||||||
|
|
||||||
</NSpace>
|
</NSpace>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user