我们可能需要对上传的文件进行预览,这里抽出了一个文件预览的组件。我们引入FilePreviewModal和useFilePreview后,可以很简单的实现word和excel的预览
xlsx文件的预览效果 多个sheet页可以正常渲染,单元格的样式也可以渲染
使用时的demo
文件列表用tag展示,上传成功和失败以及正在上传有不同的状态,点击文件,将弹出文件预览框。
<template>
// 其他代码
<Tag
v-for="file in fileList"
:key="file.name"
:closable="file.status !=='uploading'"
@close="handleRemove(file)"
:color="file.status == 'uploading' ? 'processing' : file.status"
>
<template #icon>
<sync-outlined v-if="file.status == 'uploading'" :spin="true" />
<check-circle-outlined v-else-if="file.status === 'success'" />
<close-circle-outlined v-else-if="file.status === 'error'" />
</template>
<span @click.stop="handleAssitantPreview(file)" style="cursor: pointer;">{{ file.name }}</span>
</Tag>
<!-- 预览模态框 -->
<FilePreviewModal
:preview-state="previewState"
:handle-preview-cancel="handlePreviewCancel"
:rendered="rendered"
:error-handler="errorHandler"
/>
</template>
// 导入组合式函数和组件
import { useFilePreview } from '@/views/components/common/useFilePreview'
import FilePreviewModal from '@/views/components/common/FilePreviewModal.vue'
// 使用文件预览功能
const {
previewState,
readFileAsArrayBuffer,
parseCSV,
handlePreview: handleFilePreview,
handlePreviewCancel,
rendered,
errorHandler
} = useFilePreview()
// 处理预览点击
const handleAssitantPreview = async (file: any) => {
// 从后端获取文件的二进制流,即data,
const { data } = await axiosGetFile(file.id)
file.arrayBuffer = data
await handleFilePreview(file)
}
FilePreviewModal.vue
<!-- components/FilePreviewModal.vue -->
<template>
<Modal
:visible="previewState.previewVisible"
:title="previewState.previewTitle"
:footer="null"
@cancel="handlePreviewCancel"
width="80%"
>
<!-- Word 预览 -->
<VueOfficeDocx
v-if="previewState.previewType === 'word' && previewState.fileSrc"
:src="previewState.fileSrc"
@rendered="rendered"
@error="errorHandler"
class="file-preview"
/>
<!-- Excel 预览 -->
<VueOfficeExcel
v-else-if="previewState.previewType === 'excel' && previewState.fileSrc"
:src="previewState.fileSrc"
@rendered="rendered"
@error="errorHandler"
class="file-preview"
/>
<!-- CSV/Text 预览 -->
<div v-else-if="previewState.previewType === 'csv' && previewState.fileSrc" class="file-preview">
<Table
:dataSource="previewState.tableData"
:columns="previewState.tableColumns"
:pagination="false"
bordered
size="small"
>
<template #bodyCell="{ column, text }">
<span :title="text">{{ text }}</span>
</template>
</Table>
</div>
<div v-else>
<p>暂不支持该文件的预览</p>
</div>
</Modal>
</template>
<script setup lang="ts">
import { Modal, Table } from 'ant-design-vue'
import VueOfficeExcel from '@vue-office/excel'
import VueOfficeDocx from '@vue-office/docx'
interface Props {
previewState: any
handlePreviewCancel: () => void
rendered: () => void
errorHandler: (error: any) => void
}
defineProps<Props>()
</script>
<style scoped>
.file-preview {
max-height: 70vh;
overflow: auto;
}
</style>
useFilePreview.ts
// composables/useFilePreview.ts
import { ref } from 'vue'
import { message } from 'ant-design-vue'
import * as XLSX from 'xlsx'
export interface FilePreviewState {
previewVisible: boolean
previewTitle: string
previewType: string
fileSrc: string | ArrayBuffer | null
tableData: any[]
tableColumns: any[]
}
export function useFilePreview() {
const previewState = ref<FilePreviewState>({
previewVisible: false,
previewTitle: '',
previewType: '',
fileSrc: null,
tableData: [],
tableColumns: []
})
// 将文件读取为 ArrayBuffer
const readFileAsArrayBuffer = (file: File): Promise<ArrayBuffer> => {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = (e) => {
if (e.target?.result) {
resolve(e.target.result as ArrayBuffer)
} else {
reject(new Error('文件读取失败'))
}
}
reader.onerror = reject
reader.readAsArrayBuffer(file)
})
}
// 解析 CSV 文件
const parseCSV = (arrayBuffer: ArrayBuffer): any[] => {
try {
const textDecoder = new TextDecoder('utf-8')
let csvString = textDecoder.decode(arrayBuffer)
if (csvString.includes('�')) {
try {
const gbkDecoder = new TextDecoder('gbk')
csvString = gbkDecoder.decode(arrayBuffer)
} catch (e) {
console.warn('GBK 解码失败,使用 UTF-8 结果')
}
}
const workbook = XLSX.read(csvString, {
type: 'string',
codepage: 65001,
cellDates: true,
cellText: false
})
const firstSheet = workbook.Sheets[workbook.SheetNames[0]]
const jsonData = XLSX.utils.sheet_to_json(firstSheet, { header: 1 })
return jsonData
} catch (error) {
console.error('CSV 解析错误:', error)
return parseCSVSimple(arrayBuffer)
}
}
// 简单的 CSV 解析(备用方案)
const parseCSVSimple = (arrayBuffer: ArrayBuffer): any[] => {
const textDecoder = new TextDecoder('utf-8')
let csvString = textDecoder.decode(arrayBuffer)
const lines = csvString.split('\n')
return lines.map(line => {
const result = []
let current = ''
let inQuotes = false
for (let i = 0; i < line.length; i++) {
const char = line[i]
if (char === '"') {
inQuotes = !inQuotes
} else if (char === ',' && !inQuotes) {
result.push(current)
current = ''
} else {
current += char
}
}
result.push(current)
return result
})
}
// 设置 CSV 表格数据
const setupCSVTable = (csvData: any[]) => {
if (!csvData || csvData.length === 0) {
previewState.value.tableData = []
previewState.value.tableColumns = []
return
}
const headers = csvData[0]
previewState.value.tableColumns = headers.map((header: any, index: number) => ({
title: header || `列${index + 1}`,
dataIndex: index.toString(),
key: index.toString(),
ellipsis: true,
width: 150
}))
previewState.value.tableData = csvData.slice(1).map((row, rowIndex) => {
const record: any = { key: rowIndex }
row.forEach((cell: any, colIndex: number) => {
record[colIndex.toString()] = cell
})
return record
})
}
// 处理预览点击
const handlePreview = async (file: any) => {
try {
previewState.value.previewTitle = `文件预览 - ${file.name}`
previewState.value.previewType = file.name.endsWith('xlsx') ? 'excel' :
file.name.endsWith('docx') ? 'word' :
file.name.endsWith('csv') ? 'csv' : ''
if (previewState.value.previewType === 'csv' && file.csvData) {
previewState.value.fileSrc = file.arrayBuffer
setupCSVTable(file.csvData)
} else {
previewState.value.fileSrc = file.arrayBuffer || null
}
previewState.value.previewVisible = true
} catch (error) {
console.error('预览失败:', error)
message.error('文件预览失败,请重试')
}
}
// 关闭预览模态框
const handlePreviewCancel = () => {
previewState.value.previewVisible = false
previewState.value.previewTitle = ''
previewState.value.previewType = ''
previewState.value.fileSrc = null
previewState.value.tableData = []
previewState.value.tableColumns = []
}
// 预览组件渲染完成回调
const rendered = () => {
console.log("预览渲染完成")
}
// 预览组件错误处理
const errorHandler = (error: any) => {
console.error("预览加载失败:", error)
message.error('文件预览失败,可能文件已损坏或不支持。')
}
return {
previewState,
readFileAsArrayBuffer,
parseCSV,
handlePreview,
handlePreviewCancel,
rendered,
errorHandler
}
}