本专业高风险页面修改以及删除多余文件

This commit is contained in:
2026-09-21 10:00:25 +08:00
parent 1c85b2ae92
commit 3fe8da170c
5 changed files with 48 additions and 615 deletions
+45 -130
View File
@@ -1,146 +1,62 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted } from 'vue'; import { ref, computed, onMounted } from 'vue';
import { useAppStore } from '@/store/modules/app';
import { Icon } from '@iconify/vue' import { Icon } from '@iconify/vue'
import { useThemeStore } from '@/store/modules/theme'; import { useThemeStore } from '@/store/modules/theme';
import { aiKnowledgeIngest, pollTask, AiError } from '@/service/api/ai'; import { aiKnowledgeIngest, pollTask, AiError } from '@/service/api/ai';
import { docListApi, historyVersionListApi } from '@/service/api/doc'; import { docListApi, historyVersionListApi } from '@/service/api/doc';
const appStore = useAppStore();
const themeStore = useThemeStore(); const themeStore = useThemeStore();
const gap = computed(() => (appStore.isMobile ? 0 : 16));
const uploadFiles = () => {
window.$message?.success('演示:上传资料(限资料管理员 / 各专业专责维护;上传后进入受控评审,发布生效并生成新版本号,全程留痕)')
}
const KNOWLEDGE_DOCS = [
{ name: "PID-R201-03 反应釜工艺流程图(Rev C", cat: "工艺技术资料", device: "一车间", ver: "Rev C", date: "2026-07-29" },
{ name: "聚合岗位操作规程(2026 版)", cat: "工艺技术资料", device: "一车间", ver: "V2026", date: "2026-07-26" },
{ name: "M-201A 搅拌电机设备台账", cat: "设备资料", device: "一车间", ver: "V3.2", date: "2026-07-28" },
{ name: "环己烷 MSDS(供应商 B", cat: "化学品资料", device: "原料罐区", ver: "V1.0", date: "2026-07-18" },
{ name: "AQ/T 3034 化工企业工艺安全管理实施导则", cat: "法规标准库", device: "全厂", ver: "2013 版", date: "2025-12-01" },
{ name: "PSSR 检查表模板(设备类)", cat: "检查表模板库", device: "全厂", ver: "V2.1", date: "2026-05-10" },
{ name: "2025 年度典型变更案例汇编", cat: "历史变更案例库", device: "全厂", ver: "2025 版", date: "2026-01-15" },
{ name: "HAZOP 分析报告(R-101 温度变更)", cat: "历史变更案例库", device: "一车间", ver: "V1.0", date: "2026-07-26" },
];
const libHits = computed(() => (keyword.value ? KNOWLEDGE_DOCS.filter((d:any) => d.name.includes(keyword.value) || d.cat.includes(keyword.value)) : []));
const preview = (d:any) => {
window.$message?.info(`演示:在线预览 ${d.name}`)
}
const download = (d:any) => {
window.$message?.success(`演示:下载 ${d.name}`)
}
// 已纳入 AI 知识库的条目(name -> true)与摄入中状态
const ingested = ref<Record<string, boolean>>({});
const ingesting = ref<Record<string, boolean>>({});
const call = async (d:any) => {
if (ingesting.value[d.name] || ingested.value[d.name]) return;
ingesting.value[d.name] = true;
try {
const { task_id } = await aiKnowledgeIngest({ name: d.name, version: d.ver, category: d.cat });
await pollTask(task_id, {});
ingested.value[d.name] = true;
window.$message?.success(`已将「${d.name}」纳入 AI 知识库(调用记录留痕)`);
} catch (e: any) {
if (e instanceof AiError) {
window.$message?.warning(`AI 知识摄入失败:${e.message},可稍后重试`);
} else {
window.$message?.warning('AI 知识摄入失败,可稍后重试');
}
} finally {
ingesting.value[d.name] = false;
}
}
interface LibDoc { name: string; ver: string; date: string }
interface LibFolder { label: string; docs: LibDoc[] }
interface LibGroup { cat: string; folders: LibFolder[] }
const LIB_ORGS: { org: string; groups: LibGroup[] }[] = [
{ org: "一车间", groups: [
{ cat: "工艺", folders: [
{ label: "操作规程", docs: [
{ name: "聚合岗位操作规程(2026 版)", ver: "V2026", date: "2026-07-26" },
{ name: "干燥岗位操作规程(2026 版)", ver: "V2026", date: "2026-06-30" } ]},
{ label: "PID 图纸", docs: [
{ name: "PID-R201-03 反应釜工艺流程图(Rev C", ver: "Rev C", date: "2026-07-29" },
{ name: "PID-V102-01 罐区工艺流程图(Rev B", ver: "Rev B", date: "2026-05-12" } ]},
{ label: "工艺卡片", docs: [
{ name: "R-201 反应釜工艺卡片(夏季工况)", ver: "V2.0", date: "2026-07-01" } ]},
]},
{ cat: "设备", folders: [
{ label: "设备台账", docs: [
{ name: "M-201A 搅拌电机设备台账", ver: "V3.2", date: "2026-07-28" },
{ name: "P-204 输送泵设备台账", ver: "V2.8", date: "2026-06-15" } ]},
{ label: "安全阀台账", docs: [
{ name: "一车间安全阀台账(PSV-101 ~ PSV-118", ver: "V5.1", date: "2026-07-10" } ]},
]},
{ cat: "仪表", folders: [
{ label: "报警台账", docs: [
{ name: "一车间 DCS 报警台账(2026H1", ver: "V4.0", date: "2026-07-20" } ]},
{ label: "联锁台账", docs: [
{ name: "一车间联锁逻辑台账(SIS-01 ~ SIS-09", ver: "V3.6", date: "2026-07-22" } ]},
]},
{ cat: "安全", folders: [
{ label: "总图", docs: [{ name: "一车间装置总平面布置图", ver: "Rev D", date: "2026-04-18" } ]},
{ label: "现场气体报警仪布置图", docs: [{ name: "一车间可燃 / 有毒气体报警仪布置图", ver: "Rev B", date: "2026-05-28" } ]},
{ label: "防爆分区图", docs: [{ name: "一车间爆炸危险区域划分图", ver: "Rev C", date: "2026-03-15" } ]},
]},
]},
{ org: "二车间", groups: [
{ cat: "工艺", folders: [
{ label: "操作规程", docs: [{ name: "二车间精馏岗位操作规程(2026 版)", ver: "V2026", date: "2026-06-20" } ]},
{ label: "PID 图纸", docs: [{ name: "PID-T301-02 精馏塔工艺流程图(Rev A", ver: "Rev A", date: "2026-03-08" } ]},
]},
{ cat: "设备", folders: [
{ label: "设备台账", docs: [{ name: "P-103 冷冻盐水泵设备台账", ver: "V2.2", date: "2026-05-30" } ]},
{ label: "安全阀台账", docs: [{ name: "二车间安全阀台账(PSV-201 ~ PSV-212", ver: "V4.3", date: "2026-06-11" } ]},
]},
{ cat: "仪表", folders: [
{ label: "报警台账", docs: [{ name: "二车间 DCS 报警台账(2026H1", ver: "V3.5", date: "2026-07-02" } ]},
{ label: "联锁台账", docs: [{ name: "二车间联锁逻辑台账(SIS-11 ~ SIS-16", ver: "V2.9", date: "2026-06-25" } ]},
]},
{ cat: "安全", folders: [
{ label: "总图", docs: [{ name: "二车间装置总平面布置图", ver: "Rev B", date: "2026-02-14" } ]},
{ label: "现场气体报警仪布置图", docs: [{ name: "二车间可燃 / 有毒气体报警仪布置图", ver: "Rev A", date: "2026-04-02" } ]},
{ label: "防爆分区图", docs: [{ name: "二车间爆炸危险区域划分图", ver: "Rev B", date: "2026-01-20" } ]},
]},
]},
{ org: "全厂共用", groups: [
{ cat: "法规", folders: [
{ label: "法规标准库", docs: [{ name: "AQ/T 3034 化工企业工艺安全管理实施导则", ver: "2013 版", date: "2025-12-01" } ]},
]},
{ cat: "化学品", folders: [
{ label: "MSDS", docs: [{ name: "环己烷 MSDS(供应商 B", ver: "V1.0", date: "2026-07-18" } ]},
]},
{ cat: "模板", folders: [
{ label: "检查表模板", docs: [{ name: "PSSR 检查表模板(设备类)", ver: "V2.1", date: "2026-05-10" } ]},
]},
{ cat: "案例", folders: [
{ label: "历史变更案例", docs: [
{ name: "2025 年度典型变更案例汇编", ver: "2025 版", date: "2026-01-15" },
{ name: "HAZOP 分析报告(R-101 温度变更)", ver: "V1.0", date: "2026-07-26" } ]},
]},
]},
];
const openOrgs = ref<Record<string, boolean>>({ 一车间: true });
const libSel = ref({ org: "一车间", cat: "工艺", label: "操作规程" });
const libCur = computed(() => docList.value.find((o) => o.org_name === libSel.value.org)?.categories.find((g) => g.category === libSel.value.cat)?.folders.find((f) => f.folder === libSel.value.label));
const loading = ref<boolean>(false); const loading = ref<boolean>(false);
const rightLoading = ref<boolean>(false); // 展开节点
const openOrgs = ref<Record<string, boolean>>({});
// 选中的资料库节点
const libSel = ref({ org: "", cat: "", label: "" });
// 当前选中的资料库列表
const libCur = computed(() => docList.value.find((o:any) => o.org_name === libSel.value.org)?.categories.find((g:any) => g.category === libSel.value.cat)?.folders.find((f:any) => f.folder === libSel.value.label));
// 搜索关键词 // 搜索关键词
const keyword = ref<string>(''); const keyword = ref<string>('');
// 资料库树状列表 // 资料库树状列表
const docList = ref<any[]>([]); const docList = ref<any[]>([]);
// 历史版本列表 // 历史版本列表
const historyList = ref<any[]>([]); const historyList = ref<any[]>([]);
// 展示数还是列表
const showTree = ref<boolean>(true);
// 在线预览资料
const preview = (d:any) => {
window.$message?.info(`演示:在线预览 ${d.name}`)
}
// 下载资料
const download = (d:any) => {
window.$message?.success(`演示:下载 ${d.name}`)
}
// 上传资料
const uploadFiles = () => {
window.$message?.success('演示:上传资料(限资料管理员 / 各专业专责维护;上传后进入受控评审,发布生效并生成新版本号,全程留痕)')
}
// 搜索
const searchHandle = () => {
if(!keyword.value) {
showTree.value = true;
return;
};
showTree.value = false;
}
// 获取资料库树状列表 // 获取资料库树状列表
const getDocList = async () => { const getDocList = async () => {
loading.value = true; loading.value = true;
const {data,error} = await docListApi(); const {data,error} = await docListApi();
if(!error){ if(!error){
console.log(data);
docList.value = data ?? []; docList.value = data ?? [];
console.log(docList.value);
if(docList.value.length){
openOrgs.value = {[docList.value[0]?.org_name]:true};
libSel.value = {org: docList.value[0]?.org_name, cat: docList.value[0]?.categories[0]?.category, label: docList.value[0]?.categories[0]?.folders[0]?.folder};
}
} }
loading.value = false; loading.value = false;
} }
@@ -155,7 +71,7 @@ onMounted(() => {
<!-- 我的草稿箱 --> <!-- 我的草稿箱 -->
<NSpin :show="loading" size="small"> <NSpin :show="loading" size="small">
<NCard :bordered="false" size="small" :style="{ borderRadius: themeStore.themeRadius + 'px' }"> <NCard :bordered="false" size="small" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
<NGrid :x-gap="gap" :y-gap="16" responsive="screen" item-responsive class="pb-[12px] border-b"> <NGrid :x-gap="16" :y-gap="16" responsive="screen" item-responsive class="pb-[12px] border-b">
<NGi span="24 s:24 m:16"> <NGi span="24 s:24 m:16">
<div class="h-100% flex items-center gap-2"> <div class="h-100% flex items-center gap-2">
<Icon icon="mage:book" class="size-20px" :style="{ color: themeStore.themeColor }" /> <Icon icon="mage:book" class="size-20px" :style="{ color: themeStore.themeColor }" />
@@ -164,9 +80,9 @@ onMounted(() => {
</NGi> </NGi>
<NGi span="24 s:24 m:8" class="flex items-center justify-end gap-12px"> <NGi span="24 s:24 m:8" class="flex items-center justify-end gap-12px">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<NInput v-model:value="keyword" size="small" type="text" class="w-40 text-xs" clearable placeholder="全文检索…"> <NInput v-model:value="keyword" size="small" type="text" class="w-40 text-xs" clearable placeholder="全文检索…" @clear="keyword = ''; searchHandle();">
<template #suffix> <template #suffix>
<Icon icon="akar-icons:search" class="size-14px text-slate-400 cursor-pointer" /> <Icon icon="akar-icons:search" class="size-14px text-slate-400 cursor-pointer" @click="searchHandle" />
</template> </template>
</NInput> </NInput>
<NButton size="small" type="primary" class="text-xs" @click="uploadFiles"> <NButton size="small" type="primary" class="text-xs" @click="uploadFiles">
@@ -179,10 +95,10 @@ onMounted(() => {
组织 专业类别 资料文件夹归档具体资料操作规程 / PID 图纸 / 工艺卡片 / 设备台账 / 安全阀台账 / 报警台账 / 联锁台账 / 总图 / 现场气体报警仪布置图 / 防爆分区图等资料由指定维护人资料管理员 / 各专业专责上传维护版本受控调用可将资料引用为当前变更的 AI 知识来源调用记录留痕 组织 专业类别 资料文件夹归档具体资料操作规程 / PID 图纸 / 工艺卡片 / 设备台账 / 安全阀台账 / 报警台账 / 联锁台账 / 总图 / 现场气体报警仪布置图 / 防爆分区图等资料由指定维护人资料管理员 / 各专业专责上传维护版本受控调用可将资料引用为当前变更的 AI 知识来源调用记录留痕
</div> </div>
<!-- 全文检索跨文件夹平铺结果 --> <!-- 全文检索跨文件夹平铺结果 -->
<div v-if="keyword"> <div v-if="!showTree">
<div class="mb-1.5 text-xs text-slate-400">全文检索{{ keyword }}跨全部文件夹命中 {{ libHits.length }} </div> <div class="mb-1.5 text-xs text-slate-400">全文检索{{ keyword }}跨全部文件夹命中 {{ historyList.length }} </div>
<div class="divide-y rounded-lg border bg-white"> <div class="divide-y rounded-lg border bg-white">
<div v-for="d in libHits" :key="d.name" class="flex flex-wrap items-center gap-3 px-2 py-2.5"> <div v-for="d in historyList" :key="d.name" class="flex flex-wrap items-center gap-3 px-2 py-2.5">
<FileText class="h-4 w-4 shrink-0 text-slate-400" /> <FileText class="h-4 w-4 shrink-0 text-slate-400" />
<div class="min-w-0 flex-1"> <div class="min-w-0 flex-1">
<div class="truncate text-sm">{{ d.name }}</div> <div class="truncate text-sm">{{ d.name }}</div>
@@ -196,7 +112,7 @@ onMounted(() => {
<button class="text-[#1D4E9C] hover:underline" @click="download(d)">下载</button> <button class="text-[#1D4E9C] hover:underline" @click="download(d)">下载</button>
</div> </div>
</div> </div>
<div v-if="libHits.length === 0" class="py-6 text-center text-xs text-slate-400">未找到匹配{{ keyword }}的资料</div> <div v-if="historyList.length === 0" class="py-6 text-center text-xs text-slate-400">未找到匹配{{ keyword }}的资料</div>
</div> </div>
</div> </div>
<!-- 文件夹树 + 当前文件夹资料 --> <!-- 文件夹树 + 当前文件夹资料 -->
@@ -248,10 +164,9 @@ onMounted(() => {
</div> </div>
</div> </div>
<div class="flex shrink-0 gap-3 text-xs"> <div class="flex shrink-0 gap-3 text-xs">
<NButton type="primary" text class="hover:underline text-xs" @click="preview(d)">查看</NButton> <NButton type="primary" text class="hover:underline text-xs">查看历史</NButton>
<NButton type="primary" text class="hover:underline text-xs" @click="preview(d)">预览</NButton>
<NButton type="primary" text class="hover:underline text-xs" @click="download(d)">下载</NButton> <NButton type="primary" text class="hover:underline text-xs" @click="download(d)">下载</NButton>
<NTag v-if="ingested[d.name]" size="small" type="success" class="text-[10px]">已纳入 AI 知识库</NTag>
<NButton v-else type="primary" text class="hover:underline text-xs" :loading="!!ingesting[d.name]" @click="call(d)">调用</NButton>
</div> </div>
</div> </div>
</div> </div>
-40
View File
@@ -1,54 +1,14 @@
<!-- 本专业高风险 --> <!-- 本专业高风险 -->
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue';
import { useThemeStore } from '@/store/modules/theme'; import { useThemeStore } from '@/store/modules/theme';
import Risk from './modules/risk.vue'; import Risk from './modules/risk.vue';
import History from './modules/history.vue';
import Template from './modules/template.vue';
const themeStore = useThemeStore(); const themeStore = useThemeStore();
const TABS = [
{ k: "highRisk", name: "高风险场景" },
{ k: "history", name: "历史分析检索" },
{ k: "tpl", name: "方法模板库" },
];
// 当前tab
const currentTab = ref<string>("highRisk");
// 切换tab
const tabChange = async (k: string) => {
currentTab.value = k;
}
</script> </script>
<template> <template>
<NSpace vertical :size="16"> <NSpace vertical :size="16">
<NCard :bordered="false" size="small" :style="{ borderRadius: themeStore.themeRadius + 'px' }"> <NCard :bordered="false" size="small" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
<div
class="flex flex-wrap items-center gap-2 border px-3 py-2 mb-3"
:style="{ borderRadius: themeStore.themeRadius + 'px' }"
>
<NButton size="small" round v-for="t in TABS" :key="t.k" @click="tabChange(t.k)"
class="text-xs"
:type="currentTab === t.k ? 'primary' : 'default'"
>
{{ t.name }}
</NButton>
</div>
<!-- 高风险场景 -->
<template v-if="currentTab === 'highRisk'">
<Risk /> <Risk />
</template>
<!-- 历史分析检索 -->
<template v-if="currentTab === 'history'">
<History />
</template>
<!-- 方法模板库 -->
<template v-if="currentTab === 'tpl'">
<Template />
</template>
</NCard> </NCard>
</NSpace> </NSpace>
</template> </template>
-127
View File
@@ -1,127 +0,0 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue';
import { Icon } from '@iconify/vue'
import { useThemeStore } from '@/store/modules/theme';
import { useRouterPush } from '@/hooks/common/router';
import { historyAnalysisListApi } from '@/service/api/highRisk';
const themeStore = useThemeStore();
const { routerPushByKey } = useRouterPush();
// 查看历史分析检索
const viewHistory = (id: string) => {
window.$message?.info(`演示:在线查看 ${id} 完整分析记录`)
};
// 复用到新变更
const copyHistory = (id: string) => {
window.$message?.success(`演示:${id} 已复用到新变更草稿,分析行已带入待确认`)
}
const loading = ref<boolean>(false);
// 搜索关键词
const keyword = ref<string>("");
// 插件筛选
const plugin = ref<string>("");
// 插件列表
const pluginList = ref<any[]>([
{ label: "全部", value: '' },
{ label: "风险预分析", value: 'RISK' },
{ label: "HAZOP", value: 'HAZOP' },
{ label: "FMEA", value: 'FMEA' },
{ label: "风险检查表", value: 'RISK_CHECK' },
]);
const list = ref<any[]>([]);
// 插件筛选
const pluginChange = (v: string) => {
plugin.value = v;
getList();
}
// 重置
const reset = () => {
keyword.value = '';
plugin.value = '';
getList();
};
// 获取列表
const getList = async () => {
loading.value = true;
const {data,error} = await historyAnalysisListApi({
kw: keyword.value,
method: plugin.value,
});
if(!error){
console.log(data);
list.value = data ?? [];
}
loading.value = false;
}
onMounted(() => {
getList();
})
</script>
<template>
<!-- 历史分析检索 -->
<NSpin :show="loading" size="small">
<div class="border" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
<div class="flex items-center justify-between px-4 py-3 border-b mb-3">
<div class="flex items-center gap-2">
<Icon icon="mage:book" class="size-18px" :style="{color:themeStore.themeColor}" />
<p class="text-base font-semibold">历史分析记录检索企业风险知识资产</p>
</div>
<div class="flex items-center gap-2 min-h-28px">
<NInput v-model:value="keyword" size="small" class="text-xs !w-200px" clearable placeholder="按单号 / 装置 / 关键词检索…" @clear="keyword = ''; getList()">
<template #suffix>
<Icon icon="akar-icons:search" class="size-14px text-slate-400 cursor-pointer" @click="getList" />
</template>
</NInput>
<NButton size="small" class="text-sm" @click="reset">
<Icon icon="system-uicons:reset" class="size-12px" />重置
</NButton>
</div>
</div>
<div class="px-4 mb-3 flex flex-wrap items-center gap-1.5">
<NButton
size="tiny"
round
v-for="p in pluginList"
:key="p.value"
:type="plugin === p.value ? 'primary' : 'default'"
@click="pluginChange(p.value)"
>
{{ p.label }}
</NButton>
<span class="ml-auto self-center text-[11px] text-slate-400">{{ list.length }} 条记录</span>
</div>
<div class="scroll">
<div class="space-y-3 px-4 pb-3">
<div v-for="h in list" :key="h.id" class="flex flex-wrap items-center gap-3 rounded-lg border px-4 py-2.5">
<div class="min-w-0 flex-1">
<div class="flex flex-wrap items-center gap-2">
<span class="font-mono text-[11px] text-slate-400">{{ h.change_no }}</span>
<NTag size="small" type="info" class="text-11px">{{ h.method==='RISK'?'风险预分析':h.method==='RISK_CHECK'?'风险检查': h.method }}</NTag>
<span class="text-sm text-slate-800">{{ h.title }}</span>
</div>
<div class="mt-1 text-xs text-slate-500">{{ h.org_name }} · {{ h.rows }} 行分析记录 · {{ h?.analysis_time?.slice(0,10) || '' }}</div>
</div>
<NButton text type="primary" @click="viewHistory(h.id)">
<Icon icon="lucide:eye" class="size-16px" />
</NButton>
<NButton ghost size="small" class="text-xs" @click="copyHistory(h.id)">复用到新变更</NButton>
</div>
<div v-if="list.length === 0" class="py-6 text-center text-xs text-slate-400">未找到匹配的分析记录</div>
</div>
</div>
</div>
</NSpin>
</template>
<style scoped lang="scss">
.scroll {
height: calc(100vh - 305px);
overflow-y: auto;
}
</style>
+2 -3
View File
@@ -39,7 +39,6 @@ const jumpTo = (r: any) => {
}; };
// 查看分析记录 // 查看分析记录
const viewAnalysis = (record: any) => { const viewAnalysis = (record: any) => {
console.log(record);
currentRecord.value = record; currentRecord.value = record;
pluginDrawer.value = true; pluginDrawer.value = true;
}; };
@@ -340,7 +339,7 @@ onMounted(async () => {
</div> </div>
<div class="mt-1 text-xs text-slate-600">{{ s.scene }}</div> <div class="mt-1 text-xs text-slate-600">{{ s.scene }}</div>
<div class="mt-0.5 text-xs text-slate-400"> <div class="mt-0.5 text-xs text-slate-400">
变更{{ s?.change_title }} · {{ s?.org_name }} · 分析日期 {{ s?.created_at?.slice(0, 10) }} · 责任{{ s?.applicant_name }} · 分析记录 {{ s?.record_count }} 变更{{ s?.change_title }} · {{ s?.org_name }} · 分析日期 {{ s?.created_at?.slice(0, 10) }} · 分析记录 {{ s?.record_count }}
</div> </div>
</div> </div>
<NButton ghost size="small" class="text-xs" @click="jumpTo(s)">查看变更</NButton> <NButton ghost size="small" class="text-xs" @click="jumpTo(s)">查看变更</NButton>
@@ -396,7 +395,7 @@ onMounted(async () => {
<style scoped lang="scss"> <style scoped lang="scss">
.scroll { .scroll {
height: calc(100vh - 368px); height: calc(100vh - 310px);
overflow-y: auto; overflow-y: auto;
} }
</style> </style>
-314
View File
@@ -1,314 +0,0 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue';
import { Icon } from '@iconify/vue'
import { useThemeStore } from '@/store/modules/theme';
import { useRouterPush } from '@/hooks/common/router';
import { systemConfigApi } from '@/service/api/system';
import { orgListApi } from '@/service/api/user';
import { highRiskListApi } from '@/service/api/highRisk';
const themeStore = useThemeStore();
const { routerPushByKey } = useRouterPush();
const keyword = ref<string>("");
const progressDrawer = ref<boolean>(false);
const currentTab = ref<string>("highRisk");
const range = ref<string>("近一个月");
const dept = ref<string>("全部部门");
const method = ref<string>("全部");
// 当前记录
const currentRecord = ref<any>(null);
const TABS = [
{ k: "highRisk", name: "高风险场景" },
{ k: "history", name: "历史分析检索" },
{ k: "tpl", name: "方法模板库" },
];
const RISK_RANGES = ["近一个月", "近三个月", "近半年", "全部"];
const departments = ref<any[]>([
{ id: 1, name: "全部部门" },
{ id: 2, name: "一车间" },
{ id: 3, name: "公用工程车间" },
{ id: 4, name: "供应部" },
{ id: 5, name: "三车间" },
]);
const list = ref<any[]>([]);
const highRiskList = [
{
cid: "MOC-2026-R01-0036", item: "反应飞温", scene: "温度上限提高至 82℃ 后飞温反应加速,联锁动作前安全裕量收窄",
method: "HAZOP", residual: "中", dept: "一车间", adate: "2026-08-02", owner: "王海燕",
title: '聚合反应温度控制上限调整(78℃→82℃)', status: '审批中',
info: 'MOC-2026-R01-0036 聚合反应温度控制上限调整(78℃→82℃)', count: 3,
hazopRows: [
{ node: "节点1 · 聚合釜 R-101", dev: "温度过高", cause: "温控回路失效或冷却水中断;82℃ 新上限更接近溶剂回流温度,超温溜温概率上升", cons: "反应溜温冲料,釜压骤升,物料喷溅造成灼烫与环境污染", L: 3, S: 4, safety: "DCS 温度高报 82℃、高高报及联锁 85℃(维持安全边界不变);紧急冷却有效", suggest: "投用前完成联锁测试并留存记录;投用首周每班复核温度趋势" },
{ node: "节点1 · 聚合釜 R-101", dev: "温度过高", cause: "未知杂质含量升高(0.05%→0.12%),热稳定性尚未定性,长期累积加速釜壁结垢", cons: "副反应加剧,可能引发二次分解放热,搅拌负荷异常", L: 2, S: 4, safety: "小试热稳定性筛查;安全阀泄放能力不受本次调整影响,仍然有效", suggest: "将杂质含量纳入日常分析计划(每周 1 次,连续 8 周)" },
{ node: "节点2 · DCS 温控与联锁", dev: "报警/联锁值设置不当", cause: "上限调整后报警值与联锁值未同步复核整定", cons: "报警提前量不足,操作响应时间被压缩", L: 2, S: 4, safety: "修改执行申请-复核双人确认;修改清单与变更单逐项核对", suggest: "联锁测试全覆盖并留存记录;修改期间工艺加强监控" },
],
},
{
cid: "MOC-2026-R01-0035", item: "动火与临时用电燃爆", scene: "电机更换动火与临时用电作业引发可燃气体燃爆",
method: "JSA", residual: "中", dept: "一车间", adate: "2026-07-30", owner: "王仪表",
title: '反应釜搅拌器电机更换(55kW→75kW 防爆电机)', status: '审批中',
info: 'MOC-2026-R01-0035 R-201 反应釜搅拌器电机更换(55kW→75kW 防爆电机)', count: 3,
jsaRows: [
{ item: "动火作业燃爆", scene: "步骤2:旧电机拆除与新电机安装动火,可燃蒸气聚集遇火源燃爆", inherent: "高", existing: "作业票证办理;动火点可燃气体检测合格;消防器材到位", suggest: "动火点下方设接火盘与防火毯,专人监护并每 30 分钟复测", residual: "中" },
{ item: "临时用电触电", scene: "步骤1:临时电缆敷设与配电箱接线,绝缘破损导致触电", inherent: "中", existing: "临时用电票证;漏保校验合格", suggest: "电缆架空敷设并挂牌,每日开工前绝缘检查", residual: "低" },
{ item: "起重吊装伤害", scene: "步骤3:75kW 电机吊装就位,吊具失效或指挥失误导致起重伤害", inherent: "中", existing: "吊具检验合格;起重指挥持证", suggest: "吊装区设警戒线,禁止交叉作业", residual: "低" },
],
},
{
cid: null, changeTitle: "MOC-2026-R01-0048 R-101 温度联锁修改", item: "VOCs 直排超标", scene: "活性炭吸附饱和穿透,VOCs 直排大气超标",
method: "JSA", residual: "高", dept: "一车间", adate: "2026-07-31", owner: "王仪表",
title: '温度联锁修改', status: '审批中',
info: 'MOC-2026-R01-0048 R-101 温度联锁修改', count: 2,
jsaRows: [
{ item: "吸附饱和穿透", scene: "步骤3:活性炭吸附床运行后期,吸附饱和穿透导致 VOCs 直排大气", inherent: "高", existing: "出口 VOCs 在线监测报警;按周期更换活性炭", suggest: "增设穿透点压差监测与双床切换程序;更换周期由 6 个月缩短至 4 个月", residual: "中" },
{ item: "联锁参数误改", scene: "步骤3:DCS 参数修改与联锁测试,参数误改导致报警/联锁失效", inherent: "高", existing: "参数修改执行申请-复核双人确认;修改清单与变更单逐项核对", suggest: "联锁测试全覆盖并留存记录;修改期间工艺加强监控", residual: "中" },
],
},
{
cid: "MOC-2026-R02-0041", item: "超压与窒息", scene: "氮气减压阀组失效导致下游设备超压、作业人员窒息",
method: "检查表", residual: "低", dept: "公用工程车间", adate: "2026-07-29", owner: "周设备",
title: '公用工程氮气减压阀组改造', status: '审批中',
info: 'MOC-2026-R02-0041 公用工程氮气减压阀组改造', count: 2,
jsaRows: [
{ item: "下游设备超压", scene: "减压阀失效全开,高压氮气直入低压管网导致下游设备超压", inherent: "高", existing: "下游设安全阀;管网压力高报", suggest: "减压阀双阀串联并定期校验;安全阀起跳压力复核", residual: "低" },
{ item: "人员窒息", scene: "阀组间通风不良,氮气泄漏积聚导致氧含量不足", inherent: "中", existing: "阀组间强制通风;进入前测氧", suggest: "增设固定式氧含量报警仪并联动风机", residual: "低" },
],
},
{
cid: "MOC-2026-R01-0039", item: "超压保护裕量降低", scene: "安全阀起跳压力临时上调期间,V-102 超压保护裕量降低",
method: "AI 预分析", residual: "中", dept: "一车间", adate: "2026-07-15", owner: "王海燕",
title: '紧急变更:V-102 安全阀起跳压力临时调整(补办手续)', status: '审批中',
info: 'MOC-2026-R01-0039 紧急变更:V-102 安全阀起跳压力临时调整(补办手续)', count: 2,
jsaRows: [
{ item: "超压保护裕量降低", scene: "起跳压力由 1.0MPa 临时上调至 1.15MPaV-102 超压工况下保护动作延迟", inherent: "高", existing: "临时期限 30 天;操作压力监控加强至每 2 小时记录", suggest: "恢复前完成新安全阀校验更换;临时期间压力联锁值同步下调验证", residual: "中" },
{ item: "紧急变更手续补办遗漏", scene: "紧急变更先行实施,后补手续存在资料缺项风险", inherent: "中", existing: "紧急变更 48 小时内补办审批", suggest: "资料归档清单逐项核对,安全部门复核", residual: "低" },
],
},
{
cid: "MOC-2026-R03-0040", item: "原料杂质副反应", scene: "新供应商环己烷杂质谱变化引发副反应,影响产品色相与收率",
method: "HAZOP", residual: "中", dept: "供应部", adate: "2026-07-18", owner: "陈质量",
title: '原料环己烷供应商变更(新增 B 供应商)', status: '审批中',
info: 'MOC-2026-R03-0040 原料环己烷供应商变更(新增 B 供应商)', count: 2,
hazopRows: [
{ node: "节点1 · 原料接收与储存", dev: "杂质含量高", cause: "B 供应商环己烷杂质谱(苯/硫)高于原供应商,进厂检验项目未覆盖", cons: "副反应加剧,产品色相超标、收率下降,结垢加速", L: 3, S: 3, safety: "进厂检验按原指标执行;供应商 COA 随货同行", suggest: "检验项目增加杂质谱分析;首批三批加严检验" },
{ node: "节点2 · 聚合反应", dev: "副反应加剧", cause: "微量苯参与链转移,分子量分布变宽", cons: "产品性能波动,下游加工投诉", L: 2, S: 3, safety: "聚合配方留有调节余量", suggest: "建立 B 供应商原料的配方微调作业指导" },
],
},
{
cid: "MOC-2026-R03-0042", item: "回路切换失误", scene: "自动化改造调试期间控制回路切换失误,造成装置非计划停车",
method: "JSA", residual: "中", dept: "三车间", adate: "2026-07-31", owner: "王仪表",
title: '三车间自动化和安全隐患整改项目变更', status: '审批中',
info: 'MOC-2026-R03-0042 三车间自动化和安全隐患整改项目变更', count: 2,
jsaRows: [
{ item: "回路切换失误", scene: "步骤4:新旧控制系统切换,回路误切导致装置非计划停车", inherent: "高", existing: "切换方案审批;切换前模拟演练", suggest: "切换双人复核逐项签字;保留一键回切手段 72 小时", residual: "中" },
{ item: "静电损伤卡件", scene: "步骤2:卡件插拔作业静电防护不到位,DCS 卡件损坏", inherent: "中", existing: "佩戴防静电手环", suggest: "卡件备件现场备妥,作业区铺设防静电垫", residual: "低" },
],
},
];
const historyList = [
{ id: "HAZOP-2025-018", change: "MOC-2025-R01-0023 常压炉燃料气切断阀改造", method: "HAZOP", unit: "01-常减压装置", scope: "燃料气系统节点", rows: 12, date: "2025-10-20" },
{ id: "HAZOP-2024-006", change: "MOC-2024-R01-0003 减一线换热器 E-108 芯子更换", method: "HAZOP", unit: "01-常减压装置", scope: "换热系统节点", rows: 8, date: "2024-03-15" },
{ id: "JSA-2025-041", change: "MOC-2025-R02-0014 再生器旋风分离器更换", method: "JSA", unit: "02-催化裂化装置", scope: "受限空间作业", rows: 6, date: "2025-05-08" },
{ id: "JSA-2026-012", change: "MOC-2026-R01-0035 搅拌电机更换", method: "JSA", unit: "01-常减压装置", scope: "检维修作业", rows: 5, date: "2026-07-29" },
{ id: "SCL-2025-033", change: "MOC-2025-R02-0021 气压机出口压力联锁逻辑修改", method: "检查表", unit: "02-催化裂化装置", scope: "联锁系统", rows: 14, date: "2025-09-18" },
{ id: "AI-2026-008", change: "MOC-2026-R03-0008 聚合釜搅拌器密封形式变更", method: "AI 预分析", unit: "03-聚合装置", scope: "—", rows: 4, date: "2026-05-06" },
];
const templateList = [
{ name: "HAZOP 节点划分模板", desc: "含偏差引导词库(过高/过低/无/反向等 18 个)与节点划分示例", tag: "HAZOP" },
{ name: "JSA 作业步骤分析模板", desc: "作业步骤分解 → 危害识别 → 控制措施三段式,含检维修示例", tag: "JSA" },
{ name: "安全检查表(SCL)模板库", desc: "按设备类型 28 套(机泵 / 换热器 / 塔器 / 储罐 / 电气 / 仪表)", tag: "检查表" },
{ name: "风险矩阵 5×5 分级定义", desc: "可能性 × 严重性分级标准(公司 Q/SH 0600),AI 评级引用此定义", tag: "通用" },
];
const isHazop = computed(() => !!currentRecord.value?.hazopRows);
const th = "border-b bg-slate-50 px-3 py-2 text-left text-[11px] font-semibold text-slate-500";
const td = "border-b px-3 py-2 align-top text-xs leading-relaxed text-slate-700";
const hazopNodes = computed(() => {
const scene = currentRecord.value;
if (!scene?.hazopRows) return [] as { node: string; dev: string; n: number }[];
const nodes = Array.from(new Map(scene.hazopRows.map((r:any) => [`${r.node}|${r.dev}`, { node: r.node, dev: r.dev, n: 0 }])).values());
scene.hazopRows.forEach((r:any) => { const k:any = nodes.find((n:any) => n.node === r.node && n.dev === r.dev); if (k) k.n += 1; });
return nodes;
});
const sel = ref(0);
const selRows = computed(() => {
const scene = currentRecord.value;
if (!scene?.hazopRows) return [] as any;
const n:any = hazopNodes.value[sel.value];
return scene.hazopRows.filter((r:any) => r.node === n?.node && r.dev === n?.dev);
});
// HAZOP 风险矩阵:RR = L × S
const rrTag = (L: number, S: number) => {
const v = L * S;
return v >= 12 ? { label: `较大 (${v})`, cls: "error" }
: v >= 8 ? { label: `一般 (${v})`, cls: "warning" }
: { label: `低 (${v})`, cls: "success" };
};
// 高风险场景查看变更
const openSceneChange = (record: any) => {
routerPushByKey('details_index', { query: { params: JSON.stringify(record) } })
};
// 查看分析记录
const viewAnalysis = (record: any) => {
currentRecord.value = record;
progressDrawer.value = true;
};
// 查看历史分析检索
const viewHistory = (id: string) => {
window.$message?.info(`演示:在线查看 ${id} 完整分析记录`)
};
// 复用到新变更
const copyHistory = (id: string) => {
window.$message?.success(`演示:${id} 已复用到新变更草稿,分析行已带入待确认`)
}
// 预览模板
const previewTemplate = (name: string) => {
window.$message?.info(`演示:在线预览「${name}`)
}
// 下载模板
const downloadTemplate = (name: string) => {
window.$message?.success(`演示:下载「${name}」(Word`)
}
// 导出
const exportFile = () => {
window.$message?.success('分析记录表已导出(Excel');
}
const loading = ref<boolean>(false);
// 组织树
const orgTree = ref<any[]>([]);
// 筛选条件
const formInfo = ref<any>({
kw: '',
org_id: 1,
from: null,
to: null,
});
const pagination = ref<{page: number, pageSize: number, itemCount: number}>({
page: 1,
pageSize: 20,
itemCount: 0,
})
// 变更类型列表
const typeList = ref<any[]>([{ label: "全部", value: null }]);
// 变更等级列表
const levelList = ref<any[]>([{ label: "全部", value: null }]);
// 获取列表
const getList = async () => {
loading.value = true;
const {data,error} = await highRiskListApi({
kw: formInfo.value.kw,
org_id: formInfo.value.org_id,
from: formInfo.value.from,
to: formInfo.value.to,
page_num: pagination.value.page,
page_size: pagination.value.pageSize,
});
if(!error){
list.value = data?.list ?? [];
pagination.value.itemCount = data?.total ?? 0;
}
loading.value = false;
}
// 获取配置
const getConfig = async () => {
const {data,error} = await systemConfigApi();
if(!error){
// 解析 变更类型设置
const changeTypeGroup = data.find((group:{group_key:string}) => group.group_key === 'change_type');
if (changeTypeGroup) {
typeList.value = typeList.value.concat(changeTypeGroup.items);
}
// 解析 变更等级设置
const changeLevelGroup = data.find((group:{group_key:string}) => group.group_key === 'change_level');
if (changeLevelGroup) {
levelList.value = levelList.value.concat(changeLevelGroup.items);
}
}
}
// // 获取组织列表
const getOrgList = async () => {
loading.value = true;
const {data,error} = await orgListApi();
if(!error){
orgTree.value = data;
if(orgTree.value.length > 0){
getList();
}
}
loading.value = false;
}
// 切换tab
const tabChange = async (k: string) => {
list.value = []
currentTab.value = k;
loading.value = true;
if(k === 'highRisk'){
await getConfig();
getOrgList();
}
if(k === 'history'){
setTimeout(() => {
list.value = historyList;
loading.value = false;
}, 1000);
}
if(k === 'tpl'){
setTimeout(() => {
list.value = templateList;
loading.value = false;
}, 1000);
}
}
onMounted(() => {
tabChange('tpl');
})
</script>
<template>
<!-- 方法模板库 -->
<NSpin :show="loading" size="small">
<div class="border" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
<div class="flex items-center justify-between px-4 py-3 border-b mb-3">
<div class="flex items-center gap-2">
<Icon icon="akar-icons:file" class="size-16px" :style="{color:themeStore.themeColor}" />
<p class="text-base font-semibold">方法模板库AI 风险预分析的知识来源之一</p>
</div>
</div>
<div class="scroll">
<div class="space-y-3 px-4 pb-3">
<div class="grid gap-3 grid-cols-2">
<div v-for="t in list" :key="t.name" class="rounded-lg border p-4">
<div class="flex items-center gap-2">
<span class="text-sm font-medium text-slate-800">{{ t.name }}</span>
<NTag size="small" type="info">{{ t.tag }}</NTag>
</div>
<p class="mt-1.5 text-xs leading-relaxed text-slate-500">{{ t.desc }}</p>
<div class="mt-3 flex gap-2">
<NButton ghost size="small" class="text-xs" @click="previewTemplate(t.name)">查看</NButton>
<NButton ghost size="small" class="text-xs" @click="downloadTemplate(t.name)">
<Icon icon="material-symbols:download" class="size-14px" />下载
</NButton>
</div>
</div>
</div>
<div v-if="list.length === 0" class="py-6 text-center text-xs text-slate-400">未找到匹配的方法模板库</div>
</div>
</div>
</div>
</NSpin>
</template>
<style scoped lang="scss">
.scroll {
height: calc(100vh - 267px);
overflow-y: auto;
}
</style>