295 lines
12 KiB
Vue
295 lines
12 KiB
Vue
<script setup lang="ts">
|
||
import { computed, onMounted, ref } from "vue";
|
||
import { Icon } from '@iconify/vue'
|
||
import { useThemeStore } from '@/store/modules/theme';
|
||
import { peopleListApi } from '@/service/api/system';
|
||
import { riskPreDetailApi, riskPreSaveApi } from '@/service/api/plugin';
|
||
|
||
const themeStore = useThemeStore();
|
||
const props = defineProps(['isAiOpen','currentId'])
|
||
const emit = defineEmits(['close','toTable']);
|
||
// 表格样式
|
||
const TH = "border bg-slate-100 px-2 py-1.5 text-[11px] font-medium text-slate-500";
|
||
const TD = "border px-1.5 py-1 align-top";
|
||
|
||
// 加载状态
|
||
const loading = ref(false);
|
||
// 风险分析记录
|
||
const riskRecords = ref<{id:number,risk_item:string,scene_desc:string,risk_value:string,residual_risk_value:string,safeguards:string,suggestions:string}[]>([]);
|
||
// 风险等级
|
||
const risk_grades = ref<{label:string,value:string}[]>([
|
||
{label:'低',value:'低'},
|
||
{label:'中',value:'中'},
|
||
{label:'高',value:'高'},
|
||
]);
|
||
// 人员列表源数据
|
||
const workers = ref<{user_id:number,user_name:string,org_name?:string}[]>([]);
|
||
// 选中的人员
|
||
const selectedWorkers = ref<{user_id:number,user_name:string,org_name?:string}[]>([]);
|
||
// 选择分析人员弹窗
|
||
const openSelectWorker = ref(false);
|
||
// 搜索分析人员
|
||
const searchWorker = ref("");
|
||
// 分析人员列表
|
||
const workerList = computed(() => workers.value.filter((x:{user_id:number,user_name:string,org_name?:string}) => !searchWorker.value || x.user_name.includes(searchWorker.value)));
|
||
// 选择分析人员
|
||
const selectWorker = (n: {user_id:number,user_name:string,org_name?:string}) => {
|
||
selectedWorkers.value = selectedWorkers.value.includes(n) ? selectedWorkers.value.filter((x:any) => x.user_id !== n.user_id) : [...selectedWorkers.value, n];
|
||
};
|
||
// 匹配是否有人员
|
||
const hasWorker = (id:number) => {
|
||
return selectedWorkers.value.some((item:{user_id:number,user_name:string,org_name?:string}) => item.user_id === id);
|
||
};
|
||
|
||
// 运行 AI 执行风险分析
|
||
const runAi = async () => {
|
||
|
||
};
|
||
|
||
// 导出Word
|
||
const generateReport = async () => {
|
||
window.$message?.warning("还差接口");
|
||
}
|
||
|
||
// 保存
|
||
const save = async () => {
|
||
loading.value = true;
|
||
// 处理选中的人员,移除 org_name 字段
|
||
const workers = (selectedWorkers.value ?? []).map((item: { org_name?:string; user_id: number; user_name: string }) => {
|
||
const { org_name, ...rest } = item;
|
||
return rest;
|
||
});
|
||
const {error} = await riskPreSaveApi(props.currentId, {
|
||
rows: riskRecords.value,
|
||
users: workers,
|
||
});
|
||
if(!error){
|
||
window.$message?.success("保存成功");
|
||
}
|
||
loading.value = false;
|
||
};
|
||
|
||
// AI整理如表、入表
|
||
const enterToTable = (type: string, row: any) => {
|
||
if(type==='single'){
|
||
const enrichedRow:any[] = [];
|
||
enrichedRow.push({
|
||
...row,
|
||
scene: row?.scene_desc || '',
|
||
source: 'RISK',
|
||
item: row?.risk_item || '',
|
||
inherent_level: row?.risk_value || '',
|
||
safeguards: row?.safeguards || '',
|
||
suggestions: row?.suggestions || '',
|
||
residual_level: row?.residual_risk_value || '',
|
||
to_pssr: 0
|
||
});
|
||
emit('toTable',type,enrichedRow);
|
||
}
|
||
if(type==='all'){
|
||
const enrichedRows:any[] = [];
|
||
riskRecords.value.forEach((row:any) => {
|
||
enrichedRows.push({
|
||
...row,
|
||
scene: row?.scene_desc || '',
|
||
source: 'RISK',
|
||
item: row?.risk_item || '',
|
||
inherent_level: row?.risk_value || '',
|
||
safeguards: row?.safeguards || '',
|
||
suggestions: row?.suggestions || '',
|
||
residual_level: row?.residual_risk_value || '',
|
||
to_pssr: 0
|
||
});
|
||
})
|
||
emit('toTable',type,enrichedRows);
|
||
}
|
||
}
|
||
|
||
// 新增风险分析记录
|
||
const addRisk = () => {
|
||
riskRecords.value.push({
|
||
id: 0,
|
||
risk_item: '',
|
||
scene_desc: '',
|
||
risk_value: '',
|
||
residual_risk_value: '',
|
||
safeguards: '',
|
||
suggestions: '',
|
||
})
|
||
}
|
||
// 删除行
|
||
const delRow = (rowIndex: number) => {
|
||
riskRecords.value.splice(rowIndex, 1);
|
||
}
|
||
|
||
// 获取人员列表
|
||
const getWorkers = async () => {
|
||
const {data,error} = await peopleListApi();
|
||
if(!error){
|
||
const transformed = data.map((item:any) => ({
|
||
user_id: item.id,
|
||
user_name: item.realname,
|
||
org_name: item.user_departments.map((d:any) => d.label).join(';')
|
||
}));
|
||
workers.value = transformed;
|
||
}
|
||
}
|
||
|
||
// 获取JSA详情
|
||
const getJsaDetail = async () => {
|
||
loading.value = true;
|
||
const {data,error} = await riskPreDetailApi(props.currentId);
|
||
if(!error){
|
||
riskRecords.value = data?.rows || [];
|
||
selectedWorkers.value = data?.users || [];
|
||
}
|
||
loading.value = false;
|
||
}
|
||
|
||
onMounted(() => {
|
||
getWorkers();
|
||
getJsaDetail();
|
||
})
|
||
</script>
|
||
|
||
<template>
|
||
<NSpin :show="loading" size="small">
|
||
<div class="space-y-3">
|
||
<div
|
||
class="flex flex-wrap items-center gap-2 rounded-lg border border-[#ACC6E5] bg-[#EAF0F9] px-3 py-2"
|
||
:style="{
|
||
'--theme-color': themeStore.themeColor
|
||
}"
|
||
>
|
||
<NButton size="small" ghost type="primary" class="text-xs" @click="emit('close')">
|
||
<Icon icon="basil:arrow-left-outline" class="size-18px" />
|
||
返回工具列表
|
||
</NButton>
|
||
<NButton size="small" ghost type="primary" class="text-xs" @click="save()">保存</NButton>
|
||
<span class="text-sm font-bold text-[var(--theme-color)]">风险预分析插件</span>
|
||
<NTag size="small" type="info">插件</NTag>
|
||
<span class="text-[11px] text-slate-500">{{`共${riskRecords.length} 条风险项`}}</span>
|
||
<div class="relative">
|
||
<NButton size="small" ghost type="primary" class="text-[11px]" @click="openSelectWorker = !openSelectWorker">
|
||
<Icon icon="lucide:users" class="size-12px" /> 分析组成员 · 已选 {{ selectedWorkers.length }} 人
|
||
</NButton>
|
||
<template v-if="openSelectWorker">
|
||
<div class="fixed inset-0 z-10" @click="openSelectWorker = false"></div>
|
||
<div class="absolute right-0 top-8 z-20 w-72 rounded-lg border bg-white p-2 shadow-xl">
|
||
<div class="mb-1.5 flex items-center justify-between">
|
||
<span class="text-xs font-semibold text-slate-700">选择分析组成员(岗位人员库)</span>
|
||
<span class="text-[11px] text-slate-400">已选 {{ selectedWorkers.length }} 人</span>
|
||
</div>
|
||
<NInput v-model:value="searchWorker" size="small" placeholder="搜索人名…" class="mb-1.5 text-xs" />
|
||
<div class="max-h-48 space-y-0.5 overflow-y-auto">
|
||
<div v-for="x in workerList" :key="x.user_id" @click="selectWorker(x)"
|
||
class="cursor-pointer flex w-full items-center min-h-34px gap-2 rounded-md px-2 py-1.5 text-left text-xs"
|
||
:class="hasWorker(x.user_id) ? 'bg-[#DFE9F6]' : 'hover:bg-slate-50'">
|
||
<span class="flex h-3.5 w-3.5 shrink-0 items-center justify-center rounded border text-[10px]"
|
||
:class="hasWorker(x.user_id) ? 'border-[var(--theme-color)] bg-[var(--theme-color)] text-white' : 'border-slate-300'">{{ hasWorker(x.user_id) ? "✓" : "" }}</span>
|
||
<span class="font-medium text-slate-800">{{ x.user_name }}</span>
|
||
<span class="text-[11px] text-slate-400">{{ x.org_name }}</span>
|
||
<NTag v-if="selectedWorkers[0]?.user_id === x.user_id" size="small" type="primary" class="ml-auto">组长</NTag>
|
||
</div>
|
||
</div>
|
||
<div class="mt-1.5 border-t pt-1.5 text-[11px] leading-4 text-slate-400">
|
||
首位选择者默认为组长;成员为选择式(非手写),参与记录自动归属到人,用于「参与风险分析次数」统计采集。
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
<div class="ml-auto flex items-center gap-1.5">
|
||
<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
|
||
</NButton>
|
||
<NButton v-if="isAiOpen" size="small" type="primary" class="text-xs" :disabled="loading" @click="runAi">
|
||
<Icon v-if="loading" icon="ri:loader-4-fill" class="size-14px animate-spin" />
|
||
<Icon v-else icon="lucide:sparkles" class="size-12px" />
|
||
AI 执行风险分析
|
||
</NButton>
|
||
</div>
|
||
</div>
|
||
<div class="space-y-3" :style="{'--theme-color': themeStore.themeColor}">
|
||
<!-- 作业活动分析记录 -->
|
||
<div class="bg-white">
|
||
<div class="flex items-center gap-2 px-3 py-2 text-xs font-semibold text-slate-600 border border-b-0 rounded-tl-lg rounded-tr-lg">
|
||
风险分析记录表
|
||
<NButton size="small" ghost type="primary" class="text-xs ml-2" @click="addRisk">
|
||
<Icon icon="ic:round-plus" class="size-14px" /> 新增风险分析记录
|
||
</NButton>
|
||
<span class="ml-auto text-[11px] font-normal text-slate-400">「入表」将本条选入风险分析记录表</span>
|
||
</div>
|
||
<p v-if="riskRecords.length===0" class="border py-5 text-center text-xs text-slate-400">暂无风险分析记录</p>
|
||
<div v-else class="scroll">
|
||
<table class="w-full">
|
||
<thead>
|
||
<tr>
|
||
<th :class="TH" class="w-80px">操作</th>
|
||
<th :class="TH" class="w-30">风险项</th>
|
||
<th :class="TH" class="w-60">场景描述</th>
|
||
<th :class="TH" class="w-20">固有风险</th>
|
||
<th :class="TH" class="w-50">现有措施</th>
|
||
<th :class="TH" class="w-60">建议措施</th>
|
||
<th :class="TH" class="w-20">剩余风险</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="(row, rowIndex) in riskRecords" :key="rowIndex">
|
||
<td :class="TD">
|
||
<div class="flex items-center justify-center gap-2 px-1 mt-2">
|
||
<NButton text type="primary" size="tiny" @click="enterToTable('single',row)">
|
||
入表
|
||
</NButton>
|
||
<NPopconfirm
|
||
positive-text="确定"
|
||
:positiveButtonProps="{ size: 'tiny' }"
|
||
:negativeButtonProps="{ size: 'tiny' }"
|
||
@positive-click="delRow(Number(rowIndex))"
|
||
>
|
||
<template #trigger>
|
||
<NButton text type="error" size="tiny" title="删除行">
|
||
<Icon icon="ep:delete" class="size-14px" />
|
||
</NButton>
|
||
</template>
|
||
确定删除此行吗?
|
||
</NPopconfirm>
|
||
</div>
|
||
</td>
|
||
<td :class="TD">
|
||
<NInput type="textarea" size="small" v-model:value="row.risk_item" :autosize="{minRows:1}" />
|
||
</td>
|
||
<td :class="TD">
|
||
<NInput type="textarea" size="small" v-model:value="row.scene_desc" :autosize="{minRows:1}" />
|
||
</td>
|
||
<td :class="TD" class="text-center">
|
||
<NSelect v-model:value="row.risk_value" :options="risk_grades" size="small" class="w-full" />
|
||
</td>
|
||
<td :class="TD">
|
||
<NInput type="textarea" size="small" v-model:value="row.safeguards" :autosize="{minRows:1}" />
|
||
</td>
|
||
<td :class="TD">
|
||
<NInput type="textarea" size="small" v-model:value="row.suggestions" :autosize="{minRows:1}" />
|
||
</td>
|
||
<td :class="TD" class="text-center">
|
||
<NSelect v-model:value="row.residual_risk_value" :options="risk_grades" size="small" class="w-full" />
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</NSpin>
|
||
</template>
|
||
|
||
<style scoped lang="scss">
|
||
.scroll{
|
||
height: calc(100vh - 135px);
|
||
overflow-y: auto;
|
||
}
|
||
</style>
|