@@ -8,6 +8,8 @@ import {
} from "@/typings/model" ;
import { getColorWithOpacity } from "@/utils/common" ;
import HighlightText from "@/views/home/modules/HighlightText.vue" ;
import Ed from "@/views/home/modules/Ed.vue" ;
const themeStore = useThemeStore ( ) ;
@@ -29,7 +31,7 @@ const form = ref<ApplyForm>({
name : "" , dept : "" , mainChanges : [ "" ] , related : "" , purposes : [ ] , effect : "" ,
type : "" , level : "" , duration : "" , urgent : false , date : null , restoreDate : null ,
} ) ;
const tab = ref < string > ( "form " ) ;
const tab = ref < string > ( "train " ) ;
const TABS = [
{ id : "pre" , name : "变更预识别" , icon : 'lucide:sparkles' } ,
{ id : "form" , name : "变更申请表" , icon : 'akar-icons:file' } ,
@@ -390,8 +392,94 @@ const finishApprover = () => {
sheet . value = null ;
window . $message ? . success ( '审批流程已更新(底部固定行摘要同步刷新)' ) ;
}
// 风险识别分析记录表
const TOOL _CARDS = [
{ key : "HAZOP" as const , name : "HAZOP 分析" , desc : "节点-偏离结构化分析:原因 / 后果 / LS 风险矩阵 / 安全措施" } ,
{ key : "JSA" as const , name : "JSA 分析" , desc : "作业步骤分解:危害因素识别与 LS 风险分级管控" } ,
{ key : "检查表法" as const , name : "设备 SCL" , desc : "设备 / 管理检查项目对照标准逐项核查" } ,
{ key : "通用检查表" as const , name : "风险检查表" , desc : "变更通用检查项:是 / 否 / 不涉及逐项判定,输出风险描述与管控措施" } ,
] ;
const pluginDone = ref < Record < string , number > > ( { } ) ;
const riskRecords = ref < any > ( [ ] ) ;
const updRec = ( i : number , patch : Partial < any > ) => {
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 pssrGroups = ref < { g : string ; items : any [ ] } [ ] > ( [
{ g : "一、风险分析行动项落实" , items : [
{ c : "联锁跳车电流设定值校核并留存记录" , p : "投用前" } ,
{ c : "电缆与开关容量校核升级完成" , p : "投用前" } ,
{ c : "操作人员培训完成并考核合格" , p : "投用前" } ,
] } ,
{ g : "二、现场设备安装检查确认" , items : [
{ c : "电机安装就位,联轴器对中合格" , p : "投用前" } ,
{ c : "地脚螺栓紧固,减振垫安装到位" , p : "投用前" } ,
{ c : "电缆接线、防爆格兰头密封良好" , p : "投用前" } ,
] } ,
{ g : "三、保护措施确认" , items : [
{ c : "联锁跳车设定值与新电机额定电流匹配" , p : "投用前" } ,
{ c : "电机外壳接地、防护罩完好" , p : "投用前" } ,
{ c : "紧急停机按钮功能试验正常" , p : "投用前" } ,
] } ,
{ g : "四、资料归档确认" , items : [
{ c : "P&ID(图号 PID-R201-03)已更新并受控" , p : "关闭前" } ,
{ c : "设备台账、备件清单已更新" , p : "关闭前" } ,
{ c : "培训签到表、考核记录已归档" , p : "关闭前" } ,
] } ,
] ) ;
const toPssr = ( i : number ) => {
const rec = riskRecords . value [ i ] ;
if ( ! rec || rec . pssr ) return ;
pssrGroups . value = pssrGroups . value . map ( ( g : any , gi : any ) => gi === 0
? { ... g , items : [ ... g . items , { c : ` ${ rec . suggest } (来源:风险分析记录表 · ${ rec . item } ) ` , p : "投用前" } ] }
: g ) ;
updRec ( i , { pssr : true } ) ;
window . $message ? . success ( "建议措施已转为 PSSR 行动项(见「PSSR 检查内容」· 一、风险分析行动项落实)" ) ;
} ;
const toPssrAll = ( ) => {
const rest = riskRecords . value . map ( ( x : any , i : any ) => ( x . pssr ? - 1 : i ) ) . filter ( ( i : any ) => i >= 0 ) ;
if ( ! rest . length ) { window . $message ? . info ( "所有建议措施均已转为 PSSR 行动项" ) ; return ; }
const items = rest . map ( ( i : any ) => ( { c : ` ${ riskRecords . value [ i ] . suggest } (来源:风险分析记录表 · ${ riskRecords . value [ i ] . item } ) ` , p : "投用前" } ) ) ;
pssrGroups . value = pssrGroups . value . map ( ( g : any , gi : any ) => gi === 0 ? { ... g , items : [ ... g . items , ... items ] } : g ) ;
riskRecords . value = riskRecords . value . map ( ( x : any ) => ( { ... x , pssr : true } ) ) ;
window . $message ? . success ( ` 已将 ${ rest . length } 条建议措施全部转为 PSSR 行动项(见「PSSR 检查内容」) ` ) ;
} ;
const aiOrganize = ( ) => {
const gen : any [ ] = [
{ item : "超温溜温冲料" , scene : "反应温度提升至 90℃ 后更接近溶剂回流温度,温控失效或冷却水中断时反应溜温,釜压骤升可能导致冲料与物料喷溅" , inherent : "高" , existing : "DCS 温度高报 95℃、高高报及联锁 100℃(随变更上调并重新整定);紧急冷却系统有效" , suggest : "1. 投用前完成联锁传动测试并留存记录;2. 投用首周每班复核温度趋势,纳入交接班巡检" , residual : "中" , source : "AI 整理" } ,
{ item : "未知杂质累积影响" , scene : "未知杂质含量由 0.05% 升至 0.12%,热稳定性尚未定性,长期累积加速釜壁结垢与搅拌负荷异常" , inherent : "中" , existing : "小试热稳定性筛查;安全阀泄放能力不受本次调整影响" , suggest : "1. 将杂质含量纳入日常分析计划(每周 1 次,连续 8 周);2. 跟踪结垢趋势,安排周期清洗" , residual : "低" , source : "AI 整理" } ,
{ item : "联锁 / 报警设定失效" , scene : "报警 / 联锁值上调后(高报 85→95℃、联锁 90→100℃),原有报警裕度改变,存在误报、漏报风险" , inherent : "中" , existing : "参数修改执行申请-复核双人确认" , suggest : "1. 联锁逻辑图与台账同步更新并归档;2. 联锁测试纳入 PSSR 必查项" , residual : "低" , source : "AI 整理" } ,
] ;
riskRecords . value = [ ... riskRecords . value , ... gen ] ;
window . $message ? . success ( "AI 已根据变更内容自动整理形成风险分析记录表(3 项),支持手动修改" ) ;
} ;
// ---------- 变更培训 ----------
const trainText = ref (
` 培训对象:R-201 岗位操作人员(4 人)、维修班(2 人)
培训方式:现场讲解 + 操作演示
一、变更内容
M-201A 搅拌电机由 55kW 更换为 75kW 防爆电机(ExdⅡBT4 → ExdⅡCT4),额定电流 102A → 138A;电缆与开关容量同步升级,联锁跳车电流设定值同步校核调整。
二、变化的操作方式
1. 搅拌器启动:变更前直接启动、观察电流 ≤102A;变更后启动后核对电流 ≤138A,确认变频器参数已更新(注意:首次启动空载试运行 30 分钟)
2. 高粘度工况操作:变更前粘度 >3000cP 需降速运行;变更后粘度 ≤6000cP 可全速运行(注意:严禁超过 6000cP 上限)
3. 过载跳闸处置:变更前复位后直接重启;变更后先查明原因并记录,联锁复位需班长确认(注意:跳闸电流设定值已调整)
三、潜在风险与应对措施
1. 风险:电机 / 电缆过热 → 应对:投用首周每班测温并记录,红外点检复核
2. 风险:联锁设定错误导致拒动 / 误动 → 应对:投用前完成跳车试验并留存记录
3. 风险:防爆接合面或格兰头密封失效 → 应对:检查防爆接合面与格兰头,严禁带电开盖 `
) ;
< / script >
@@ -757,6 +845,142 @@ const finishApprover = () => {
< / div >
< / template >
<!-- === == 风险记录表 === == -- >
< template v-if = "tab === 'risk'" >
< div class = "border p-3" : style = "{ borderRadius: themeStore.themeRadius + 'px' }" >
< div class = "flex flex-wrap items-center gap-2" >
< span class = "flex items-center gap-1.5 text-sm font-semibold text-slate-700" >
< Icon icon = "lucide:shield-alert" class = "size-16px" / >
风险识别工具 ( 按变更等级匹配 )
< / span >
< NTag size = "small" : type = "form?.level === '重要' ? 'error' : 'default'" >
{ { form ? . level === '重要' ? '重要变更' : '一般变更' } }
< / NTag >
< span class = "text-xs text-slate-400" >
{ { form ? . level === '重要' ? '规则:HAZOP 必做,同时开展 JSA 与检查表法(SCL)' : '规则:JSA + 检查表法(SCL),涉及工艺安全边界的应升级 HAZOP' } } ; 可在申请表 「 风险分析 」 栏手动重选
< / span >
< / div >
< div class = "grid gap-2 grid-cols-4 mt-3" >
< div v-for = "t in TOOL_CARDS" :key="t.key" :title="t.desc"
class = "flex items-center gap-2 rounded-lg border px-3 py-2 transition"
: class = "pluginDone[t.key] !== undefined ? 'border-[var(--theme-color)] bg-[var(--theme-color)]' : tools.includes(t.key) ? 'border-[var(--theme-color)] bg-white' : 'border-slate-200 bg-white'"
: style = "{'--theme-color': themeStore.themeColor}"
>
< div class = "min-w-0 flex-1" >
< div
class = "flex items-center gap-1.5 text-xs font-semibold"
: class = "pluginDone[t.key] !== undefined ? 'text-white' : tools.includes(t.key) ? 'text-[var(--theme-color)]' : 'text-slate-500'"
: style = "{'--theme-color': themeStore.themeColor}"
>
< Icon icon = "fluent:wand-24-regular" class = "size-16px" / >
{ { t . name } }
< / div >
< div class = "mt-0.5 text-[10px]" : class = "pluginDone[t.key] !== undefined ? 'text-white/75' : 'text-slate-400'" >
{ { pluginDone [ t . key ] !== undefined ? ` 已使用并分析了 ${ pluginDone [ t . key ] } 条内容 ` : '未使用此工具分析' } }
< / div >
< / div >
< NButton size = "small" class = "text-xs" : type = "pluginDone[t.key] !== undefined ? 'default' : tools.includes(t.key) ? 'primary' : 'default'"
@click ="if (!tools.includes(t.key)) toggleTool(t.key); plugin = t.key;" >
使用
< / NButton >
< / div >
< / div >
< / div >
< div class = "border" : style = "{ borderRadius: themeStore.themeRadius + 'px' }" >
< div class = "flex flex-wrap items-center gap-2 bg-slate-100 px-3 py-2" >
< span class = "flex items-center gap-1.5 text-sm font-medium text-slate-700" >
< Icon icon = "proicons:alert-triangle" class = "size-16px text-amber-500" / >
风险分析记录表
< / span >
< span class = "text-[11px] text-slate-400" > 共 { { riskRecords . length } } 项 < / span >
< span class = "ml-auto hidden text-[11px] text-slate-400 lg:block" > 插件结果 「 入表 」 或 AI 自动整理形成 ; 单元格点击修改 , 风险等级点击切换 < / span >
< NButton ghost size = "small" color = "#7c3aed" class = "text-xs" @click ="toPssrAll" >
< Icon icon = "tabler:clipboard-check" class = "size-14px" / >
全部转 PSSR 行动项
< / NButton >
< NButton ghost size = "small" type = "primary" class = "text-xs" @click ="aiOrganize" >
< Icon icon = "lucide:sparkles" class = "size-12px" / >
AI 自动整理
< / NButton >
< / div >
< div v-if = "riskRecords.length === 0" class="px-3 py-6 text-center text-xs text-slate-400" >
尚无风险记录 : 调用上方插件完成分析后逐条 「 入表 」 , 或点击 「 AI 自动整理 」 形成记录表
< / div >
< div v-else class = "overflow-x-auto p-3" >
< table class = "w-full min-w-[960px] border-collapse text-xs" >
< thead >
< tr >
< th v-for = "h in ['风险项', '场景描述', '固有风险', '现有措施', '建议措施', '剩余风险', '来源', '操作']" :key="h"
class = "border bg-slate-50 px-2 py-1.5 text-[11px] font-medium text-slate-500" > { { h } } < / th >
< / tr >
< / thead >
< tbody >
< tr v-for = "(rec, i) in riskRecords" :key="i" >
< td class = "border px-1.5 py-1 align-top font-medium text-slate-700" >
< Ed :v = "rec.item" @update ="(x: string) => updRec(i, { item: x })" / >
< / td >
< td class = "border px-1.5 py-1 align-top" > < Ed :v = "rec.scene" @update ="(x: string) => updRec(i, { scene: x })" / > < / td >
< td class = "border px-1.5 py-1 text-center align-top" >
< NTag v-if = "rec.inherent==='高'" type="error" size="small" class="cursor-pointer" title="点击切换等级" @click="updRec(i, { inherent: lvlNext(rec.inherent) as '高' | '中' | '低' })" >
{{ rec.inherent }}
< / NTag >
< NTag v-if = "rec.inherent==='中'" type="warning" size="small" class="cursor-pointer" title="点击切换等级" @click="updRec(i, { inherent: lvlNext(rec.inherent) as '高' | '中' | '低' })" >
{{ rec.inherent }}
< / NTag >
< NTag v-if = "rec.inherent==='低'" type="success" size="small" class="cursor-pointer" title="点击切换等级" @click="updRec(i, { inherent: lvlNext(rec.inherent) as '高' | '中' | '低' })" >
{{ rec.inherent }}
< / NTag >
< / td >
< td class = "border px-1.5 py-1 align-top" > < Ed :v = "rec.existing" @update ="(x: string) => updRec(i, { existing: x })" / > < / td >
< td class = "border px-1.5 py-1 align-top" > < Ed :v = "rec.suggest" @update ="(x: string) => updRec(i, { suggest: x })" / > < / td >
< td class = "border px-1.5 py-1 text-center align-top" >
< NTag v-if = "rec.residual==='高'" type="error" size="small" class="cursor-pointer" title="点击切换等级" @click="updRec(i, { residual: lvlNext(rec.residual) as '高' | '中' | '低' })" >
{{ rec.residual }}
< / NTag >
< NTag v-if = "rec.residual==='中'" type="warning" size="small" class="cursor-pointer" title="点击切换等级" @click="updRec(i, { residual: lvlNext(rec.residual) as '高' | '中' | '低' })" >
{{ rec.residual }}
< / NTag >
< NTag v-if = "rec.residual==='低'" type="success" size="small" class="cursor-pointer" title="点击切换等级" @click="updRec(i, { residual: lvlNext(rec.residual) as '高' | '中' | '低' })" >
{{ rec.residual }}
< / NTag >
< / td >
< td class = "border px-1.5 py-1 text-center align-top" >
< NTag size = "small" type = "info" > { { rec . source } } < / NTag >
< / td >
< td class = "border px-1.5 py-1 text-center align-top" >
< NTag v-if = "rec.pssr" size="small" :color="{ color: '#f5f3ff', textColor: '#7c3aed', borderColor: '#f5f3ff' }" > 已转 PSSR < / NTag >
< NButton text size = "tiny" color = "#7c3aed" v-else class = "hover:underline" @click ="toPssr(i)" > 转 PSSR < / NButton >
< NButton text type = "error" size = "tiny" class = "mt-2" @click ="riskRecords = riskRecords.filter((_, j) => j !== i)" > 删除 < / NButton >
< / td >
< / tr >
< / tbody >
< / table >
< / div >
< / div >
< / template >
< ! - - 变更培训内容 - - >
< template v-if = "tab === 'train'" >
< div class = "flex flex-wrap items-center gap-3 mb-3" >
< span class = "flex items-center gap-1.5 text-sm font-semibold text-slate-700" >
< Icon icon = "lucide:graduation-cap" class = "size-16px text-violet-500" / >
变更培训内容
< / span >
< span class = "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 >
< span class = "ml-auto text-xs text-slate-400" > AI 生成内容均可点击修改自定义 ; 审批通过后 , 相关人员在 PC / 移动端完成学习与签到确认 < / span >
< / div >
<!-- 培训内容 -- >
< div class = "border" : style = "{ borderRadius: themeStore.themeRadius + 'px' }" >
< div class = "flex flex-wrap items-center gap-2 bg-slate-100 px-3 py-2" >
< span class = "text-sm font-medium text-slate-700" > 培训内容 < / span >
< span class = "text-[11px] text-slate-400" > 含培训对象 、 培训方式 、 变更内容 、 操作方式 、 潜在风险与应对措施 , 一个文本框自动分段换行 < / span >
< span class = "ml-auto text-[11px] text-slate-400" > 内容支持直接修改 < / span >
< / div >
< NInput v -model :value = "trainText" type = "textarea" :bordered = "false" : autosize = "{minRows: 4}" class = "font-mono text-sm" placeholder = "" / >
< / div >
< / template >
< / div >
< / div >
<!-- 底部 -- >
@@ -978,11 +1202,9 @@ const finishApprover = () => {
. scroll {
height : calc ( 100 vh - 326 px ) ;
overflow - y : auto ;
@ include scrollbar ( ) ;
}
. scroll2 {
height : calc ( 100 vh - 409 px ) ;
overflow - y : auto ;
@ include scrollbar ( ) ;
}
< / style >