Files
moc/src/views/details/closeDoc/index.vue
T
2026-09-18 14:04:16 +08:00

678 lines
36 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 { 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, 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, VueOfficePptx } from 'vue3-office-preview'
import 'vue3-office-preview/lib/style.css'
const themeStore = useThemeStore();
const { routerPushByKey } = useRouterPush();
const route = useRoute();
const loading = ref<boolean>(false);
// 当前变更详情
const currentInfo = ref<any>(null);
// 当前变更id
const currentId = ref<number>(0);
// 是否开启 AI 辅助功能
const isAiOn = ref<boolean>(false);
// 培训签到上传文件列表
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>([
{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 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']
const pptExtensions = ['pptx']
if(imageExtensions.includes(type)){
return 'image'
}
if(wordExtensions.includes(type)){
return 'word'
}
if(excelExtensions.includes(type)){
return 'excel'
}
if(pptExtensions.includes(type)){
return 'ppt'
}
if(type==='pdf'){
return 'pdf'
}
}
return ''
}
// 打开预览
const preview = (file:any) => {
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;
}
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;
}
}
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();
}
}
})
</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="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">上传线下纸质表照片 / 扫描件即视为完成确认上传后可查阅下载空白模板可下载</span>
</div>
<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: trainFiles.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">上传培训签到</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>
<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"
:key="trainFiles.length?'train_'+trainFiles[0]?.id:'train'"
accept="image/*,.pdf,.docx,.xlsx,.pptx"
: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>
<NUpload
class="w-auto"
:key="pssrFiles.length?'pssr_'+pssrFiles[0]?.id:'pssr'"
accept="image/*,.pdf,.docx,.xlsx,.pptx"
: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>
<!-- 变更投用时间 -->
<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>
</div>
<div class="p-3">
<div class="flex items-center gap-2">
<p class="text-sm text-slate-600">实际投用时间</p>
<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 检查 未完成的变更不得投用</div>
</div>
</div>
<!-- 变更验收 -->
<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">投用后组织验收上传验收资料并选择验收结论上传后可查阅下载验收表可下载</span>
</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: acceptFiles.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">上传验收资料</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-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>
<NUpload
class="w-auto"
:key="acceptFiles.length?'accept_'+acceptFiles[0]?.id:'accept'"
accept="image/*,.pdf,.docx,.xlsx,.pptx"
: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 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>
<!-- 变更关闭资料归档确认 -->
<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"> = 已归档 · = 待归档本地上传或从变更附件选择完成后自动转为」)· 不涉及 = 无需处理</span>
<NButton size="small" type="primary" ghost class="ml-auto text-xs">AI辅助判定</NButton>
</div>
<div class="p-3">
<NTable size="small">
<thead>
<tr>
<th>资料名称</th>
<th class="!text-center">是否归档</th>
<th class="w-200px !text-center">操作</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, index) in list" :key="index">
<td>
<p>{{item.name}}</p>
<div v-if="item.files.length" class="flex flex-wrap gap-2">
<div
v-for="(file, fileIndex) in item.files"
: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('accept')">下载</NButton>
<NPopconfirm
positive-text="确定"
:positiveButtonProps="{ size: 'tiny' }"
:negativeButtonProps="{ size: 'tiny' }"
@positive-click="removeFile(Number(index), Number(fileIndex))"
>
<template #trigger>
<NButton text type="error" class="text-11px">删除</NButton>
</template>
确定删除吗
</NPopconfirm>
</div>
</div>
</td>
<td>
<div class="flex justify-center gap-2">
<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.status === 2" class="flex justify-center gap-2">
<NButton size="small" class="text-xs" @click="openFileList(Number(index))">从变更附件选择</NButton>
<NUpload
class="w-auto"
:key="item.id"
accept="image/*,.pdf,.docx,.xlsx,.pptx"
: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.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>
<span v-if="item.files.length">已归档{{item.files.length}}</span>
<span v-else>已归档</span>
</NTag>
</div>
<div v-if="item.status === 0" class="flex justify-end gap-2">
<span class="text-xs text-slate-400">无需处理</span>
</div>
</td>
</tr>
</tbody>
</NTable>
</div>
</div>
</div>
<div class="flex items-center justify-between border-t pt-3 mt-3">
<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" @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: '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>
<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 items-center" />
</template>
<!-- Excel 预览 -->
<template v-if="handleFileType(previewFile?.file_type) === 'excel'">
<VueOfficeExcel :src="previewFile?.file_url" class="w-full items-center" />
</template>
<!-- Ppt 预览 -->
<template v-if="handleFileType(previewFile?.file_type) === 'ppt'">
<VueOfficePptx :src="previewFile?.file_url" class="w-full items-center" />
</template>
<!-- PDF 预览 -->
<template v-if="handleFileType(previewFile?.file_type) === 'pdf'">
<div class="h-[68vh]">
<PDF :src="previewFile?.file_url" class="w-full h-full items-center" />
</div>
</template>
</div>
</div>
</template>
</NModal>
</NCard>
</NSpin>
</NSpace>
</template>
<style scoped lang="scss">
.scroll {
height: calc(100vh - 263px);
overflow-y: auto;
}
</style>