高风险场景列表完成

This commit is contained in:
2026-09-20 14:49:28 +08:00
parent 0bf469d37d
commit 05b7357ca6
7 changed files with 973 additions and 477 deletions
+11
View File
@@ -0,0 +1,11 @@
import { request } from '../request';
// 列表查询
export function highRiskListApi(params:any) {
return request({
url: `/api/risk-records/high`,
method: 'get',
params,
});
}
+37
View File
@@ -197,6 +197,43 @@ export function generateSecurePassword(passwordLength:number) {
// 打乱字符顺序
return shuffle(password.split('')).join('');
}
// 根据当前时间,获取指定的时间范围(返回时间戳)
export function getTimeRangeStamp(timeRange: string){
const endTime = anyToTimestamp(new Date());
if(!endTime) {
return null;
}
// 近一个月
if(timeRange === 'month'){
return {
from: endTime - (30 * 24 * 60 * 60 * 1000),
to: endTime
}
}
// 近三个月
if(timeRange === 'quarter'){
return {
from: endTime - (90 * 24 * 60 * 60 * 1000),
to: endTime
}
}
// 近半年
if(timeRange === 'half_year'){
return {
from: endTime - (180 * 24 * 60 * 60 * 1000),
to: endTime
}
}
// 近一年
if(timeRange === 'year'){
return {
from: endTime - (365 * 24 * 60 * 60 * 1000),
to: endTime
}
}
}
// 获取 token
export const getToken = () => {
return localStorage.getItem('token');
+47 -22
View File
@@ -3,7 +3,7 @@ import { ref, computed, onMounted, h } from 'vue';
import { Icon } from '@iconify/vue'
import { useThemeStore } from '@/store/modules/theme';
import { useRouterPush } from '@/hooks/common/router';
import { tagTextColor, tagBgColor, tagBorderColor } from '@/utils/common';
import { tagTextColor, tagBgColor, tagBorderColor, getTimeRangeStamp, formatTimestamp } from '@/utils/common';
import { systemConfigApi } from '@/service/api/system';
import { orgListApi } from '@/service/api/user';
import { changeLedgerListApi } from '@/service/api/changeLedger';
@@ -13,6 +13,8 @@ const themeStore = useThemeStore();
const props = defineProps(['self', 'keyValue'])
const loading = ref<boolean>(false);
// 当前选中时间范围
const period = ref<string>("year");
// 组织树
const orgTree = ref<any[]>([]);
// 筛选条件
@@ -23,8 +25,8 @@ const formInfo = ref<any>({
duration: null,
status: '',//英文逗号隔开
org_id: 1,
apply_time_start: null,
apply_time_end: null,
apply_time_start: getTimeRangeStamp(period.value)?.from ?? null,
apply_time_end: getTimeRangeStamp(period.value)?.to ?? null,
self: props.self,
});
const pagination = ref<{page: number, pageSize: number, itemCount: number}>({
@@ -70,8 +72,6 @@ const TIME_OPTIONS = [
{ label: "近一个月", value: 'month' },
{ label: "自定义", value: 'custom' },
];
// 当前选中时间范围
const period = ref<string>("year");
// 是否显示全部
const isAll = ref<boolean>(true);
// 当前选中行
@@ -86,18 +86,6 @@ const revokeSaving = ref<boolean>(false);
// 列表
const list = ref<any[]>([]);
// 重置
const reset = () => {
formInfo.value.kw = '';
formInfo.value.type = null;
formInfo.value.level = null;
formInfo.value.duration = null;
formInfo.value.status = '';
formInfo.value.sort = '';
period.value = 'year';
isAll.value = true;
window.$message?.success("筛选条件已重置");
};
// 导出台账
const exportChanges = () => {
window.$message?.info('还差接口');
@@ -127,6 +115,21 @@ const searchHandle = () => {
pagination.value.page = 1;
getList();
}
// 重置
const reset = () => {
period.value = 'year';
formInfo.value.kw = '';
formInfo.value.type = null;
formInfo.value.level = null;
formInfo.value.duration = null;
formInfo.value.status = '';
formInfo.value.org_id = 1;
formInfo.value.sort = '';
formInfo.value.apply_time_start = getTimeRangeStamp(period.value)?.from ?? null;
formInfo.value.apply_time_end = getTimeRangeStamp(period.value)?.to ?? null;
isAll.value = true;
searchHandle();
};
// 组织选择改变
const orgChange = (val:any) => {
formInfo.value.org_id = val;
@@ -155,8 +158,30 @@ const durChoose = (val:any) => {
// 时间范围选择
const timeChoose = (val:string) => {
period.value = val;
if(val === 'custom'){
formInfo.value.apply_time_start = null;
formInfo.value.apply_time_end = null;
return
}else{
formInfo.value.apply_time_start = getTimeRangeStamp(val)?.from ?? null;
formInfo.value.apply_time_end = getTimeRangeStamp(val)?.to ?? null;
}
searchHandle();
}
// 自定义时间选择
const timeChange = (type:string,val:any) => {
if(type === 'start'){
formInfo.value.apply_time_start = val;
}else{
formInfo.value.apply_time_end = val;
}
if(formInfo.value.apply_time_start && formInfo.value.apply_time_end){
if(formInfo.value.apply_time_start > formInfo.value.apply_time_end){
return window.$message?.warning('开始时间不能大于结束时间');
}
searchHandle();
}
}
// 状态选择
const statusChoose = (val:string) => {
formInfo.value.status = val;
@@ -231,8 +256,8 @@ const getList = async () => {
duration: formInfo.value.duration,
status: formInfo.value.status,
org_id: formInfo.value.org_id,
apply_time_start: formInfo.value.apply_time_start,
apply_time_end: formInfo.value.apply_time_end,
apply_time_start: formatTimestamp(formInfo.value.apply_time_start),
apply_time_end: formatTimestamp(formInfo.value.apply_time_end),
self: formInfo.value.self,
page_num: pagination.value.page,
page_size: pagination.value.pageSize,
@@ -324,7 +349,7 @@ onMounted(async () => {
显示未关闭
</NRadio>
<div class="relative ml-auto">
<NInput v-model:value="formInfo.kw" size="small" type="text" clearable placeholder="搜索变更名称 / 编号" @clear="searchHandle">
<NInput v-model:value="formInfo.kw" size="small" type="text" clearable placeholder="搜索变更名称 / 编号" @clear="formInfo.kw = ''; searchHandle()">
<template #suffix>
<Icon icon="akar-icons:search" class="size-14px text-slate-400 cursor-pointer" @click="searchHandle" />
</template>
@@ -355,9 +380,9 @@ onMounted(async () => {
<span class="text-xs text-slate-400">时间</span>
<NButton size="tiny" round v-for="p in TIME_OPTIONS" :key="p.value" :ghost="period === p.value ? false : true" :type="period === p.value ? 'primary' : 'default'" @click="timeChoose(p.value)">{{ p.label }}</NButton>
<span v-if="period === 'custom'" class="flex items-center gap-1">
<NDatePicker v-model:value="formInfo.apply_time_start" type="date" size="small" class="w-130px" />
<NDatePicker :value="formInfo.apply_time_start" type="date" size="small" class="w-130px" :on-update:value="(v) => timeChange('start',v)" />
<span class="text-xs text-slate-400"></span>
<NDatePicker v-model:value="formInfo.apply_time_end" type="date" size="small" class="w-130px" />
<NDatePicker :value="formInfo.apply_time_end" type="date" size="small" class="w-130px" :on-update:value="(v) => timeChange('end',v)" />
</span>
</p>
</div>
+33 -455
View File
@@ -1,476 +1,54 @@
<!-- 本专业高风险 -->
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue';
import { Icon } from '@iconify/vue'
import { ref } from 'vue';
import { useThemeStore } from '@/store/modules/theme';
import { useRouterPush } from '@/hooks/common/router';
import Risk from './modules/risk.vue';
import History from './modules/history.vue';
import Template from './modules/template.vue';
const themeStore = useThemeStore();
const { routerPushByKey } = useRouterPush();
const loading = ref<boolean>(false);
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" };
};
// 当前tab
const currentTab = ref<string>("highRisk");
// 切换tab
const tabChange = (k: string) => {
list.value = []
const tabChange = async (k: string) => {
currentTab.value = k;
loading.value = true;
if(k === 'highRisk'){
setTimeout(() => {
list.value = highRiskList;
loading.value = false;
}, 1000);
}
if(k === 'history'){
setTimeout(() => {
list.value = historyList;
loading.value = false;
}, 1000);
}
if(k === 'tpl'){
setTimeout(() => {
list.value = templateList;
loading.value = false;
}, 1000);
}
}
// 高风险场景查看变更
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');
}
onMounted(() => {
tabChange('highRisk');
})
</script>
<template>
<NSpace vertical :size="16">
<!-- 本专业高风险 -->
<NSpin :show="loading" size="small">
<NCard :bordered="false" size="small" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
<div
class="flex flex-wrap items-center gap-2 border border-slate-200 px-3 py-2 mb-3"
: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'"
>
<div v-for="t in TABS" :key="t.k" @click="tabChange(t.k)"
class="flex items-center gap-1.5 px-3 py-1 text-xs cursor-pointer select-none rounded-[20px] border"
:style="{ '--theme-color': themeStore.themeColor}"
:class="currentTab === t.k ? 'bg-[var(--theme-color)] font-medium text-white border-[var(--theme-color)]' : 'text-slate-600 hover:bg-slate-100'"
>
{{ t.name }}
</div>
</div>
<!-- 高风险场景 -->
<div v-if="currentTab === 'highRisk'" class="border border-slate-200" :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="lucide:shield-check" class="size-16px" :style="{color:themeStore.themeColor}" />
<p class="text-base font-semibold">变更新增高风险场景</p>
</div>
<div class="flex items-center gap-2 min-h-28px">
<NTag size="small" type="primary" class="text-11px font-medium">单笔变更的分析编制在变更上下文内完成</NTag>
</div>
</div>
<div class="px-4 mb-3 flex flex-wrap items-center gap-1.5">
<span class="mr-1 text-xs text-slate-400">时间</span>
<NButton
size="tiny"
round
:type="range === r ? 'primary' : 'default'"
v-for="r in RISK_RANGES"
:key="r"
@click="range = r"
>
{{ r }}
</NButton>
<span class="ml-3 mr-1 text-xs text-slate-400">部门</span>
<NButton
size="tiny"
round
v-for="d in departments"
:key="d.id"
:type="dept === d.name ? 'primary' : 'default'"
@click="dept = d.name"
>
{{ d.name }}
</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="(s, i) in list" :key="`${s.cid ?? 'x'}-${i}`" class="flex flex-wrap items-center gap-3 rounded-lg border px-4 py-3">
<div class="min-w-0 flex-1">
<div class="flex flex-wrap items-center gap-2">
<NTag type="info" size="small">{{ s.method }}</NTag>
<span class="text-sm font-medium text-slate-800">{{ s.item }}</span>
<NTag type="error" size="small">固有风险 · </NTag>
<NTag v-if="s.residual==='高'" type="error" size="small">剩余风险 · {{ s.residual }}</NTag>
<NTag v-if="s.residual==='中'" type="warning" size="small">剩余风险 · {{ s.residual }}</NTag>
<NTag v-if="s.residual==='低'" type="info" size="small">剩余风险 · {{ s.residual }}</NTag>
</div>
<div class="mt-0.5 text-xs text-slate-600">{{ s.scene }}</div>
<div class="mt-0.5 text-xs text-slate-400">
变更{{ s.info }} · {{ s.dept }} · 分析日期 {{ s.adate }} · 责任{{ s.owner }} · 分析记录 {{ s.count }}
</div>
</div>
<NButton ghost size="small" class="text-xs" @click="openSceneChange(s)">查看变更</NButton>
<NButton size="small" type="primary" class="text-xs" @click="viewAnalysis(s)">查看分析记录</NButton>
</div>
<div v-if="list.length === 0" class="py-6 text-center text-xs text-slate-400">当前筛选条件下无新增高风险场景</div>
</div>
</div>
</div>
<!-- 历史分析检索 -->
<div v-if="currentTab === 'history'" class="border border-slate-200" :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" placeholder="按单号 / 装置 / 关键词检索…">
<template #suffix>
<Icon icon="akar-icons:search" class="size-14px text-slate-400 cursor-pointer" />
</template>
</NInput>
</div>
</div>
<div class="px-4 mb-3 flex flex-wrap items-center gap-1.5">
<NButton
size="tiny"
round
v-for="m in ['全部', 'HAZOP', 'JSA', '检查表', 'AI 预分析']"
:key="m"
:type="method === m ? 'primary' : 'default'"
@click="method = m"
>
{{ m }}
</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.id }}</span>
<NTag type="info" size="small">{{ h.method }}</NTag>
<span class="text-sm text-slate-800">{{ h.change }}</span>
</div>
<div class="mt-0.5 text-xs text-slate-500">{{ h.unit }} · 范围{{ h.scope }} · {{ h.rows }} 行分析记录 · {{ h.date }}</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>
<!-- 方法模板库 -->
<div v-if="currentTab === 'tpl'" class="border border-slate-200" :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>
<!-- 进度弹框 -->
<NDrawer v-model:show="progressDrawer" width="85%" placement="right">
<NDrawerContent closable>
<template #header>
<span class="flex flex-wrap items-center gap-2 text-base">
风险分析记录表
<NTag size="small" type="info">{{ currentRecord.method }}</NTag>
<NTag size="small" type="error">含固有风险 · 场景</NTag>
</span>
</template>
<div>
<div class="mb-3 flex flex-wrap gap-x-5 gap-y-1 text-xs text-slate-500">
<span>变更<span class="font-medium text-slate-700">{{ currentRecord.info }}</span></span>
<span>分析日期{{ currentRecord.adate }}</span>
<span>责任{{ currentRecord.owner }}</span>
<span class="ml-auto text-slate-400">分析编制与修改在变更上下文内完成此处只读</span>
</div>
<div v-if="!isHazop" class="overflow-x-auto rounded-lg border border-b-0">
<table class="w-full min-w-[860px] border-collapse">
<thead>
<tr>
<th :class="th" class="w-[130px]">风险项</th>
<th :class="th">场景描述</th>
<th :class="th" class="w-[76px] text-center">固有风险</th>
<th :class="th">现有措施</th>
<th :class="th">建议措施</th>
<th :class="th" class="w-[76px] text-center">剩余风险</th>
</tr>
</thead>
<tbody>
<tr v-for="(r, i) in currentRecord.jsaRows" :key="i" :class="r.item === currentRecord.item && r.inherent === '高' ? 'bg-red-50/40' : ''">
<td :class="td" class="font-medium text-slate-800">{{ r.item }}</td>
<td :class="td">{{ r.scene }}</td>
<td :class="td" class="text-center">
<NTag size="small" type="error" v-if="r.inherent==='高'">{{ r.inherent }}</NTag>
<NTag size="small" type="warning" v-if="r.inherent==='中'">{{ r.inherent }}</NTag>
<NTag size="small" type="info" v-if="r.inherent==='低'">{{ r.inherent }}</NTag>
</td>
<td :class="td">{{ r.existing }}</td>
<td :class="td">{{ r.suggest }}</td>
<td :class="td" class="text-center">
<NTag size="small" type="error" v-if="r.residual==='高'">{{ r.residual }}</NTag>
<NTag size="small" type="warning" v-if="r.residual==='中'">{{ r.residual }}</NTag>
<NTag size="small" type="info" v-if="r.residual==='低'">{{ r.residual }}</NTag>
</td>
</tr>
</tbody>
</table>
</div>
<div v-else class="flex gap-3">
<!-- 节点 · 偏离导航 -->
<div class="w-48 shrink-0 space-y-1.5">
<div v-for="(n, i) in hazopNodes" :key="i" @click="sel = i"
class="w-full rounded-lg border px-3 py-2 text-left text-xs cursor-pointer"
:class="sel === i ? 'border-[var(--theme-color)] bg-[var(--theme-color)] text-white' : 'hover:border-[var(--theme-color)]'"
:style="{'--theme-color': themeStore.themeColor}"
>
<span class="block truncate font-medium">{{ n.dev }}</span>
<span class="mt-0.5 flex items-center justify-between text-[11px]" :class="sel === i ? 'text-white/80' : 'text-slate-400'">
<span class="truncate">{{ n.node.replace(/^节点\d+ · /, '') }}</span>
<NTag size="tiny" round class="ml-1" :type="sel === i ? 'default' : 'success'">{{ n.n }} </NTag>
</span>
</div>
<p class="px-1 pt-1 text-[11px] leading-relaxed text-slate-400">L / S 取值 15RR 5×5 风险矩阵实时重算</p>
</div>
<!-- 偏差分析表 -->
<div class="min-w-0 flex-1 overflow-x-auto rounded-lg border border-b-0">
<table class="w-full min-w-[760px] border-collapse">
<thead>
<tr>
<th :class="th">原因</th>
<th :class="th">后果</th>
<th :class="th" class="w-10 !text-center">L</th>
<th :class="th" class="w-10 !text-center">S</th>
<th :class="th" class="w-[92px] !text-center">RR</th>
<th :class="th">安全措施</th>
<th :class="th">建议措施</th>
</tr>
</thead>
<tbody>
<tr v-for="(r, i) in selRows" :key="i">
<td :class="td">{{ r.cause }}</td>
<td :class="td">{{ r.cons }}</td>
<td :class="td" class="text-center">{{ r.L }}</td>
<td :class="td" class="text-center">{{ r.S }}</td>
<td :class="td" class="text-center">
<NTag size="small" :type="rrTag(r.L, r.S).cls">{{ rrTag(r.L, r.S).label }}</NTag>
</td>
<td :class="td">{{ r.safety }}</td>
<td :class="td">{{ r.suggest }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<template #footer>
<div class="flex justify-end gap-2">
<NButton ghost @click="exportFile"><Icon icon="material-symbols:download" class="size-16px" />导出</NButton>
<NButton type="primary" @click="progressDrawer = false">关闭</NButton>
</div>
</template>
</NDrawerContent>
</NDrawer>
</NCard>
</NSpin>
{{ t.name }}
</NButton>
</div>
<!-- 高风险场景 -->
<template v-if="currentTab === 'highRisk'">
<Risk />
</template>
<!-- 历史分析检索 -->
<template v-if="currentTab === 'history'">
<History />
</template>
<!-- 方法模板库 -->
<template v-if="currentTab === 'tpl'">
<Template />
</template>
</NCard>
</NSpace>
</template>
<style scoped lang="scss">
.scroll {
height: calc(100vh - 303px);
overflow-y: auto;
}
</style>
+146
View File
@@ -0,0 +1,146 @@
<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 currentTab = ref<string>("history");
const method = ref<string>("全部");
const list = ref<any[]>([]);
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 viewHistory = (id: string) => {
window.$message?.info(`演示:在线查看 ${id} 完整分析记录`)
};
// 复用到新变更
const copyHistory = (id: string) => {
window.$message?.success(`演示:${id} 已复用到新变更草稿,分析行已带入待确认`)
}
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;
}
// 切换tab
const tabChange = async (k: string) => {
list.value = []
currentTab.value = k;
loading.value = true;
if(k === 'history'){
setTimeout(() => {
list.value = historyList;
loading.value = false;
}, 1000);
}
}
onMounted(() => {
tabChange('history');
})
</script>
<template>
<!-- 历史分析检索 -->
<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" placeholder="按单号 / 装置 / 关键词检索…">
<template #suffix>
<Icon icon="akar-icons:search" class="size-14px text-slate-400 cursor-pointer" />
</template>
</NInput>
</div>
</div>
<div class="px-4 mb-3 flex flex-wrap items-center gap-1.5">
<NButton
size="tiny"
round
v-for="m in ['全部', 'HAZOP', 'JSA', '检查表', 'AI 预分析']"
:key="m"
:type="method === m ? 'primary' : 'default'"
@click="method = m"
>
{{ m }}
</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.id }}</span>
<NTag type="info" size="small">{{ h.method }}</NTag>
<span class="text-sm text-slate-800">{{ h.change }}</span>
</div>
<div class="mt-0.5 text-xs text-slate-500">{{ h.unit }} · 范围{{ h.scope }} · {{ h.rows }} 行分析记录 · {{ h.date }}</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>
</template>
<style scoped lang="scss">
.scroll {
height: calc(100vh - 303px);
overflow-y: auto;
}
</style>
+387
View File
@@ -0,0 +1,387 @@
<script setup lang="ts">
import { ref, onMounted, h } from 'vue';
import { Icon } from '@iconify/vue'
import { useThemeStore } from '@/store/modules/theme';
import { useRouterPush } from '@/hooks/common/router';
import { getTimeRangeStamp, formatTimestamp } from '@/utils/common';
import { systemConfigApi, matrixConfigApi } from '@/service/api/system';
import { orgListApi } from '@/service/api/user';
import { highRiskListApi } from '@/service/api/highRisk';
const themeStore = useThemeStore();
const { routerPushByKey } = useRouterPush();
const loading = ref<boolean>(false);
// 当前记录
const currentRecord = ref<any>(null);
const RISK_RANGES = [
{ label: "近一个月", value: "month" },
{ label: "近三个月", value: "quarter" },
{ label: "近半年", value: "half_year" },
{ label: "全部", value: 'all' }
];
// 风险矩阵配置
const hazopMatrix = ref<any>(null);
const fmeaMatrix = ref<any>(null);
// 高风险场景列表
const list = ref<any[]>([]);
// 查看变更
const jumpTo = (r: any) => {
routerPushByKey("details_index",{ query: { id: r?.change_id?.toString(),status:r?.status?.toString() || '',key:'my_highrisk' } });
};
// 查看分析记录
const viewAnalysis = (record: any) => {
currentRecord.value = record;
};
// 时间范围
const timeRange = ref<string>('month');
// 组织树
const orgTree = ref<any[]>([]);
// 筛选条件
const formInfo = ref<any>({
kw: '',
org_id: 1,
from: getTimeRangeStamp(timeRange.value)?.from ?? null,
to: getTimeRangeStamp(timeRange.value)?.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 renderSwitcherIcon = (node:any) => {
// 如果节点没有 children 或 children 为空数组,则不显示图标
if (!node.option.children || node.option.children.length === 0) {
return null; // 返回 null 表示不渲染任何图标
}
// 有子节点:返回 undefined 表示使用默认展开图标,也可自定义
return h(Icon,{
icon: node.option.expanded ? 'bi:caret-down-fill' : 'bi:caret-right-fill',
class: 'size-12px',
});
}
// 搜索
const searchHandle = () => {
pagination.value.page = 1;
getList();
}
// 组织选择改变
const orgChange = (val:any) => {
formInfo.value.org_id = val;
searchHandle();
}
// 时间范围改变
const timeRangeChange = (val: string) => {
timeRange.value = val;
if(val === 'all'){
formInfo.value.from = null;
formInfo.value.to = null;
}else{
const timeRange = getTimeRangeStamp(val);
formInfo.value.from = timeRange?.from ?? null;
formInfo.value.to = timeRange?.to ?? null;
}
searchHandle();
}
// 重置
const reset = () => {
timeRange.value = 'month';
formInfo.value.kw = '';
formInfo.value.org_id = 1;
formInfo.value.from = getTimeRangeStamp('month')?.from ?? null;
formInfo.value.to = getTimeRangeStamp('month')?.to ?? null;
searchHandle();
};
// 分页改变
const pageChange = (page: number) => {
pagination.value.page = page;
getList();
}
// 获取列表
const getList = async () => {
loading.value = true;
const {data,error} = await highRiskListApi({
kw: formInfo.value.kw,
org_id: formInfo.value.org_id,
from: formatTimestamp(formInfo.value.from),
to: formatTimestamp(formInfo.value.to),
page_num: pagination.value.page,
page_size: pagination.value.pageSize,
});
if(!error){
list.value = data?.list ?? [];
console.log(list.value);
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;
}
// 获取风险矩阵配置
const matrixConfig = async () => {
const {data,error} = await matrixConfigApi();
if(!error){
hazopMatrix.value = data?.hazop || null;
fmeaMatrix.value = data?.fmea || null;
}
}
// 风险矩阵等级匹配
const matchRiskLevel = (configList:any,name: string) => {
if(!configList){
return null;
}
const target = configList.find((item:any) => item.name === name);
return target ?? null;
}
onMounted(async () => {
matrixConfig();
await getConfig();
getOrgList();
})
</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="lucide:shield-check" class="size-16px" :style="{color:themeStore.themeColor}" />
<p class="text-base font-semibold">变更新增高风险场景</p>
<span class="text-xs text-slate-400 mt-2px">{{ list.length }} 个高风险场景</span>
</div>
<div class="flex items-center gap-2 min-h-28px">
<NTag size="small" type="primary" class="text-11px font-medium">单笔变更的分析编制在变更上下文内完成</NTag>
</div>
</div>
<div class="px-4 mb-3 flex flex-wrap items-center gap-1.5">
<NTreeSelect
class="!w-200px"
:value="formInfo.org_id"
:options="orgTree"
size="small"
default-expand-all
label-field="label"
key-field="id"
:render-switcher-icon="renderSwitcherIcon"
placeholder="请选择组织层级"
@update:value="orgChange"
/>
<span class="mr-1 text-xs text-slate-400">时间</span>
<NButton
size="tiny"
round
v-for="r in RISK_RANGES"
:key="r.value"
:type="timeRange === r.value ? 'primary' : 'default'"
@click="timeRangeChange(r.value)"
>
{{ r.label }}
</NButton>
<NInput v-model:value="formInfo.kw" size="small" class="text-xs !w-200px ml-auto" clearable placeholder="关键词检索…" @clear="formInfo.kw = ''; searchHandle()">
<template #suffix>
<Icon icon="akar-icons:search" class="size-14px text-slate-400 cursor-pointer" @click="searchHandle" />
</template>
</NInput>
<NButton size="small" class="text-sm" @click="reset">
<Icon icon="system-uicons:reset" class="size-12px" />重置
</NButton>
</div>
<div class="scroll">
<div class="space-y-3 px-4 pb-3">
<div v-for="(s, i) in list" :key="`${s.cid ?? 'x'}-${i}`" class="flex flex-wrap items-center gap-3 rounded-lg border px-4 py-3">
<div class="min-w-0 flex-1">
<div class="flex flex-wrap items-center gap-2">
<NTag type="info" size="small">{{ s.source }}</NTag>
<span class="text-sm font-medium text-slate-800">{{ s.item }}</span>
<!-- 固有风险等级 -->
<template v-if="s.source==='RISK'">
<NTag
v-if="s.inherent_level && s.inherent_level === '低'"
class="text-[11px]"
size="small"
type="success"
>
{{s.inherent_level}}
</NTag>
<NTag
v-if="s.inherent_level && s.inherent_level === '中'"
class="text-[11px]"
size="small"
type="warning"
>
{{s.inherent_level}}
</NTag>
<NTag
v-if="s.inherent_level && s.inherent_level === '高'"
class="text-[11px]"
size="small"
type="error"
>
{{s.inherent_level}}
</NTag>
</template>
<template v-if="s.source==='HAZOP'">
<NTag
v-if="s.inherent_level"
size="small"
:color="{
color: matchRiskLevel(hazopMatrix?.risk_grades,s.inherent_level)?.bg_color,
textColor: matchRiskLevel(hazopMatrix?.risk_grades,s.inherent_level)?.font_color,
borderColor: matchRiskLevel(hazopMatrix?.risk_grades,s.inherent_level)?.bg_color,
}"
>
固有风险 · {{s.inherent_level}}
</NTag>
</template>
<template v-if="s.source==='FMEA'">
<NTag
v-if="s.inherent_level"
size="small"
:color="{
color: matchRiskLevel(fmeaMatrix?.risk_grades,s.inherent_level)?.bg_color,
textColor: matchRiskLevel(fmeaMatrix?.risk_grades,s.inherent_level)?.font_color,
borderColor: matchRiskLevel(fmeaMatrix?.risk_grades,s.inherent_level)?.bg_color,
}"
>
固有风险 · {{s.inherent_level}}
</NTag>
</template>
<template v-if="s.source==='RISK_CHECK'">
<span>
{{s.inherent_level?s.inherent_level:'-'}}
</span>
</template>
<!-- 剩余风险等级 -->
<template v-if="s.source==='RISK'">
<NTag
v-if="s.residual_level && s.residual_level === '低'"
class="text-[11px]"
size="small"
type="success"
>
{{s.residual_level}}
</NTag>
<NTag
v-if="s.residual_level && s.residual_level === '中'"
class="text-[11px]"
size="small"
type="warning"
>
{{s.residual_level}}
</NTag>
<NTag
v-if="s.residual_level && s.residual_level === '高'"
class="text-[11px]"
size="small"
type="error"
>
{{s.residual_level}}
</NTag>
</template>
<template v-if="s.source==='HAZOP'">
<NTag
v-if="s.residual_level"
size="small"
:color="{
color: matchRiskLevel(hazopMatrix?.risk_grades,s.residual_level)?.bg_color,
textColor: matchRiskLevel(hazopMatrix?.risk_grades,s.residual_level)?.font_color,
borderColor: matchRiskLevel(hazopMatrix?.risk_grades,s.residual_level)?.bg_color,
}"
>
剩余风险 · {{s.residual_level}}
</NTag>
</template>
<template v-if="s.source==='FMEA'">
<NTag
v-if="s.residual_level"
size="small"
:color="{
color: matchRiskLevel(fmeaMatrix?.risk_grades,s.residual_level)?.bg_color,
textColor: matchRiskLevel(fmeaMatrix?.risk_grades,s.residual_level)?.font_color,
borderColor: matchRiskLevel(fmeaMatrix?.risk_grades,s.residual_level)?.bg_color,
}"
>
剩余风险 · {{s.residual_level}}
</NTag>
</template>
<template v-if="s.source==='RISK_CHECK'">
<span>
{{s.residual_level?s.residual_level:'-'}}
</span>
</template>
</div>
<div class="mt-1 text-xs text-slate-600">{{ s.scene }}</div>
<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 }}
</div>
</div>
<NButton ghost size="small" class="text-xs" @click="jumpTo(s)">查看变更</NButton>
<NButton size="small" type="primary" class="text-xs" @click="viewAnalysis(s)">查看分析记录</NButton>
</div>
<div v-if="list.length === 0" class="py-6 text-center text-xs text-slate-400">当前筛选条件下无新增高风险场景</div>
</div>
</div>
<div class="flex justify-end mt-3 px-4 py-2 border-t">
<NPagination
v-model:page="pagination.page"
:page-size="pagination.pageSize"
:item-count="pagination.itemCount"
:on-update:page="pageChange"
>
<template #prefix="{ itemCount }">
{{ itemCount }}
</template>
</NPagination>
</div>
</div>
</NSpin>
</template>
<style scoped lang="scss">
.scroll {
height: calc(100vh - 368px);
overflow-y: auto;
}
</style>
+312
View File
@@ -0,0 +1,312 @@
<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>
<!-- 方法模板库 -->
<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>
</template>
<style scoped lang="scss">
.scroll {
height: calc(100vh - 303px);
overflow-y: auto;
}
</style>