文件预览

This commit is contained in:
cwchen 2025-11-07 13:34:04 +08:00
parent 2bda2b75a8
commit 7d38fc745b
1 changed files with 136 additions and 623 deletions

View File

@ -1,303 +1,156 @@
<!-- OnlyOfficeViewer.vue -->
<template> <template>
<div class="onlyoffice-viewer" :style="{ height: height }"> <div class="onlyoffice-container">
<!-- 工具栏 --> <div id="placeholder" class="editor-placeholder"></div>
<div v-if="!loading && !error" class="viewer-toolbar">
<div class="toolbar-left">
<button @click="handleClose" class="toolbar-btn" :disabled="closing">
<i class="el-icon-back"></i> 返回
</button>
<span class="document-title" :title="documentName">{{ documentName }}</span>
</div>
<div class="toolbar-right">
<button v-if="allowDownload" @click="downloadDocument" class="toolbar-btn download" :disabled="downloading">
<i class="el-icon-download"></i> {{ downloading ? '下载中...' : '下载' }}
</button>
<button v-if="!isMobile && allowFullscreen" @click="toggleFullscreen" class="toolbar-btn">
<i :class="isFullscreen ? 'el-icon-copy-document' : 'el-icon-full-screen'"></i>
{{ isFullscreen ? '退出全屏' : '全屏' }}
</button>
</div>
</div>
<!-- 加载状态 -->
<div v-if="loading" class="viewer-loading">
<div class="loading-content">
<div class="loading-spinner"></div>
<p>{{ loadingText }}</p>
</div>
</div>
<!-- 错误状态 -->
<div v-if="error" class="viewer-error">
<div class="error-content">
<div class="error-icon"></div>
<h3>加载失败</h3>
<p>{{ errorMessage }}</p>
<button @click="retry" class="retry-btn" :disabled="loading">重试</button>
</div>
</div>
<!-- 编辑器容器 -->
<div v-if="!loading && !error" ref="editorContainer" class="editor-container"
:class="{ 'fullscreen': isFullscreen }" :style="{ height: containerHeight, width: '100%' }"></div>
<!-- 移动端提示 -->
<div v-if="isMobile && !loading && !error" class="mobile-tip">
<p>💡 在移动端建议横屏查看以获得更好体验</p>
</div>
</div> </div>
</template> </template>
<script> <script>
import { getConfigAPI } from '@/api/common/onlyOfficeViewer'; import { getConfigAPI, generateCallbackTokenAPI } from '@/api/common/onlyOfficeViewer'
import { downloadFileWithLoading } from '@/utils/download';
export default { export default {
name: 'OnlyOfficeViewer', name: 'OnlyOfficeViewer',
props: { props: {
// ID // props
documentId: { documentUrl: {
type: String, type: String,
required: true default: 'http://192.168.0.14:9090/smart-bid/technicalSolutionDatabase/2025/11/03/716d9f3d89434c56bc49296dbbccc226.docx'
}, },
// documentTitle: {
documentName: {
type: String, type: String,
required: true default: '716d9f3d89434c56bc49296dbbccc226.docx'
}, },
// documentKey: {
height: {
type: String, type: String,
default: '600px' default: '1'
}, },
// fileName: {
allowDownload: { type: String,
type: Boolean, default: 'technicalSolutionDatabase/2025/11/03/716d9f3d89434c56bc49296dbbccc226.docx'
default: true
}, },
//
allowFullscreen: {
type: Boolean,
default: true
},
// view edit
mode: { mode: {
type: String, type: String,
default: 'view', default: 'view', // 'view' 'edit'
validator: (value) => ['view', 'edit'].includes(value) validator: (value) => ['view', 'edit'].includes(value)
}, },
// API使 type: {
downloadUrl: {
type: String, type: String,
default: '' default: 'desktop', // 'desktop', 'mobile', 'embedded'
validator: (value) => ['desktop', 'mobile', 'embedded'].includes(value)
} }
}, },
data() { data() {
return { return {
loading: false, docEditor: null,
loadingText: '正在加载文档...', onlyOfficeScriptLoaded: false,
error: false, editorConfig: {}
errorMessage: '',
editor: null,
isFullscreen: false,
isMobile: false,
downloading: false,
closing: false
}; };
}, },
created() {
computed: { this.getConfig();
// APIURL
apiBaseUrl() {
return process.env.VUE_APP_BASE_API || '';
}, },
// OnlyOfficeURL
onlyOfficeUrl() {
return process.env.VUE_APP_ONLYOFFICE_URL || process.env.VUE_APP_BASE_API || '';
},
//
containerHeight() {
if (this.isFullscreen) {
return '100vh';
}
// 48px
if (this.height.includes('vh')) {
// vh
const vhValue = parseFloat(this.height);
return `calc(${vhValue}vh - 48px)`;
}
const heightValue = parseInt(this.height);
if (isNaN(heightValue)) {
return 'calc(100% - 48px)';
}
return `${Math.max(heightValue - 48, 200)}px`; // 200px
}
},
mounted() { mounted() {
this.checkMobile(); this.initOnlyOffice();
this.setupFullscreenListener();
this.initViewer();
}, },
beforeDestroy() { beforeDestroy() {
this.cleanup(); this.destroyEditor();
}, },
methods: { methods: {
/** //
* 初始化查看器 async getConfig() {
*/
async initViewer() {
try { try {
this.loading = true; const res = await getConfigAPI({
this.loadingText = '正在加载文档...'; fileId: this.documentKey,
this.error = false; fileName: this.fileName,
this.errorMessage = ''; mode: this.mode,
type: this.type
// })
if (!this.$refs.editorContainer) { if (res.code !== 200) {
throw new Error('编辑器容器未找到'); throw new Error(res.msg || '获取编辑器配置失败');
}
if (!res.data) {
throw new Error('配置数据为空');
} }
this.editorConfig = res.data;
this.$set(this.editorConfig, 'events', {
onDocumentReady: this.onDocumentReady,
onError: this.onEditorError,
onAppReady: this.onAppReady
});
console.log('编辑器配置', this.editorConfig);
}
catch (error) {
console.error('获取编辑器配置失败:', error);
throw new Error('获取编辑器配置失败');
}
},
/**
* 初始化 OnlyOffice
*/
async initOnlyOffice() {
try {
// OnlyOffice // OnlyOffice
this.loadingText = '正在加载 OnlyOffice 脚本...';
await this.loadOnlyOfficeScript(); await this.loadOnlyOfficeScript();
// //
this.loadingText = '正在获取文档配置...'; this.initDocEditor();
const config = await this.getEditorConfig();
console.log('OnlyOffice 配置:', config);
//
if (!config || typeof config !== 'object') {
throw new Error('获取的配置格式不正确');
}
//
if (!config.document || !config.document.url) {
throw new Error('配置中缺少文档URL');
}
if (!config.documentType) {
console.warn('配置中缺少 documentType将尝试自动检测');
}
//
if (!window.DocsAPI) {
throw new Error('OnlyOffice API 未加载成功');
}
this.loadingText = '正在初始化编辑器...';
//
const container = this.$refs.editorContainer;
if (container) {
//
container.style.display = 'block';
container.style.width = '100%';
// tickDOM
await this.$nextTick();
//
const rect = container.getBoundingClientRect();
if (rect.width === 0 || rect.height === 0) {
console.warn('容器尺寸为0等待尺寸调整...', rect);
//
await new Promise(resolve => setTimeout(resolve, 500));
}
}
//
try {
this.editor = new window.DocsAPI.DocEditor(
this.$refs.editorContainer,
config
);
//
if (this.editor && this.editor.events) {
//
this.editor.events.on('onDocumentReady', () => {
console.log('OnlyOffice 文档已准备就绪');
this.loading = false;
});
this.editor.events.on('onAppReady', () => {
console.log('OnlyOffice 应用已准备就绪');
//
setTimeout(() => {
if (this.loading) {
this.loading = false;
}
}, 1000);
});
this.editor.events.on('onError', (error) => {
console.error('OnlyOffice 错误:', error);
this.handleError(`编辑器错误: ${error?.data || error?.message || '未知错误'}`);
});
}
//
setTimeout(() => {
if (this.loading) {
this.loading = false;
console.warn('编辑器初始化超时,但可能仍在加载中');
}
}, 30000); // 30
//
this.loading = false;
this.$emit('loaded', this.editor);
} catch (editorError) {
console.error('创建编辑器实例失败:', editorError);
throw new Error(`创建编辑器失败: ${editorError.message}`);
}
} catch (error) { } catch (error) {
console.error('初始化文档查看器失败:', error); console.error('初始化 OnlyOffice 失败:', error);
const errorMsg = error?.message || error?.response?.data?.msg || '加载文档失败,请重试'; this.$emit('error', error);
this.handleError(errorMsg);
} }
}, },
/** /**
* 加载 OnlyOffice 脚本 * 加载 OnlyOffice 脚本
* @returns {Promise}
*/ */
loadOnlyOfficeScript() { loadOnlyOfficeScript() {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
// //
if (window.DocsAPI) { if (window.DocsAPI) {
this.onlyOfficeScriptLoaded = true;
resolve(); resolve();
return; return;
} }
// //
const existingScript = document.querySelector('script[data-onlyoffice-api]'); const existingScript = document.querySelector('script[data-onlyoffice]');
if (existingScript) { if (existingScript) {
existingScript.addEventListener('load', resolve); existingScript.addEventListener('load', () => {
existingScript.addEventListener('error', () => { this.onlyOfficeScriptLoaded = true;
reject(new Error('加载 OnlyOffice 脚本失败')); resolve();
}); });
existingScript.addEventListener('error', reject);
return; return;
} }
// //
const script = document.createElement('script'); const script = document.createElement('script');
script.src = `${this.onlyOfficeUrl}/web-apps/apps/api/documents/api.js`; // onlyOfficeUrl OnlyOffice
script.setAttribute('data-onlyoffice-api', 'true'); const onlyOfficeUrl = process.env.VUE_APP_ONLYOFFICE_URL;
script.src = `${onlyOfficeUrl}/web-apps/apps/api/documents/api.js`;
script.setAttribute('data-onlyoffice', 'true');
script.onload = () => { script.onload = () => {
// API this.onlyOfficeScriptLoaded = true;
setTimeout(resolve, 100); // API
setTimeout(() => {
if (window.DocsAPI) {
resolve();
} else {
reject(new Error('OnlyOffice API 未正确加载'));
}
}, 100);
}; };
script.onerror = () => { script.onerror = () => {
reject(new Error('加载 OnlyOffice 脚本失败,请检查 OnlyOffice 服务是否正常运行')); reject(new Error('加载 OnlyOffice 脚本失败'));
}; };
document.head.appendChild(script); document.head.appendChild(script);
@ -305,438 +158,98 @@ export default {
}, },
/** /**
* 获取编辑器配置 * 初始化文档编辑器
* @returns {Promise<Object>}
*/ */
async getEditorConfig() { initDocEditor() {
try { if (!window.DocsAPI) {
const res = await getConfigAPI({ console.error('DocsAPI 未加载');
fileId: this.documentId,
fileName: this.documentName,
mode: this.mode
});
if (res.code !== 200) {
throw new Error(res.msg || '获取编辑器配置失败');
}
return res.data;
} catch (error) {
console.error('获取编辑器配置失败:', error);
const errorMsg = error?.response?.data?.msg || error?.message || '获取文档配置失败';
throw new Error(errorMsg);
}
},
/**
* 下载文档
*/
async downloadDocument() {
if (!this.allowDownload || this.downloading) {
return; return;
} }
try { try {
this.downloading = true; this.docEditor = new window.DocsAPI.DocEditor("placeholder", this.editorConfig);
this.$emit('downloading'); this.$emit('initialized', this.docEditor);
// 使
let downloadPath = this.downloadUrl;
if (!downloadPath) {
//
downloadPath = `smartBid/documents/download?fileId=${this.documentId}&fileName=${encodeURIComponent(this.documentName)}`;
}
await downloadFileWithLoading(
downloadPath,
this.documentName,
'正在下载文档,请稍候...'
);
this.$emit('download-success');
} catch (error) { } catch (error) {
console.error('下载文档失败:', error); console.error('创建 OnlyOffice 编辑器失败:', error);
this.$message?.error?.('下载文档失败,请稍后重试'); this.$emit('error', error);
this.$emit('download-error', error);
} finally {
this.downloading = false;
} }
}, },
/** /**
* 切换全屏 * 文档准备就绪回调
*/ */
toggleFullscreen() { onDocumentReady() {
if (!this.allowFullscreen) { console.log("文档准备好了");
return; this.$emit('document-ready');
}
if (!this.isFullscreen) {
this.enterFullscreen();
} else {
this.exitFullscreen();
}
}, },
/** /**
* 进入全屏 * 应用准备就绪回调
*/ */
enterFullscreen() { onAppReady() {
const element = this.$refs.editorContainer; console.log("OnlyOffice 应用准备就绪");
if (!element) { this.$emit('app-ready');
return;
}
const requestFullscreen =
element.requestFullscreen ||
element.webkitRequestFullscreen ||
element.mozRequestFullScreen ||
element.msRequestFullscreen;
if (requestFullscreen) {
requestFullscreen.call(element);
} else {
this.$message?.warning?.('您的浏览器不支持全屏功能');
}
}, },
/** /**
* 退出全屏 * 编辑器错误回调
*/ */
exitFullscreen() { onEditorError(error) {
const exitFullscreen = console.error("OnlyOffice 错误:", error);
document.exitFullscreen || this.$emit('error', error);
document.webkitExitFullscreen ||
document.mozCancelFullScreen ||
document.msExitFullscreen;
if (exitFullscreen) {
exitFullscreen.call(document);
}
},
/**
* 设置全屏监听
*/
setupFullscreenListener() {
const events = [
'fullscreenchange',
'webkitfullscreenchange',
'mozfullscreenchange',
'msfullscreenchange'
];
events.forEach(event => {
document.addEventListener(event, this.handleFullscreenChange);
});
},
/**
* 清理全屏监听
*/
cleanupFullscreenListener() {
const events = [
'fullscreenchange',
'webkitfullscreenchange',
'mozfullscreenchange',
'msfullscreenchange'
];
events.forEach(event => {
document.removeEventListener(event, this.handleFullscreenChange);
});
},
/**
* 处理全屏变化
*/
handleFullscreenChange() {
this.isFullscreen = !!(
document.fullscreenElement ||
document.webkitFullscreenElement ||
document.mozFullScreenElement ||
document.msFullscreenElement
);
},
/**
* 检查移动端
*/
checkMobile() {
const userAgent = navigator.userAgent || navigator.vendor || window.opera;
this.isMobile = /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(
userAgent.toLowerCase()
);
}, },
/** /**
* 销毁编辑器 * 销毁编辑器
*/ */
destroyEditor() { destroyEditor() {
if (this.docEditor && typeof this.docEditor.destroy === 'function') {
try { try {
if (this.editor && typeof this.editor.destroy === 'function') { this.docEditor.destroy();
this.editor.destroy();
}
} catch (error) { } catch (error) {
console.warn('销毁编辑器时出错:', error); console.warn('销毁编辑器时出错:', error);
} finally {
const container = this.$refs.editorContainer;
if (container) {
container.innerHTML = '';
} }
this.editor = null;
} }
this.docEditor = null;
}, },
/** /**
* 处理错误 * 重新加载文档
* @param {String} message - 错误消息
*/ */
handleError(message) { reloadDocument(newConfig = {}) {
this.error = true;
this.errorMessage = message || '未知错误';
this.loading = false;
this.$emit('error', message);
},
/**
* 重试加载
*/
retry() {
if (this.loading) {
return;
}
this.error = false;
this.errorMessage = '';
this.initViewer();
},
/**
* 关闭查看器
*/
handleClose() {
if (this.closing) {
return;
}
this.closing = true;
this.$emit('close');
},
/**
* 清理资源
*/
cleanup() {
this.destroyEditor(); this.destroyEditor();
this.cleanupFullscreenListener();
//
if (newConfig.documentUrl) {
this.documentUrl = newConfig.documentUrl;
}
if (newConfig.documentTitle) {
this.documentTitle = newConfig.documentTitle;
}
if (newConfig.mode) {
this.mode = newConfig.mode;
}
// DOM
this.$nextTick(() => {
this.initDocEditor();
});
} }
} }
}; };
</script> </script>
<style scoped lang="scss"> <style scoped>
.onlyoffice-viewer { .onlyoffice-container {
border: 1px solid #e0e0e0;
border-radius: 8px;
overflow: hidden;
background: white;
display: flex;
flex-direction: column;
height: 100%;
}
.viewer-toolbar {
background: #f8f9fa;
padding: 12px 16px;
border-bottom: 1px solid #e0e0e0;
display: flex;
justify-content: space-between;
align-items: center;
flex-shrink: 0;
min-height: 48px;
box-sizing: border-box;
}
.toolbar-left,
.toolbar-right {
display: flex;
align-items: center;
gap: 12px;
flex: 1;
}
.toolbar-right {
justify-content: flex-end;
}
.toolbar-btn {
padding: 6px 12px;
border: 1px solid #ddd;
background: white;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: all 0.2s;
display: inline-flex;
align-items: center;
gap: 4px;
white-space: nowrap;
i {
font-size: 14px;
}
&:hover:not(:disabled) {
background: #f0f0f0;
border-color: #bbb;
}
&:disabled {
opacity: 0.6;
cursor: not-allowed;
}
&.download {
background: #27ae60;
color: white;
border-color: #27ae60;
&:hover:not(:disabled) {
background: #219a52;
border-color: #219a52;
}
}
}
.document-title {
font-weight: 500;
color: #333;
margin-left: 8px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 300px;
flex: 1;
}
.viewer-loading,
.viewer-error {
display: flex;
align-items: center;
justify-content: center;
min-height: 400px;
padding: 40px;
}
.loading-content,
.error-content {
text-align: center;
}
.loading-spinner {
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 16px;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.error-icon {
font-size: 48px;
margin-bottom: 16px;
}
.error-content h3 {
margin: 0 0 8px 0;
color: #e74c3c;
}
.error-content p {
margin: 0 0 16px 0;
color: #666;
}
.retry-btn {
background: #3498db;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
}
.retry-btn:hover {
background: #2980b9;
}
.editor-container {
width: 100%; width: 100%;
flex: 1; height: 90vh;
overflow: hidden; display: flex;
position: relative;
transition: all 0.3s ease;
background: #f5f5f5;
min-height: 200px;
display: block !important;
/* 确保容器始终显示 */
/* 确保 OnlyOffice iframe 正确显示 */
::v-deep iframe {
width: 100% !important;
height: 100% !important;
border: none;
display: block;
}
}
.editor-container.fullscreen {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 9999;
background: white;
border-radius: 0;
}
.mobile-tip {
background: #fff3cd;
border: 1px solid #ffeaa7;
color: #856404;
padding: 8px 16px;
text-align: center;
font-size: 14px;
}
@media (max-width: 768px) {
.viewer-toolbar {
padding: 8px 12px;
flex-direction: column; flex-direction: column;
gap: 8px; }
align-items: stretch;
}
.toolbar-left, .editor-placeholder {
.toolbar-right { width: 100%;
justify-content: space-between; height: 100vh;
} min-height: 600px;
.document-title {
font-size: 14px;
text-align: center;
margin: 4px 0;
}
} }
</style> </style>