Files
moc/src/views/my/initiateChange/modules/riskPre.vue
T
2026-09-11 17:01:35 +08:00

295 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>