Vue3中实现world、excel、txt、pdf文件预览,(vue-office的使用)
依赖
"@vue-office/docx": "^1.3.0", "@vue-office/excel": "^1.4.5", "@vue-office/pdf": "^1.5.3",
代码案例
核心代码
<div class="preview-box"> <a-modal class="preview-modal" v-model:visible="previewVisible" :footer="null" :width="1200" style="max-height: 600px" :onCancel="onPreviewModalClick"> <div class="preview-container"> <vue-office-docx v-if="previewType === 'word'" :src="previewUrl" @rendered="renderingCompleted" /> <vue-office-excel v-if="previewType === 'excel'" :src="previewUrl" @rendered="renderingCompleted" /> <vue-office-pdf v-if="previewType === 'pdf'" :src="previewUrl" @rendered="renderingCompleted "/> <img v-if="previewType === 'img'" :src="previewUrl" style="width: 960px; height: 720px; display: block; margin: 0 auto" alt=""/> <div v-if="previewType === 'txt'" class="txt" style="white-space: pre-wrap;"> {{ textContent }} </div> </div> <template #closeIcon> <span class="close-modal-item"> <CloseOutlined/> </span> </template> </a-modal> </div>
全部代码
<template> <div class="apply-container"> <a-modal style="position: fixed; left: 25%; top: 12%;" class="apply-modal" v-model:visible="props.isVisible" :title="props.disabledType ? '工作报告详情' : '工作报告上报'" width="900px" :footer="null" @cancel="$emit('close')" > <a-spin tip="数据加载中..." :spinning="props.spinning"> <div class="box"> <a-form :model="formState" :label-col="{ span: 4 }" :wrapper-col="{ span: 23 }" > <a-form-item label="工作报告标题" :rules="[{ required: true }]"> <a-input placeholder="请输入" allow-clear :disabled="props.disabledType" v-model:value="formState.title" /> </a-form-item> <a-form-item label="工作报告类型" :rules="[{ required: true }]"> <a-select style="width: 100%" :disabled="props.disabledType" v-model:value="formState.reportType" placeholder="请选择" allowClear :getPopupContainer="triggerNode => triggerNode.parentNode" > <a-select-option v-for="(selItem, index) of state.typeList" :value="selItem.code" :label="selItem.description" :key="index" > {{ selItem.description }} </a-select-option> </a-select> </a-form-item> <!-- <a-form-item label="报告内容" :rules="[{ required: true }]">--> <!-- <a-textarea--> <!-- :rows="3"--> <!-- :style="{ resize: 'none' }"--> <!-- :disabled="props.disabledType"--> <!-- v-model:value="formState.content"--> <!-- />--> <!-- </a-form-item>--> <a-form-item label="附件"> <a-upload v-if="!props.disabledType" :disabled="props.disabledType" v-model:file-list="formState.fileList" name="file" :multiple="true" action="/empower/attachment/upload" @download="doDownload" :showUploadList="{showDownloadIcon: true}" @change="handleFileChange" > <a-button class="btn-color" :disabled="props.disabledType"> <upload-outlined></upload-outlined> 上传附件 </a-button> <span class="btn-font"> <upload-outlined style="display: none"></upload-outlined>* 选择本地文件上传</span > </a-upload> <div v-else style="max-height: 520px; overflow-y: scroll"> <div v-for="item in formState.fileList" class="echo-file-item"> <a-button type="link" @click=""><span class="file-item-name">{{ item.name }}</span> </a-button> <span class="icon-box"> <download-outlined class="down-icon" @click="doDownload(item)" title="下载"></download-outlined> <eye-outlined class="eye-icon" @click="doPreview(item.response.data)" title="预览"></eye-outlined> </span> </div> </div> </a-form-item> <div style="display: flex"> <!-- <a-form-item label="报送人"--> <!-- style="width: 100%;"--> <!-- :labelCol=" {span: 6, offset: 2}"--> <!-- :rules="[{ required: true }]">--> <!-- <a-input--> <!-- :disabled="props.disabledType"--> <!-- style="width: 100%; "--> <!-- v-model:value="formState.reportPerson"--> <!-- />--> <!-- </a-form-item>--> <!-- <a-form-item style="width: 100%" label="报送单位"--> <!-- :labelCol=" {span: 6, offset: 2}"--> <!-- :rules="[{ required: true }]">--> <!-- <a-input--> <!-- :disabled="props.disabledType"--> <!-- style="width: 100%; "--> <!-- v-model:value="formState.reportDept"--> <!-- />--> <!-- </a-form-item>--> </div> <!-- <a-form-item label="报送时间" :rules="[{ required: true }]">--> <!-- <a-date-picker--> <!-- :disabled="props.disabledType"--> <!-- v-model:value="formState.reportTime"--> <!-- show-time--> <!-- format="YYYY-MM-DD HH:mm:ss"--> <!-- value-format="YYYY-MM-DD HH:mm:ss"--> <!-- />--> <!-- </a-form-item>--> </a-form> </div> <div class="modal-btn" v-show="!props.disabledType"> <a-space> <a-button @click="cancel">取消</a-button> <a-button type="primary" @click="submitHandle" :loading="state.loading"> 提交 </a-button> </a-space> </div> </a-spin> </a-modal> <div class="preview-box"> <a-modal class="preview-modal" v-model:visible="previewVisible" :footer="null" :width="1200" style="max-height: 600px" :onCancel="onPreviewModalClick"> <div class="preview-container"> <vue-office-docx v-if="previewType === 'word'" :src="previewUrl" @rendered="renderingCompleted" /> <vue-office-excel v-if="previewType === 'excel'" :src="previewUrl" @rendered="renderingCompleted" /> <vue-office-pdf v-if="previewType === 'pdf'" :src="previewUrl" @rendered="renderingCompleted "/> <img v-if="previewType === 'img'" :src="previewUrl" style="width: 960px; height: 720px; display: block; margin: 0 auto" alt=""/> <div v-if="previewType === 'txt'" class="txt" style="white-space: pre-wrap;"> {{ textContent }} </div> </div> <template #closeIcon> <span class="close-modal-item"> <CloseOutlined/> </span> </template> </a-modal> </div> </div> </template> <script setup> import {onMounted, reactive, ref, watch, onUnmounted, toRefs} from "vue"; import {message} from "ant-design-vue"; import mitt from "@/utils/mitt.js"; import {UploadOutlined, DownloadOutlined, EyeOutlined, CloseOutlined} from "@ant-design/icons-vue"; import {RESPONSE_STATUS} from "@/utils/bean"; import {getDictList} from "@/api/victory"; import {addWorkReport} from "@/api/workOverview"; import {nanoid} from "nanoid"; import {getUserInfo} from "@/api/user"; import VueOfficeDocx from '@vue-office/docx'; import VueOfficeExcel from '@vue-office/excel' import VueOfficePdf from '@vue-office/pdf' import '@vue-office/docx/lib/index.css' import axios from "axios"; const props = defineProps({ spinning: { type: Boolean, default: false, }, disabledType: { type: Boolean, default: false, }, detailData: { type: Object, default: null, }, isVisible: { type: Boolean, default: false, }, }); const previewVisible = ref(false); const previewUrl = ref(''); const previewType = ref(''); // word | excel | pdf | img | oldWord | txt const textContent = ref(''); const renderingCompleted = () => { console.log("渲染完成"); }; function setPreviewType(item) { if(item.extName === 'docx') { previewType.value = 'word' return; } if (item.extName === 'doc') { message.warn('暂不支持doc文件预览,请上传docx文件类型'); previewType.value = '' return; } if (item.extName.includes('xls')) { previewType.value = 'excel' return; } if (item.extName.includes('pdf') || item.extName.includes('PDF')) { previewType.value = 'pdf' return; } if (item.extName === 'txt' || item.extName === 'TXT') { previewType.value = 'txt' axios.get(item.address, { responseType: 'text' }).then(res => { textContent.value = res.data }) return; } if (item.extName.includes('jpg') || item.extName.includes('png') || item.extName.includes('jpeg') || item.extName.includes('svg') || item.extName.includes('JPEG') || item.extName.includes('JPG') ) { previewType.value = 'img' return; } if (previewType.value === '') { message.warn('该文件类型不支持预览'); } } const doPreview = item => { previewUrl.value = '' previewType.value = '' previewVisible.value = false setPreviewType(item); console.log('output-> item [doPreivew] ', item, previewType.value) if(previewType.value !== '') { previewVisible.value = true; previewUrl.value = item.address; }else { previewVisible.value = false; } } const onPreviewModalClick = () => { previewVisible.value = false } const doDownload = item => { let aUrl = item.response.data.address let extNameIcon = item.response.data.extName let extName = item.response.data.name if (extName.includes('.')) { extName = extName.split('.')[0] } let xml = new XMLHttpRequest() xml.open('GET', aUrl, true) xml.responseType = 'blob' xml.onload = () => { let url = window.URL.createObjectURL(xml.response) let a = document.createElement('a') a.href = url a.download = `${extName}.${extNameIcon}` a.style.display = 'none' a.click() } xml.send() } watch(() => props.disabledType, () => { if (props.disabledType) { state.spinning = true; formState.value = props.detailData; formState.value.fileList = (props.detailData.attachmentVo && props.detailData.attachmentVo.map(img => { return { uid: `vc-upload-${nanoid()}`, name: `${img?.name}.${img?.extName}`, type: 'image/jpeg', status: 'done', response: { status: 0, message: 'success', traceId: null, data: { address: `${img?.address}`, name: `${img?.name}`, id: img?.id, extName: `${img?.extName}`, url: `${img?.address}` } } } })) || [] state.spinning = false; } else { resetFormState(); } }) const state = reactive({ loading: false, spinning: false, typeList: [] }); const emit = defineEmits(["close", "loading"]); let formState = ref({ title: '', // 工作标题 reportType: undefined, // 工作报告类型 content: '', // 报告内容 reportPerson: '', // 报送人 reportTime: undefined, // 报送时间 fileList: [], // 附件 reportDept: '', // 报送单位 }); const init = async () => { const res = await getDictList({pageNo: 1, pageSize: 200, parentCode: "report_type"}); // 工作报告类型 state.typeList = res.data.data; await initUserInfo(); } const initUserInfo = async () => { if (localStorage.getItem("userInfo") === null) { const info = await getUserInfo(); let userState = info?.data?.data || {}; localStorage.setItem("userInfo", JSON.stringify(userState)); } let userState = JSON.parse(localStorage.getItem('userInfo')); formState.value.reportPerson = userState.name; formState.value.reportDept = userState.officeVos[0].officeName; console.log('output-> formState::: ', formState.value) }; onMounted(() => { init(); }); onUnmounted(() => { }); const resetFormState = () => { let formStateObj = formState.value; formStateObj.title = ''; formStateObj.reportType = undefined; formStateObj.content = ''; // formStateObj.reportPerson = ''; // formStateObj.reportDept = ''; // formStateObj.reportTime = ''; formStateObj.fileList = []; } const submitHandle = async () => { console.log(formState.value, " formState.value"); if (!formState.value.title) { message.warn('工作报告标题不能为空') return; } if (!formState.value.reportType) { message.warn('工作报告类型不能为空') return; } // if (!formState.value.content) { // message.warn('报告内容不能为空') // return; // } // if (!formState.value.reportPerson) { // message.warn('报送人不能为空') // return; // } // if (!formState.value.reportDept) { // message.warn('报送单位不能为空') // return; // } // if (!formState.value.reportTime) { // message.warn('报送时间不能为空') // return; // } state.loading = true; console.log('output-> formState.value.fileList::: ', formState.value.fileList) let attachmentListVal = formState.value.fileList?.map(item => { if (item.response) { return item['response']['data'].id } }) let payload = { ...formState.value, attachment: attachmentListVal.length ? attachmentListVal.join(',') : '', } console.log('output-> payload:::: ', payload) try { const res = await addWorkReport(payload); if (RESPONSE_STATUS.success === res.status) { state.loading = false; message.success('工作报告上报成功'); mitt.emit('refreshReportTableData'); resetFormState(); emit("close"); } } catch (e) { state.loading = false; resetFormState(); emit("close"); } }; const handleFileChange = (e) => { let fileList = [...e.fileList] fileList = fileList.map(file => { if (file.response) { file.url = file.response.url } return file }) formState.fileList = fileList } const cancel = () => { emit("close"); }; </script> <style lang="scss" scoped> .preview-container { max-height: 880px; overflow-y: scroll; } .close-modal-item { position: relative; font-size: 25px; top: -16px; right: -12px; } .echo-file-item:hover { background-color: #f5f5f5; border-radius: 9px; } .echo-file-item { display: flex; align-items: center; justify-content: space-between; .file-item-name { -webkit-line-clamp: 1; display: inline-block; max-width: 520px; -webkit-box-orient: vertical; overflow: hidden; text-overflow: ellipsis; } .icon-box { margin-right: 8px; .down-icon { } .eye-icon { margin-left: 28px; margin-right: 8px; } } } .btn-color { background-color: rgba(43, 121, 255, 0.1); color: #1f71ff; } .btn-font { margin-left: 10px; font-family: 'PingFang SC'; font-style: normal; font-weight: 400; font-size: 12px; color: rgba(21, 22, 24, 0.48); } .box { padding-left: 50px; } .upload-item { display: flex; cursor: pointer; justify-content: space-between; color: #1890ff; } .date { margin-top: 20px; } .flow { display: flex; align-content: center; input { width: 200px; margin-right: 10px; } } .upload-item:hover { color: #1890ff; background-color: #fcf7f7; } .info-box { display: flex; justify-content: space-around; font-size: 14px; margin-bottom: 20px; .info-item { position: relative; left: -170px; } &-title { color: rgba(21, 22, 24, 0.48); } } .modal-btn { text-align: right; } .weight { font-weight: 600; } .downLoad { cursor: pointer; color: #1f71ff; } .form-box { margin-top: 10px; padding-left: 55px; } :deep(.ant-form-item-label > label) { font-family: "PingFang SC"; font-weight: 600; color: rgba(21, 22, 24, 0.48); // margin-right: 30px !important; } </style>
学而不思则罔,思而不学则殆!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
2022-01-09 如何运行一个“拿来”的Vue项目???