<template>
<div>
<!-- 头部组件 -->
<Header />
<div class="notice-container" style="width: 90%; margin: 0 auto;">
<!-- 面包屑导航 -->
<div class="breadcrumb">
<el-breadcrumb separator-class="el-icon-arrow-right">
<el-breadcrumb-item :to="{ path: '/front' }">首页</el-breadcrumb-item>
<el-breadcrumb-item>值班信息列表</el-breadcrumb-item>
</el-breadcrumb>
</div>
<!-- 数据表格 -->
<el-table
v-loading="loading"
:data="tableData"
style="width: 100%"
border
>
<el-table-column prop="name" label="部门" width="150"></el-table-column>
<el-table-column label="标题">
<template v-slot="{ row }">
<span class="title-text">{{ row.title }}</span>
</template>
</el-table-column>
<el-table-column prop="updateTime" label="更新时间" width="180">
<template v-slot="{ row }">
{{ formatTime(row.updateTime) }}
</template>
</el-table-column>
<el-table-column label="操作" width="180">
<template v-slot="{ row }">
<el-button
size="mini"
type="primary"
@click="downloadFile(row)"
:disabled="!row.fileUrl"
>
下载
</el-button>
<el-button
size="mini"
type="success"
@click="previewFile(row)"
:disabled="!canPreview(row.fileUrl)"
>
预览
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页组件 -->
<div class="pagination">
<el-pagination
background
@current-change="handlePageChange"
@size-change="handleSizeChange"
:current-page="pageNum"
:page-size="pageSize"
:page-sizes="[10, 20, 30, 50]"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
></el-pagination>
</div>
<!-- 预览对话框 -->
<el-dialog
:title="previewTitle"
:visible.sync="previewVisible"
width="80%"
top="5vh"
>
<div v-if="previewType === 'pdf'" class="pdf-preview">
<div class="pagination-controls">
<el-button
@click="prevPage"
:disabled="currentPage === 1"
icon="el-icon-arrow-left"
circle
></el-button>
<div class="page-info">
<span>第 {{ currentPage }} / {{ totalPages }} 页</span>
</div>
<el-button
@click="nextPage"
:disabled="currentPage === totalPages"
icon="el-icon-arrow-right"
circle
></el-button>
<div class="page-jump">
<el-input-number
v-model="jumpPage"
:min="1"
:max="totalPages"
controls-position="right"
size="mini"
></el-input-number>
<el-button
type="primary"
size="mini"
@click="jumpToPage"
>跳转</el-button>
</div>
<div class="zoom-controls">
<el-button
@click="changeZoom(-0.1)"
icon="el-icon-minus"
circle
size="mini"
></el-button>
<span>{{ (zoomLevel * 100).toFixed(0) }}%</span>
<el-button
@click="changeZoom(0.1)"
icon="el-icon-plus"
circle
size="mini"
></el-button>
</div>
</div>
<div class="pdf-viewer">
<canvas ref="pdfCanvas"></canvas>
</div>
</div>
<div v-else-if="previewType === 'docx'" class="docx-preview">
<div ref="docxContainer" class="docx-container"></div>
</div>
<div v-else-if="previewType === 'xlsx'" class="excel-preview">
<div id="luckysheet" style="height: 600px"></div>
</div>
<div v-else-if="isImage" class="image-preview">
<el-image
:src="previewFileUrl"
fit="contain"
style="max-height: 70vh; width: 100%"
>
<div slot="error" class="image-error">
<i class="el-icon-picture-outline"></i>
<p>图片加载失败</p>
</div>
</el-image>
</div>
<div v-else class="unsupported-preview">
<div class="unsupported-content">
<i class="el-icon-warning-outline"></i>
<h3>不支持预览此文件类型</h3>
<p>请下载文件后使用本地应用打开</p>
</div>
</div>
</el-dialog>
</div>
<Footer />
</div>
</template>
<script>
import { filePreviewUtil } from '@/utils/filePreview';
import { getFileExtension } from '@/utils/fileUtils';
import Footer from '@/components/Footer.vue'
import Header from '@/components/Header.vue';
export default {
name: 'DutyMore',
components: {
Header,
Footer
},
data() {
return {
loading: false,
pageNum: 1,
pageSize: 10,
total: 0,
tableData: [],
searchParams: {
name: '',
title: ''
},
// 预览相关数据
previewVisible: false,
previewTitle: '',
previewFileUrl: '',
previewType: '',
isImage: false,
// PDF预览相关
pdfDoc: null,
currentPage: 1,
totalPages: 0,
jumpPage: 1,
zoomLevel: 1.0,
};
},
created() {
this.loadData();
},
methods: {
// 加载数据
async loadData() {
this.loading = true;
try {
const res = await this.$request.get('/duty/selectPage', {
params: {
pageNum: this.pageNum,
pageSize: this.pageSize,
...this.searchParams
}
});
if (res.code === '200') {
this.tableData = res.data?.list || [];
this.total = res.data?.total || 0;
} else {
this.$message.error(res.msg || '加载数据失败');
}
} catch (error) {
console.error('加载值班数据失败:', error);
this.$message.error('数据加载失败');
} finally {
this.loading = false;
}
},
// 分页大小变化
handleSizeChange(newSize) {
this.pageSize = newSize;
this.pageNum = 1;
this.loadData();
},
// 页码变化
handlePageChange(val) {
this.pageNum = val;
this.loadData();
},
// 时间格式化
formatTime(time) {
if (!time) return '';
const date = new Date(time);
return `${date.getFullYear()}-${(date.getMonth()+1).toString().padStart(2,'0')}-${date.getDate().toString().padStart(2,'0')}`;
},
// 下载文件
downloadFile(row) {
if (!row.fileUrl) {
this.$message.warning('该记录没有可下载的文件');
return;
}
const link = document.createElement('a');
link.href = this.getFullFileUrl(row.fileUrl);
link.download = row.title || '值班文件';
link.style.display = 'none';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
},
// 判断是否支持预览
canPreview(fileUrl) {
if (!fileUrl) return false;
const ext = getFileExtension(fileUrl).toLowerCase();
return ['pdf', 'docx', 'xlsx', 'jpg', 'jpeg', 'png', 'gif'].includes(ext);
},
// 预览文件
previewFile(row) {
if (!row.fileUrl) {
this.$message.warning('该记录没有可预览的文件');
return;
}
this.previewTitle = row.title;
this.previewFileUrl = this.getFullFileUrl(row.fileUrl);
this.previewType = getFileExtension(row.fileUrl).toLowerCase();
this.isImage = ['jpg', 'jpeg', 'png', 'gif'].includes(this.previewType);
// 在打开新弹窗前清理上一次的预览资源
filePreviewUtil.resetPreviousPreview();
if (!this.canPreview(row.fileUrl)) {
this.$message.warning('不支持预览此文件类型');
this.previewVisible = true;
return;
}
this.previewVisible = true;
this.$nextTick(async () => {
try {
this.loading = true;
if (this.previewType === 'pdf') {
await filePreviewUtil.togglePreview(
'pdf',
this.previewFileUrl,
{
canvas: this.$refs.pdfCanvas,
currentPage: this.currentPage,
zoomLevel: this.zoomLevel
}
);
} else if (this.previewType === 'docx') {
await filePreviewUtil.togglePreview(
'docx',
this.previewFileUrl,
{
container: this.$refs.docxContainer
}
);
} else if (this.previewType === 'xlsx') {
await filePreviewUtil.togglePreview(
'xlsx',
this.previewFileUrl,
{
containerId: 'luckysheet',
fileName: this.previewTitle
}
);
}
} catch (error) {
console.error('预览失败:', error);
this.$message.error('预览失败: ' + error.message);
} finally {
this.loading = false;
}
});
},
// 获取完整文件URL
getFullFileUrl(url) {
return url.includes('http') ? url : (this.$baseUrl + url);
},
// PDF预览方法
async previewPDF() {
try {
const { pdfDoc, totalPages } = await filePreviewUtil.previewPDF(
this.previewFileUrl,
{
canvas: this.$refs.pdfCanvas,
currentPage: this.currentPage,
zoomLevel: this.zoomLevel
}
);
this.pdfDoc = pdfDoc;
this.totalPages = totalPages;
this.currentPage = 1;
this.jumpPage = 1;
} catch (error) {
console.error('PDF预览失败:', error);
throw error;
}
},
// DOCX预览方法
async previewDocx() {
try {
await filePreviewUtil.previewDocx(this.previewFileUrl, {
container: this.$refs.docxContainer
});
} catch (error) {
console.error('DOCX预览失败:', error);
throw error;
}
},
// Excel预览方法
async previewExcel() {
try {
await filePreviewUtil.previewExcel(this.previewFileUrl, {
containerId: 'luckysheet',
fileName: this.previewTitle
});
} catch (error) {
console.error('Excel预览失败:', error);
throw error;
}
},
// PDF分页控制方法
async prevPage() {
if (this.currentPage > 1) {
this.currentPage--;
this.jumpPage = this.currentPage;
await this.renderPDFPage(this.currentPage);
}
},
async nextPage() {
if (this.currentPage < this.totalPages) {
this.currentPage++;
this.jumpPage = this.currentPage;
await this.renderPDFPage(this.currentPage);
}
},
async jumpToPage() {
if (this.jumpPage >= 1 && this.jumpPage <= this.totalPages) {
this.currentPage = this.jumpPage;
await this.renderPDFPage(this.currentPage);
} else {
this.$message.warning('请输入有效的页码');
}
},
async renderPDFPage(pageNum) {
if (!this.pdfDoc || pageNum < 1 || pageNum > this.totalPages) return;
try {
await filePreviewUtil.renderPDFPage(
this.pdfDoc,
pageNum,
this.$refs.pdfCanvas,
this.zoomLevel
);
} catch (error) {
console.error('渲染PDF页面失败:', error);
this.$message.error('渲染页面失败: ' + error.message);
}
},
async changeZoom(delta) {
this.zoomLevel = Math.min(Math.max(this.zoomLevel + delta, 0.25), 3.0);
await this.renderPDFPage(this.currentPage);
}
},
beforeDestroy() {
// 清理预览资源
if (this.previewType === 'xlsx') {
filePreviewUtil.resetExcel();
}
if (this.previewType === 'pdf') {
filePreviewUtil.resetPDF(this.$refs.pdfCanvas);
}
}
};
</script>
<style scoped>
.notice-container {
margin-top: 20px;
}
.breadcrumb {
margin-bottom: 20px;
padding: 10px 0;
border-bottom: 1px solid #eee;
}
.title-text {
color: #333;
}
.pagination {
margin-top: 20px;
text-align: center;
}
.el-table {
margin-top: 10px;
}
/* 预览相关样式 */
.pdf-preview {
display: flex;
flex-direction: column;
height: 100%;
}
.pagination-controls {
display: flex;
justify-content: center;
align-items: center;
gap: 15px;
padding: 10px;
margin-bottom: 10px;
flex-wrap: wrap;
}
.page-info {
font-size: 14px;
min-width: 100px;
text-align: center;
}
.page-jump {
display: flex;
align-items: center;
gap: 8px;
}
.zoom-controls {
display: flex;
align-items: center;
gap: 8px;
}
.pdf-viewer {
min-height: 500px;
max-height: 70vh;
overflow: auto;
}
.docx-preview {
min-height: 500px;
max-height: 70vh;
overflow: auto;
}
.excel-preview {
min-height: 500px;
max-height: 70vh;
}
.image-preview {
display: flex;
justify-content: center;
align-items: center;
min-height: 500px;
max-height: 70vh;
}
.unsupported-preview {
display: flex;
justify-content: center;
align-items: center;
min-height: 200px;
}
.unsupported-content {
text-align: center;
}
.unsupported-content i {
font-size: 40px;
color: #e6a23c;
margin-bottom: 10px;
}
.unsupported-content h3 {
margin-bottom: 10px;
color: #606266;
}
</style>// src/utils/filePreview.js
import { renderAsync } from "docx-preview";
import LuckyExcel from "luckyexcel";
import axios from "axios";
import { asynLoad } from "@/utils/excel";
// src/utils/filePreview.js
export const previewPDF = {
async initPDF(fileUrl) {
const pdfjsLib = await this.loadPDFJS();
const pdfData = await this.fetchPDFData(fileUrl);
// 设置 workerSrc
pdfjsLib.GlobalWorkerOptions.workerSrc =
"https://cdn.jsdelivr.net/npm/pdfjs-dist@2.16.105/build/pdf.worker.min.js";
// 初始化 PDF 文档
return pdfjsLib.getDocument({
data: pdfData,
cMapUrl: "https://cdn.jsdelivr.net/npm/pdfjs-dist@2.16.105/cmaps/",
cMapPacked: true
}).promise;
},
async fetchPDFData(fileUrl) {
try {
const response = await fetch(fileUrl, { method: 'GET', mode: 'cors' });
if (!response.ok) throw new Error('Failed to fetch PDF file');
const arrayBuffer = await response.arrayBuffer();
return new Uint8Array(arrayBuffer);
} catch (error) {
console.error('Error fetching PDF:', error);
throw error;
}
},
async loadPDFJS() {
const scripts = [
"https://unpkg.com/pdfjs-dist@2.16.105/build/pdf.min.js",
"https://unpkg.com/pdfjs-dist@2.16.105/build/pdf.worker.min.js"
];
await Promise.all(scripts.map(src => asynLoad(src)));
return window.pdfjsLib;
},
async renderPage(pdfDoc, pageNum, canvas, zoomLevel = 1.0) {
if (!pdfDoc || pageNum < 1 || pageNum > pdfDoc.numPages) return;
try {
const page = await pdfDoc.getPage(pageNum);
const viewport = page.getViewport({ scale: zoomLevel });
// 设置 Canvas 尺寸
canvas.width = viewport.width;
canvas.height = viewport.height;
const renderContext = {
canvasContext: canvas.getContext('2d'),
viewport: viewport
};
await page.render(renderContext).promise;
return pdfDoc.numPages;
} catch (error) {
console.error('Error rendering PDF page:', error);
throw error;
}
}
};
// DOCX预览工具
export const previewDocx = {
async render(fileUrl, containerElement) {
try {
const response = await fetch(fileUrl);
const blob = await response.blob();
await renderAsync(blob, containerElement);
return true;
} catch (error) {
throw new Error('DOCX预览失败: ' + error.message);
}
}
};
// src/utils/filePreview.js
export const previewExcel = {
async render(fileUrl, fileName, containerId) {
try {
// 动态加载 LuckySheet 资源
const baseURL = "https://unpkg.com/luckysheet@latest/dist"; // 替换为备用 CDN
const resources = [
`${baseURL}/plugins/css/pluginsCss.css`,
`${baseURL}/plugins/plugins.css`,
`${baseURL}/css/luckysheet.css`,
`${baseURL}/assets/iconfont/iconfont.css`,
`${baseURL}/plugins/js/plugin.js`,
`${baseURL}/luckysheet.umd.js`
];
await Promise.all(resources.map(src => asynLoad(src, src.endsWith('.css'))));
if (!window.luckysheet) {
throw new Error('Luckysheet 加载失败');
}
// 获取 Excel 文件
const response = await axios.get(fileUrl, { responseType: 'arraybuffer' });
const arrayBuffer = new Uint8Array(response.data);
const file = new File([arrayBuffer], fileName);
// 转换并渲染 Excel
return new Promise((resolve, reject) => {
LuckyExcel.transformExcelToLucky(file, (exportJson, luckyIns) => {
if (!exportJson.sheets || !exportJson.sheets.length) {
reject('无法读取 Excel 文件内容');
return;
}
window.luckysheet.create({
container: containerId,
data: exportJson.sheets,
title: exportJson.info.name,
lang: 'zh',
showinfobar: false,
allowEdit: false
});
resolve(luckyIns);
}, () => {
reject('转换 Excel 文件失败');
});
});
} catch (error) {
console.error('Error rendering Excel:', error);
throw error;
}
},
destroy(instance) {
if (instance && typeof instance.destroy === 'function') {
instance.destroy();
}
}
};
// 通用预览工具
// src/utils/filePreview.js
export const filePreviewUtil = {
async togglePreview(fileType, fileUrl, options) {
// 清理上一次的预览资源
this.resetPreviousPreview();
switch (fileType.toLowerCase()) {
case 'pdf':
await this.previewPDF(fileUrl, options);
break;
case 'docx':
await this.previewDocx(fileUrl, options);
break;
case 'xlsx':
await this.previewExcel(fileUrl, options);
break;
default:
throw new Error('Unsupported file type for preview');
}
},
async previewPDF(fileUrl, { canvas, currentPage = 1, zoomLevel = 1.0 }) {
const pdfDoc = await previewPDF.initPDF(fileUrl);
await previewPDF.renderPage(pdfDoc, currentPage, canvas, zoomLevel);
return {
pdfDoc,
totalPages: pdfDoc.numPages
};
},
async previewDocx(fileUrl, { container }) {
await previewDocx.render(fileUrl, container);
},
async previewExcel(fileUrl, { containerId, fileName }) {
return previewExcel.render(fileUrl, fileName, containerId);
},
resetPreviousPreview() {
// 清理 PDF 预览
if (this.currentPreviewType === 'pdf') {
this.resetPDF(this.currentCanvas);
}
// 清理 Excel 预览
if (this.currentPreviewType === 'xlsx') {
this.resetExcel();
}
// 清理 DOCX 预览
if (this.currentPreviewType === 'docx') {
this.resetDocx(this.currentDocxContainer);
}
// 重置当前预览类型
this.currentPreviewType = null;
this.currentCanvas = null;
this.currentDocxContainer = null;
},
resetPDF(canvas) {
if (canvas) {
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
canvas.width = 0;
canvas.height = 0;
}
},
resetExcel() {
if (window.luckysheet && typeof window.luckysheet.destroy === 'function') {
window.luckysheet.destroy();
}
window.luckysheet = null;
},
resetDocx(container) {
if (container) {
container.innerHTML = '';
}
}
};
如果我先预览pdf在预览excel不会有问题,但我现预览excel在预览pdf时,excel弹窗依旧纯在,报错Uncaught runtime errors:
×
ERROR
Script error.
at handleError (webpack-internal:///./node_modules/webpack-dev-server/client/overlay.js:299:58)
at eval (webpack-internal:///./node_modules/webpack-dev-server/client/overlay.js:318:7)以及location.js:61 Uncaught TypeError: Cannot read properties of undefined (reading 'left')
at $s (location.js:61:37)
at Object.overshow (postil.js:161:21)
at HTMLDocument.<anonymous> (handler.js:1533:26)
at HTMLDocument.dispatch (plugin.js:1:39175)
at d.handle (plugin.js:1:37266)