@@ -1,295 +1,353 @@
<!-- 风险分析记录表 -- >
< script setup lang = "ts" >
import { ref } from 'vue' ;
import { Icon } from '@iconify/vue'
import { useThemeStore } from '@/store/modules/theme' ;
import Ed from "./Ed.vue ";
import Hazop from "./hazop .vue" ;
import Jsa from "./jsa .vue" ;
import Scl from "./scl .vue" ;
import RiskCheck from "./riskCheck .vue" ;
import Footer from './footer.vue'
const themeStore = useThemeStore ( ) ;
const props = defineProps ( [ 'type' , 'isAiOpen' , 'title' , 'currentId' , 'formInfo' ] )
const emit = defineEmits ( [ 'sav e' , 'submit' , 'confirmTab '] ) ;
const form = ref < { content : string } > ( {
content : ''
} ) ;
const pluginDone = ref < Record < string , number > > ( { } ) ;
const tools = ref < string [ ] > ( [ "HAZOP" , "JSA" , "SCL" , "RISK_CHECK" ] ) ;
const riskRecords = ref < any > ( [ ] ) ;
const pluginDrawer = ref < boolean > ( false ) ;
const currentPlugin = ref < string > ( '' ) ;
const lvlNext = ( v : string ) => ( v === "高" ? "中" : v === "中" ? "低" : "高" ) ;
// 风险识别分析记录表
const TOOL _CARDS = [
{ key : "HAZOP" as const , name : "HAZOP 分析" , desc : "节点-偏离结构化分析:原因 / 后果 / LS 风险矩阵 / 安全措施" } ,
{ key : "JSA" as const , name : "JSA 分析" , desc : "作业步骤分解:危害因素识别与 LS 风险分级管控" } ,
{ key : "SCL" as const , name : "设备 SCL" , desc : "设备 / 管理检查项目对照标准逐项核查" } ,
{ key : "RISK_CHECK" as const , name : "风险检查表" , desc : "变更通用检查项:是 / 否 / 不涉及逐项判定,输出风险描述与管控措施" } ,
] ;
const pssrGroups = ref < { g : string ; items : any [ ] } [ ] > ( [
{ g : "一、风险分析行动项落实" , items : [
{ c : "联锁跳车电流设定值校核并留存记录" , p : "投用前" } ,
{ c : "电缆与开关容量校核升级完成" , p : "投用前" } ,
{ c : "操作人员培训完成并考核合格" , p : "投用前" } ,
] } ,
{ g : "二、现场设备安装检查确认" , items : [
{ c : "电机安装就位,联轴器对中合格" , p : "投用前" } ,
{ c : "地脚螺栓紧固,减振垫安装到位" , p : "投用前" } ,
{ c : "电缆接线、防爆格兰头密封良好" , p : "投用前" } ,
] } ,
{ g : "三、保护措施确认" , items : [
{ c : "联锁跳车设定值与新电机额定电流匹配" , p : "投用前" } ,
{ c : "电机外壳接地、防护罩完好" , p : "投用前" } ,
{ c : "紧急停机按钮功能试验正常" , p : "投用前" } ,
] } ,
{ g : "四、资料归档确认" , items : [
{ c : "P&ID(图号 PID-R201-03)已更新并受控" , p : "关闭前" } ,
{ c : "设备台账、备件清单已更新" , p : "关闭前" } ,
{ c : "培训签到表、考核记录已归档" , p : "关闭前" } ,
] } ,
] ) ;
const toPssrAll = ( ) => {
const rest = riskRecords . value . map ( ( x : any , i : any ) => ( x . pssr ? - 1 : i ) ) . filter ( ( i : any ) => i >= 0 ) ;
if ( ! rest . length ) { window . $message ? . info ( "所有建议措施均已转为 PSSR 行动项" ) ; return ; }
const items = rest . map ( ( i : any ) => ( { c : ` ${ riskRecords . value [ i ] . suggest } (来源:风险分析记录表 · ${ riskRecords . value [ i ] . item } ) ` , p : "投用前" } ) ) ;
pssrGroups . value = pssrGroups . value . map ( ( g : any , gi : any ) => gi === 0 ? { ... g , items : [ ... g . items , ... items ] } : g ) ;
riskRecords . value = riskRecords . value . map ( ( x : any ) => ( { ... x , pssr : true } ) ) ;
window . $message ? . success ( ` 已将 ${ rest . length } 条建议措施全部转为 PSSR 行动项(见「PSSR 检查内容」) ` ) ;
} ;
const lsLevel = ( l : number | null , s : number | null ) => {
const rr = ( l ? ? 0 ) * ( s ? ? 0 ) ;
return rr >= 10 ? "高" : rr > = 5 ? "中" : "低" ;
} ;
const aiOrganize = async ( ) => {
// let form = changePreRef.value?.form;
// analyzing.value = true;
// try {
// const res = await aiRiskOrganize(form.description, (form.compare_rows ?? []).map((x: any) => ({ item: x.item, before_text: x.before_text, after_text: x.after_text })));
// const gen = (res?.records ?? []).map((r) => ({
// item: r.item,
// scene: r.scene,
// inherent: lsLevel(r.l, r.s),
// existing: r.existing,
// suggest: r.suggest,
// residual: lsLevel(r.residual_l, r.residual_s),
// source: "AI 整理",
// }));
// riskRecords.value = [...riskRecords.value, ...gen];
// window.$message?.success(`AI 已根据变更内容自动整理形成风险分析记录表(${gen.length} 项),支持手动修改`);
// } catch (e: any) {
// aiFail(e);
// } finally {
// analyzing.value = false;
// }
} ;
const toPssr = ( i : number ) => {
const rec = riskRecords . value [ i ] ;
if ( ! rec || rec . pssr ) return ;
pssrGroups . value = pssrGroups . value . map ( ( g : any , gi : any ) => gi === 0
? { ... g , items : [ ... g . items , { c : ` ${ rec . suggest } (来源:风险分析记录表 · ${ rec . item } ) ` , p : "投用前" } ] }
: g ) ;
updRec ( i , { pssr : true } ) ;
window . $message ? . success ( "建议措施已转为 PSSR 行动项(见「PSSR 检查内容」· 一、风险分析行动项落实)" ) ;
} ;
const updRec = ( i : number , patch : Partial < any > ) => {
riskRecords . value = riskRecords . value . map ( ( x , j ) => ( j === i ? { ... x , ... patch } : x ) ) ;
} ;
const exportAll = ( items : any [ ] , mode : "manual" | "ai" ) => {
riskRecords . value = [ ... riskRecords . value , ... items ] ;
if ( mode === "ai ") {
pluginDrawer . value = false ;
currentPlugin . value = '' ;
window . $message ? . success ( ` AI 已自动整理形成风险分析记录表(新增 ${ items . length } 项),支持手动修改 ` ) ;
} else {
window . $message ? . success ( ` 已选入 ${ items . length } 条风险记录,可继续挑选或返回查看记录表 ` ) ;
}
}
// 使用插件弹框
const usePlugin = ( key : string ) => {
currentPlugin . value = key ;
pluginDrawer . value = true ;
}
// 关闭插件弹框
const closePlugin = ( ) => {
pluginDrawer . value = false ;
}
// 保存草稿
const saveDraft = ( ) => {
emit ( 'save' ) ;
}
// 提交
const submitConfirm = ( ) => {
emit ( 'submit' , props . type ) ;
}
// // 确认标签内容
const confirmTab = ( tab : string , val : boolean ) => {
emit ( 'confirmTab' , tab , val ) ;
}
defineExpose ( { form } )
< / script >
< template >
< div class = "p-4" >
< div class = "space-y-4 scroll" >
< div class = "border p-3" : style = "{ borderRadius: themeStore.themeRadius + 'px' }" >
< div class = "flex flex-wrap items-center gap-2" >
< span class = "flex items-center gap-1.5 text-sm font-semibold text-slate-700" >
< Icon icon = "lucide:shield-alert" class = "size-16px" / >
风险识别工具 ( 按变更等级匹配 )
< / span >
< NTag size = "small" : type = "formInfo?.change_level === 2 ? 'error' : 'default'" >
{ { formInfo ? . change _level === 2 ? '重要变更' : '一般变更' } }
< / NTag >
< span class = "text-xs text-slate-400" >
{ { formInfo ? . change _level === 2 ? '规则: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 = "primary" @click ="usePlugin(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 v-if = "isAiOpen" ghost size="small" type="primary" class="text-xs" @click="aiOrganize" >
< Icon icon = "lucide:sparkles" class = "size-12px" / >
AI 自动整理
< / NButton >
< / div >
< div v-if = "riskRecords.length === 0" class="px-3 py-6 text-center text-xs text-slate-400 " >
尚无风险记录 : 调用上方插件完成分析后逐条 「 入表 」 , 或点击 「 AI 自动整理 」 形成记录表
< / div >
< div v-else class = "overflow-x-auto p-3 ">
< table class = "w-full min-w-[960px] border-collapse text-xs" >
< thead >
< tr >
< th v-for = "h in ['风险项', '场景描述', '固有风险', '现有措施', '建议措施', '剩余风险', '来源', '操作']" :key="h"
class = "border bg-slate-50 px-2 py-1.5 text-[11px] font-medium text-slate-500" > { { h } } < / th >
< / tr >
< / thead >
< tbody >
< tr v-for = "(rec, i) in riskRecords" :key="i" >
< td class = "border px-1.5 py-1 align-top font-medium text-slate-700" >
< Ed :v = "rec.item" @update ="(x) => updRec(i, { item: x })" / >
< / td >
< td class = "border px-1.5 py-1 align-top" > < Ed :v = "rec.scene" @update ="(x) => updRec(i, { scene: x })" / > < / td >
< td class = "border px-1.5 py-1 text-center align-top" >
< NTag v-if = "rec.inherent==='高'" type="error" size="small" class="cursor-pointer" title="点击切换等级" @click="updRec(i, { inherent: lvlNext(rec.inherent) as '高' | '中' | '低' })" >
{{ rec.inherent }}
< / NTag >
< NTag v-if = "rec.inherent==='中'" type="warning" size="small" class="cursor-pointer" title="点击切换等级" @click="updRec(i, { inherent: lvlNext(rec.inherent) as '高' | '中' | '低' })" >
{{ rec.inherent }}
< / NTag >
< NTag v-if = "rec.inherent==='低'" type="success" size="small" class="cursor-pointer" title="点击切换等级" @click="updRec(i, { inherent: lvlNext(rec.inherent) as '高' | '中' | '低' })" >
{{ rec.inherent }}
< / NTag >
< / td >
< td class = "border px-1.5 py-1 align-top" > < Ed :v = "rec.existing" @update ="(x) => updRec(i, { existing: x })" / > < / td >
< td class = "border px-1.5 py-1 align-top" > < Ed :v = "rec.suggest" @update ="(x) => updRec(i, { suggest: x })" / > < / td >
< td class = "border px-1.5 py-1 text-center align-top" >
< NTag v-if = "rec.residual==='高'" type="error" size="small" class="cursor-pointer" title="点击切换等级" @click="updRec(i, { residual: lvlNext(rec.residual) as '高' | '中' | '低' })" >
{{ rec.residual }}
< / NTag >
< NTag v-if = "rec.residual==='中'" type="warning" size="small" class="cursor-pointer" title="点击切换等级" @click="updRec(i, { residual: lvlNext(rec.residual) as '高' | '中' | '低' })" >
{{ rec.residual }}
< / NTag >
< NTag v-if = "rec.residual==='低'" type="success" size="small" class="cursor-pointer" title="点击切换等级" @click="updRec(i, { residual: lvlNext(rec.residual) as '高' | '中' | '低' })" >
{{ rec.residual }}
< / NTag >
< / td >
< td class = "border px-1.5 py-1 text-center align-top " >
< NTag size = "small" type = "info" > { { rec . source } } < / NTag >
< / td >
< td class = "border px-1.5 py-1 text-center align-top" >
< NTag v-if = "rec.pssr" size="small" :color="{ color: '#f5f3ff', textColor: '#7c3aed', borderColor: '#f5f3ff' }" > 已转 PSSR < / NTag >
< NButton text size = "tiny" color = "#7c3aed" v-else class = "hover:underline" @click ="toPssr(i)" > 转 PSSR < / NButton >
< NButton text type = "error" size = "tiny" class = "mt-2" @click ="riskRecords = riskRecords.filter((_, j) => j !== i) "> 删除 < / NButton >
< / td >
< / tr >
< / tbody >
< / table >
< / div >
< / div >
< / div >
< / div >
< Footer
ref = "footerRef"
:type = "props.type"
:currentId = "props.currentId"
:title = "props.title"
:isAiOpen = "props.isAiOpen"
:currentForm = "form"
@save ="saveDraft"
@submit ="submitConfirm "
@confirmTab ="confirmTab"
/ >
<!-- 插件抽屉 -- >
< NDrawer v -model :show = "pluginDrawer" :width = "1200" placement = "right" >
< NDrawerContent >
< template v-if = "currentPlugin === 'HAZOP' " >
< Hazop :isAiOpen = "props.isAiOpen" :currentId = "props.currentId" @close ="closePlugin" @export ="exportAll " / >
< / template >
< template v-else-if = "currentPlugin === 'JSA'" >
< Jsa @close ="closePlugin" @export ="exportAll" / >
< / template >
< template v-else-if = "currentPlugin === 'SCL'" >
< Scl @close ="closePlugin" @export ="exportAll" / >
< / template >
< template v-else-if = "currentPlugin === 'RISK_CHECK'" >
< RiskCheck @close ="closePlugin" / >
< / template >
< / NDrawerContent >
< / NDrawer >
< / template >
< style scoped lang = "scss" >
. scroll {
height : calc ( 100 vh - 393 px ) ;
overflow - y : auto ;
}
<!-- 风险分析记录表 -- >
< script setup lang = "ts" >
import { ref } from 'vue' ;
import { Icon } from '@iconify/vue'
import { useThemeStore } from '@/store/modules/theme' ;
import { AiError , aiRiskCheck } from "@/service/api/ai ";
import Ed from "./Ed .vue" ;
import Hazop from "./hazop .vue" ;
import Jsa from "./jsa .vue" ;
import Scl from "./scl .vue" ;
import RiskCheck from "./riskCheck.vue" ;
import Footer from './footer.vue'
const themeStore = useThemeStore ( ) ;
const props = defineProps ( [ 'typ e' , 'isAiOpen' , 'title' , 'currentId' , 'formInfo' , 'preInfo '] )
const emit = defineEmits ( [ 'save' , 'submit' , 'confirmTab' ] ) ;
const footerRef = ref < any > ( null ) ;
const aiFail = ( e : any ) => {
if ( e instanceof AiError ) window . $message ? . warning ( ` AI 分析失败: ${ e . message } ,可手动填写 ` ) ;
else window . $message ? . warning ( "AI 分析失败,可手动填写" ) ;
} ;
const form = ref < { content : string } > ( {
content : ''
} ) ;
const pluginDone = ref < Record < string , number > > ( { } ) ;
const tools = ref < string [ ] > ( [ "HAZOP" , "JSA" , "SCL" , "RISK_CHECK" ] ) ;
const riskRecords = ref < any > ( [ ] ) ;
// 风险检查表检查项(数据提升到本组件:AI 预填/重新生成随时可写,插件抽屉打开即渲染)
const riskChecks = ref < any [ ] > ( [ ] ) ;
const riskCheckLoading = ref < boolean > ( false ) ;
const pluginDrawer = ref < boolean > ( false ) ;
const currentPlugin = ref < string > ( '' ) ;
const lvlNext = ( v : string ) => ( v === "高" ? "中" : v === "中" ? "低" : "高" ) ;
// AI 生成/重新生成风险检查表(footer 派发;整表生成约 1~3 分钟,42901 闸门满自动重试一次)
const generateRiskCheck = async ( ) => {
const info = props . preInfo ;
if ( ! info ? . description ) {
return window . $message ? . warning ( "缺少变更描述,请先在 [变更预识别] 里面输入变更描述" ) ;
}
if ( riskCheckLoading . value ) return ;
riskCheckLoading . value = true ;
footerRef . value ? . setRegenerateLoading ( true ) ;
try {
for ( let attempt = 0 ; attempt < 2 ; attempt ++ ) {
try {
const res = await aiRiskCheck ( info . description , ( info . compare _rows ? ? [ ] ) . map ( ( x : any ) => ( { item : x . item , before _text : x . before _text , after _text : x . after _text } ) ) ) ;
const items = ( res ? . items ? ? [ ] ) . map ( ( x : any , i : number ) => ( {
id : i + 1 ,
group : x . group || '安全健康' ,
q : x . q ? ? '' ,
ans : x . ans ? ? 'NA' ,
risk : x . risk ? ? '' ,
measure : x . measure ? ? '' ,
} ) ) ;
if ( ! items . length ) {
window . $message ? . warning ( "AI 未生成检查项,可在风险检查表插件中手动维护" ) ;
return ;
}
riskChecks . value = items ;
window . $message ? . success ( ` 风险检查表已由 AI 生成( ${ items . length } 项),打开「风险检查表」工具卡逐项确认 ` ) ;
return ;
} catch ( e : any ) {
if ( e instanceof AiError && e . code === 42901 && attempt === 0 ) {
const waitSec = ( e . data ? . retry _after as number ) ? ? 30 ;
window . $message ? . info ( ` 当前 AI 生成任务较多, ${ waitSec } 秒后自动重试一次… ` ) ;
await new Promise ( ( r ) => setTimeout ( r , waitSec * 1000 ) ) ;
continue ;
}
aiFail ( e ) ;
return ;
}
}
} finally {
riskCheckLoading . value = false ;
footerRef . value ? . setRegenerateLoading ( false ) ;
}
} ;
const onRegenerate = generateRiskCheck ;
// 风险识别分析记录表
const TOOL _CARDS = [
{ key : "HAZOP" as const , name : "HAZOP 分析" , desc : "节点-偏离结构化分析:原因 / 后果 / LS 风险矩阵 / 安全措施" } ,
{ key : "JSA" as const , name : "JSA 分析" , desc : "作业步骤分解:危害因素识别与 LS 风险分级管控" } ,
{ key : "SCL" as const , name : "设备 SCL" , desc : "设备 / 管理检查项目对照标准逐项核查" } ,
{ key : "RISK_CHECK" as const , name : "风险检查表" , desc : "变更通用检查项:是 / 否 / 不涉及逐项判定,输出风险描述与管控措施" } ,
] ;
const pssrGroups = ref < { g : string ; items : any [ ] } [ ] > ( [
{ g : "一、风险分析行动项落实" , items : [
{ c : "联锁跳车电流设定值校核并留存记录" , p : "投用前" } ,
{ c : "电缆与开关容量校核升级完成" , p : "投用前" } ,
{ c : "操作人员培训完成并考核合格" , p : "投用前" } ,
] } ,
{ g : "二、现场设备安装检查确认" , items : [
{ c : "电机安装就位,联轴器对中合格" , p : "投用前" } ,
{ c : "地脚螺栓紧固,减振垫安装到位" , p : "投用前" } ,
{ c : "电缆接线、防爆格兰头密封良好" , p : "投用前" } ,
] } ,
{ g : "三、保护措施确认" , items : [
{ c : "联锁跳车设定值与新电机额定电流匹配" , p : "投用前" } ,
{ c : "电机外壳接地、防护罩完好" , p : "投用前" } ,
{ c : "紧急停机按钮功能试验正常" , p : "投用前" } ,
] } ,
{ g : "四、资料归档确认" , items : [
{ c : "P&ID(图号 PID-R201-03)已更新并受控" , p : "关闭前" } ,
{ c : "设备台账、备件清单已更新" , p : "关闭前" } ,
{ c : "培训签到表、考核记录已归档 ", p : "关闭前" } ,
] } ,
] ) ;
const toPssrAll = ( ) => {
const rest = riskRecords . value . map ( ( x : any , i : any ) => ( x . pssr ? - 1 : i ) ) . filter ( ( i : any ) => i >= 0 ) ;
if ( ! rest . length ) { window . $message ? . info ( "所有建议措施均已转为 PSSR 行动项" ) ; return ; }
const items = rest . map ( ( i : any ) => ( { c : ` ${ riskRecords . value [ i ] . suggest } (来源:风险分析记录表 · ${ riskRecords . value [ i ] . item } ) ` , p : "投用前" } ) ) ;
pssrGroups . value = pssrGroups . value . map ( ( g : any , gi : any ) => gi === 0 ? { ... g , items : [ ... g . items , ... items ] } : g ) ;
riskRecords . value = riskRecords . value . map ( ( x : any ) => ( { ... x , pssr : true } ) ) ;
window . $message ? . success ( ` 已将 ${ rest . length } 条建议措施全部转为 PSSR 行动项(见「PSSR 检查内容」) ` ) ;
} ;
const lsLevel = ( l : number | null , s : number | null ) => {
const rr = ( l ? ? 0 ) * ( s ? ? 0 ) ;
return rr > = 10 ? "高" : rr >= 5 ? "中" : "低" ;
} ;
const aiOrganize = async ( ) => {
// let form = changePreRef.value?.form;
// analyzing.value = true;
// try {
// const res = await aiRiskOrganize(form.description, (form.compare_rows ?? []).map((x: any) => ({ item: x.item, before_text: x.before_text, after_text: x.after_text })));
// const gen = (res?.records ?? []).map((r) => ({
// item: r.item,
// scene: r.scene,
// inherent: lsLevel(r.l, r.s),
// existing: r.existing,
// suggest: r.suggest,
// residual: lsLevel(r.residual_l, r.residual_s),
// source: "AI 整理",
// }));
// riskRecords.value = [...riskRecords.value, ...gen];
// window.$message?.success(`AI 已根据变更内容自动整理形成风险分析记录表(${gen.length} 项),支持手动修改`);
// } catch (e: any) {
// aiFail(e);
// } finally {
// analyzing.value = false;
// }
} ;
const toPssr = ( i : number ) => {
const rec = riskRecords . value [ i ] ;
if ( ! rec || rec . pssr ) return ;
pssrGroups . value = pssrGroups . value . map ( ( g : any , gi : any ) => gi === 0
? { ... g , items : [ ... g . items , { c : ` ${ rec . suggest } (来源:风险分析记录表 · ${ rec . item } ) ` , p : "投用前" } ] }
: g ) ;
updRec ( i , { pssr : true } ) ;
window . $message ? . success ( "建议措施已转为 PSSR 行动项(见「PSSR 检查内容」· 一、风险分析行动项落实)" ) ;
} ;
const updRec = ( i : number , patch : Partial < any > ) => {
riskRecords . value = riskRecords . value . map ( ( x , j ) => ( j === i ? { ... x , ... patch } : x ) ) ;
} ;
const exportAll = ( items : any [ ] , mode : "manual" | "ai" ) => {
riskRecords . value = [ ... riskRecords . value , ... items ] ;
if ( mode === "ai" ) {
pluginDrawer . value = false ;
currentPlugin . value = '' ;
window . $message ? . success ( ` AI 已自动整理形成风险分析记录表(新增 ${ items . length } 项),支持手动修改 ` ) ;
} else {
window . $message ? . success ( ` 已选入 ${ items . length } 条风险记录,可继续挑选或返回查看记录表 ` ) ;
}
}
// 使用插件弹框
const usePlugin = ( key : string ) => {
currentPlugin . value = key ;
pluginDrawer . value = true ;
}
// 关闭插件弹框
const closePlugin = ( ) => {
pluginDrawer . value = false ;
}
// 保存草稿
const saveDraft = ( ) => {
emit ( 'save' ) ;
}
// 提交
const submitConfirm = ( ) => {
emit ( 'submit' , props . type ) ;
}
// // 确认标签内容
const confirmTab = ( tab : string , val : boolean ) => {
emit ( 'confirmTab' , tab , val ) ;
}
defineExpose ( { form , generateRiskCheck } )
< / script >
< template >
< div class = "p-4" >
< div class = "space-y-4 scroll " >
< div class = "border p-3" : style = "{ borderRadius: themeStore.themeRadius + 'px' }" >
< div class = "flex flex-wrap items-center gap-2" >
< span class = "flex items-center gap-1.5 text-sm font-semibold text-slate-700 ">
< Icon icon = "lucide:shield-alert" class = "size-16px" / >
风险识别工具 ( 按变更等级匹配 )
< / span >
< NTag size = "small" : type = "formInfo?.change_level === 2 ? 'error' : 'default'" >
{ { formInfo ? . change _level === 2 ? '重要变更' : '一般变更' } }
< / NTag >
< span class = "text-xs text-slate-400" >
{ { formInfo ? . change _level === 2 ? '规则: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 = "primary" @click ="usePlugin(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 v-if = "isAiOpen" ghost size="small" type="primary" class="text-xs" @click="aiOrganize" >
< Icon icon = "lucide:sparkles" class = "size-12px" / >
AI 自动整理
< / NButton >
< / div >
< div v-if = "riskRecords.length === 0" class="px-3 py-6 text-center text-xs text-slate-400" >
尚无风险记录 : 调用上方插件完成分析后逐条 「 入表 」 , 或点击 「 AI 自动整理 」 形成记录表
< / div >
< div v-else class = "overflow-x-auto p-3" >
< table class = "w-full min-w-[960px] border-collapse text-xs" >
< thead >
< tr >
< th v-for = "h in ['风险项', '场景描述', '固有风险', '现有措施', '建议措施', '剩余风险', '来源', '操作']" :key="h "
class = "border bg-slate-50 px-2 py-1.5 text-[11px] font-medium text-slate-500" > { { h } } < / th >
< / tr >
< / thead >
< tbody >
< tr v-for = "(rec, i) in riskRecords" :key="i" >
< td class = "border px-1.5 py-1 align-top font-medium text-slate-700 " >
< Ed :v = "rec.item" @update ="(x) => updRec(i, { item: x }) " / >
< / td >
< td class = "border px-1.5 py-1 align-top" > < Ed :v = "rec.scene" @update ="(x) => updRec(i, { scene: x })" / > < / td >
< td class = "border px-1.5 py-1 text-center align-top" >
< NTag v-if = "rec.inherent==='高'" type="error" size="small" class="cursor-pointer" title="点击切换等级" @click="updRec(i, { inherent: lvlNext(rec.inherent) as '高' | '中' | '低' })" >
{{ rec.inherent }}
< / NTag >
< NTag v-if = "rec.inherent==='中'" type="warning" size="small" class="cursor-pointer" title="点击切换等级" @click="updRec(i, { inherent: lvlNext(rec.inherent) as '高' | '中' | '低' })" >
{{ rec.inherent }}
< / NTag >
< NTag v-if = "rec.inherent==='低'" type="success" size="small" class="cursor-pointer" title="点击切换等级" @click="updRec(i, { inherent: lvlNext(rec.inherent) as '高' | '中' | '低' })" >
{{ rec.inherent }}
< / NTag >
< / td >
< td class = "border px-1.5 py-1 align-top" > < Ed :v = "rec.existing" @update ="(x) => updRec(i, { existing: x })" / > < / td >
< td class = "border px-1.5 py-1 align-top" > < Ed :v = "rec.suggest" @update ="(x) => updRec(i, { suggest: x })" / > < / td >
< td class = "border px-1.5 py-1 text-center align-top" >
< NTag v-if = "rec.residual==='高'" type="error" size="small" class="cursor-pointer" title="点击切换等级" @click="updRec(i, { residual: lvlNext(rec.residual) as '高' | '中' | '低' })" >
{{ rec.residual } }
< / NTag >
< NTag v-if = "rec.residual==='中'" type="warning" size="small" class="cursor-pointer" title="点击切换等级" @click="updRec(i, { residual: lvlNext(rec.residual) as '高' | '中' | '低' })" >
{{ rec.residual }}
< / NTag >
< NTag v-if = "rec.residual==='低'" type="success" size="small" class="cursor-pointer" title="点击切换等级" @click="updRec(i, { residual: lvlNext(rec.residual) as '高' | '中' | '低' })" >
{{ rec.residual }}
< / NTag >
< / td >
< td class = "border px-1.5 py-1 text-center align-top" >
< NTag size = "small" type = "info" > { { rec . source } } < / NTag >
< / td >
< td class = "border px-1.5 py-1 text-center align-top" >
< NTag v-if = "rec.pssr" size="small" :color="{ color: '#f5f3ff', textColor: '#7c3aed', borderColor: '#f5f3ff' }" > 已转 PSSR < / NTag >
< NButton text size = "tiny" color = "#7c3aed" v-else class = "hover:underline" @click ="toPssr(i)" > 转 PSSR < / NButton >
< NButton text type = "error" size = "tiny" class = "mt-2" @click ="riskRecords = riskRecords.filter((_, j) => j !== i)" > 删除 < / NButton >
< / td >
< / tr >
< / tbody >
< / table >
< / div >
< / div >
< / div >
< / div >
< Footer
ref = "footerRef"
:type = "props.type"
:currentId = "props.currentId"
:title = "props.title"
:isAiOpen = "props.isAiOpen"
:currentForm = "form"
@save ="saveDraft"
@submit ="submitConfirm"
@confirmTab ="confirmTab"
@regenerate ="onRegenerate"
/ >
<!-- 插件抽屉 -- >
< NDrawer v -model :show = "pluginDrawer" :width = "1200" placement = "right" >
< NDrawerContent >
< template v-if = "currentPlugin === 'HAZOP'" >
< Hazop :isAiOpen = "props.isAiOpen" :currentId = "props.currentId" @close ="closePlugin" @export ="exportAll" / >
< / template >
< template v-else-if = "currentPlugin === 'JSA'" >
< Jsa @close ="closePlugin" @export ="exportAll" / >
< / template >
< template v-else-if = "currentPlugin === 'SCL'" >
< Scl @close ="closePlugin" @export ="exportAll" / >
< / template >
< template v-else-if = "currentPlugin === 'RISK_CHECK'" >
< RiskCheck v-model = "riskChecks" :loading="riskCheckLoading" @close="closePlugin" @back="closePlugin" / >
< / template >
< / NDrawerContent >
< / NDrawer >
< / template >
< style scoped lang = "scss" >
. scroll {
height : calc ( 100 vh - 393 px ) ;
overflow - y : auto ;
}
< / style >