740 lines
38 KiB
Vue
740 lines
38 KiB
Vue
<script setup lang="ts">
|
||
import { ref, onMounted, computed } from 'vue';
|
||
import { Icon } from '@iconify/vue'
|
||
import { useThemeStore } from '@/store/modules/theme';
|
||
import { useRouterPush } from '@/hooks/common/router';
|
||
import { useRoute } from 'vue-router';
|
||
import { tagTextColor, tagBgColor, tagBorderColor, downloadFileFun } from '@/utils/common';
|
||
import { AiError, aiTypeJudge } from "@/service/api/ai";
|
||
import { systemConfigApi } from '@/service/api/system';
|
||
import { getChangeDetailApi, urgeApi } from '@/service/api/change';
|
||
import previewComponent from '@/components/previewFile/index.vue'
|
||
|
||
const themeStore = useThemeStore();
|
||
const { routerPushByKey } = useRouterPush();
|
||
const route = useRoute();
|
||
|
||
const loading = ref<boolean>(false);
|
||
// 返回键
|
||
const backKey = ref<string>('');
|
||
// 判定说明
|
||
const typeAiLoading = ref<boolean>(false);
|
||
const typeAi = ref<{ type: string; confidence: number; conclusion: string; hit_rules: string[]; exclude_rules: string[]; knowledge_refs: string[] } | null>(null);
|
||
// 当前变更详情
|
||
const currentInfo = ref<any>(null);
|
||
// 当前变更id
|
||
const currentId = ref<number>(0);
|
||
// 是否开启 AI 辅助功能
|
||
const isAiOn = ref<boolean>(false);
|
||
// 变更等级选项
|
||
const levelList = ref<any>([]);
|
||
// 变更类型选项
|
||
const typeList = ref<any>([]);
|
||
// 变更等级
|
||
const level = computed(() => {
|
||
return currentInfo.value?.apply_form?.change_level;
|
||
});
|
||
// 总得分
|
||
const totalScore = computed(() => {
|
||
return currentInfo.value?.apply_form?.level_score?.reduce((sum: number, item: any) => {
|
||
const n = Number(item.score);
|
||
return sum + (Number.isNaN(n) ? 0 : n);
|
||
}, 0);
|
||
});
|
||
// 当前审批所有人员
|
||
const allMembers = computed(() => {
|
||
return currentInfo.value?.approval_flow?.steps?.map((item: any) => item.members).flat();
|
||
});
|
||
// pssr 检查内容
|
||
const pssrlist = ref<{ g: string; type:number; na_flag:number; items: any[] }[]>([
|
||
{ g: "一、风险分析行动项落实", type:1, na_flag:0, items: [] },
|
||
{ g: "二、现场设备安装检查确认", type:2, na_flag:0, items: [] },
|
||
{ g: "三、保护措施确认", type:3, na_flag:0, items: [] },
|
||
{ g: "四、资料归档确认", type:4, na_flag:0, items: [] },
|
||
]);
|
||
// 验收评价
|
||
const acceptList = ref<any>([]);
|
||
// 当前预览文件
|
||
const previewFile = ref<any>(null);
|
||
// 预览文件弹框
|
||
const previewModal = ref<boolean>(false);
|
||
// 等级判断问题
|
||
const questions = ref<any>([
|
||
{
|
||
question_id: 1,
|
||
name: "1. 设备影响",
|
||
note: "不合适的材质将会导致",
|
||
score: 0,
|
||
options: [
|
||
{ label: "对设备带来一定的损害及腐蚀", score: 1 },
|
||
{ label: "对设备带来中等的损害及腐蚀", score: 2 },
|
||
{ label: "对设备带来明显的损害及腐蚀", score: 3 },
|
||
{ label: "对设备带来重大的损害及腐蚀", score: 4 },
|
||
],
|
||
},
|
||
{
|
||
question_id: 2,
|
||
name: "2. 财务影响",
|
||
note: "工艺设计错误将会导致",
|
||
score: 0,
|
||
options: [
|
||
{ label: "一定损失,a<1万人民币", score: 1 },
|
||
{ label: "中等损失,直接损失1万≤a<10万人民币", score: 2 },
|
||
{ label: "大量损失,直接损失10万≤a<100万人民币", score: 3 },
|
||
{ label: "巨额损失,直接损失100万人民币≤a", score: 4 },
|
||
],
|
||
},
|
||
{
|
||
question_id: 3,
|
||
name: "3. 质量影响",
|
||
note: "",
|
||
score: 0,
|
||
options: [
|
||
{ label: "对质量带来一定的风险(非关键工席、非关键设备设施的变更)", score: 1 },
|
||
{ label: "对质量带来中等风险(B类原料替代、生产辅助设备变更)", score: 2 },
|
||
{ label: "对质量带来明显的风险(主要工艺参数、中间体生产工艺变更)", score: 3 },
|
||
{ label: "对质量有很高风险(主要工艺路线、关键工艺参数变更)", score: 4 },
|
||
],
|
||
},
|
||
{
|
||
question_id: 4,
|
||
name: "4. 设计的复杂程度",
|
||
note: "工艺设计包含了",
|
||
score: 0,
|
||
options: [
|
||
{ label: "少量的简单系统(单个设备或单个部件)", score: 1 },
|
||
{ label: "大量的简单系统(多个单类设备或部件)", score: 2 },
|
||
{ label: "至少一个复杂系统(涉及多设备/工艺相应调整)", score: 3 },
|
||
{ label: "大量的复杂系统(需要装置配套改造调整)", score: 4 },
|
||
],
|
||
},
|
||
{
|
||
question_id: 5,
|
||
name: "5. 工艺设计的成熟度",
|
||
note: "",
|
||
score: 0,
|
||
options: [
|
||
{ label: "被频繁使用,且被证明为专利方设计", score: 1 },
|
||
{ label: "被少量使用,或被证明为专利方设计", score: 2 },
|
||
{ label: "被相关工业设计方法或参考文献认可(非专利方)", score: 3 },
|
||
{ label: "新的,未发表的", score: 4 },
|
||
],
|
||
},
|
||
{
|
||
question_id: 6,
|
||
name: "6. 工艺危害",
|
||
note: "物料性质和工艺条件",
|
||
score: 0,
|
||
options: [
|
||
{ label: "无危害(常压、-10℃≤温度<40℃、无毒、无腐蚀不燃)", score: 1 },
|
||
{ label: "可燃/负压/低正压(0~0.3MPa)、40℃≤温度<80℃", score: 2 },
|
||
{ label: "易燃/中压(0.3~1.0MPa)、80℃≤温度<150℃", score: 3 },
|
||
{ label: "极度危害/高压(>1.0MPa)、温度≥150℃", score: 4 },
|
||
],
|
||
},
|
||
]);
|
||
// 判定说明抽屉
|
||
const describeDrawer = ref<boolean>(false);
|
||
const levelDrawer = ref<boolean>(false);
|
||
|
||
const aiFail = (e: any) => {
|
||
if (e instanceof AiError) window.$message?.warning(`AI 分析失败:${e.message},可手动填写`);
|
||
else window.$message?.warning("AI 分析失败,可手动填写");
|
||
};
|
||
// 打开变更类型抽屉
|
||
const openTypeSheet = async () => {
|
||
typeAi.value = null;
|
||
describeDrawer.value = true;
|
||
if (typeAiLoading.value) return;
|
||
typeAiLoading.value = true;
|
||
try {
|
||
const res = await aiTypeJudge(currentInfo.value?.precheck?.description, (currentInfo.value?.precheck?.compare_rows ?? []).map((x: any) => ({ item: x.item, before_text: x.before_text, after_text: x.after_text })));
|
||
typeAi.value = {
|
||
type: res?.type ?? "",
|
||
confidence: res?.confidence ?? 0,
|
||
conclusion: res?.conclusion ?? "",
|
||
hit_rules: res?.hit_rules ?? [],
|
||
exclude_rules: res?.exclude_rules ?? [],
|
||
knowledge_refs: res?.knowledge_refs ?? [],
|
||
};
|
||
} catch (e: any) {
|
||
aiFail(e);
|
||
} finally {
|
||
typeAiLoading.value = false;
|
||
}
|
||
};
|
||
|
||
// 返回
|
||
const back = () => {
|
||
routerPushByKey(backKey.value as any);
|
||
};
|
||
|
||
// 导出
|
||
const exportFile = (type: string) => {
|
||
window.$message?.info('差接口')
|
||
// if(type === 'change'){
|
||
// window.$message?.success('导出变更申请表成功');
|
||
// }
|
||
// if(type === 'risk'){
|
||
// window.$message?.success('导出风险检查行动项表成功');
|
||
// }
|
||
};
|
||
|
||
// 变更附带资料下载/一键下载
|
||
const downloadFile = (file:any) => {
|
||
if(file){
|
||
downloadFileFun(file?.file_url, file?.doc_name || file?.file_name);
|
||
}else{
|
||
window.$message?.info('差接口')
|
||
}
|
||
};
|
||
// 变更附带资料预览
|
||
const openPreview = (file:any) => {
|
||
previewFile.value = file;
|
||
previewModal.value = true;
|
||
}
|
||
|
||
// 催办
|
||
const handleReminder = async (m: any) => {
|
||
if(loading.value) return;
|
||
loading.value = true;
|
||
const {error} = await urgeApi(currentId.value);
|
||
if(!error){
|
||
window.$message?.success('催办成功');
|
||
}
|
||
loading.value = false;
|
||
}
|
||
|
||
// 匹配评分依据
|
||
const matchBasis = (items:any,score: number) => {
|
||
return items.find((item:any) => item.score === score)?.label || '';
|
||
}
|
||
|
||
// 匹配label
|
||
const matchLabel = (list:any,v: number) => {
|
||
return list.find((item:any) => item.value === v) || null;
|
||
}
|
||
|
||
// 获取变更详情
|
||
const getDetail = async () => {
|
||
loading.value = true;
|
||
const {data,error} = await getChangeDetailApi(currentId.value)
|
||
if(!error){
|
||
currentInfo.value = data;
|
||
|
||
// ******解析 PSSR 检查内容********
|
||
// 1. 按 group_type 分组
|
||
const map = new Map();
|
||
currentInfo.value?.pssr?.items?.forEach((item:any) => {
|
||
const t = item.group_type;
|
||
if (!map.has(t)) map.set(t, []);
|
||
map.get(t).push(item);
|
||
});
|
||
|
||
// 2. 生成目标数组
|
||
const result = pssrlist.value.map(({ type, g }:any) => {
|
||
const items = map.get(type) || [];
|
||
// na_flag 取该组第一个元素的 na_flag,无数据时默认为 0
|
||
const na_flag = items.length > 0 ? items[0].na_flag : 0;
|
||
return {
|
||
g,
|
||
type,
|
||
na_flag,
|
||
items
|
||
};
|
||
});
|
||
// 3. 过滤掉不需要的字段
|
||
const omitFields = ['created_at', 'updated_at', 'change_id', 'group_name'];
|
||
const newResult = result.map((group:any) => ({
|
||
...group,
|
||
items: group.items.map((item:any) =>
|
||
Object.fromEntries(Object.entries(item).filter(([k]) => !omitFields.includes(k)))
|
||
)
|
||
}));
|
||
pssrlist.value = newResult;
|
||
|
||
// ******解析 验收评价********
|
||
const omit = (obj:any, fields: string[]) => Object.fromEntries(Object.entries(obj).filter(([k]) => !fields.includes(k)));
|
||
const result2 = currentInfo.value?.accept_form?.items.map((item:any) => omit(item, [
|
||
'created_at', 'updated_at', 'change_id'
|
||
]));
|
||
acceptList.value = result2 || [];
|
||
|
||
// ******解析 等级判断********
|
||
// 建立映射:question_id -> score
|
||
const scoreMap = new Map(
|
||
currentInfo.value?.apply_form?.level_score?.map((item:any) => [item.question_id, item.score])
|
||
);
|
||
// 遍历第一个数组,匹配到就替换 score
|
||
questions.value.forEach((item:any) => {
|
||
if (scoreMap.has(item.question_id)) {
|
||
item.score = scoreMap.get(item.question_id);
|
||
}
|
||
});
|
||
}
|
||
loading.value = false;
|
||
};
|
||
// 获取系统配置
|
||
const getConfig = async () => {
|
||
loading.value = true;
|
||
const {data,error} = await systemConfigApi();
|
||
if(!error){
|
||
// 解析 AI 辅助功能
|
||
const aiAssistGroup = data.find((group:{group_key:string}) => group.group_key === 'ai_assist');
|
||
if (aiAssistGroup) {
|
||
const target = aiAssistGroup?.items?.find((item:any) => item.item_key === 'enabled');
|
||
if (target) {
|
||
isAiOn.value = target.value==1?true:false;
|
||
}
|
||
}
|
||
// 解析 变更类型设置
|
||
const changeTypeGroup = data.find((group:{group_key:string}) => group.group_key === 'change_type');
|
||
if (changeTypeGroup) {
|
||
typeList.value = changeTypeGroup.items.map((item:any) => ({
|
||
...item,
|
||
value: Number(item.value)
|
||
}));
|
||
}
|
||
// 解析 变更等级设置
|
||
const changeLevelGroup = data.find((group:{group_key:string}) => group.group_key === 'change_level');
|
||
if (changeLevelGroup) {
|
||
levelList.value = changeLevelGroup.items.map((item:any) => ({
|
||
...item,
|
||
value: Number(item.value)
|
||
}));
|
||
}
|
||
getDetail();
|
||
}
|
||
loading.value = false;
|
||
}
|
||
|
||
onMounted(() => {
|
||
if(route.query){
|
||
if(route.query.key){
|
||
backKey.value = route.query.key.toString();
|
||
}
|
||
if(route.query.id){
|
||
currentId.value = Number(route.query.id);
|
||
getConfig();
|
||
}
|
||
}
|
||
})
|
||
</script>
|
||
|
||
<template>
|
||
<NSpace vertical :size="16">
|
||
<!-- 高风险 -->
|
||
<NSpin :show="loading" size="small">
|
||
<NCard :bordered="false" size="small" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
|
||
<div
|
||
class="flex flex-wrap items-center gap-3 border border-slate-200 px-3 py-2 mb-3"
|
||
:style="{ borderRadius: themeStore.themeRadius + 'px' }"
|
||
>
|
||
<NButton ghost type="primary" @click="back">
|
||
<Icon icon="ep:back" class="size-16px mr-1" />返回
|
||
</NButton>
|
||
<div class="min-w-0 flex-1 truncate text-base font-bold text-slate-800">{{ currentInfo?.title }}</div>
|
||
<NTag size="small" type="info" class="font-mono text-xs">编号:{{ currentInfo?.change_no }}</NTag>
|
||
</div>
|
||
<div class="space-y-3 scroll">
|
||
<!-- 变更申请表 -->
|
||
<div class="flex items-center justify-between">
|
||
<p
|
||
class="rounded-sm bg-[var(--theme-color)] px-3 py-1 text-xs font-semibold text-white"
|
||
:style="{'--theme-color': themeStore.themeColor}"
|
||
>变更申请表</p>
|
||
<NButton ghost size="small" class="text-xs" @click="exportFile('change')"><Icon icon="material-symbols:download" class="size-16px" />导出变更申请表</NButton>
|
||
</div>
|
||
<div
|
||
class="border border-slate-200"
|
||
:style="{ borderRadius: themeStore.themeRadius + 'px' }"
|
||
>
|
||
<div class="grid border-b grid-cols-[100px_1fr_100px_1fr]">
|
||
<p class="flex items-center justify-end px-3 py-1.6 text-xs text-slate-500 bg-[#fafafc]">变更发起人</p>
|
||
<div class="px-3 py-1.6 text-sm leading-relaxed text-slate-800">{{ currentInfo?.applicant_name }}</div>
|
||
<p class="flex items-center justify-end px-3 py-1.6 text-xs text-slate-500 bg-[#fafafc]">申请单位</p>
|
||
<div class="px-3 py-1.6 text-sm leading-relaxed text-slate-800">{{ currentInfo?.apply_form?.org_name }}</div>
|
||
</div>
|
||
<div class="grid border-b grid-cols-[100px_1fr_100px_1fr]">
|
||
<p class="flex items-center justify-end px-3 py-1.6 text-xs text-slate-500 bg-[#fafafc]">发起日期</p>
|
||
<div class="px-3 py-1.6 text-sm leading-relaxed text-slate-800">{{ currentInfo?.apply_form?.created_at?.slice(0,10) }}</div>
|
||
<p class="flex items-center justify-end px-3 py-1.6 text-xs text-slate-500 bg-[#fafafc]">计划投用日期</p>
|
||
<div class="px-3 py-1.6 text-sm leading-relaxed text-slate-800">{{ currentInfo?.apply_form?.plan_use_date?.slice(0,10) }}</div>
|
||
</div>
|
||
<div class="grid border-b grid-cols-[100px_1fr_100px_1fr]">
|
||
<p class="flex items-center justify-end px-3 py-1.6 text-xs text-slate-500 bg-[#fafafc]">变更时限</p>
|
||
<div class="px-3 py-1.6 text-sm leading-relaxed text-slate-800">{{ currentInfo?.apply_form?.duration_type === 1 ? '永久变更' : '临时变更' }}</div>
|
||
<p class="flex items-center justify-end px-3 py-1.6 text-xs text-slate-500 bg-[#fafafc]">紧急</p>
|
||
<div class="px-3 py-1.6 text-sm leading-relaxed text-slate-800">{{ currentInfo?.apply_form?.urgent === 1 ? '是' : '否' }}</div>
|
||
</div>
|
||
<div class="grid border-b grid-cols-[100px_1fr_100px_1fr]">
|
||
<p class="flex items-center justify-end px-3 py-1.6 text-xs text-slate-500 bg-[#fafafc]">变更等级</p>
|
||
<div class="flex items-center gap-2 px-3 py-1.6 text-sm leading-relaxed text-slate-800">
|
||
<NRadioGroup v-model:value="level">
|
||
<NRadio v-for="item in levelList" :key="item.value" :value="item.value" :disabled="true">{{item.label}}</NRadio>
|
||
</NRadioGroup>
|
||
<NTag type="error" size="small">合计 {{totalScore}} 分</NTag>
|
||
<NButton ghost type="warning" size="small" round class="text-xs" @click="levelDrawer = true">
|
||
<Icon icon="lucide:eye" class="size-12px mr-1" />查看等级判定
|
||
</NButton>
|
||
</div>
|
||
<p class="flex items-center justify-end px-3 py-1.6 text-xs text-slate-500 bg-[#fafafc]">变更类别</p>
|
||
<div class="flex items-center gap-2 px-3 py-1.6 text-sm leading-relaxed text-slate-800">
|
||
{{matchLabel(typeList,currentInfo?.apply_form?.change_type)?.label}}
|
||
<NButton v-if="isAiOn" ghost type="primary" size="small" round class="text-xs" @click="openTypeSheet">
|
||
<Icon icon="lucide:sparkles" class="size-12px mr-1" />判定说明
|
||
</NButton>
|
||
</div>
|
||
</div>
|
||
<div class="grid border-b grid-cols-[100px_1fr]">
|
||
<p class="flex items-center justify-end px-3 py-1.6 text-xs text-slate-500 bg-[#fafafc]">变更目的</p>
|
||
<div class="px-3 py-1.6 text-sm leading-relaxed text-slate-800">
|
||
{{currentInfo?.apply_form?.purposes?.join('、')}}
|
||
</div>
|
||
</div>
|
||
<div class="grid border-b grid-cols-[100px_1fr]">
|
||
<p class="flex items-center justify-end px-3 py-1.6 text-xs text-slate-500 bg-[#fafafc]">预期效果</p>
|
||
<div class="px-3 py-1.6 text-sm leading-relaxed text-slate-800">
|
||
{{currentInfo?.apply_form?.effect}}
|
||
</div>
|
||
</div>
|
||
<div class="grid border-b grid-cols-[100px_1fr]">
|
||
<p class="flex items-center justify-end px-3 py-1.6 text-xs text-slate-500 bg-[#fafafc]">主要变更</p>
|
||
<div class="px-3 py-1.6 text-sm leading-relaxed text-slate-800">
|
||
<p v-for="(main,mIndex) in currentInfo?.apply_form?.main_changes" :key="mIndex">{{Number(mIndex)+1}}.{{main}}</p>
|
||
</div>
|
||
</div>
|
||
<div class="grid border-b grid-cols-[100px_1fr]">
|
||
<p class="flex items-center justify-end px-3 py-1.6 text-xs text-slate-500 bg-[#fafafc]">连带变更</p>
|
||
<div class="px-3 py-1.6 text-sm leading-relaxed text-slate-800">
|
||
<p v-for="(related,rIndex) in currentInfo?.apply_form?.related_changes" :key="rIndex">{{Number(rIndex)+1}}.{{related}}</p>
|
||
</div>
|
||
</div>
|
||
<div class="grid border-b grid-cols-[100px_1fr]">
|
||
<p class="flex items-center justify-end px-3 py-1.6 text-xs text-slate-500 bg-[#fafafc]">风险评估方法</p>
|
||
<div class="px-3 py-1.6 text-sm leading-relaxed text-slate-800">
|
||
{{currentInfo?.apply_form?.risk_tools?.join('、')}}
|
||
</div>
|
||
</div>
|
||
<div class="grid border-b grid-cols-[100px_1fr]">
|
||
<p class="flex items-center justify-end px-3 py-1.6 text-xs text-slate-500 bg-[#fafafc]">所需材料</p>
|
||
<div class="px-3 py-1.6 text-sm leading-relaxed text-slate-800">
|
||
{{currentInfo?.apply_form?.materials}}
|
||
</div>
|
||
</div>
|
||
<div class="grid border-b grid-cols-[100px_1fr]">
|
||
<p class="flex items-center justify-end px-3 py-1.6 text-xs text-slate-500 bg-[#fafafc]">需更新资料</p>
|
||
<div class="px-3 py-1.6 text-sm leading-relaxed text-slate-800">
|
||
{{currentInfo?.apply_form?.update_docs?.join('、')}}
|
||
</div>
|
||
</div>
|
||
<div class="grid grid-cols-[100px_1fr]">
|
||
<p class="flex items-center justify-end px-3 py-1.6 text-xs text-slate-500 bg-[#fafafc]">涉及专业</p>
|
||
<div class="px-3 py-1.6 text-sm leading-relaxed text-slate-800">
|
||
{{currentInfo?.apply_form?.disciplines?.join('、')}}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- 变更附带资料 -->
|
||
<div class="flex items-center justify-between">
|
||
<p
|
||
class="rounded-sm bg-[var(--theme-color)] px-3 py-1 text-xs font-semibold text-white"
|
||
:style="{'--theme-color': themeStore.themeColor}"
|
||
>变更附带资料</p>
|
||
<NButton ghost size="small" class="text-xs" @click="downloadFile(null)"><Icon icon="material-symbols:download" class="size-16px" />一键下载</NButton>
|
||
</div>
|
||
<NTable size="small">
|
||
<thead>
|
||
<tr>
|
||
<th class="w-60px !text-center">序号</th>
|
||
<th>文件名</th>
|
||
<th class="w-100px !text-center">类型</th>
|
||
<th class="w-100px !text-center">文件大小</th>
|
||
<th class="w-120px !text-center">上传时间</th>
|
||
<th class="w-120px !text-center">上传阶段</th>
|
||
<th class="w-90px !text-center">操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="(doc,dIndex) in currentInfo?.apply_form?.attachments" :key="dIndex">
|
||
<td class="text-center">1</td>
|
||
<td>{{doc.file_name}}</td>
|
||
<td class="text-center text-xs">{{doc?.file_type}}</td>
|
||
<td class="text-center text-xs">{{doc?.file_size}}</td>
|
||
<td class="text-center text-xs">{{doc?.created_at?.slice(0,16)}}</td>
|
||
<td class="text-center text-xs">
|
||
<span v-if="doc?.step_no===1">变更申请表</span>
|
||
</td>
|
||
<td>
|
||
<div class="flex items-center justify-center gap-2">
|
||
<NButton text type="primary" class="text-xs" @click="openPreview(doc)">预览</NButton>
|
||
<NButton text type="primary" class="text-xs" @click="downloadFile(doc)">下载</NButton>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</NTable>
|
||
|
||
<!-- 风险分析记录表 -->
|
||
<div class="flex items-center justify-between">
|
||
<p
|
||
class="rounded-sm bg-[var(--theme-color)] px-3 py-1 text-xs font-semibold text-white"
|
||
:style="{'--theme-color': themeStore.themeColor}"
|
||
>风险分析记录表</p>
|
||
<NButton ghost size="small" class="text-xs" @click="exportFile('risk')"><Icon icon="material-symbols:download" class="size-16px" />导出风险分析记录表</NButton>
|
||
</div>
|
||
<NTable size="small">
|
||
<thead>
|
||
<tr>
|
||
<th class="w-60px !text-center">序号</th>
|
||
<th>风险项</th>
|
||
<th>场景描述</th>
|
||
<th class="w-80px !text-center">固有风险</th>
|
||
<th>现有措施</th>
|
||
<th>建议措施</th>
|
||
<th class="w-80px !text-center">剩余风险</th>
|
||
<th class="w-100px !text-center">来源</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="(risk,index) in currentInfo?.risk_records?.records" :key="index">
|
||
<td class="text-center">{{Number(index)+1}}</td>
|
||
<td>{{risk.item}}</td>
|
||
<td>{{risk.scene}}</td>
|
||
<td class="text-center">{{risk.inherent_level}}</td>
|
||
<td>{{risk.safeguards}}</td>
|
||
<td>{{risk.suggestions}}</td>
|
||
<td class="text-center">{{risk.residual_level}}</td>
|
||
<td class="text-center">
|
||
<NTag size="small" type="info" class="text-11px">{{ risk.source==='RISK'?'风险预分析':risk.source==='RISK_CHECK'?'风险检查': risk.source }}</NTag>
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</NTable>
|
||
|
||
<!-- 变更培训内容 -->
|
||
<div>
|
||
<div class="flex items-center gap-2 mb-1.5">
|
||
<Icon icon="lucide:graduation-cap" class="size-16px text-violet-500" />
|
||
<span class="text-sm font-semibold text-slate-700">变更培训内容</span>
|
||
</div>
|
||
<div class="space-y-2.5 border p-3" :style="{borderRadius: themeStore.themeRadius + 'px'}">
|
||
<p>{{currentInfo?.train?.content}}</p>
|
||
</div>
|
||
</div>
|
||
<!-- PSSR 检查内容 -->
|
||
<div>
|
||
<div class="flex items-center gap-2 mb-1.5">
|
||
<Icon icon="lucide:list-checks" class="size-16px text-violet-500" />
|
||
<span class="text-sm font-semibold text-slate-700">PSSR 检查内容</span>
|
||
</div>
|
||
<div class="space-y-2.5 border p-3" :style="{borderRadius: themeStore.themeRadius + 'px'}">
|
||
<div v-for="(pssr,index) in pssrlist" class="border" :key="index" :style="{borderRadius: themeStore.themeRadius + 'px'}">
|
||
<div class="bg-slate-100 px-3 py-1.6 font-medium text-slate-600">{{pssr.g}}</div>
|
||
<div v-for="item in pssr.items" :key="item.id" class="flex items-center border-t px-3 py-1.6">
|
||
<p class="flex-1">{{item.content}}</p>
|
||
<NTag v-if="item.phase===1" size="small" type="info">投用前完成</NTag>
|
||
<NTag v-else size="small" type="info">关闭前完成</NTag>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- 验收评价 -->
|
||
<div>
|
||
<div class="flex items-center gap-2 mb-1.5">
|
||
<Icon icon="ix:success" class="size-16px text-emerald-500" />
|
||
<span class="text-sm font-semibold text-slate-700">验收评价</span>
|
||
</div>
|
||
<div class="border p-3" :style="{borderRadius: themeStore.themeRadius + 'px'}">
|
||
<NTable size="small">
|
||
<thead>
|
||
<tr>
|
||
<th class="w-56">验收项目</th>
|
||
<th>量化验收标准</th>
|
||
<th>验收方式</th>
|
||
<th class="w-46 !text-center">组织层级</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="(accept,index) in acceptList" :key="index">
|
||
<td class="text-xs">{{accept.item}}</td>
|
||
<td class="text-xs">{{accept.standard}}</td>
|
||
<td class="text-xs">{{accept.method}}</td>
|
||
<td class="text-xs text-center">{{accept.org_name}}</td>
|
||
</tr>
|
||
</tbody>
|
||
</NTable>
|
||
</div>
|
||
</div>
|
||
<!-- 审批记录 -->
|
||
<div class="flex items-center justify-between">
|
||
<p
|
||
class="rounded-sm bg-[var(--theme-color)] px-3 py-1 text-xs font-semibold text-white"
|
||
:style="{'--theme-color': themeStore.themeColor}"
|
||
>审批记录</p>
|
||
</div>
|
||
<NTable size="small">
|
||
<thead>
|
||
<tr>
|
||
<th class="w-120px">流程阶段</th>
|
||
<th class="w-120px">操作人</th>
|
||
<th class="w-140px !text-center">操作时间</th>
|
||
<th>操作内容和意见</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="(record,index) in currentInfo?.approval_records" :key="index">
|
||
<td class="text-xs">{{record.action}}</td>
|
||
<td class="text-xs">{{record.user_name}}</td>
|
||
<td class="text-center text-xs">{{record.updated_at?.slice(0,16)}}</td>
|
||
<td class="text-xs">{{record.remark}}</td>
|
||
</tr>
|
||
</tbody>
|
||
</NTable>
|
||
|
||
<!-- 审批流程进度 -->
|
||
<div class="space-y-2">
|
||
<div class="text-sm font-semibold">审批流程{{ matchLabel(levelList,level)?.label? '('+matchLabel(levelList,level)?.label+'变更)':'' }}</div>
|
||
<div class="flex flex-wrap items-center gap-1">
|
||
<template v-for="(n, i) in currentInfo?.approval_flow?.steps" :key="n.name">
|
||
<div class="flex items-center gap-1">
|
||
<Icon v-if="i !== 0" icon="formkit:right" class="size-14px text-slate-300" />
|
||
<div class="rounded-lg border px-3 py-2 text-center"
|
||
:class="n.status === 'PASSED' ? 'border-emerald-300 bg-emerald-50' : n.status === 'PENDING' ? 'border-amber-400 bg-amber-50' : n.status === 'REJECTED' ? 'border-red-300 bg-red-50' :'border-amber-400 bg-amber-50'">
|
||
<p class="text-xs font-medium">
|
||
{{ n.name }}
|
||
</p>
|
||
<div class="flex items-center justify-center gap-1">
|
||
<span v-for="member in n.members" :key="member.id" class="text-xs text-slate-500">{{ member.user_name }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
<div v-if="allMembers?.length" class="rounded-lg border border-amber-200 bg-amber-50/60 p-3">
|
||
<div class="mb-2 flex flex-wrap items-center gap-2 text-xs font-semibold text-amber-700">
|
||
会签进度(全体通过方可流转)
|
||
<span class="font-normal text-amber-600">
|
||
{{ allMembers.filter((m) => m.status === 'PASSED').length }}/{{ allMembers.length }} 已签
|
||
</span>
|
||
</div>
|
||
<div class="flex flex-wrap items-center gap-2">
|
||
<span v-for="m in allMembers" :key="m.id" class="flex items-center gap-1">
|
||
<NTag size="small" :type="m.status === 'PASSED' ? 'success' : m.status === 'REJECTED' ? 'error' : 'default'">
|
||
<template #icon>
|
||
<Icon v-if="m.status === 'PASSED'" icon="ix:success" class="size-14px" />
|
||
<Icon v-if="m.status === 'PENDING'" icon="ant-design:clock-circle-outlined" class="size-12px text-slate-400" />
|
||
<Icon v-if="m.status === 'REJECTED'" icon="ix:close" class="size-12px" />
|
||
</template>
|
||
{{ m.user_name }}
|
||
</NTag>
|
||
<div v-if="m.status === 'PENDING'"
|
||
class="cursor-pointer inline-flex h-6 items-center gap-1 rounded-md border border-amber-300 bg-white px-2 text-[11px] text-amber-700 transition hover:bg-amber-100"
|
||
@click="handleReminder(m)"
|
||
>
|
||
<Icon icon="lucide:bell" class="size-12px" /> 催办
|
||
</div>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
</NCard>
|
||
<!-- 变更类型判定说明(右侧抽屉) -->
|
||
<NDrawer v-model:show="describeDrawer" :width="576" placement="right">
|
||
<NDrawerContent title="变更类型判定说明" closable>
|
||
<NTag v-if="typeAiLoading" type="info" size="small">
|
||
<template #icon>
|
||
<Icon icon="ri:loader-4-fill" class="size-12px animate-spin" />
|
||
</template>
|
||
AI 判定中…
|
||
</NTag>
|
||
<div v-else class="flex items-center gap-2 max-h-[22px] mb-3">
|
||
<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>
|
||
<NTag v-if="typeAi?.type" type="info" size="small">判定:{{ typeAi?.type }}</NTag>
|
||
<NTag v-if="typeAi" size="small">置信度:{{ `${Math.round(typeAi.confidence * 100)}%` }}</NTag>
|
||
</div>
|
||
<div v-if="typeAi" class="space-y-3 text-sm leading-relaxed text-slate-700">
|
||
<p><b>判定结论:</b>{{ typeAi.conclusion }}</p>
|
||
<p><b>判定依据:</b></p>
|
||
<ul class="list-disc space-y-1 pl-5">
|
||
<li v-for="r in typeAi.hit_rules" :key="r">命中规则:{{ r }}</li>
|
||
<li v-for="r in typeAi.exclude_rules" :key="r">排除规则:{{ r }}</li>
|
||
</ul>
|
||
<p v-if="typeAi.knowledge_refs.length"><b>知识库引用:</b>{{ typeAi.knowledge_refs.join('、') }}。</p>
|
||
<p class="rounded-lg bg-amber-50 p-3 text-xs text-amber-700">AI 判定结果仅供参考,最终类型以人工确认为准;如调整为其他类型,系统将记录修改留痕。</p>
|
||
</div>
|
||
</NDrawerContent>
|
||
</NDrawer>
|
||
<!-- 变更等级判定表(右侧抽屉查看) -->
|
||
<NDrawer v-model:show="levelDrawer" :width="576" placement="right">
|
||
<NDrawerContent title="变更等级判定表" closable>
|
||
<div class="space-y-4">
|
||
<div class="flex items-center gap-2">
|
||
<NTag
|
||
size="small"
|
||
:color="{
|
||
color: tagBgColor(matchLabel(levelList,level)?.color),
|
||
textColor: tagTextColor(matchLabel(levelList,level)?.color),
|
||
borderColor: tagBorderColor(matchLabel(levelList,level)?.color),
|
||
}"
|
||
>{{ matchLabel(levelList,level)?.label? matchLabel(levelList,level)?.label+'变更':'' }}</NTag>
|
||
<span class="text-xs text-slate-400">合计得分 <b class="text-[#17407F]">{{ totalScore }}</b> 分(<20 分为一般,≥20 分为重要)</span>
|
||
</div>
|
||
<NTable size="small">
|
||
<thead>
|
||
<tr class="border-b bg-slate-50">
|
||
<th class="w-40">评估维度</th>
|
||
<th class="w-20 !text-center">得分</th>
|
||
<th>评分依据</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="s in questions" :key="s.dim" class="border-b last:border-b-0">
|
||
<td class="font-medium">{{ s.name }}</td>
|
||
<td class="font-bold text-[#17407F] text-center">{{ s.score }}</td>
|
||
<td class="text-xs leading-relaxed text-slate-600">{{ matchBasis(s.options,s.score) }}</td>
|
||
</tr>
|
||
</tbody>
|
||
</NTable>
|
||
</div>
|
||
</NDrawerContent>
|
||
</NDrawer>
|
||
<!-- 附件在线预览弹窗 -->
|
||
<NModal
|
||
v-model:show="previewModal"
|
||
preset="card"
|
||
:auto-focus="false"
|
||
:style="{ width: '80%', height: 'auto' }"
|
||
:segmented="{ content: true, footer: true }"
|
||
>
|
||
<template #header>
|
||
<div class="flex items-center gap-2 text-base font-semibold text-slate-800">
|
||
<Icon icon="akar-icons:file" class="size-18px" /> {{ previewFile?.file_name }}
|
||
</div>
|
||
</template>
|
||
<template #default>
|
||
<div class="space-y-3">
|
||
<div class="flex flex-wrap gap-x-4 gap-y-1 text-xs text-slate-400">
|
||
<span>上传人:{{ previewFile?.uploader_name }}</span>
|
||
<span>上传时间:{{ previewFile?.created_at }}</span>
|
||
<span v-if="previewFile?.version">{{ `版本:${previewFile?.version}(受控)` }}</span>
|
||
</div>
|
||
<previewComponent v-if="previewFile" :previewFile="previewFile" />
|
||
</div>
|
||
</template>
|
||
</NModal>
|
||
</NSpin>
|
||
</NSpace>
|
||
</template>
|
||
|
||
<style scoped lang="scss">
|
||
.scroll {
|
||
height: calc(100vh - 210px);
|
||
overflow-y: auto;
|
||
}
|
||
</style>
|