hazop插件修改完成

This commit is contained in:
2026-09-11 17:01:35 +08:00
parent 1c4c8e7b62
commit c310ab351e
6 changed files with 580 additions and 44 deletions
+6
View File
@@ -42,6 +42,8 @@ declare module 'vue' {
NCascader: typeof import('naive-ui')['NCascader']
NCheckbox: typeof import('naive-ui')['NCheckbox']
NCheckboxGroup: typeof import('naive-ui')['NCheckboxGroup']
NCollapse: typeof import('naive-ui')['NCollapse']
NCollapseItem: typeof import('naive-ui')['NCollapseItem']
NColorPicker: typeof import('naive-ui')['NColorPicker']
NDataTable: typeof import('naive-ui')['NDataTable']
NDatePicker: typeof import('naive-ui')['NDatePicker']
@@ -59,6 +61,7 @@ declare module 'vue' {
NImage: typeof import('naive-ui')['NImage']
NInput: typeof import('naive-ui')['NInput']
NInputGroup: typeof import('naive-ui')['NInputGroup']
NInputGroupLabel: typeof import('naive-ui')['NInputGroupLabel']
NInputNumber: typeof import('naive-ui')['NInputNumber']
NLoadingBarProvider: typeof import('naive-ui')['NLoadingBarProvider']
NMenu: typeof import('naive-ui')['NMenu']
@@ -132,6 +135,8 @@ declare global {
const NCascader: typeof import('naive-ui')['NCascader']
const NCheckbox: typeof import('naive-ui')['NCheckbox']
const NCheckboxGroup: typeof import('naive-ui')['NCheckboxGroup']
const NCollapse: typeof import('naive-ui')['NCollapse']
const NCollapseItem: typeof import('naive-ui')['NCollapseItem']
const NColorPicker: typeof import('naive-ui')['NColorPicker']
const NDataTable: typeof import('naive-ui')['NDataTable']
const NDatePicker: typeof import('naive-ui')['NDatePicker']
@@ -149,6 +154,7 @@ declare global {
const NImage: typeof import('naive-ui')['NImage']
const NInput: typeof import('naive-ui')['NInput']
const NInputGroup: typeof import('naive-ui')['NInputGroup']
const NInputGroupLabel: typeof import('naive-ui')['NInputGroupLabel']
const NInputNumber: typeof import('naive-ui')['NInputNumber']
const NLoadingBarProvider: typeof import('naive-ui')['NLoadingBarProvider']
const NMenu: typeof import('naive-ui')['NMenu']
@@ -69,7 +69,6 @@ const columns = computed<DataTableColumns<ChangeItem>>(() => {
{
title: '变更前',
key: 'before_text',
width: 200,
render: (row: ChangeItem) =>
props.editable
? h(RichTextInput, {
@@ -86,7 +85,6 @@ const columns = computed<DataTableColumns<ChangeItem>>(() => {
{
title: '变更后',
key: 'after_text',
width: 200,
render: (row: ChangeItem) =>
props.editable
? h(RichTextInput, {
+33 -17
View File
@@ -32,6 +32,16 @@ const regenerateLoading = ref<boolean>(false);
const submitLoading = ref<boolean>(false);
const confirmed = ref<boolean>(false);
// 是否重新生成
const regenerateTypes = ref<Record<string, boolean>>({
'form':false,
'risk':false,
'train':false,
'pssr':false,
'evaluation':false,
'close':false,
});
// 存储每个步骤选中的审核人 user_id
const approvers = ref<Record<number, number[]>>({})
const flowList = ref<any[]>([]);
@@ -95,6 +105,7 @@ const download = async () => {
}
// 重新生成:AI 调用逻辑由各标签页组件实现(按 type 分发),此处仅派发事件
const regenerate = async () => {
regenerateTypes.value[props.type] = true;
emit('regenerate', props.type);
}
// 供父组件控制「重新生成」按钮 loading
@@ -173,25 +184,30 @@ defineExpose({clearApprovers, setRegenerateLoading})
<div>
<div class="flex flex-wrap items-center justify-between gap-2 border-t p-4">
<div class="flex flex-wrap gap-2">
<NButton ghost type="success" :disabled="btnDisabled" :loading="downloadLoading" @click="download">
<Icon icon="material-symbols:download" class="size-16px" />
<span v-if="props.type==='form'">下载变更申请表</span>
<span v-if="props.type==='risk'">下载风险分析记录表</span>
<span v-if="props.type==='train'">下载变更培训内容</span>
<span v-if="props.type==='pssr'">下载PSSR检查内容</span>
<span v-if="props.type==='evaluation'">下载验收评价</span>
<span v-if="props.type==='close'">下载变更关闭确认表</span>
<NButton v-if="isAiOpen && type!=='risk'" ghost type="warning" :disabled="btnDisabled || regenerateLoading" @click="regenerate">
<Icon icon="tdesign:refresh" class="size-14px mr-1" :class="regenerateLoading?'animate-spin':''" />
<template v-if="regenerateLoading">
<span>生成中...</span>
</template>
<template v-else>
<span v-if="type==='form'">{{regenerateTypes[type]?'重新生成变更申请表':'生成变更申请表'}}</span>
<span v-if="type==='train'">{{regenerateTypes[type]?'重新生成变更培训内容':'生成变更培训内容'}}</span>
<span v-if="type==='pssr'">{{regenerateTypes[type]?'重新生成PSSR检查内容':'生成PSSR检查内容'}}</span>
<span v-if="type==='evaluation'">{{regenerateTypes[type]?'重新生成验收评价':'生成验收评价'}}</span>
<span v-if="type==='close'">{{regenerateTypes[type]?'重新生成变更关闭确认表':'生成变更关闭确认表'}}</span>
</template>
</NButton>
<NButton v-if="props.isAiOpen" ghost type="warning" :disabled="btnDisabled" :loading="regenerateLoading" @click="regenerate">
<Icon icon="tdesign:refresh" class="size-14px mr-1" />
<span v-if="props.type==='form'">重新生成变更申请表</span>
<span v-if="props.type==='risk'">重新生成风险分析记录</span>
<span v-if="props.type==='train'">重新生成变更培训内容</span>
<span v-if="props.type==='pssr'">重新生成PSSR检查内容</span>
<span v-if="props.type==='evaluation'">重新生成验收评价</span>
<span v-if="props.type==='close'">重新生成变更关闭确认表</span>
<NButton ghost type="success" :disabled="btnDisabled || downloadLoading" @click="download">
<Icon v-if="downloadLoading" icon="tdesign:refresh" class="size-14px mr-1 animate-spin" />
<Icon v-else icon="material-symbols:download" class="size-16px" />
<span v-if="type==='form'">下载变更申请</span>
<span v-if="type==='risk'">下载风险分析记录表</span>
<span v-if="type==='train'">下载变更培训内容</span>
<span v-if="type==='pssr'">下载PSSR检查内容</span>
<span v-if="type==='evaluation'">下载验收评价</span>
<span v-if="type==='close'">下载变更关闭确认表</span>
</NButton>
<NButton v-if="props.isAiOpen" ghost :type="confirmed ? 'success' : 'primary'" :disabled="btnDisabled" @click="confirmTab">
<NButton v-if="isAiOpen" ghost :type="confirmed ? 'success' : 'primary'" :disabled="btnDisabled" @click="confirmTab">
<Icon icon="ix:success" class="size-16px mr-1" /> {{ confirmed ? '已确认完毕' : '本标签内容确认完毕' }}
</NButton>
</div>
+538 -22
View File
@@ -1,8 +1,8 @@
<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref } from "vue";
import { computed, h, onBeforeUnmount, onMounted, ref } from "vue";
import { Icon } from '@iconify/vue'
import { useThemeStore } from '@/store/modules/theme';
import { useDialog } from 'naive-ui';
import { useDialog, NInput, NPopconfirm, NButton } from 'naive-ui';
import { peopleListApi } from '@/service/api/system';
import { hazopNodesApi, hazopSaveApi } from "@/service/api/plugin";
@@ -13,7 +13,10 @@ const props = defineProps(['isAiOpen','currentId','hazopRiskMatrixConfig'])
const emit = defineEmits(['close','toTable']);
const loading = ref<boolean>(false);
const analyzing = ref(false);
const analyzing = ref<boolean>(false);
const inferring = ref<boolean>(false)
const inferred = ref<boolean>(false)
const keyword = ref<string>('');
// 新增/编辑弹窗
const editModal = ref<boolean>(false);
// 当前弹框标题
@@ -22,7 +25,266 @@ const currentModalTitle = ref<string>('');
const currentModalType = ref<string>('');
// 当前弹框对象
const currentModalObj = ref<string>('');
// 总体工艺描述与节点设置弹窗
const openSetModal = ref<boolean>(false);
// 总体工艺描述
const description = ref<string>('');
const nodeList = ref<any[]>([]);
const name = ref<string>('');
// 节点描述
const node_description = ref<string>('');
// 节点操作方式
const node_way = ref<string>('');
// 节点保护措施
const node_protection = ref<string>('');
// 分析列表弹窗
const openListModal = ref<boolean>(false);
// 分析列表弹框使用节点
const currentNodeListSource = ref<any>([])
const currentNodeList = ref<any>([])
const currentNodeIndex = ref<number>(-1)
// 保存数据
const saveData = () => {
currentNodeListSource.value = JSON.parse(JSON.stringify(currentNodeList.value))
}
// 搜索关键字
const handleSearch = () => {
const kw = keyword.value.trim()
if(!kw){
currentNodeList.value = JSON.parse(JSON.stringify(currentNodeListSource.value))
return
}
currentNodeList.value = currentNodeListSource.value.map((item:any) => {
const objMatched = item.obj_name?.includes(kw)
const matchedDeviations = (item.deviations || []).filter((d:any) =>
d.deviation?.includes(kw)
)
// obj_name 命中 → 保留全部 deviations;否则只保留命中的 deviations
if (objMatched) {
return { ...item } // 浅拷贝,结构不变
}
// 只有 deviation 命中 → 保留该对象,但 deviations 只留下命中的
if (matchedDeviations.length) {
return {
...item, // 保留 id / node_id / obj_name 等
deviations: matchedDeviations // 命中项原样保留,rows 等都在
}
}
return null
}).filter(Boolean)
}
// 点击偏离
const clickDev = (dev:any,nodeIndex:number) => {
currentNodeIndex.value = nodeIndex
currentNodeList.value = nodes.value[nodeIndex]?.objects || []
currentNodeListSource.value = JSON.parse(JSON.stringify(currentNodeList.value))
currentDev.value = dev
}
// 分析列表弹框添加
const addListItem = (type: string,objIndex:number = -1) => {
// 分析对象
if(type === 'obj'){
currentNodeList.value.push({
id: 0,
obj_name: '',
node_id: nodes.value[currentNodeIndex.value]?.id,
deviations: []
})
}
// 偏离
if(type === 'dev'){
currentNodeList.value[objIndex].deviations.push({
id: 0,
object_id: currentNodeList.value[objIndex]?.id,
deviation: '',
rows: []
})
}
currentNodeListSource.value = JSON.parse(JSON.stringify(currentNodeList.value))
}
// 分析列表弹框删除
const removeListItem = (type: string,objIndex:number = -1,devIndex:number = -1) => {
// 分析对象
if(type === 'obj'){
currentNodeList.value.splice(objIndex, 1)
}
// 偏离
if(type === 'dev'){
currentNodeList.value[objIndex].deviations.splice(devIndex, 1)
}
currentNodeListSource.value = JSON.parse(JSON.stringify(currentNodeList.value))
}
// 分析列表弹框复制
const copyListItem = (type: string,objIndex:number = -1,devIndex:number = -1) => {
// 分析对象
if(type === 'obj'){
let list = currentNodeList.value[objIndex]?.deviations?.map((item:any) => ({
...item,
id: 0,
object_id: 0
}));
currentNodeList.value.push({
id: 0,
obj_name: currentNodeList.value[objIndex]?.obj_name,
node_id: currentNodeList.value[objIndex]?.node_id,
deviations: list || []
})
}
// 偏离
if(type === 'dev'){
let list = currentNodeList.value[objIndex]?.deviations[devIndex]?.rows?.map((item:any) => ({
...item,
id: 0,
deviation_id: 0
}));
currentNodeList.value[objIndex].deviations.push({
id: 0,
object_id: currentNodeList.value[objIndex]?.id,
deviation: currentNodeList.value[objIndex]?.deviations[devIndex]?.deviation,
rows: list || []
})
}
currentNodeListSource.value = JSON.parse(JSON.stringify(currentNodeList.value))
}
// 分析列表弹框保存
const listSave = () => {
const hasEmptyObjName = currentNodeList.value.some((item:any) => item.obj_name.trim() === '')
if (hasEmptyObjName) {
window.$message?.warning('分析对象名称不能为空')
}
const hasEmptyDeviation = currentNodeList.value.some((item:any) =>
item.deviations?.some((d:any) => d.deviation.trim() === '')
)
if (hasEmptyDeviation) {
window.$message?.warning('偏离名称不能为空')
}
nodes.value[currentNodeIndex.value].objects = currentNodeList.value
openListModal.value = false
}
const options = [
{
label: '分析当前偏离',
key: 'current'
},
{
label: '分析全部偏离',
key: 'all'
}
]
const columns = ref([
{
title: '序号',
key: 'index',
width: 60,
align: 'center',
render: (_:any, index:number) => {
return h('span',{}, { default:()=>index+1 })
}
},
{
title: '节点名称',
key: 'node_name',
align: 'center',
render: (row:any) => {
return h(NInput,{
size:'small',
type:'textarea',
placeholder:'节点名称',
class:'text-left',
value:row.node_name,
autosize: { minRows: 1 },
clearable: true,
onUpdateValue: (val:string) => {
row.node_name = val
}
})
}
},
{
title: '节点描述',
key: 'description',
align: 'center',
render: (row:any) => {
return h(NInput,{
size:'small',
type:'textarea',
placeholder:'如:设计温度XX、设计压力XX、容积XX',
class:'text-left',
value:row.description,
autosize: { minRows: 1 },
clearable: true,
onUpdateValue: (val:string) => {
row.description = val
}
})
}
},
{
title: '操作',
key: 'action',
width: 60,
align: 'center',
fixed: 'right',
render: (_:any,index:number) => {
return h('div', {class:'flex items-center justify-center gap-[5px]'}, {
default: () => [
h(NPopconfirm,{
// 组件 Props
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
removeItem(index)
},
// 可选:自定义确认/取消按钮的样式
positiveButtonProps: { size: 'tiny', type: 'primary' },
negativeButtonProps: { size: 'tiny' }
},{
// 插槽 (Slots)
trigger: () => h(
NButton,
{
text: true,
size: 'tiny',
type: 'error',
},
{
icon: () => h(Icon, { icon: 'ep:delete',class:'size-14px' }),
default: () => '' // 按钮文字
}
),
default: () => '确定要删除吗?' // 弹出框的提示内容
}),
]
})
}
},
])
// 弹框新增节点
const addNode = () => {
nodeList.value.push({id:0,node_name:name.value,change_id:props.currentId,objects:[]})
}
// 删除节点
const removeItem = (index:number) => {
nodeList.value.splice(index, 1);
}
const setSave = async () => {
const hasEmptyNodeName = nodeList.value.some((item:any) => item.node_name === '');
if(hasEmptyNodeName){
return window.$message?.warning('请填写节点名称');
}
nodes.value = nodes.value.concat(nodeList.value);
openSetModal.value = false;
}
// 处理下拉菜单选择
const handleSelect = (key: string) => {
runAi(key);
}
const selectOptions = computed(() => {
const l = props.hazopRiskMatrixConfig?.probability?.map((item:{key:string,label:string}, index:number) => ({
@@ -230,15 +492,31 @@ const exportWord = () => {
window.$message?.info('还差接口')
}
// AI 分析当前偏离
const runAi = async () => {
if(!currentDev.value){
return window.$message?.warning("请先在左侧选择偏离")
}
// AI 自动分析偏离
const runAi = async (type: string) => {
if(currentDev.value.rows.length){
return window.$message?.info("当前偏离已有分析记录,可手动新增行补充");
}
// 分析当前偏离
if(type==='current'){
if(!currentDev.value){
return window.$message?.warning("请先在左侧选择偏离")
}
}
// 分析全部偏离
if(type==='all'){
}
};
// AI 自动推演
const runInference = async () => {
inferring.value = true
inferred.value = false
setTimeout(() => {
inferring.value = false
inferred.value = true
}, 1000)
}
// 根据l和s计算RR风险等级
const riskLevel = (l: number, s: number) => {
@@ -329,11 +607,12 @@ const leftToggle = (id: string) => {
}
// 打开弹窗
const openModal = (type: string,obj: string, title: string,editName: string = '') => {
const openModal = (type: string,obj: string, title: string,editName: string = '',editDescription: string = '') => {
if(type==='add'){
name.value = '';
}else{
name.value = editName;
node_description.value = editDescription;
}
currentModalType.value = type;
currentModalObj.value = obj;
@@ -348,13 +627,14 @@ const modalSave = async () => {
}
// 新增节点
if(currentModalType.value==='add' && currentModalObj.value==='node'){
nodes.value.push({id:0,node_name:name.value,change_id:props.currentId,objects:[]});
nodes.value.push({id:0,node_name:name.value,change_id:props.currentId,objects:[],description:node_description.value});
window.$message?.success("新增节点成功")
editModal.value = false;
}
// 编辑节点
if(currentModalType.value==='edit' && currentModalObj.value==='node'){
nodes.value[contextTarget.value?.nodeIndex].node_name = name.value;
nodes.value[contextTarget.value?.nodeIndex].description = node_description.value;
window.$message?.success("编辑节点成功")
editModal.value = false;
}
@@ -500,7 +780,7 @@ const handleMenuSelect = (key: string) => {
break
case 'editNode':
// 编辑节点
openModal('edit','node','编辑节点',node?.node_name);
openModal('edit','node','编辑节点',node?.node_name,node?.description);
break
case 'delNode':
// 删除
@@ -556,7 +836,7 @@ const getHazopNodes = async () => {
if(!error){
nodes.value = data.nodes;
selectedWorkers.value = data.users;
currentDev.value = nodes.value[0]?.objects[0]?.deviations?.[0] || null;
clickDev(nodes.value[0]?.objects[0]?.deviations?.[0] || null,0)
}
loading.value = false;
}
@@ -630,17 +910,28 @@ onBeforeUnmount(() => {
</template>
</div>
<div class="ml-auto flex items-center gap-1.5">
<NButton v-if="isAiOpen" size="small" ghost type="primary" class="text-xs" :disabled="recTotal === 0" @click="enterToTable('all')">
<Icon icon="lucide:sparkles" class="size-12px mr-1" /> AI 整理入表
<NButton size="small" ghost type="primary" class="text-xs" @click="openSetModal = true;nodeList = [];description = '';">
<Icon icon="akar-icons:file" class="size-12px mr-1" /> 总体工艺描述与节点设置
</NButton>
<NButton size="small" ghost type="primary" class="text-xs" :disabled="recTotal === 0" @click="enterToTable('all')">
<Icon icon="carbon:document-import" class="size-14px mr-1" /> 全部整理入表
</NButton>
<NButton size="small" ghost type="primary" class="text-xs" @click="exportWord">
<Icon icon="material-symbols:download" class="size-14px" /> 导出 Word
</NButton>
<NButton v-if="isAiOpen" size="small" type="primary" class="text-xs" :disabled="analyzing" @click="runAi">
<Icon v-if="analyzing" icon="ri:loader-4-fill" class="size-14px animate-spin" />
<Icon v-else icon="lucide:sparkles" class="size-12px" />
AI 分析当前偏离
<Icon icon="material-symbols:download" class="size-14px" /> 导出 Word
</NButton>
<NDropdown
v-if="isAiOpen"
trigger="hover"
:options="options"
@select="handleSelect"
>
<NButton size="small" type="primary" class="text-xs" :disabled="analyzing">
<Icon v-if="analyzing" icon="ri:loader-4-fill" class="size-14px animate-spin" />
<Icon v-else icon="lucide:sparkles" class="size-12px" />
AI 自动分析
<Icon icon="ant-design:caret-down-filled" class="size-12px ml-1" />
</NButton>
</NDropdown>
</div>
</div>
@@ -666,6 +957,7 @@ onBeforeUnmount(() => {
<Icon icon="ic:round-plus" class="size-14px" />新增节点
</NButton>
<div class="shrink-0 rounded-md border p-2 scroll">
<p v-if="nodes.length === 0" class="text-center text-xs text-slate-400 py-10">暂无节点数据</p>
<div v-for="(node, nodeIndex) in nodes" :key="nodeIndex">
<div
class="cursor-pointer flex items-center select-none"
@@ -689,7 +981,7 @@ onBeforeUnmount(() => {
</div>
<template v-if="!collapsed[node.id+'_'+obj.id]">
<div v-for="(dev, devIndex) in obj.deviations" :key="devIndex"
@click="currentDev = dev"
@click="clickDev(dev,Number(nodeIndex))"
@contextmenu.prevent="handleContextMenu($event, 'dev', node, obj, dev, Number(nodeIndex), Number(objIndex), Number(devIndex))"
class="cursor-pointer flex w-full items-center ml-2 gap-1.5 rounded-md px-3 py-1.5 text-left text-xs transition"
:class="currentDev?.id === dev.id ? 'bg-[var(--theme-color)] text-white' : 'text-slate-600 hover:bg-slate-100'"
@@ -713,7 +1005,15 @@ onBeforeUnmount(() => {
<NButton size="small" ghost type="primary" class="text-xs" @click="addRow">
<Icon icon="ic:round-plus" class="size-14px" /> 新增行
</NButton>
<span class="text-[11px] text-slate-400">单元格点击即可修改风险等级按风险矩阵L / S 取值实时重算入表将本条选入风险分析记录表</span>
<NButton v-if="currentDev" size="small" type="primary" class="text-xs" @click="openListModal = true">
<Icon icon="ant-design:bars-outlined" class="size-14px" /> 分析列表
</NButton>
<NButton v-if="isAiOpen && currentDev" size="small" type="primary" class="text-xs" :disabled="analyzing" @click="runAi('current')">
<Icon v-if="analyzing" icon="ri:loader-4-fill" class="size-14px animate-spin" />
<Icon v-else icon="lucide:sparkles" class="size-12px" />
AI 分析当前偏离
</NButton>
<span class="text-[11px] text-slate-400">风险等级按风险矩阵L / S 取值实时重算入表将本条选入风险分析记录表</span>
</div>
<div class="scroll">
<table class="w-full border-collapse text-xs">
@@ -881,6 +1181,7 @@ onBeforeUnmount(() => {
>
<div>
<NInput v-model:value="name" placeholder="请输入名称" />
<NInput v-if="currentModalObj==='node'" class="mt-3" v-model:value="node_description" placeholder="请输入节点描述" />
</div>
<template #footer>
<div class="flex justify-end gap-[10px]">
@@ -889,6 +1190,221 @@ onBeforeUnmount(() => {
</div>
</template>
</NModal>
<!-- 总体工艺描述与节点设置弹窗 -->
<NModal
v-model:show="openSetModal"
preset="card"
title="总体工艺描述与节点设置"
:auto-focus="true"
:style="{ width: '1000px', height: 'auto' }"
:segmented="{ content: true, footer: true }"
>
<div>
<div v-if="isAiOpen" class="rounded-lg border border-indigo-100 bg-indigo-50/60 p-3 mb-3">
<div class="flex items-center justify-between">
<div class="flex items-center gap-2 text-sm text-indigo-700">
<Icon icon="bx:bot" class="size-16px" />
{{
inferring
? 'AI正在根据MOC工艺描述推演工艺流程、涉及设备与关键参数…'
: inferred
? 'AI推演完成,已补全空缺内容,可手动修改后保存'
: '可点击右侧按钮,由AI根据工艺描述自动推演需要分析的设备及参数'
}}
</div>
<n-button size="small" type="primary" @click="runInference" :disabled="inferring || !description">
<Icon v-if="inferring" icon="ri:loader-4-fill" class="size-14px animate-spin" />
<Icon v-else icon="lucide:sparkles" class="size-12px" />
{{ inferring ? '推演中…' : 'AI自动推演' }}
</n-button>
</div>
</div>
<p class="mb-1">工艺描述</p>
<NInput
v-model:value="description"
type="textarea"
:rows="4"
clearable
placeholder="简述本节点的工艺流程、物料组成、上下游关系……"
/>
<div class="flex items-center justify-between mt-3">
<label class="block text-sm font-medium text-slate-700">
节点设置
<span class="ml-2 text-xs font-normal text-slate-400">MOC场景通常一个节点即可涉及多个时逐行罗列</span>
</label>
<NButton size="small" dashed type="primary" @click="addNode">
<Icon icon="ic:round-plus" class="size-14px" />新增节点
</NButton>
</div>
<NDataTable
class="mt-1"
size="small"
:single-line="false"
:columns="columns"
:data="nodeList"
:pagination="false"
:max-height="220"
>
<template #empty>
<p class="text-slate-400 text-sm">
暂无节点 点击新增节点手动添加{{isAiOpen?',或点击上方「AI自动推演」自动生成':''}}
</p>
</template>
</NDataTable>
</div>
<template #footer>
<div class="flex justify-end gap-[10px]">
<NButton size="small" @click="openSetModal = false">取消</NButton>
<NButton size="small" type="primary" @click="setSave">确定</NButton>
</div>
</template>
</NModal>
<!-- 分析列表弹窗 -->
<NModal
v-model:show="openListModal"
preset="card"
:auto-focus="true"
:style="{ width: '1200px', height: 'auto' }"
:segmented="{ content: true, footer: true }"
>
<template #header>
<div class="flex items-center justify-between">
<h2 class="flex items-center gap-2 text-base text-lg leading-none font-semibold">
分析列表
<span class="text-xs font-normal text-slate-400">编辑全部分析对象及对应偏离支持增删改与拖拽排序</span>
</h2>
<div class="flex items-center gap-[10px] mr-[10px]">
<NButton size="small" dashed type="primary" @click="addListItem('obj')"><Icon icon="ic:round-plus" class="size-14px" />分析对象</NButton>
<NButton size="small" dashed type="primary">AI设置偏离</NButton>
<NInput
size="small"
v-model:value="keyword"
clearable
placeholder="输入关键字"
@clear="keyword = ''; handleSearch()"
>
<template #suffix>
<NButton text @click="handleSearch">
<Icon icon="akar-icons:search" class="size-14px text-slate-400 ml-1" />
</NButton>
</template>
</NInput>
</div>
</div>
</template>
<template #default>
<div class="flex">
<div class="flex-1 max-h-[60vh] overflow-y-auto border-r mr-3 pr-3">
<div>
<p class="mb-1">本节点流程描述</p>
<NInput v-model:value="node_description" type="textarea" :autosize="{ minRows: 4,}" clearable placeholder="请输入本节点流程描述" />
</div>
<div class="mt-3">
<p class="mb-1">本节点操作方式描述</p>
<NInput v-model:value="node_way" type="textarea" :autosize="{ minRows: 4,}" clearable placeholder="请输入本节点操作方式描述" />
</div>
<div class="mt-3">
<p class="mb-1">本节点现有保护措施罗列</p>
<NInput v-model:value="node_protection" type="textarea" :autosize="{ minRows: 4,}" clearable placeholder="请输入本节点现有保护措施罗列" />
</div>
</div>
<div class="w-500px max-h-[60vh] overflow-y-auto">
<NCollapse v-if="currentNodeList.length" :trigger-areas="['arrow']">
<template v-for="(obj,objIndex) in currentNodeList" :key="objIndex">
<NCollapseItem
:title="obj.obj_name"
:name="objIndex"
:draggable="true"
@dragstart="onDeviceDragStart($event, objIndex)"
@dragover.prevent
@drop="onDeviceDrop($event, objIndex)"
>
<template #header>
<div class="mr-[5px] cursor-grab" :draggable="true">
<Icon icon="gravity-ui:grip" class="size-12px" />
</div>
<NInput v-model:value="obj.obj_name" size="small" clearable placeholder="分析对象名称" style="width: 300px;" @blur="saveData" />
</template>
<template #header-extra>
<div class="flex items-center gap-[10px]">
<NButton size="tiny" dashed @click="addListItem('dev',Number(objIndex))"><Icon icon="ic:round-plus" class="size-12px" />偏离</NButton>
<NButton text @click="copyListItem('obj',Number(objIndex))">
<Icon icon="boxicons:copy" class="size-14px" />
</NButton>
<NPopconfirm
positive-text="确定"
:positiveButtonProps="{ size: 'tiny', type: 'primary' }"
:negativeButtonProps="{ size: 'tiny' }"
@positive-click="removeListItem('obj',Number(objIndex))"
>
<template #trigger>
<NButton text type="error">
<Icon icon="ep:delete" class="size-14px" />
</NButton>
</template>
确定删除吗
</NPopconfirm>
</div>
</template>
<div v-if="obj?.deviations?.length" class="flex flex-col gap-[15px] ml-[40px]">
<div
v-for="(dev, devIndex) in obj.deviations"
:key="devIndex"
class="flex items-center justify-between"
:draggable="true"
@dragstart="onDeviateDragStart($event, objIndex, devIndex)"
@dragover.prevent
@drop="onDeviateDrop($event, objIndex, devIndex)"
>
<div class="flex items-center gap-[5px]">
<div class="mr-[5px] cursor-grab" :draggable="true">
<Icon icon="gravity-ui:grip" class="size-12px" />
</div>
<Split :size="14" class="text-orange-400" />
<NInput v-model:value="dev.deviation" size="small" clearable placeholder="偏离名称" style="width: 300px;" @blur="saveData" />
</div>
<div class="flex items-center gap-[10px]">
<span v-if="dev?.rows?.length" class="rounded-full bg-emerald-100 px-1.5 text-[10px] text-emerald-600">
{{ dev?.rows?.length }}
</span>
<span v-else class="rounded-full bg-slate-100 px-1.5 text-[10px] text-slate-400">待分析</span>
<NButton text @click="copyListItem('dev',Number(objIndex), Number(devIndex))">
<Icon icon="boxicons:copy" class="size-14px" />
</NButton>
<NPopconfirm
positive-text="确定"
:positiveButtonProps="{ size: 'tiny', type: 'primary' }"
:negativeButtonProps="{ size: 'tiny' }"
@positive-click="removeListItem('dev',Number(objIndex), Number(devIndex))"
>
<template #trigger>
<NButton text type="error">
<Icon icon="ep:delete" class="size-14px" />
</NButton>
</template>
确定删除吗
</NPopconfirm>
</div>
</div>
</div>
</NCollapseItem>
</template>
</NCollapse>
<NEmpty v-else description="暂无数据" size="large">
<template #icon>
<Icon icon="lucide:file-search-corner" class="size-48px" />
</template>
</NEmpty>
</div>
</div>
</template>
<template #footer>
<div class="flex justify-end gap-[10px]">
<NButton size="small" @click="openListModal = false">取消</NButton>
<NButton size="small" type="primary" @click="listSave">确定</NButton>
</div>
</template>
</NModal>
</template>
<style scoped lang="scss">
@@ -462,7 +462,7 @@ onMounted(() => {
@confirmTab="confirmTab"
/>
<!-- 插件抽屉 -->
<NDrawer v-model:show="pluginDrawer" width="85%" placement="right">
<NDrawer v-model:show="pluginDrawer" width="100%" placement="right">
<NDrawerContent>
<template v-if="currentPlugin === 'RISK'">
<RiskPre :isAiOpen="props.isAiOpen" :currentId="props.currentId" @close="closePlugin" @toTable="exportAll" />
@@ -199,8 +199,8 @@ onMounted(() => {
</template>
</div>
<div class="ml-auto flex items-center gap-1.5">
<NButton v-if="isAiOpen" size="small" ghost type="primary" class="text-xs" :disabled="riskRecords.length === 0" @click="enterToTable('all',null)">
<Icon icon="lucide:sparkles" class="size-12px mr-1" /> AI 整理入表
<NButton size="small" ghost type="primary" class="text-xs" :disabled="riskRecords.length === 0" @click="enterToTable('all',null)">
<Icon icon="carbon:document-import" class="size-14px mr-1" /> 全部整理入表
</NButton>
<NButton size="small" ghost type="primary" class="text-xs" @click="generateReport">
<Icon icon="material-symbols:download" class="size-14px" /> 导出 Word