docx、xlsx、pdf、图片等文件预览功能添加

This commit is contained in:
2026-09-04 16:53:27 +08:00
parent 10115eb422
commit fa268fec92
6 changed files with 2857 additions and 1524 deletions
+1
View File
@@ -84,6 +84,7 @@
"vue-i18n": "11.4.2",
"vue-pdf-embed": "2.1.4",
"vue-router": "5.0.7",
"vue3-office-preview": "^0.1.2",
"wangeditor": "4.7.15",
"xgplayer": "3.0.23",
"xlsx": "0.18.5"
+2730 -1501
View File
File diff suppressed because it is too large Load Diff
+18
View File
@@ -0,0 +1,18 @@
import { request } from '../request';
// 上传文件
export function uploadFileApi(id: number, params: any) {
return request({
url: `/api/changes/${id}/attachments`,
method: 'post',
data: params
});
}
// 删除文件
export function deleteFileApi(id: number, fileId: number) {
return request({
url: `/api/changes/${id}/attachments/${fileId}`,
method: 'delete',
});
}
+2
View File
@@ -56,6 +56,7 @@ declare module 'vue' {
NFormItemGi: typeof import('naive-ui')['NFormItemGi']
NGi: typeof import('naive-ui')['NGi']
NGrid: typeof import('naive-ui')['NGrid']
NImage: typeof import('naive-ui')['NImage']
NInput: typeof import('naive-ui')['NInput']
NInputGroup: typeof import('naive-ui')['NInputGroup']
NInputNumber: typeof import('naive-ui')['NInputNumber']
@@ -145,6 +146,7 @@ declare global {
const NFormItemGi: typeof import('naive-ui')['NFormItemGi']
const NGi: typeof import('naive-ui')['NGi']
const NGrid: typeof import('naive-ui')['NGrid']
const NImage: typeof import('naive-ui')['NImage']
const NInput: typeof import('naive-ui')['NInput']
const NInputGroup: typeof import('naive-ui')['NInputGroup']
const NInputNumber: typeof import('naive-ui')['NInputNumber']
@@ -6,8 +6,13 @@ import { useThemeStore } from '@/store/modules/theme';
import { tagTextColor, tagBgColor, tagBorderColor } from '@/utils/common';
import { localStg } from '@/utils/storage';
import { useChangeStore } from '@/store/modules/change';
import Footer from './footer.vue'
import { AiError, aiTypeJudge, aiLevelScore } from "@/service/api/ai";
import { uploadFileApi, deleteFileApi } from "@/service/api/file";
import Footer from './footer.vue'
import VuePdfEmbed from 'vue-pdf-embed';
import { VueOfficeDocx, VueOfficeExcel } from 'vue3-office-preview'
import 'vue3-office-preview/lib/style.css'
const themeStore = useThemeStore();
const changeStore = useChangeStore();
@@ -19,9 +24,10 @@ const aiFail = (e: any) => {
if (e instanceof AiError) window.$message?.warning(`AI 分析失败:${e.message},可手动填写`);
else window.$message?.warning("AI 分析失败,可手动填写");
};
const today = () => new Date().toISOString().slice(0, 10);
const uploadFileRef = ref<any>(null);
const footerRef = ref<any>(null);
const uploadLoading = ref<boolean>(false);
const specialties = ['生产','技术', '仪表', '电气', '设备', '安全', '质量'];
// 总分数
const totalScore = ref<number>(0);
@@ -30,8 +36,11 @@ const currentLevel = computed(() => props.levelList.find((x:{value:number | null
// 当前用户
const userInfo = ref<any>(localStg.get('userInfo'));
// 预览文件
const previewFile = ref<string | null>(null);
const previewFile = ref<any>(null);
const sheet = ref<string>('');
// 上传文件
const uploadFile = ref<{id:number, file_name:string, file_url:string, uploader_name:string, file_type:string, created_at:string, version:string | null}[]>([]);
// 表单
const form = ref<{
change_type: number | null,
@@ -279,6 +288,52 @@ const renderSwitcherIcon = (node:any) => {
});
}
// 判断文件类型
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 onUploadFile = async (e:any) => {
const file = e.target.files[0];
if (!file) return;
const formData = new FormData();
formData.append('file', file);
formData.append('step', '2');
uploadLoading.value = true;
const {data,error} = await uploadFileApi(props.currentId, formData);
if(!error){
window.$message?.success('上传成功');
uploadFile.value = [...uploadFile.value, data];
}
uploadLoading.value = false;
}
// 删除资料
const removeFile = async (index:number) => {
const {error} = await deleteFileApi(props.currentId, uploadFile.value[index].id);
if(!error){
window.$message?.success('删除成功');
uploadFile.value.splice(index, 1)
}
}
// 保存草稿
const saveDraft = () => {
emit('save');
@@ -483,22 +538,35 @@ defineExpose({form})
<div
class="relative"
:style="{'--theme-color':themeStore.themeColor}"
v-for="(f,index) in form.update_docs" :key="f"
v-for="(f,index) in uploadFile" :key="f.id"
>
<div
class="text-xs cursor-pointer flex items-center border py-1.5 px-2 rounded-md hover:border-[var(--theme-color)] hover:text-[var(--theme-color)]"
title="点击在线预览"
@click="previewFile = f"
>
<Icon icon="akar-icons:file" class="size-14px mr-1" />{{ f }}
<Icon icon="akar-icons:file" class="size-14px mr-1" />{{ f.file_name }}
</div>
<NButton text type="error" class="absolute top-[-8px] right-[-8px] bg-[#fff]" @click="form.update_docs.splice(index, 1)">
<Icon icon="ci:close-circle" class="size-16px" />
</NButton>
<NPopconfirm
positive-text="确定"
:positiveButtonProps="{ size: 'tiny' }"
:negativeButtonProps="{ size: 'tiny' }"
@positive-click="removeFile(index)"
>
<template #trigger>
<NButton text type="error" class="absolute top-[-8px] right-[-8px] bg-[#fff]">
<Icon icon="ci:close-circle" class="size-16px" />
</NButton>
</template>
确定删除吗
</NPopconfirm>
</div>
<NButton @click="form.update_docs = [...form.update_docs, `资料_${form.update_docs.length + 1}.pdf`]">
<Icon icon="material-symbols:upload" class="size-16px" /> 资料上传
<NButton :disabled="uploadLoading" @click="uploadFileRef?.click()">
<Icon v-if="uploadLoading" icon="ri:loader-4-fill" class="size-16px animate-spin" />
<Icon v-else icon="material-symbols:upload" class="size-16px" />
资料上传
</NButton>
<input ref="uploadFileRef" type="file" accept="image/*,.pdf,.docx,.xlsx" class="hidden" @change="onUploadFile" />
<span class="text-xs text-slate-400">点击附件名弹窗预览支持 PDF / Word / 图片在线查看</span>
</div>
</div>
@@ -652,24 +720,39 @@ defineExpose({form})
:show="previewFile !== null"
preset="card"
:auto-focus="false"
:style="{ width: '700px', height: 'auto' }"
:style="{ width: '1000px', height: 'auto' }"
:segmented="{ content: true, footer: true }"
@update:show="(v) => !v && (previewFile = null)"
>
<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 }}
<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>上传人{{ userInfo?.real_name }}</span><span>上传时间{{ today() }}</span><span>版本V1受控</span>
<span>上传人{{ previewFile?.uploader_name }}</span>
<span>上传时间{{ previewFile?.created_at }}</span>
<span v-if="previewFile?.version">{{ `版本${previewFile?.version}受控` }}</span>
</div>
<div class="flex h-72 flex-col items-center justify-center gap-2 rounded-lg border bg-slate-50 text-slate-400">
<Icon icon="akar-icons:file" class="size-42px" />
<span class="text-sm">文件在线预览区演示</span>
<span class="text-xs">正式环境由文档预览服务渲染 PDF / Word / 图片内容支持缩放翻页与移动端查看</span>
<div v-if="previewFile" class="flex max-h-[70vh] overflow-x-auto items-center justify-center rounded-lg border py-2">
<!-- 图片预览 -->
<template v-if="handleFileType(previewFile?.file_type) === 'image'">
<NImage :src="previewFile?.file_url" />
</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'">
<VuePdfEmbed :source="previewFile?.file_url" annotation-layer text-layer class="w-full" />
</template>
</div>
</div>
</template>
+6 -6
View File
@@ -98,12 +98,12 @@ defineExpose({form})
AI 生成 · 需人工确认
</span>
<span class="flex items-center gap-2">
<NButton ghost size="small" class="text-xs" title="上传 PSSR 纸质确认表照片 / 扫描件(JPG / PNG / PDF),系统不做内容识别,上传即视为全部检查项完成确认" @click="pssrFileRef?.click()">
<Icon icon="material-symbols:upload" class="size-14px" />
上传纸质确认表
</NButton>
<input ref="pssrFileRef" type="file" accept="image/*,.pdf" class="hidden" @change="onPssrFile" />
<span class="hidden text-xs text-slate-400 xl:inline">申请阶段点击条目内容直接编辑可增删项完成时点互斥选择审批通过后逐项点击确认全部完成自动推送属地负责人</span>
<NButton ghost size="small" class="text-xs" title="上传 PSSR 纸质确认表照片 / 扫描件(JPG / PNG / PDF),系统不做内容识别,上传即视为全部检查项完成确认" @click="pssrFileRef?.click()">
<Icon icon="material-symbols:upload" class="size-14px" />
上传纸质确认表
</NButton>
<input ref="pssrFileRef" type="file" accept="image/*,.pdf" class="hidden" @change="onPssrFile" />
<span class="hidden text-xs text-slate-400 xl:inline">申请阶段点击条目内容直接编辑可增删项完成时点互斥选择审批通过后逐项点击确认全部完成自动推送属地负责人</span>
</span>
</div>
<div v-if="pssrSheet" class="flex items-center gap-2 rounded-lg border border-emerald-200 bg-emerald-50 px-4 py-2 text-xs text-emerald-700">