docx、xlsx、pdf、图片等文件预览功能添加
This commit is contained in:
@@ -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"
|
||||||
|
|||||||
Generated
+2730
-1501
File diff suppressed because it is too large
Load Diff
@@ -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',
|
||||||
|
});
|
||||||
|
}
|
||||||
Vendored
+2
@@ -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
|
||||||
<Icon icon="ci:close-circle" class="size-16px" />
|
positive-text="确定"
|
||||||
</NButton>
|
: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>
|
</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>
|
||||||
|
|||||||
@@ -98,12 +98,12 @@ defineExpose({form})
|
|||||||
✨ AI 生成 · 需人工确认
|
✨ AI 生成 · 需人工确认
|
||||||
</span>
|
</span>
|
||||||
<span class="flex items-center gap-2">
|
<span class="flex items-center gap-2">
|
||||||
<NButton ghost size="small" class="text-xs" title="上传 PSSR 纸质确认表照片 / 扫描件(JPG / PNG / PDF),系统不做内容识别,上传即视为全部检查项完成确认" @click="pssrFileRef?.click()">
|
<NButton ghost size="small" class="text-xs" title="上传 PSSR 纸质确认表照片 / 扫描件(JPG / PNG / PDF),系统不做内容识别,上传即视为全部检查项完成确认" @click="pssrFileRef?.click()">
|
||||||
<Icon icon="material-symbols:upload" class="size-14px" />
|
<Icon icon="material-symbols:upload" class="size-14px" />
|
||||||
上传纸质确认表
|
上传纸质确认表
|
||||||
</NButton>
|
</NButton>
|
||||||
<input ref="pssrFileRef" type="file" accept="image/*,.pdf" class="hidden" @change="onPssrFile" />
|
<input ref="pssrFileRef" type="file" accept="image/*,.pdf" class="hidden" @change="onPssrFile" />
|
||||||
<span class="hidden text-xs text-slate-400 xl:inline">申请阶段:点击条目内容直接编辑,可增删项,完成时点互斥选择;审批通过后逐项点击确认,全部完成自动推送属地负责人</span>
|
<span class="hidden text-xs text-slate-400 xl:inline">申请阶段:点击条目内容直接编辑,可增删项,完成时点互斥选择;审批通过后逐项点击确认,全部完成自动推送属地负责人</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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">
|
<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">
|
||||||
|
|||||||
Reference in New Issue
Block a user