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-i18n": "11.4.2",
"vue-pdf-embed": "2.1.4", "vue-pdf-embed": "2.1.4",
"vue-router": "5.0.7", "vue-router": "5.0.7",
"vue3-office-preview": "^0.1.2",
"wangeditor": "4.7.15", "wangeditor": "4.7.15",
"xgplayer": "3.0.23", "xgplayer": "3.0.23",
"xlsx": "0.18.5" "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'] NFormItemGi: typeof import('naive-ui')['NFormItemGi']
NGi: typeof import('naive-ui')['NGi'] NGi: typeof import('naive-ui')['NGi']
NGrid: typeof import('naive-ui')['NGrid'] NGrid: typeof import('naive-ui')['NGrid']
NImage: typeof import('naive-ui')['NImage']
NInput: typeof import('naive-ui')['NInput'] NInput: typeof import('naive-ui')['NInput']
NInputGroup: typeof import('naive-ui')['NInputGroup'] NInputGroup: typeof import('naive-ui')['NInputGroup']
NInputNumber: typeof import('naive-ui')['NInputNumber'] NInputNumber: typeof import('naive-ui')['NInputNumber']
@@ -145,6 +146,7 @@ declare global {
const NFormItemGi: typeof import('naive-ui')['NFormItemGi'] const NFormItemGi: typeof import('naive-ui')['NFormItemGi']
const NGi: typeof import('naive-ui')['NGi'] const NGi: typeof import('naive-ui')['NGi']
const NGrid: typeof import('naive-ui')['NGrid'] const NGrid: typeof import('naive-ui')['NGrid']
const NImage: typeof import('naive-ui')['NImage']
const NInput: typeof import('naive-ui')['NInput'] const NInput: typeof import('naive-ui')['NInput']
const NInputGroup: typeof import('naive-ui')['NInputGroup'] const NInputGroup: typeof import('naive-ui')['NInputGroup']
const NInputNumber: typeof import('naive-ui')['NInputNumber'] 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 { tagTextColor, tagBgColor, tagBorderColor } from '@/utils/common';
import { localStg } from '@/utils/storage'; import { localStg } from '@/utils/storage';
import { useChangeStore } from '@/store/modules/change'; import { useChangeStore } from '@/store/modules/change';
import Footer from './footer.vue'
import { AiError, aiTypeJudge, aiLevelScore } from "@/service/api/ai"; 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 themeStore = useThemeStore();
const changeStore = useChangeStore(); const changeStore = useChangeStore();
@@ -19,9 +24,10 @@ const aiFail = (e: any) => {
if (e instanceof AiError) window.$message?.warning(`AI 分析失败:${e.message},可手动填写`); if (e instanceof AiError) window.$message?.warning(`AI 分析失败:${e.message},可手动填写`);
else window.$message?.warning("AI 分析失败,可手动填写"); else window.$message?.warning("AI 分析失败,可手动填写");
}; };
const today = () => new Date().toISOString().slice(0, 10);
const uploadFileRef = ref<any>(null);
const footerRef = ref<any>(null); const footerRef = ref<any>(null);
const uploadLoading = ref<boolean>(false);
const specialties = ['生产','技术', '仪表', '电气', '设备', '安全', '质量']; const specialties = ['生产','技术', '仪表', '电气', '设备', '安全', '质量'];
// 总分数 // 总分数
const totalScore = ref<number>(0); 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 userInfo = ref<any>(localStg.get('userInfo'));
// 预览文件 // 预览文件
const previewFile = ref<string | null>(null); const previewFile = ref<any>(null);
const sheet = ref<string>(''); 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<{ const form = ref<{
change_type: number | null, 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 = () => { const saveDraft = () => {
emit('save'); emit('save');
@@ -483,22 +538,35 @@ defineExpose({form})
<div <div
class="relative" class="relative"
:style="{'--theme-color':themeStore.themeColor}" :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 <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)]" 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="点击在线预览" title="点击在线预览"
@click="previewFile = f" @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> </div>
<NButton text type="error" class="absolute top-[-8px] right-[-8px] bg-[#fff]" @click="form.update_docs.splice(index, 1)"> <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" /> <Icon icon="ci:close-circle" class="size-16px" />
</NButton> </NButton>
</template>
确定删除吗
</NPopconfirm>
</div> </div>
<NButton @click="form.update_docs = [...form.update_docs, `资料_${form.update_docs.length + 1}.pdf`]"> <NButton :disabled="uploadLoading" @click="uploadFileRef?.click()">
<Icon icon="material-symbols:upload" class="size-16px" /> 资料上传 <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> </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> <span class="text-xs text-slate-400">点击附件名弹窗预览支持 PDF / Word / 图片在线查看</span>
</div> </div>
</div> </div>
@@ -652,24 +720,39 @@ defineExpose({form})
:show="previewFile !== null" :show="previewFile !== null"
preset="card" preset="card"
:auto-focus="false" :auto-focus="false"
:style="{ width: '700px', height: 'auto' }" :style="{ width: '1000px', height: 'auto' }"
:segmented="{ content: true, footer: true }" :segmented="{ content: true, footer: true }"
@update:show="(v) => !v && (previewFile = null)" @update:show="(v) => !v && (previewFile = null)"
> >
<template #header> <template #header>
<div class="flex items-center gap-2 text-base font-semibold text-slate-800"> <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> </div>
</template> </template>
<template #default> <template #default>
<div class="space-y-3"> <div class="space-y-3">
<div class="flex flex-wrap gap-x-4 gap-y-1 text-xs text-slate-400"> <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>
<div class="flex h-72 flex-col items-center justify-center gap-2 rounded-lg border bg-slate-50 text-slate-400"> <div v-if="previewFile" class="flex max-h-[70vh] overflow-x-auto items-center justify-center rounded-lg border py-2">
<Icon icon="akar-icons:file" class="size-42px" /> <!-- 图片预览 -->
<span class="text-sm">文件在线预览区演示</span> <template v-if="handleFileType(previewFile?.file_type) === 'image'">
<span class="text-xs">正式环境由文档预览服务渲染 PDF / Word / 图片内容支持缩放翻页与移动端查看</span> <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>
</div> </div>
</template> </template>