feat(文件预览): 添加对PDF、Word和Excel文件的预览支持

添加新的文件预览组件,支持PDF、Word(docx)、Excel(xlsx/xls)文件类型预览
重构图片预览为独立组件,并添加相关依赖包
This commit is contained in:
砂糖
2026-04-10 08:42:59 +08:00
parent 255a6dc616
commit a19c4e4eaf
7 changed files with 482 additions and 94 deletions

View File

@@ -40,6 +40,9 @@
"@babel/parser": "7.7.4", "@babel/parser": "7.7.4",
"@jiaminghi/data-view": "^2.10.0", "@jiaminghi/data-view": "^2.10.0",
"@riophae/vue-treeselect": "0.4.0", "@riophae/vue-treeselect": "0.4.0",
"@vue-office/docx": "^1.6.3",
"@vue-office/excel": "^1.7.14",
"@vue/composition-api": "^1.7.2",
"axios": "0.24.0", "axios": "0.24.0",
"bpmn-js-token-simulation": "0.10.0", "bpmn-js-token-simulation": "0.10.0",
"clipboard": "2.0.8", "clipboard": "2.0.8",
@@ -74,6 +77,7 @@
"vue": "2.6.12", "vue": "2.6.12",
"vue-count-to": "1.0.13", "vue-count-to": "1.0.13",
"vue-cropper": "0.5.5", "vue-cropper": "0.5.5",
"vue-demi": "^0.14.10",
"vue-flv-player": "^1.0.3", "vue-flv-player": "^1.0.3",
"vue-konva": "^2.1.7", "vue-konva": "^2.1.7",
"vue-meta": "2.4.0", "vue-meta": "2.4.0",

View File

@@ -9,30 +9,19 @@
@close="handleClose" @close="handleClose"
> >
<!-- 图片预览 --> <!-- 图片预览 -->
<div v-if="fileType === 'image'" class="preview-image"> <ImagePreview v-if="fileType === 'image'" :src="fileUrl" />
<div class="image-controls">
<el-button type="primary" size="small" @click="zoomIn">放大</el-button>
<el-button type="primary" size="small" @click="zoomOut">缩小</el-button>
<el-button type="primary" size="small" @click="resetZoom">重置</el-button>
</div>
<div class="image-container" ref="imageContainer">
<img
:src="fileUrl"
:style="{ transform: `scale(${scale})` }"
class="preview-image-content"
@wheel="handleWheel"
/>
</div>
</div>
<!-- PDF预览 --> <!-- PDF预览 -->
<div v-else-if="fileType === 'pdf'" class="preview-pdf"> <PdfPreview v-else-if="fileType === 'pdf'" :src="fileUrl" />
<iframe
:src="fileUrl" <!-- Word预览 -->
class="preview-pdf-content" <DocxPreview v-else-if="fileType === 'docx'" :src="fileUrl" />
frameborder="0"
/> <!-- Excel预览 (xlsx) -->
</div> <XlsxPreview v-else-if="fileType === 'xlsx'" :src="fileUrl" />
<!-- Excel预览 (xls) -->
<XlsPreview v-else-if="fileType === 'xls'" :src="fileUrl" />
<!-- 不支持的文件类型 --> <!-- 不支持的文件类型 -->
<div v-else class="preview-not-supported"> <div v-else class="preview-not-supported">
@@ -42,8 +31,21 @@
</template> </template>
<script> <script>
import ImagePreview from './preview/image/index.vue';
import PdfPreview from './preview/pdf/index.vue';
import DocxPreview from './preview/docx/index.vue';
import XlsxPreview from './preview/xlsx/index.vue';
import XlsPreview from './preview/xls/index.vue';
export default { export default {
name: "FilePreview", name: "FilePreview",
components: {
ImagePreview,
PdfPreview,
DocxPreview,
XlsxPreview,
XlsPreview
},
props: { props: {
visible: { visible: {
type: Boolean, type: Boolean,
@@ -64,8 +66,7 @@ export default {
}, },
data() { data() {
return { return {
dialogVisible: false, dialogVisible: false
scale: 1
}; };
}, },
watch: { watch: {
@@ -92,6 +93,12 @@ export default {
return 'image'; return 'image';
} else if (ext === 'pdf') { } else if (ext === 'pdf') {
return 'pdf'; return 'pdf';
} else if (ext === 'docx') {
return 'docx';
} else if (ext === 'xlsx') {
return 'xlsx';
} else if (ext === 'xls') {
return 'xls';
} else { } else {
return 'other'; return 'other';
} }
@@ -100,82 +107,12 @@ export default {
methods: { methods: {
handleClose() { handleClose() {
this.$emit('update:visible', false); this.$emit('update:visible', false);
},
// 放大图片
zoomIn() {
if (this.scale < 3) {
this.scale += 0.1;
}
},
// 缩小图片
zoomOut() {
if (this.scale > 0.1) {
this.scale -= 0.1;
}
},
// 重置缩放
resetZoom() {
this.scale = 1;
},
// 鼠标滚轮缩放
handleWheel(event) {
event.preventDefault();
const delta = event.deltaY > 0 ? -0.1 : 0.1;
if ((this.scale > 0.1 || delta > 0) && (this.scale < 3 || delta < 0)) {
this.scale += delta;
}
} }
} }
}; };
</script> </script>
<style scoped> <style scoped>
.preview-image {
width: 100%;
height: 70vh;
background-color: #f5f7fa;
display: flex;
flex-direction: column;
}
.image-controls {
padding: 10px;
display: flex;
gap: 10px;
border-bottom: 1px solid #e4e7ed;
background-color: #ffffff;
}
.image-container {
flex: 1;
overflow: auto;
display: flex;
justify-content: center;
align-items: flex-start;
padding: 20px;
}
.preview-image-content {
transition: transform 0.3s ease;
cursor: zoom-in;
max-width: 100%;
max-height: 100%;
}
.preview-image-content:hover {
cursor: zoom-in;
}
.preview-pdf {
width: 100%;
height: 70vh;
}
.preview-pdf-content {
width: 100%;
height: 100%;
}
.preview-not-supported { .preview-not-supported {
display: flex; display: flex;
justify-content: center; justify-content: center;

View File

@@ -0,0 +1,38 @@
<template>
<div class="docx-preview">
<vue-office-docx :src="src" @render-error="handleDocxError" />
</div>
</template>
<script>
import VueOfficeDocx from '@vue-office/docx';
import '@vue-office/docx/lib/index.css';
export default {
name: "DocxPreview",
components: {
VueOfficeDocx
},
props: {
src: {
type: String,
required: true,
description: "Word文件的URL或路径"
}
},
methods: {
handleDocxError(err) {
console.error('docx预览失败:', err);
this.$message.error('文档预览失败,请下载查看');
}
}
};
</script>
<style scoped>
.docx-preview {
width: 100%;
height: 70vh;
overflow-y: auto;
}
</style>

View File

@@ -0,0 +1,99 @@
<template>
<div class="preview-image">
<div class="image-controls">
<el-button type="primary" size="small" @click="zoomIn">放大</el-button>
<el-button type="primary" size="small" @click="zoomOut">缩小</el-button>
<el-button type="primary" size="small" @click="resetZoom">重置</el-button>
</div>
<div class="image-container" ref="imageContainer">
<img
:src="src"
:style="{ transform: `scale(${scale})` }"
class="preview-image-content"
@wheel="handleWheel"
/>
</div>
</div>
</template>
<script>
export default {
name: "ImagePreview",
props: {
src: {
type: String,
required: true,
description: "图片文件的URL或路径"
}
},
data() {
return {
scale: 1
};
},
methods: {
// 放大图片
zoomIn() {
if (this.scale < 3) {
this.scale += 0.1;
}
},
// 缩小图片
zoomOut() {
if (this.scale > 0.1) {
this.scale -= 0.1;
}
},
// 重置缩放
resetZoom() {
this.scale = 1;
},
// 鼠标滚轮缩放
handleWheel(event) {
event.preventDefault();
const delta = event.deltaY > 0 ? -0.1 : 0.1;
if ((this.scale > 0.1 || delta > 0) && (this.scale < 3 || delta < 0)) {
this.scale += delta;
}
}
}
};
</script>
<style scoped>
.preview-image {
width: 100%;
height: 70vh;
background-color: #f5f7fa;
display: flex;
flex-direction: column;
}
.image-controls {
padding: 10px;
display: flex;
gap: 10px;
border-bottom: 1px solid #e4e7ed;
background-color: #ffffff;
}
.image-container {
flex: 1;
overflow: auto;
display: flex;
justify-content: center;
align-items: flex-start;
padding: 20px;
}
.preview-image-content {
transition: transform 0.3s ease;
cursor: zoom-in;
max-width: 100%;
max-height: 100%;
}
.preview-image-content:hover {
cursor: zoom-in;
}
</style>

View File

@@ -0,0 +1,34 @@
<template>
<div class="preview-pdf">
<iframe
:src="src"
class="preview-pdf-content"
frameborder="0"
/>
</div>
</template>
<script>
export default {
name: "PdfPreview",
props: {
src: {
type: String,
required: true,
description: "PDF文件的URL或路径"
}
}
};
</script>
<style scoped>
.preview-pdf {
width: 100%;
height: 70vh;
}
.preview-pdf-content {
width: 100%;
height: 100%;
}
</style>

View File

@@ -0,0 +1,238 @@
<template>
<div class="xls-preview">
<!-- 加载状态 -->
<div v-if="loading" class="loading">
<div class="spinner"></div>
<p>正在加载Excel文件...</p>
</div>
<!-- 错误信息 -->
<div v-if="error" class="error">{{ error }}</div>
<!-- Excel内容展示 -->
<div v-if="workbook" class="workbook-container">
<!-- 工作表标签 -->
<div class="sheet-tabs">
<button v-for="(sheet, index) in workbook.SheetNames" :key="index"
:class="{ active: activeSheetIndex === index }" @click="activeSheetIndex = index">
{{ sheet }}
</button>
</div>
<!-- 表格内容区域 -->
<div class="sheet-content">
<table v-if="activeSheetData">
<tbody>
<tr v-for="(row, rowIndex) in activeSheetData" :key="rowIndex">
<td v-for="(cell, colIndex) in row" :key="colIndex" :class="{
'header-cell': rowIndex === 0,
'odd-row': rowIndex % 2 === 1
}">
{{ cell || '' }}
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</template>
<script>
import * as XLSX from 'xlsx';
export default {
name: "XlsPreview",
props: {
src: {
type: String,
required: true,
description: "Excel文件的URL或路径"
}
},
data () {
return {
workbook: null, // 解析后的Excel工作簿
activeSheetIndex: 0, // 当前激活的工作表索引
loading: false, // 加载状态
error: null // 错误信息
};
},
watch: {
src: {
immediate: true,
handler () {
this.loadAndParseExcel();
}
}
},
computed: {
// 获取当前激活的工作表数据
activeSheetData () {
if (!this.workbook || !this.workbook.SheetNames.length) return null;
const sheetName = this.workbook.SheetNames[this.activeSheetIndex];
const worksheet = this.workbook.Sheets[sheetName];
// 将工作表转换为二维数组
return XLSX.utils.sheet_to_json(worksheet, { header: 1 });
}
},
methods: {
/**
* 加载并解析Excel文件
*/
async loadAndParseExcel () {
this.loading = true;
this.error = null;
this.workbook = null;
try {
// 验证URL格式
if (!this.src || (this.src.startsWith('http') && !this.isValidUrl(this.src))) {
throw new Error('无效的文件路径');
}
// 加载文件
const response = await fetch(this.src);
if (!response.ok) {
throw new Error(`加载失败: ${response.status} ${response.statusText}`);
}
// 转换为ArrayBuffer
const arrayBuffer = await response.arrayBuffer();
// 解析Excel
this.workbook = XLSX.read(arrayBuffer, {
type: 'array',
cellDates: true, // 解析日期类型
cellText: false // 保持单元格原始类型
});
// 重置到第一个工作表
this.activeSheetIndex = 0;
} catch (err) {
this.error = err.message;
console.error('Excel处理错误:', err);
} finally {
this.loading = false;
}
},
/**
* 验证URL格式
*/
isValidUrl (url) {
try {
new URL(url);
return true;
} catch (e) {
return false;
}
}
}
};
</script>
<style scoped>
.xls-preview {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
border: 1px solid #e5e7eb;
border-radius: 6px;
overflow: hidden;
width: 100%;
height: 70vh;
}
/* 加载状态样式 */
.loading {
padding: 40px 20px;
text-align: center;
color: #6b7280;
}
.spinner {
width: 40px;
height: 40px;
margin: 0 auto 16px;
border: 4px solid #e5e7eb;
border-top-color: #3b82f6;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* 错误信息样式 */
.error {
padding: 20px;
color: #dc2626;
background-color: #fee2e2;
border-bottom: 1px solid #fecaca;
}
/* 工作表标签样式 */
.sheet-tabs {
display: flex;
background-color: #f9fafb;
border-bottom: 1px solid #e5e7eb;
overflow-x: auto;
white-space: nowrap;
}
.sheet-tabs button {
padding: 8px 16px;
border: none;
background: none;
font-size: 14px;
color: #4b5563;
cursor: pointer;
border-bottom: 2px solid transparent;
transition: all 0.2s;
}
.sheet-tabs button:hover:not(.active) {
background-color: #f3f4f6;
color: #1f2937;
}
.sheet-tabs button.active {
border-bottom-color: #3b82f6;
color: #3b82f6;
font-weight: 500;
}
/* 表格内容样式 */
.sheet-content {
overflow: auto;
max-height: 600px;
}
table {
width: 100%;
border-collapse: collapse;
min-width: max-content;
}
td {
padding: 8px 12px;
border: 1px solid #e5e7eb;
min-width: 80px;
font-size: 14px;
color: #1f2937;
}
.header-cell {
background-color: #f3f4f6;
font-weight: 500;
color: #111827;
}
.odd-row {
background-color: #f9fafb;
}
</style>

View File

@@ -0,0 +1,38 @@
<template>
<div class="xlsx-preview">
<vue-office-excel :src="src" @render-error="handleExcelError" />
</div>
</template>
<script>
import VueOfficeExcel from '@vue-office/excel';
import '@vue-office/excel/lib/index.css';
export default {
name: "XlsxPreview",
components: {
VueOfficeExcel
},
props: {
src: {
type: String,
required: true,
description: "Excel文件的URL或路径"
}
},
methods: {
handleExcelError(err) {
console.error('excel预览失败:', err);
this.$message.error('Excel预览失败请下载查看');
}
}
};
</script>
<style scoped>
.xlsx-preview {
width: 100%;
height: 70vh;
overflow-y: auto;
}
</style>