<template>
|
<div>
|
<div v-if="isImage">
|
<img :src="imgUrl" alt="Image Preview" />
|
</div>
|
<div v-if="isPdf" style="height: 80vh;">
|
<onlyoffice v-if="useOnlyOffice" ref="onlyoffice" :options="option" style="width: 100%;height: 100%;" />
|
<iframe v-else-if="pdfUrl" :src="pdfUrl" title="PDF 预览" style="width: 100%;height: 100%;border: none;"></iframe>
|
<p v-else-if="pdfError">PDF 预览失败,<a :href="fileUrl" style="color: var(--el-color-primary);" target="_blank">点此下载查看</a></p>
|
<p v-else>正在加载 PDF…</p>
|
</div>
|
<div v-if="isDoc">
|
<p v-if="!isDocShow">文档无法直接预览,请下载查看。</p>
|
<a :href="fileUrl" v-if="!isDocShow">下载文件</a>
|
<vue-office-docx v-else :src="fileUrl" style="height: 100vh;" @rendered="renderedHandler" @error="errorHandler" />
|
</div>
|
<div v-if="isXls">
|
<p v-if="!isDocShow">文档无法直接预览,请下载查看。</p>
|
<a :href="fileUrl" v-if="!isDocShow">下载文件</a>
|
<vue-office-excel v-else :src="fileUrl" :options="excelOptions" style="height: 100vh;" @rendered="renderedHandler"
|
@error="errorHandler" />
|
</div>
|
<div v-if="isZipOrRar">
|
<p>压缩文件无法直接预览,请下载查看。</p>
|
<a :href="fileUrl">下载文件</a>
|
</div>
|
<div v-if="isCsv">
|
<p v-if="csvList.length == 0">CSV 文件无法直接预览,请下载查看。</p>
|
<a :href="fileUrl" v-if="csvList.length == 0">下载文件</a>
|
<el-tabs type="border-card" v-if="csvList.length > 0" tab-position="bottom">
|
<el-tab-pane :label="item.sheetName" v-for="(item, index) in csvList" :key="index">
|
<el-table :data="item.tableData" height="75vh">
|
<el-table-column :label="m.label" :prop="m.prop" v-for="(m, i) in item.column" :key="i" min-width="120px"
|
show-overflow-tooltip>
|
<template #header="scope">
|
<div>
|
<el-tooltip :content="m.label" placement="top">
|
<div class="oneLine">
|
<span>{{ m.label }}</span>
|
</div>
|
</el-tooltip>
|
</div>
|
</template>
|
</el-table-column>
|
</el-table>
|
</el-tab-pane>
|
</el-tabs>
|
</div>
|
<div v-if="!isSupported">
|
<p>不支持的文件格式</p>
|
</div>
|
</div>
|
</template>
|
|
<script>
|
import axios from 'axios'
|
import VueOfficeDocx from '@vue-office/docx'
|
//引入相关样式
|
import '@vue-office/docx/lib/index.css'
|
import VueOfficeExcel from '@vue-office/excel'
|
//引入相关样式
|
import '@vue-office/excel/lib/index.css'
|
import onlyoffice from '@/components/Onlyoffice/onlyoffice.vue'
|
|
export default {
|
components: {
|
onlyoffice,
|
VueOfficeDocx,
|
VueOfficeExcel
|
},
|
props: {
|
fileUrl: {
|
type: String,
|
required: true
|
},
|
currentFile: {
|
type: Object,
|
required: true
|
},
|
option: {
|
type: Object,
|
default: () => ({})
|
}
|
},
|
data() {
|
return {
|
isDocShow: true,
|
csvList: [], //csv文件数据
|
pdfUrl: '', //pdf本地预览地址
|
pdfError: false
|
}
|
},
|
computed: {
|
// 原实现把 isImage / isXls / isCsv 写成了「读文件后缀 + 顺手改别的响应式状态」的 computed:
|
// isImage 里写 imgUrl、isXls 里写 options.xls、isCsv 里直接发请求。
|
// computed 里写状态属于副作用,在 Vue3 中会在渲染期触发状态变更,很容易演变成递归更新。
|
// 这里拆成纯计算属性,副作用统一交给下面那个 watch。
|
isImage() {
|
return /\.(jpg|jpeg|png|gif)$/i.test(this.fileUrl)
|
},
|
imgUrl() {
|
// 原 isImage 无论真假都会写 imgUrl = fileUrl(命中图片时把 word 换成 img)
|
return this.isImage ? this.fileUrl.replaceAll('word', 'img') : ''
|
},
|
isPdf() {
|
return /\.pdf$/i.test(this.fileUrl)
|
},
|
isDoc() {
|
return /\.(doc|docx)$/i.test(this.fileUrl)
|
},
|
isXls() {
|
return /\.(xls|xlsx)$/i.test(this.fileUrl)
|
},
|
// vue-office 的 excel 组件用 xls 标志区分 xlsx / xls
|
excelOptions() {
|
return {
|
xls: !/\.xlsx$/i.test(this.fileUrl), //预览xlsx文件设为false;预览xls文件设为true
|
minColLength: 0, // excel最少渲染多少列,如果想实现xlsx文件内容有几列,就渲染几列,可以将此值设置为0.
|
minRowLength: 0, // excel最少渲染多少行,如果想实现根据xlsx实际函数渲染,可以将此值设置为0.
|
widthOffset: 10, //如果渲染出来的结果感觉单元格宽度不够,可以在默认渲染的列表宽度上再加 Npx宽
|
heightOffset: 10, //在默认渲染的列表高度上再加 Npx高
|
beforeTransformData: workbookData => {
|
return workbookData
|
}, //底层通过exceljs获取excel文件内容,通过该钩子函数,可以对获取的excel文件内容进行修改,比如某个单元格的数据显示不正确,可以在此自行修改每个单元格的value值。
|
transformData: workbookData => {
|
return workbookData
|
} //将获取到的excel数据进行处理之后且渲染到页面之前,可通过transformData对即将渲染的数据及样式进行修改,此时每个单元格的text值就是即将渲染到页面上的内容
|
}
|
},
|
isZipOrRar() {
|
return /\.(zip|rar)$/i.test(this.fileUrl)
|
},
|
isCsv() {
|
return /\.csv$/i.test(this.fileUrl)
|
},
|
isSupported() {
|
return this.isImage || this.isPdf || this.isDoc || this.isZipOrRar || this.isCsv || this.isXls
|
},
|
// 调用方传了 OnlyOffice 的配置才走文档服务,否则用浏览器内置阅读器
|
useOnlyOffice() {
|
return !!(this.option && this.option.url)
|
}
|
},
|
watch: {
|
fileUrl: {
|
handler() {
|
this.loadPdf()
|
if (this.isCsv) {
|
this.loadCSVData()
|
}
|
},
|
immediate: true
|
}
|
},
|
beforeUnmount() {
|
this.releasePdfUrl()
|
},
|
methods: {
|
releasePdfUrl() {
|
if (this.pdfUrl) {
|
URL.revokeObjectURL(this.pdfUrl)
|
this.pdfUrl = ''
|
}
|
},
|
async loadPdf() {
|
this.releasePdfUrl()
|
this.pdfError = false
|
if (!this.isPdf || this.useOnlyOffice) {
|
return
|
}
|
try {
|
const { data } = await axios.get(this.fileUrl, { responseType: 'blob' })
|
// 后端返回的 Content-Type 不一定带 application/pdf,这里强制指定,
|
// 浏览器才会用内置阅读器打开而不是直接下载
|
this.pdfUrl = URL.createObjectURL(new Blob([data], { type: 'application/pdf' }))
|
} catch (error) {
|
console.error(error)
|
this.pdfError = true
|
}
|
},
|
renderedHandler() {
|
console.log('渲染完成')
|
this.isDocShow = true
|
this.resetStyle()
|
},
|
errorHandler() {
|
console.log('渲染失败')
|
this.isDocShow = false
|
},
|
async loadCSVData() {
|
// 原实现依赖 this.$axios / this.$api,而这两个全局变量在项目里从未注册过
|
// (main.js 没有挂载,也没有 api 聚合对象),一旦进入 CSV 分支就抛 TypeError。
|
// 在拿到真实接口之前先显式降级为「下载查看」,与用户实际看到的结果一致。
|
if (!this.$axios || !this.$api?.insOrderPlan?.preview) {
|
console.warn('[filePreview] 未配置 $axios/$api,CSV 转表格不可用,已降级为下载查看')
|
this.csvList = []
|
return
|
}
|
this.$axios
|
.post(this.$api.insOrderPlan.preview, {
|
id: this.currentFile.id
|
})
|
.then(res => {
|
let arr = res.data
|
arr = arr.map(m => {
|
const obj = {
|
sheetName: m.sheetName,
|
tableData: [],
|
column: []
|
}
|
obj.tableData = this.formatCSVToTable(m.content.replaceAll('null', ' '))
|
// .replaceAll('MIN','=MIN').replaceAll('MAX','=MAX').replaceAll('AVERAGE','=AVERAGE')
|
for (const item in obj.tableData[0]) {
|
obj.column.push({
|
label: item,
|
prop: item
|
})
|
}
|
return obj
|
})
|
this.csvList = arr
|
})
|
.catch(err => {
|
console.log(err)
|
})
|
},
|
formatCSVToTable(str) {
|
const result = []
|
const jsonObj = str.split('\n')
|
let arrHeader = []
|
for (const i in jsonObj) {
|
if (typeof jsonObj[i] === 'string' && jsonObj[i].length > 0) {
|
const row = `${jsonObj[i]}`
|
if (row.trim().length > 0) {
|
const kv = jsonObj[i].split(',')
|
if (i == 0) {
|
// 获取column表头
|
arrHeader = kv
|
} else {
|
const obj = {}
|
for (let index = 0; index < arrHeader.length; index++) {
|
// 组装表格数据
|
const name = String(arrHeader[index])
|
if (!arrHeader[index]) continue
|
if (!obj[name]) {
|
try {
|
if (kv[index]) {
|
obj[name] = String(kv[index])
|
} else {
|
obj[name] = ''
|
}
|
} catch (err) {
|
obj[name] = ''
|
}
|
}
|
}
|
result.push(obj)
|
}
|
}
|
}
|
}
|
return result
|
},
|
resetStyle() {
|
// 注:原实现是 style.replace(/pt/g, 'px'),会把样式里任意位置的 pt 都换掉
|
// (例如 font-family 里含 "pt" 的字体名也会被改坏)。因为无法在此验证 vue-office
|
// 的实际输出,这里保持原样不改,仅留记号。
|
const elements = document.querySelectorAll('[style*="pt"]')
|
for (const element of elements) {
|
const style = element.getAttribute('style')
|
if (style) {
|
element.setAttribute('style', style.replace(/pt/g, 'px'))
|
}
|
}
|
}
|
}
|
}
|
</script>
|
|
<style scoped>
|
img {
|
max-width: 100%;
|
}
|
|
.oneLine {
|
overflow: hidden;
|
white-space: nowrap;
|
text-overflow: ellipsis;
|
}
|
</style>
|