资料关闭完成

This commit is contained in:
2026-09-17 13:25:39 +08:00
parent 79ab4b9b9f
commit 4b31fbc3f1
13 changed files with 1455 additions and 68 deletions
+1
View File
@@ -227,6 +227,7 @@ const local: App.I18n.Schema = {
details: '详情',
details_closedoc: '资料关闭',
details_index: '变更详情',
details_disposal: '处置申请',
changepassword: '修改密码',
home: '首页',
my: '我的操作台',
+1
View File
@@ -23,6 +23,7 @@ export const views: Record<LastLevelRouteKey, RouteComponent | (() => Promise<Ro
changepassword: () => import("@/views/changePassword/index.vue"),
changestatistics: () => import("@/views/changeStatistics/index.vue"),
details_closedoc: () => import("@/views/details/closeDoc/index.vue"),
details_disposal: () => import("@/views/details/disposal/index.vue"),
details_index: () => import("@/views/details/index/index.vue"),
docs_archives: () => import("@/views/docs/archives/index.vue"),
docs_query: () => import("@/views/docs/query/index.vue"),
+11 -1
View File
@@ -108,13 +108,23 @@ export const generatedRoutes: GeneratedRoute[] = [
path: '/details/closedoc',
component: 'view.details_closedoc',
meta: {
// activeMenu: 'my_mytask',
title: '资料关闭',
i18nKey: 'route.details_closedoc',
constant: true,
hideInMenu: true
}
},
{
name: 'details_disposal',
path: '/details/disposal',
component: 'view.details_disposal',
meta: {
title: '处置申请',
i18nKey: 'route.details_disposal',
constant: true,
hideInMenu: true
}
},
{
name: 'details_index',
path: '/details/index',
+1
View File
@@ -173,6 +173,7 @@ const routeMap: RouteMap = {
"changestatistics": "/changestatistics",
"details": "/details",
"details_closedoc": "/details/closedoc",
"details_disposal": "/details/disposal",
"details_index": "/details/index",
"docs": "/docs",
"docs_archives": "/docs/archives",
+8
View File
@@ -16,3 +16,11 @@ export function deleteFileApi(id: number, fileId: number) {
method: 'delete',
});
}
// 文件列表
export function fileListApi(change_id: number) {
return request({
url: `/api/changes/${change_id}/attachments`,
method: 'get',
});
}
+1 -1
View File
@@ -25,7 +25,7 @@ export function myTasksCloseApi(change_id:number) {
// 资料关闭
export function myTasksCloseMaterialApi(change_id:number,data: any) {
return request({
url: `/api/materials/${change_id}/material-close`,
url: `/api/changes/${change_id}/material-close`,
method: 'post',
data,
});
+2
View File
@@ -87,6 +87,7 @@ declare module 'vue' {
NTooltip: typeof import('naive-ui')['NTooltip']
NTree: typeof import('naive-ui')['NTree']
NTreeSelect: typeof import('naive-ui')['NTreeSelect']
NUpload: typeof import('naive-ui')['NUpload']
NWatermark: typeof import('naive-ui')['NWatermark']
PinToggler: typeof import('./../components/common/pin-toggler.vue')['default']
ReloadButton: typeof import('./../components/common/reload-button.vue')['default']
@@ -180,6 +181,7 @@ declare global {
const NTooltip: typeof import('naive-ui')['NTooltip']
const NTree: typeof import('naive-ui')['NTree']
const NTreeSelect: typeof import('naive-ui')['NTreeSelect']
const NUpload: typeof import('naive-ui')['NUpload']
const NWatermark: typeof import('naive-ui')['NWatermark']
const PinToggler: typeof import('./../components/common/pin-toggler.vue')['default']
const ReloadButton: typeof import('./../components/common/reload-button.vue')['default']
+2
View File
@@ -27,6 +27,7 @@ declare module "@elegant-router/types" {
"changestatistics": "/changestatistics";
"details": "/details";
"details_closedoc": "/details/closedoc";
"details_disposal": "/details/disposal";
"details_index": "/details/index";
"docs": "/docs";
"docs_archives": "/docs/archives";
@@ -123,6 +124,7 @@ declare module "@elegant-router/types" {
| "changepassword"
| "changestatistics"
| "details_closedoc"
| "details_disposal"
| "details_index"
| "docs_archives"
| "docs_query"
+456 -65
View File
@@ -4,9 +4,15 @@ import { Icon } from '@iconify/vue'
import { useThemeStore } from '@/store/modules/theme';
import { useRouterPush } from '@/hooks/common/router';
import { useRoute } from 'vue-router';
import { getColorWithOpacity, tagTextColor, tagBgColor, tagBorderColor } from '@/utils/common';
import { getColorWithOpacity, tagTextColor, tagBgColor, tagBorderColor, formatTimestamp, anyToTimestamp } from '@/utils/common';
import { uploadFileApi, deleteFileApi, fileListApi } from "@/service/api/file";
import { systemConfigApi } from '@/service/api/system';
import { getChangeDetailApi } from '@/service/api/change';
import { myTasksCloseMaterialApi } from '@/service/api/myTasks';
import PDF from 'pdf-vue3'
import { VueOfficeDocx, VueOfficeExcel } from 'vue3-office-preview'
import 'vue3-office-preview/lib/style.css'
const themeStore = useThemeStore();
const { routerPushByKey } = useRouterPush();
@@ -19,43 +25,220 @@ const currentInfo = ref<any>(null);
const currentId = ref<number>(0);
// 是否开启 AI 辅助功能
const isAiOn = ref<boolean>(false);
// 变更等级选项
const levelList = ref<any>([]);
// 变更类型选项
const typeList = ref<any>([]);
// 培训签到上传文件列表
const trainFiles = ref<any>([]);
// pssr上传文件列表
const pssrFiles = ref<any>([]);
// 验收上传文件列表
const acceptFiles = ref<any>([]);
// 实际投用日期
const useDate = ref<number | null>(null);
// 变更材料和实施费(万元)
const price = ref<number | null>(null);
// 验收状态
const resultStatus = ref<number>(0);
// 验收结论
const acceptDesc = ref<string>('');
// 验收时间
const acceptDate = ref<number | null>(null);
// 附件选择列表弹框
const filesModal = ref<boolean>(false);
// 选中的文件列表
const checkedFiles = ref<any>([]);
// 附件列表
const fileList = ref<any>([]);
// 当前从变更附件选择的row下标
const currentRowIndex = ref<number>(-1);
// 当前预览文件
const previewFile = ref<any>(null);
// 预览文件弹框
const previewModal = ref<boolean>(false);
const list = ref<any>([
{isArchiving:0,name:'变更申请表及审批记录',files:[{file_name:'审批记录(含会签意见).pdf',file_type:'pdf',file_url:''}]},
{isArchiving:1,name:'风险分析记录(风险预分析 / HAZOP / FMEA / 风险检查表)',files:[]},
{isArchiving:0,name:'培训签到与考核记录',files:[]},
{isArchiving:0,name:'PSSR 检查记录(含现场照片)',files:[]},
{isArchiving:2,name:'验收报告与运行数据',files:[]},
{isArchiving:0,name:'更新后的 P&ID / 操作规程(受控最新版)',files:[]},
{isArchiving:0,name:'设备 / 联锁台账更新记录',files:[]},
{isArchiving:0,name:'应急预案修订',files:[]},
{id:1,status:null,name:'变更申请表及审批记录',files:[]},
{id:2,status:null,name:'风险分析记录(风险预分析 / HAZOP / FMEA / 风险检查表)',files:[]},
{id:3,status:null,name:'培训签到与考核记录',files:[]},
{id:4,status:null,name:'PSSR 检查记录(含现场照片)',files:[]},
{id:5,status:null,name:'验收报告与运行数据',files:[]},
{id:6,status:null,name:'更新后的 P&ID / 操作规程(受控最新版)',files:[]},
{id:7,status:null,name:'设备 / 联锁台账更新记录',files:[]},
{id:8,status:null,name:'应急预案修订',files:[]},
]);
// 删除文件
const removeFile = (item:any, fileIndex:number) => {
item.files.splice(fileIndex, 1);
}
// 返回
const back = () => {
routerPushByKey('my_mytask');
};
// 上传文件
const onUploadFile = async (type:string = '',files:any,index:number = -1) => {
const file = files?.fileList[0]?.file;
if (!file) return;
const formData = new FormData();
formData.append('file', file);
// 3:培训签到;4:PSSR5:验收
if(type==='3' || type==='4' || type==='5'){
formData.append('step', type);
}else{
formData.append('close_doc_id', currentId.value.toString());
}
loading.value = true;
const {data,error} = await uploadFileApi(currentId.value, formData);
if(!error){
// 是否是本地上传
data.isLocalUpload = true;
console.log(data)
window.$message?.success('上传成功');
if(type==='3'){
trainFiles.value=[data];
}else if(type==='4'){
pssrFiles.value=[data];
}else if(type==='5'){
acceptFiles.value=[data];
}else{
list.value[index].files.push(data);
list.value[index].status = 1;
}
}
loading.value = false;
}
// 下载文件
const downloadFile = (type:string) => {
window.$message?.info('还差接口')
}
// 删除文件(真删)
const removeFileReal = async (type:string, fileIndex:number,index:number = -1) => {
let id = 0;
if(type==='3'){
id = trainFiles.value[fileIndex].id;
}else if(type==='4'){
id = pssrFiles.value[fileIndex].id;
}else if(type==='5'){
id = acceptFiles.value[fileIndex].id;
}else{
id = list.value[index]?.files[fileIndex]?.id;
}
loading.value = true;
const {error} = await deleteFileApi(currentId.value,id);
if(!error){
if(type==='3'){
trainFiles.value.splice(fileIndex, 1);
}else if(type==='4'){
pssrFiles.value.splice(fileIndex, 1);
}else if(type==='5'){
acceptFiles.value.splice(fileIndex, 1);
}else{
list.value[index]?.files.splice(fileIndex, 1);
if(list.value[index]?.files.length === 0){
list.value[index].status = 2;
}
}
window.$message?.success('删除成功');
}
loading.value = false;
}
// 删除文件(假删)
const removeFile = (index:number, fileIndex:number) => {
// 判断是否是本地上传
if(list.value[index]?.files[fileIndex]?.isLocalUpload){
removeFileReal(list.value[index]?.name, fileIndex, index);
}else{
list.value[index]?.files.splice(fileIndex, 1);
if(list.value[index]?.files.length === 0){
list.value[index].status = 2;
}
}
}
// 判断文件类型
const handleFileType = (type:string) => {
if(type){
const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg']
const wordExtensions = ['docx']
const excelExtensions = ['xlsx']
if(wordExtensions.includes(type)){
return 'word'
}
if(excelExtensions.includes(type)){
return 'excel'
}
if(imageExtensions.includes(type)){
return 'image'
}
if(type==='pdf'){
return 'pdf'
}
}
return ''
}
// 打开预览
const preview = (file:any) => {
console.log(file)
previewFile.value = file;
previewModal.value = true;
}
// 打开附件选择
const openFileList = (index:number) => {
currentRowIndex.value = index;
filesModal.value = true;
}
// 选择文件
const chooseFiles = () => {
// 过滤出选中的文件
const ids = new Set(checkedFiles.value);
const result = fileList.value.filter((item:any) => ids.has(item.id));
// 合并文件列表
let files = list.value[currentRowIndex.value]?.files.concat(result);
// 按id去重
const uniqueList = [
...new Map(files.map((item:any) => [item.id, item])).values(),
];
list.value[currentRowIndex.value].files = uniqueList;
if(uniqueList.length > 0){
list.value[currentRowIndex.value].status = 1;
}
filesModal.value = false;
}
// 保存/申请关闭
const submitCloseDoc = async (type:string) => {
if(!useDate.value){
return window.$message?.error('请选择实际投用日期');
}
if(!resultStatus.value){
return window.$message?.error('请选择验收结论');
}
const result = list.value.map(({ id, status }:{id:number,status:number} ) => ({ id, status }));
loading.value = true;
const {error} = await myTasksCloseMaterialApi(currentId.value,{
type:type,
result_status:resultStatus.value,
note:acceptDesc.value,
use_date:formatTimestamp(useDate.value,'yyyy-MM-dd'),
accept_date:formatTimestamp(acceptDate.value,'yyyy-MM-dd'),
cost:price.value,
close_docs:result,
});
if(!error){
if(type==='save'){
window.$message?.success('保存成功');
}
if(type==='close'){
back();
window.$message?.success('申请关闭成功');
}
}
loading.value = false;
}
// 获取变更详情
const getDetail = async () => {
loading.value = true;
const {data,error} = await getChangeDetailApi(currentId.value)
if(!error){
currentInfo.value = data;
console.log(data)
}
loading.value = false;
};
@@ -72,31 +255,25 @@ const getConfig = async () => {
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;
}
// 获取文件列表
const getFileList = async () => {
const {data,error} = await fileListApi(currentId.value);
if(!error){
console.log(data)
fileList.value = data;
}
}
onMounted(async () => {
useDate.value = new Date().getTime();
if(route.query && route.query.id){
if(route.query.id){
currentId.value = Number(route.query.id);
getFileList();
getConfig();
}
}
@@ -105,7 +282,7 @@ onMounted(async () => {
<template>
<NSpace vertical :size="16">
<!-- 本专业高风险 -->
<!-- 资料关闭详情-->
<NSpin :show="loading" size="small">
<NCard :bordered="false" size="small" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
<div
@@ -123,20 +300,104 @@ onMounted(async () => {
<div class="border rounded-md">
<div class="flex items-center gap-x-3 px-4 py-2.5 border-b">
<p class="font-600 text-slate-800 text-sm">投用前确认含变更培训投用前检查</p>
<span class="text-slate-400 text-xs">上传线下纸质表照片 / 扫描件即视为完成确认上传后可查阅下载也可在移动端 / PC 端逐条确认空白模板可下载</span>
<span class="text-slate-400 text-xs">上传线下纸质表照片 / 扫描件即视为完成确认上传后可查阅下载空白模板可下载</span>
</div>
<div class="p-3">
<div class="p-3 space-y-3">
<div class="border rounded-lg py-3 px-4 flex items-center gap-3">
<Icon icon="bi:upload" class="size-18px" :style="{ color: themeStore.themeColor }" />
<Icon icon="bi:upload" class="size-18px" :style="{ color: trainFiles.length?themeStore.otherColor.success:themeStore.themeColor }" />
<div class="flex-1">
<p class="text-slate-800 text-sm font-600">上传 PSSR 检查</p>
<p class="text-slate-400 text-xs mt-0.5 leading-4">PSSR 纸质确认表照片或扫描件上传后视为 PSSR 全项确认完成具备投用条件</p>
<div class="flex items-center gap-2">
<span class="text-slate-800 text-sm font-600">上传培训签到</span>
<p v-if="trainFiles.length" class="py-0.5 px-1.5 text-11px bg-emerald-100 rounded-md text-emerald-700">已上传 · 视为完成确认</p>
</div>
<NButton ghost type="primary" size="small" class="text-xs">
<p class="text-slate-400 text-xs mt-1 leading-4">线下培训签到表照片或扫描件上传后视为培训完成确认签到记录随单归档</p>
<div v-if="trainFiles.length" class="flex flex-wrap gap-2 mt-1">
<div
v-for="(file, fileIndex) in trainFiles"
:key="fileIndex"
class="text-11px flex items-center gap-2 border py-0.5 px-1.5 rounded-md text-[#17407F] border-[#ACC6E5] bg-[#EAF0F9]"
>
<Icon icon="akar-icons:file" class="size-12px" />
{{file.file_name}}
<NButton text type="primary" class="text-11px" @click="preview(file)">预览</NButton>
<NButton text type="primary" class="text-11px" @click="downloadFile('pssr')">下载</NButton>
<NPopconfirm
positive-text="确定"
:positiveButtonProps="{ size: 'tiny' }"
:negativeButtonProps="{ size: 'tiny' }"
@positive-click="removeFileReal('3', Number(fileIndex))"
>
<template #trigger>
<NButton text type="error" class="text-11px">删除</NButton>
</template>
确定删除吗
</NPopconfirm>
</div>
</div>
</div>
<NButton ghost type="primary" size="small" class="text-xs" @click="downloadFile('pssr')">
<Icon icon="akar-icons:download" class="size-14px mr-1" />
下载培训签到表
</NButton>
<NUpload
class="w-auto"
accept="image/*,.pdf,.docx,.xlsx"
:default-upload="false"
:show-file-list="false"
@change="(files) => onUploadFile('3', files)"
>
<NButton type="primary" size="small" class="text-xs" :disabled="loading">
{{trainFiles.length?'重新上传':'上传'}}
</NButton>
</NUpload>
</div>
<div class="border rounded-lg py-3 px-4 flex items-center gap-3">
<Icon icon="bi:upload" class="size-18px" :style="{ color: pssrFiles.length?themeStore.otherColor.success:themeStore.themeColor }" />
<div class="flex-1">
<div class="flex items-center gap-2">
<span class="text-slate-800 text-sm font-600">上传 PSSR 检查</span>
<p v-if="pssrFiles.length" class="py-0.5 px-1.5 text-11px bg-emerald-100 rounded-md text-emerald-700">已上传 · 视为完成确认</p>
</div>
<p class="text-slate-400 text-xs mt-1 leading-4">PSSR 纸质确认表照片或扫描件上传后视为 PSSR 全项确认完成具备投用条件</p>
<div v-if="pssrFiles.length" class="flex flex-wrap gap-2 mt-1">
<div
v-for="(file, fileIndex) in pssrFiles"
:key="fileIndex"
class="text-11px flex items-center gap-2 border py-0.5 px-1.5 rounded-md text-[#17407F] border-[#ACC6E5] bg-[#EAF0F9]"
>
<Icon icon="akar-icons:file" class="size-12px" />
{{file.file_name}}
<NButton text type="primary" class="text-11px" @click="preview(file)">预览</NButton>
<NButton text type="primary" class="text-11px" @click="downloadFile('pssr')">下载</NButton>
<NPopconfirm
positive-text="确定"
:positiveButtonProps="{ size: 'tiny' }"
:negativeButtonProps="{ size: 'tiny' }"
@positive-click="pssrFiles.splice(fileIndex, 1)"
>
<template #trigger>
<NButton text type="error" class="text-11px">删除</NButton>
</template>
确定删除吗
</NPopconfirm>
</div>
</div>
</div>
<NButton ghost type="primary" size="small" class="text-xs" @click="downloadFile('pssr')">
<Icon icon="akar-icons:download" class="size-14px mr-1" />
下载PSSR检查表
</NButton>
<NButton type="primary" size="small" class="text-xs">上传</NButton>
<NUpload
class="w-auto"
accept="image/*,.pdf,.docx,.xlsx"
:default-upload="false"
:show-file-list="false"
@change="(files) => onUploadFile('4', files)"
>
<NButton type="primary" size="small" class="text-xs" :disabled="loading">
{{pssrFiles.length?'重新上传':'上传'}}
</NButton>
</NUpload>
</div>
</div>
</div>
@@ -151,7 +412,7 @@ onMounted(async () => {
<NDatePicker v-model:value="useDate" type="date" size="small" />
<p class="text-xs text-slate-400 mt-1px">默认为当天日期可按实际投用日期调整</p>
</div>
<div class="mt-2.5 rounded-lg border px-3 py-2 text-[11px] leading-5 border-amber-200 bg-amber-50/60 text-amber-700"> 投用提示投用前应完成 PSSR 检查和培训上方入口上传或移动端 / PC 端逐条确认 当前培训签到PSSR 检查尚未确认未完成的变更不得投用</div>
<div class="mt-2.5 rounded-lg border px-3 py-2 text-[11px] leading-5 border-amber-200 bg-amber-50/60 text-amber-700"> 投用提示投用前应完成 PSSR 检查 未完成的变更不得投用</div>
</div>
</div>
<!-- 变更验收 -->
@@ -162,22 +423,71 @@ onMounted(async () => {
</div>
<div class="p-3 space-y-2.5">
<div class="border rounded-lg py-3 px-4 flex items-center gap-3">
<Icon icon="bi:upload" class="size-18px" :style="{ color: themeStore.themeColor }" />
<Icon icon="bi:upload" class="size-18px" :style="{ color: acceptFiles.length?themeStore.otherColor.success:themeStore.themeColor }" />
<div class="flex-1">
<p class="text-slate-800 text-sm font-600">上传验收资料</p>
<p class="text-slate-400 text-xs mt-0.5 leading-4">验收报告运行数据记录等验收证明材料上传后纳入归档清单</p>
<div class="flex items-center gap-2">
<span class="text-slate-800 text-sm font-600">上传验收资料</span>
<p v-if="pssrFiles.length" class="py-0.5 px-1.5 text-11px bg-emerald-100 rounded-md text-emerald-700">已上传 · 视为完成确认</p>
</div>
<NButton ghost type="primary" size="small" class="text-xs">
<p class="text-slate-400 text-xs mt-0.5 leading-4">验收报告运行数据记录等验收证明材料上传后纳入归档清单</p>
<div v-if="acceptFiles.length" class="flex flex-wrap gap-2 mt-1">
<div
v-for="(file, fileIndex) in acceptFiles"
:key="fileIndex"
class="text-11px flex items-center gap-2 border py-0.5 px-1.5 rounded-md text-[#17407F] border-[#ACC6E5] bg-[#EAF0F9]"
>
<Icon icon="akar-icons:file" class="size-12px" />
{{file.file_name}}
<NButton text type="primary" class="text-11px" @click="preview(file)">预览</NButton>
<NButton text type="primary" class="text-11px" @click="downloadFile('pssr')">下载</NButton>
<NPopconfirm
positive-text="确定"
:positiveButtonProps="{ size: 'tiny' }"
:negativeButtonProps="{ size: 'tiny' }"
@positive-click="acceptFiles.splice(fileIndex, 1)"
>
<template #trigger>
<NButton text type="error" class="text-11px">删除</NButton>
</template>
确定删除吗
</NPopconfirm>
</div>
</div>
</div>
<NButton ghost type="primary" size="small" class="text-xs" @click="downloadFile('accept')">
<Icon icon="akar-icons:download" class="size-14px mr-1" />
下载验收表
</NButton>
<NButton type="primary" size="small" class="text-xs">上传</NButton>
<NUpload
class="w-auto"
accept="image/*,.pdf,.docx,.xlsx"
:default-upload="false"
:show-file-list="false"
@change="(files) => onUploadFile('5', files)"
>
<NButton type="primary" size="small" class="text-xs" :disabled="loading">
{{acceptFiles.length?'重新上传':'上传'}}
</NButton>
</NUpload>
</div>
<div class="border rounded-lg py-3 px-4 flex items-center gap-3">
<div class="border rounded-lg py-3 px-4 flex flex-col gap-2">
<div class="flex items-center gap-2">
<p class="text-xs font-600" :style="{ color: themeStore.themeColor }">验收结论互斥选择</p>
<NButton size="small" :type="resultStatus === 3?'primary':'default'" class="text-xs" @click="resultStatus = 3">完全达到</NButton>
<NButton size="small" :type="resultStatus === 2?'primary':'default'" class="text-xs" @click="resultStatus = 2">基本达到</NButton>
<NButton size="small" :type="resultStatus === 1?'primary':'default'" class="text-xs" @click="resultStatus = 1">未达到</NButton>
</div>
<NInput v-model:value="acceptDesc" type="textarea" size="small" :autosize="{ minRows: 2 }" placeholder="请输入验收结论" />
</div>
<div class="border rounded-lg py-3 px-4 flex items-center gap-2">
<p class="text-xs font-600" :style="{ color: themeStore.themeColor }">变更材料和实施费万元</p>
<NInputNumber v-model:value="price" size="small" :min="0" :show-button="false" placeholder="如 12.6" />
<p class="text-xs text-slate-400 mt-1px">非必填 · 填报本次变更材料与实施费用合计用于变更投入与效果统计</p>
</div>
<div class="border rounded-lg py-3 px-4 flex items-center gap-2">
<p class="text-xs font-600" :style="{ color: themeStore.themeColor }">验收时间</p>
<NDatePicker v-model:value="acceptDate" type="date" size="small" />
</div>
</div>
</div>
<!-- 变更关闭资料归档确认 -->
@@ -200,7 +510,7 @@ onMounted(async () => {
<tr v-for="(item, index) in list" :key="index">
<td>
<p>{{item.name}}</p>
<div v-if="item.files.length" class="flex flex-wrap">
<div v-if="item.files.length" class="flex flex-wrap gap-2">
<div
v-for="(file, fileIndex) in item.files"
:key="fileIndex"
@@ -208,13 +518,13 @@ onMounted(async () => {
>
<Icon icon="akar-icons:file" class="size-12px" />
{{file.file_name}}
<NButton text type="primary" class="text-11px">预览</NButton>
<NButton text type="primary" class="text-11px">下载</NButton>
<NButton text type="primary" class="text-11px" @click="preview(file)">预览</NButton>
<NButton text type="primary" class="text-11px" @click="downloadFile('accept')">下载</NButton>
<NPopconfirm
positive-text="确定"
:positiveButtonProps="{ size: 'tiny' }"
:negativeButtonProps="{ size: 'tiny' }"
@positive-click="removeFile(item, fileIndex)"
@positive-click="removeFile(Number(index), Number(fileIndex))"
>
<template #trigger>
<NButton text type="error" class="text-11px">删除</NButton>
@@ -226,25 +536,36 @@ onMounted(async () => {
</td>
<td>
<div class="flex justify-center gap-2">
<NButton size="small" :type="item.isArchiving === 1 ? 'primary' : 'default'" class="text-xs"></NButton>
<NButton size="small" :type="item.isArchiving === 0 ? 'primary' : 'default'" class="text-xs"></NButton>
<NButton size="small" :type="item.isArchiving === 2 ? 'primary' : 'default'" class="text-xs">不涉及</NButton>
<NButton size="small" :type="item.status === 1 ? 'primary' : 'default'" class="text-xs" @click="item.status = 1"></NButton>
<NButton size="small" :type="item.status === 2 ? 'primary' : 'default'" class="text-xs" @click="item.status = 2"></NButton>
<NButton size="small" :type="item.status === 0 ? 'primary' : 'default'" class="text-xs" @click="item.status = 0">不涉及</NButton>
</div>
</td>
<td>
<div v-if="item.isArchiving === 0" class="flex justify-center gap-2">
<NButton size="small" class="text-xs">从变更附件选择</NButton>
<NButton size="small" type="primary" ghost class="text-xs">本地上传</NButton>
<div v-if="item.status === 2" class="flex justify-center gap-2">
<NButton size="small" class="text-xs" @click="openFileList(Number(index))">从变更附件选择</NButton>
<NUpload
class="w-auto"
accept="image/*,.pdf,.docx,.xlsx"
:default-upload="false"
:show-file-list="false"
@change="(files) => onUploadFile(item.name, files, Number(index))"
>
<NButton type="primary" size="small" ghost class="text-xs" :disabled="loading">
本地上传
</NButton>
</NUpload>
</div>
<div v-if="item.isArchiving === 1" class="flex justify-end gap-2">
<div v-if="item.status === 1" class="flex justify-end gap-2">
<NTag size="small" type="success" class="text-xs">
<template #icon>
<Icon icon="akar-icons:circle-check" class="size-12px" />
</template>
已归档1件
<span v-if="item.files.length">已归档{{item.files.length}}</span>
<span v-else>已归档</span>
</NTag>
</div>
<div v-if="item.isArchiving === 2" class="flex justify-end gap-2">
<div v-if="item.status === 0" class="flex justify-end gap-2">
<span class="text-xs text-slate-400">无需处理</span>
</div>
</td>
@@ -258,10 +579,80 @@ onMounted(async () => {
<p class="text-xs text-slate-400">待归档 5 培训签到与考核记录PSSR 检查记录含现场照片验收报告与运行数据更新后的 P&ID / 操作规程受控最新版设备 / 联锁台账更新记录</p>
<div class="flex gap-2">
<NButton ghost size="small" @click="back">取消</NButton>
<NButton ghost type="primary" size="small">保存</NButton>
<NButton type="primary" size="small">申请关闭</NButton>
<NButton ghost type="primary" size="small" @click="submitCloseDoc('save')">保存</NButton>
<NButton type="primary" size="small" @click="submitCloseDoc('close')">申请关闭</NButton>
</div>
</div>
<!-- 附件选择弹框 -->
<NModal
v-model:show="filesModal"
preset="card"
title="从变更附件选择"
:auto-focus="false"
:style="{ width: '450px', height: 'auto' }"
:segmented="{ content: true, footer: true }"
>
<div class="max-h-60vh overflow-y-auto">
<NCheckboxGroup v-model:value="checkedFiles">
<div class="space-y-2">
<div v-for="item in fileList" :key="item.id">
<NCheckbox :value="item.id" :label="item.file_name" />
</div>
</div>
</NCheckboxGroup>
</div>
<template #footer>
<div class="flex justify-end gap-[10px]">
<NButton size="small" @click="filesModal = false">取消</NButton>
<NButton size="small" type="primary" @click="chooseFiles">确定选择</NButton>
</div>
</template>
</NModal>
<!-- 附件在线预览弹窗 -->
<NModal
v-model:show="previewModal"
preset="card"
:auto-focus="false"
:style="{ width: '1100px', 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>
<div v-if="previewFile" class="max-h-[70vh] overflow-x-auto rounded-lg border">
<!-- 图片预览 -->
<template v-if="handleFileType(previewFile?.file_type) === 'image'">
<div class="flex items-center justify-center py-2">
<NImage :src="previewFile?.file_url" />
</div>
</template>
<!-- Word 预览 -->
<template v-if="handleFileType(previewFile?.file_type) === 'word'">
<VueOfficeDocx :src="previewFile?.file_url" class="w-full" />
</template>
<!-- Excel 预览 -->
<template v-if="handleFileType(previewFile?.file_type) === 'excel'">
<VueOfficeExcel :src="previewFile?.file_url" class="w-full" />
</template>
<!-- PDF 预览 -->
<template v-if="handleFileType(previewFile?.file_type) === 'pdf'">
<div class="h-[68vh]">
<PDF :src="previewFile?.file_url" class="w-full h-full" />
</div>
</template>
</div>
</div>
</template>
</NModal>
</NCard>
</NSpin>
</NSpace>
+970
View File
@@ -0,0 +1,970 @@
<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 { getColorWithOpacity, tagTextColor, tagBgColor, tagBorderColor } from '@/utils/common';
import { AiError, aiTypeJudge } from "@/service/api/ai";
import { systemConfigApi } from '@/service/api/system';
import { getChangeDetailApi } from '@/service/api/change';
const themeStore = useThemeStore();
const { routerPushByKey } = useRouterPush();
const route = useRoute();
const loading = ref<boolean>(false);
// 判定说明
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);
// 当前变更状态
const currentStatus = ref<string>('');
// 是否开启 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 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 role = ref<string>("applicant");
// 判定说明抽屉
const describeDrawer = ref<boolean>(false);
const levelDrawer = ref<boolean>(false);
// ai 标注抽屉
const aiDrawer = ref<boolean>(false);
const mandClosed = ref<boolean>(false);
const confirmClose = () => {
mandClosed.value = true;
window.$message?.success('强制合规项已确认闭环,操作已留痕');
}
const conflictHide = ref<boolean>(false);
// 关键参数对比(静态示例作 fallback,AI 审批辅助完成后回填)
const paramDiff = ref([
{ k: "反应温度", before: "80℃", after: "90℃", over: false, note: "仍在溶剂沸点与物料分解温度的安全裕度内" },
{ k: "保温时间", before: "8 h", after: "4 h", over: false, note: "终点判断窗口变窄,须修订操作规程" },
{ k: "DCS 温度高报警", before: "85℃", after: "95℃", over: true, note: "超出原报警裕度,须重新整定验证" },
{ k: "联锁高高报", before: "90℃", after: "100℃", over: true, note: "接近设备设计温度上限,投用前须完成联锁测试" },
{ k: "未知杂质含量", before: "0.05%", after: "0.12%", over: false, note: "结构未定,须纳入日常分析计划" },
]);
const aiRead = ref<boolean>(false);
const roleName = ref<string>('张工艺');
const suggs = ref([
{ id: 1, text: "将未知杂质含量纳入日常分析计划(每周 1 次,连续 8 周)", st: "待定" },
{ id: 2, text: "修订操作规程并补充超温应急处置卡后再投用", st: "待定" },
]);
const opinion = ref<string>("");
// 已阅 AI 建议
const readSuggest = () => {
aiRead.value = true;
window.$message?.success(`${roleName} 已阅 AI 建议,时间与操作已写入审批留痕`);
}
// 采纳为行动项
const acceptSuggest = (s: any) => {
suggs.value = suggs.value.map((x:any) => (x.id === s.id ? { ...x, st: '已采纳' } : x));
opinion.value = (opinion.value ? `${opinion.value}\n` : '') + `采纳 AI 建议:${s.text}`;
window.$message?.success('已采纳,自动生成 PSSR 行动项(来源:AI 建议),并纳入审批意见');
}
// 不采纳 AI 建议
const rejectSuggest = (s: any) => {
suggs.value = suggs.value.map((x:any) => (x.id === s.id ? { ...x, st: '不采纳' } : x));
window.$message?.info('不采纳理由「现有分析频次已覆盖风险」已记录留痕');
}
// 查看归档包
const viewArchive = (name: string) => {
window.$message?.info(`新窗口打开 ${name} 归档包,不影响当前审批`);
}
// AI 审批辅助(抽屉打开时触发,异步任务轮询;失败保留静态示例,不阻塞页面)
const aiAssistLoading = ref<boolean>(false);
const aiAssistStep = ref<string>('');
const aiAssistProgress = ref<number>(0);
// 强制合规描述(静态示例作 fallback)
const mandText = ref<string>('本次变更涉及 DCS 联锁设定值修改,依据 AQ/T 3034《化工企业工艺安全管理实施导则》,投用前须完成联锁测试并留存记录。');
const mandItem = ref<string>('联锁测试记录须在投用前完成并上传归档');
// 冲突提示
const conflictText = ref<string>('涉及设备 R-101 关联变更 MOC-2026-0038「机封冲洗方案调整」正在审批中,请确认施工时序与隔离方案无冲突。');
// 属地影响摘要
const deptRows = ref([
{ label: '涉及单元:', text: '一车间 R-201 反应釜及配套 DCS 回路,不影响相邻单元。' },
{ label: '现行规程影响:', text: '操作规程第 4.2 节(报警处置)需同步修订,变更关闭前完成受控换版。' },
{ label: '交接班提示:', text: '投用后首个班次需向班组交代新报警值与处置卡位置,建议纳入班前会内容。' },
]);
// 参考信息(相似变更 / 历史经验)
const similarText = ref<string>('相似变更 MOC-2025-0112「酯化反应温度上调」投用后运行正常,无异常记录');
const experienceText = ref<string>('历史经验:温度类变更投用后首周超温报警占比约 38%,建议关注投用首周报警趋势');
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('my_mytask');
};
// 导出
const exportFile = (type: string) => {
window.$message?.info('还差接口')
if(type === 'change'){
window.$message?.success('导出变更申请表成功');
}
if(type === 'risk'){
window.$message?.success('导出风险检查行动项表成功');
}
};
// 变更附带资料下载/一键下载
const downloadFile = (type: string) => {
window.$message?.info('还差接口')
if(type === 'all'){
window.$message?.success('打包下载全部附件');
}else{
window.$message?.success('下载当前附件成功');
}
};
// 变更附带资料预览
const previewFile = () => {
window.$message?.info('还没导入插件');
}
// 催办
const handleReminder = (m: any) => {
window.$message?.info('还差接口')
window.$message?.success(`已催办 ${m.user_name}(系统消息 + 企业微信 + 短信),连续 2 次未响应将上报部门负责人`)
}
// 匹配评分依据
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;
console.log(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(async () => {
if(route.query && route.query.id){
if(route.query.id){
currentId.value = Number(route.query.id);
getConfig();
}
if(route.query.status){
currentStatus.value = route.query.status.toString();
}
}
})
</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>
<!-- 审批中 -->
<NButton
v-if="currentStatus === 'APPROVING' && isAiOn"
ghost
size="small"
type="primary"
class="text-xs"
@click="aiDrawer = !aiDrawer"
>
<Icon icon="lucide:sparkles" class="size-12px mr-1" /> AI 标注
</NButton>
</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('all')"><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="previewFile">预览</NButton>
<NButton text type="primary" class="text-xs" @click="downloadFile('single')">下载</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> &lt;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>
<!-- AI 审批辅助右侧抽屉查看 -->
<NDrawer v-model:show="aiDrawer" :width="576" placement="right">
<NDrawerContent closable>
<template #header>
<span class="flex items-center gap-1.5 text-sm font-bold text-slate-700">
<Icon icon="lucide:sparkles" class="size-16px text-violet-500 mr-1" />
AI 审批辅助抽屉式正文全窗口阅读
</span>
</template>
<div class="space-y-4">
<!-- AI 分析进度异步任务轮询 -->
<div v-if="aiAssistLoading" class="rounded-lg border border-violet-200 bg-violet-50 px-3 py-2.5 text-xs text-violet-700">
<div class="flex items-center gap-2">
<Icon icon="eos-icons:loading" class="size-14px" />
<span>{{ aiAssistStep || 'AI 分析中…' }}{{ aiAssistProgress }}%</span>
</div>
<NProgress type="line" :percentage="aiAssistProgress" :show-indicator="false" class="mt-1.5" processing />
</div>
<!-- 合规警告条强制合规项不可关闭闭环后转绿 -->
<div
class="px-3 py-2.5 text-sm"
:class="mandClosed ? 'text-[var(--success-color)] bg-[var(--success-bg-color)]' : 'text-[var(--warning-color)] bg-[var(--warning-bg-color)]'"
:style="{
borderRadius:themeStore.themeRadius+'px',
'--success-color':themeStore.otherColor.success,
'--success-bg-color':getColorWithOpacity(themeStore.otherColor.success,0.1),
'--warning-color':themeStore.otherColor.warning,
'--warning-bg-color':getColorWithOpacity(themeStore.otherColor.warning,0.1)
}"
>
<div class="flex items-center gap-2">
<Icon icon="proicons:alert-triangle" class="size-16px" />
<span class="font-semibold">强制合规</span>
<span class="ml-auto shrink-0">
<NTag v-if="mandClosed" size="small" type="success">
<template #icon>
<Icon icon="lucide:shield-check" class="size-12px" />
</template>
已闭环
</NTag>
<NTag v-else size="small" type="warning">未闭环</NTag>
</span>
</div>
<p class="mt-1 text-xs leading-relaxed">
{{ mandText }}
</p>
<div v-if="!mandClosed" class="mt-1 text-right">
<NButton text type="primary" class="text-xs" @click="confirmClose">确认闭环留痕</NButton>
</div>
</div>
<!-- 变更冲突提示同设备在途变更可收起 -->
<div
v-if="!conflictHide"
class="bg-[var(--bg-color)] px-3 py-2.5 text-sm text-[var(--theme-color)]"
:style="{
borderRadius:themeStore.themeRadius+'px',
'--theme-color':themeStore.themeColor,
'--bg-color':getColorWithOpacity(themeStore.themeColor,0.1),
}"
>
<div class="flex items-center gap-2">
<Icon icon="lucide:bell" class="size-14px" />
<span class="font-semibold">冲突提示</span>
<NButton text type="primary" class="ml-auto shrink-0 text-xs" @click="conflictHide = true">知道了</NButton>
</div>
<p class="mt-1 text-xs leading-relaxed">
{{ conflictText }}
</p>
</div>
<!-- 属地影响摘要仅属地负责人视角 -->
<div v-if="role === 'dept'" class="border" :style="{borderRadius:themeStore.themeRadius+'px'}">
<div class="flex items-center gap-2 border-b bg-slate-50/70 px-3 py-2">
<Icon icon="lucide:sparkles" class="size-16px text-violet-500" />
<span class="text-sm font-semibold text-slate-700">属地影响摘要</span>
<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>
</div>
<div class="space-y-1.5 px-3 py-2.5 text-xs leading-relaxed text-slate-600">
<p v-for="(r, i) in deptRows" :key="i"><b class="text-slate-700">{{ r.label }}</b>{{ r.text }}</p>
</div>
</div>
<!-- 变更前后关键参数对比变化标蓝 / 超限标红 -->
<div
class="border"
:style="{
borderRadius:themeStore.themeRadius+'px',
'--error-color':themeStore.otherColor.error,
'--theme-color':themeStore.themeColor
}"
>
<div class="border-b bg-slate-50/70 px-3 py-2 text-xs font-semibold text-slate-600">
关键参数对比AI 提取 · <span class="font-bold text-[var(--theme-color)]">=变化</span> · <span class="font-bold text-[var(--error-color)]">=超限</span>
</div>
<div v-for="d in paramDiff" :key="d.k" class="border-b px-3 py-2 last:border-b-0">
<div class="flex flex-wrap items-baseline gap-x-2 text-sm">
<span class="w-24 shrink-0 text-xs text-slate-500">{{ d.k }}</span>
<span class="text-xs text-slate-400">{{ d.before }}</span>
<span class="text-slate-300"></span>
<span
class="text-sm font-semibold"
:class="d.over ? 'text-[var(--error-color)]' : 'text-[var(--theme-color)]'"
>{{ d.after }}</span>
<NTag v-if="d.over" size="small" type="error">超限</NTag>
</div>
<div
class="mt-0.5 text-[11px] leading-snug"
:class="d.over ? 'text-[var(--error-color)]' : 'text-slate-400'"
>{{ d.note }}</div>
</div>
</div>
<!-- AI 审批建议版本冻结 · 建议分级 · 已阅留痕 · 无感审计 -->
<div
class="border"
:style="{
borderRadius:themeStore.themeRadius+'px',
'--error-color':themeStore.otherColor.error,
'--theme-color':themeStore.themeColor,
'--warning-color':themeStore.otherColor.warning,
}"
>
<div class="flex flex-wrap items-center gap-2 border-b bg-slate-50/70 px-3 py-2">
<Icon icon="lucide:sparkles" class="size-14px text-violet-500" />
<span class="text-sm font-semibold text-slate-700">AI 审批建议</span>
<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 size="small">基于提交版本 V1 一次性分析 · 版本冻结</NTag>
<div class="ml-auto">
<NTag v-if="aiRead" size="small" type="success"> {{ roleName }} 已阅 AI 建议已留痕</NTag>
<NButton v-else size="small" class="text-xs" @click="readSuggest">已阅 AI 建议</NButton>
</div>
</div>
<div class="space-y-3 p-3">
<!-- 强制合规项 -->
<div class="mb-1 flex items-center gap-1.5 text-xs font-semibold text-[var(--error-color)]"><span class="inline-block h-2 w-2 rounded-full bg-[var(--error-color)]" /> 强制合规项驳回前必须处理否则无法通过</div>
<div class="flex flex-wrap items-center gap-2 rounded-lg border border-red-100 bg-red-50/50 px-3 py-2 text-sm">
<span class="min-w-0 flex-1 text-slate-700">{{ mandItem }}</span>
<NTag v-if="mandClosed" size="small" type="success">已闭环</NTag>
<NTag v-else size="small" type="warning">未闭环</NTag>
</div>
<!-- 推荐改进项 -->
<div>
<div class="mb-1 flex items-center gap-1.5 text-xs font-semibold text-[var(--warning-color)]"><span class="inline-block h-2 w-2 rounded-full bg-[var(--warning-color)]" /> 推荐改进项可采纳为行动项可忽略但须填理由</div>
<div class="space-y-1.5">
<div v-for="s in suggs" :key="s.id" class="rounded-lg border px-3 py-2 text-sm">
<div class="leading-relaxed text-slate-700">{{ s.text }}</div>
<div class="mt-1.5">
<div v-if="s.st === '待定'" class="flex gap-3">
<NButton text type="success" class="text-xs hover:underline"
@click="acceptSuggest(s)">
采纳为行动项
</NButton>
<NButton text class="text-xs hover:underline"
@click="rejectSuggest(s)">
已知悉不采纳
</NButton>
</div>
<NTag v-else-if="s.st === '已采纳'" size="small" type="success">已采纳 已生成 PSSR 行动项</NTag>
<NTag v-else size="small" type="default">已知悉不采纳理由已留痕</NTag>
</div>
</div>
</div>
</div>
<!-- 参考信息 -->
<div>
<div class="mb-1 flex items-center gap-1.5 text-xs font-semibold text-[var(--theme-color)]"><span class="inline-block h-2 w-2 rounded-full bg-[var(--theme-color)]" /> 参考信息纯参考无强制力</div>
<div class="space-y-1.5">
<div class="flex flex-wrap items-center gap-2 rounded-lg border border-[#DFE9F6] bg-[#EAF0F9]/60 px-3 py-2 text-sm">
<span class="min-w-0 flex-1 text-slate-700">{{ similarText }}</span>
<NButton text type="primary" class="hover:underline text-xs" @click="viewArchive('MOC-2025-0112')">查看归档包</NButton>
</div>
<div class="rounded-lg border border-[#DFE9F6] bg-[#EAF0F9]/60 px-3 py-2 text-sm text-slate-700">
{{ experienceText }}
</div>
</div>
</div>
<div class="rounded-lg bg-slate-50 px-3 py-2 text-[11px] leading-5 text-slate-400">
查看采纳已阅等操作均由后台自动记录无感审计供内审追溯AI 建议不强制采纳决策主导权在审批人驳回重新提交后AI 将对新版本重新分析一次
</div>
</div>
</div>
</div>
</NDrawerContent>
</NDrawer>
</NSpin>
</NSpace>
</template>
<style scoped lang="scss">
.scroll {
height: calc(100vh - 210px);
overflow-y: auto;
}
</style>
@@ -392,7 +392,7 @@ const onUploadFile = async (e:any) => {
if (!file) return;
const formData = new FormData();
formData.append('file', file);
formData.append('step', '2');
formData.append('step', '1');
uploadLoading.value = true;
const {data,error} = await uploadFileApi(props.currentId, formData);
if(!error){
+1 -1
View File
@@ -119,7 +119,7 @@ const uploadFile = async (e: any) => {
if (!file) return;
const formData = new FormData();
formData.append('file', file);
formData.append('step', '5');
formData.append('step', '4');
uploadLoading.value = true;
const {error} = await uploadFileApi(props.currentId, formData);
if(!error){
+1
View File
@@ -384,6 +384,7 @@ onMounted(() => {
<div v-if="c.status === 'APPROVING'" class="flex gap-1.5">
<NButton class="text-xs" text type="primary" @click="openDetail(c,'detail')">详情</NButton>
<NButton class="text-xs" text type="warning" @click="openProgress(c)">进度</NButton>
<!-- <NButton class="text-xs" text type="primary" @click="openDisposal(c)">处置申请</NButton> -->
<NButton class="text-xs" text type="error" @click="openWithdraw(c)">撤回</NButton>
</div>
<!-- 审批通过 -->