483 lines
12 KiB
Vue
483 lines
12 KiB
Vue
<template>
|
||
<div class="onlyoffice-container">
|
||
<!-- 加载状态 -->
|
||
<div v-if="loading" class="loading-state">
|
||
<div class="loading-spinner"></div>
|
||
<p>正在加载文档...</p>
|
||
</div>
|
||
|
||
<!-- 错误状态 -->
|
||
<div v-if="error" class="error-state">
|
||
<div class="error-icon">❌</div>
|
||
<h3>加载失败</h3>
|
||
<p>{{ error }}</p>
|
||
<button @click="retry" class="retry-btn">重试</button>
|
||
</div>
|
||
|
||
<!-- 编辑器容器(始终存在,通过样式控制显示) -->
|
||
<div v-show="!loading && !error" id="placeholder" class="editor-placeholder"></div>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
import { getConfigAPI, generateCallbackTokenAPI } from '@/api/common/onlyOfficeViewer'
|
||
export default {
|
||
name: 'OnlyOfficeViewer',
|
||
props: {
|
||
// 可以添加 props 来动态配置
|
||
documentUrl: {
|
||
type: String,
|
||
default: 'http://192.168.0.14:9090/smart-bid/technicalSolutionDatabase/2025/11/03/716d9f3d89434c56bc49296dbbccc226.docx'
|
||
},
|
||
documentTitle: {
|
||
type: String,
|
||
default: '716d9f3d89434c56bc49296dbbccc226.docx'
|
||
},
|
||
documentKey: {
|
||
type: String,
|
||
default: '1'
|
||
},
|
||
fileName: {
|
||
type: String,
|
||
default: 'technicalSolutionDatabase/2025/11/03/716d9f3d89434c56bc49296dbbccc226.docx'
|
||
},
|
||
mode: {
|
||
type: String,
|
||
default: 'view', // 'view' 或 'edit'
|
||
validator: (value) => ['view', 'edit'].includes(value)
|
||
},
|
||
type: {
|
||
type: String,
|
||
default: 'desktop', // 'desktop', 'mobile', 'embedded'
|
||
validator: (value) => ['desktop', 'mobile', 'embedded'].includes(value)
|
||
}
|
||
},
|
||
|
||
|
||
data() {
|
||
return {
|
||
docEditor: null,
|
||
onlyOfficeScriptLoaded: false,
|
||
editorConfig: null,
|
||
configReady: false,
|
||
loading: false,
|
||
error: null
|
||
};
|
||
},
|
||
async mounted() {
|
||
try {
|
||
this.loading = true;
|
||
this.error = null;
|
||
if(this.docEditor){
|
||
this.destroyEditor();
|
||
}
|
||
// 先加载配置
|
||
await this.getConfig();
|
||
// 再初始化 OnlyOffice(成功后会自动设置 loading = false)
|
||
await this.initOnlyOffice();
|
||
} catch (error) {
|
||
console.error('初始化失败:', error);
|
||
this.loading = false;
|
||
this.error = error.message || '初始化失败';
|
||
this.$emit('error', error);
|
||
}
|
||
},
|
||
|
||
beforeDestroy() {
|
||
this.destroyEditor();
|
||
},
|
||
|
||
methods: {
|
||
// 加载编辑器配置
|
||
async getConfig() {
|
||
try {
|
||
console.log('开始获取编辑器配置...', {
|
||
fileId: this.documentKey,
|
||
mode: this.mode,
|
||
type: this.type
|
||
});
|
||
|
||
const res = await getConfigAPI({
|
||
fileId: this.documentKey,
|
||
mode: this.mode,
|
||
type: this.type
|
||
});
|
||
|
||
console.log('获取配置响应:', res);
|
||
|
||
if (res.code !== 200) {
|
||
throw new Error(res.msg || '获取编辑器配置失败');
|
||
}
|
||
if (!res.data) {
|
||
throw new Error('配置数据为空');
|
||
}
|
||
|
||
// 验证必要的配置字段
|
||
if (!res.data.document || !res.data.document.url) {
|
||
throw new Error('配置中缺少文档URL');
|
||
}
|
||
|
||
// 设置配置,添加事件回调
|
||
this.editorConfig = {
|
||
...res.data,
|
||
events: {
|
||
onDocumentReady: () => this.onDocumentReady(),
|
||
onError: (error) => this.onEditorError(error),
|
||
onAppReady: () => this.onAppReady(),
|
||
}
|
||
};
|
||
|
||
this.configReady = true;
|
||
console.log('编辑器配置准备完成:', this.editorConfig);
|
||
|
||
} catch (error) {
|
||
console.error('获取编辑器配置失败:', error);
|
||
this.error = error.message || '获取编辑器配置失败';
|
||
throw error;
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 初始化 OnlyOffice
|
||
*/
|
||
async initOnlyOffice() {
|
||
try {
|
||
// 确保配置已准备好
|
||
if (!this.configReady || !this.editorConfig) {
|
||
throw new Error('编辑器配置未准备好,请先加载配置');
|
||
}
|
||
|
||
// 加载 OnlyOffice 脚本
|
||
console.log('开始加载 OnlyOffice 脚本...');
|
||
await this.loadOnlyOfficeScript();
|
||
console.log('OnlyOffice 脚本加载完成');
|
||
|
||
// 等待 DOM 准备好
|
||
await this.$nextTick();
|
||
|
||
// 初始化编辑器
|
||
await this.initDocEditor();
|
||
} catch (error) {
|
||
console.error('初始化 OnlyOffice 失败:', error);
|
||
this.error = error.message || '初始化 OnlyOffice 失败';
|
||
this.$emit('error', error);
|
||
throw error;
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 加载 OnlyOffice 脚本
|
||
*/
|
||
loadOnlyOfficeScript() {
|
||
return new Promise((resolve, reject) => {
|
||
// 如果已经加载,直接返回
|
||
if (window.DocsAPI) {
|
||
this.onlyOfficeScriptLoaded = true;
|
||
resolve();
|
||
return;
|
||
}
|
||
|
||
// 检查是否正在加载
|
||
const existingScript = document.querySelector('script[data-onlyoffice]');
|
||
if (existingScript) {
|
||
existingScript.addEventListener('load', () => {
|
||
this.onlyOfficeScriptLoaded = true;
|
||
resolve();
|
||
});
|
||
existingScript.addEventListener('error', reject);
|
||
return;
|
||
}
|
||
|
||
// 创建新的脚本元素
|
||
const script = document.createElement('script');
|
||
// onlyOfficeUrl 是 OnlyOffice 服务地址
|
||
const onlyOfficeUrl = process.env.VUE_APP_ONLYOFFICE_URL || process.env.VUE_APP_BASE_API || '';
|
||
|
||
if (!onlyOfficeUrl) {
|
||
reject(new Error('OnlyOffice 服务地址未配置,请检查环境变量 VUE_APP_ONLYOFFICE_URL'));
|
||
return;
|
||
}
|
||
|
||
const scriptUrl = `${onlyOfficeUrl}/web-apps/apps/api/documents/api.js`;
|
||
console.log('加载 OnlyOffice 脚本,URL:', scriptUrl);
|
||
script.src = scriptUrl;
|
||
script.setAttribute('data-onlyoffice', 'true');
|
||
|
||
script.onload = () => {
|
||
this.onlyOfficeScriptLoaded = true;
|
||
// 等待 API 完全初始化
|
||
setTimeout(() => {
|
||
if (window.DocsAPI) {
|
||
resolve();
|
||
} else {
|
||
reject(new Error('OnlyOffice API 未正确加载'));
|
||
}
|
||
}, 100);
|
||
};
|
||
|
||
script.onerror = () => {
|
||
reject(new Error('加载 OnlyOffice 脚本失败'));
|
||
};
|
||
|
||
document.head.appendChild(script);
|
||
});
|
||
},
|
||
|
||
/**
|
||
* 初始化文档编辑器
|
||
*/
|
||
async initDocEditor() {
|
||
// 验证 DocsAPI
|
||
if (!window.DocsAPI || !window.DocsAPI.DocEditor) {
|
||
throw new Error('OnlyOffice DocsAPI 未正确加载,请检查脚本是否已加载');
|
||
}
|
||
|
||
// 验证配置
|
||
if (!this.editorConfig) {
|
||
throw new Error('编辑器配置不存在');
|
||
}
|
||
|
||
// 验证容器元素(容器可能被 v-show 隐藏,但 DOM 中应该存在)
|
||
await this.$nextTick();
|
||
let container = document.getElementById('placeholder');
|
||
|
||
// 如果容器不存在,等待一下再试
|
||
if (!container) {
|
||
console.warn('容器元素不存在,等待 DOM 渲染...');
|
||
await new Promise(resolve => setTimeout(resolve, 100));
|
||
await this.$nextTick();
|
||
container = document.getElementById('placeholder');
|
||
}
|
||
|
||
if (!container) {
|
||
throw new Error('找不到编辑器容器元素 #placeholder,请确保容器已正确渲染');
|
||
}
|
||
|
||
// 即使容器被 v-show 隐藏,也要强制显示以便创建编辑器
|
||
container.style.display = 'block';
|
||
container.style.visibility = 'visible';
|
||
container.style.width = '100%';
|
||
container.style.height = '100%';
|
||
|
||
console.log('容器元素:', container);
|
||
console.log('容器尺寸:', {
|
||
width: container.offsetWidth,
|
||
height: container.offsetHeight,
|
||
rect: container.getBoundingClientRect()
|
||
});
|
||
|
||
// 确保容器有尺寸
|
||
if (container.offsetWidth === 0 || container.offsetHeight === 0) {
|
||
console.warn('容器尺寸为0,等待尺寸调整...');
|
||
await new Promise(resolve => setTimeout(resolve, 300));
|
||
|
||
if (container.offsetWidth === 0 || container.offsetHeight === 0) {
|
||
console.warn('容器尺寸仍为0,但继续尝试创建编辑器');
|
||
}
|
||
}
|
||
|
||
try {
|
||
console.log('创建 OnlyOffice 编辑器,配置:', this.editorConfig);
|
||
|
||
// 清空容器
|
||
container.innerHTML = '';
|
||
|
||
// 创建编辑器实例
|
||
this.docEditor = new window.DocsAPI.DocEditor('placeholder', this.editorConfig);
|
||
|
||
console.log('编辑器实例创建成功:', this.docEditor);
|
||
|
||
// 编辑器创建成功,隐藏加载状态
|
||
this.loading = false;
|
||
this.$emit('initialized', this.docEditor);
|
||
} catch (error) {
|
||
console.error('创建 OnlyOffice 编辑器失败:', error);
|
||
console.error('错误详情:', {
|
||
message: error.message,
|
||
stack: error.stack,
|
||
config: this.editorConfig
|
||
});
|
||
this.loading = false;
|
||
this.error = error.message || '创建编辑器失败';
|
||
this.$emit('error', error);
|
||
throw error;
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 文档准备就绪回调
|
||
*/
|
||
onDocumentReady() {
|
||
console.log("文档准备好了");
|
||
this.$emit('document-ready');
|
||
},
|
||
|
||
/**
|
||
* 应用准备就绪回调
|
||
*/
|
||
onAppReady() {
|
||
console.log("OnlyOffice 应用准备就绪");
|
||
this.$emit('app-ready');
|
||
},
|
||
|
||
/**
|
||
* 编辑器错误回调
|
||
*/
|
||
onEditorError(error) {
|
||
console.error("OnlyOffice 错误:", error);
|
||
this.$emit('error', error);
|
||
},
|
||
|
||
/**
|
||
* 销毁编辑器
|
||
*/
|
||
destroyEditor() {
|
||
if (this.docEditor) {
|
||
try {
|
||
this.docEditor.destroyEditor()
|
||
} catch (error) {
|
||
console.warn('销毁编辑器时出错:', error);
|
||
}
|
||
}
|
||
this.docEditor = null;
|
||
},
|
||
|
||
/**
|
||
* 重新加载文档
|
||
*/
|
||
async reloadDocument(newConfig = {}) {
|
||
this.destroyEditor();
|
||
this.configReady = false;
|
||
this.error = null;
|
||
|
||
// 可以在这里更新配置
|
||
if (newConfig.documentUrl) {
|
||
this.documentUrl = newConfig.documentUrl;
|
||
}
|
||
if (newConfig.documentTitle) {
|
||
this.documentTitle = newConfig.documentTitle;
|
||
}
|
||
if (newConfig.mode) {
|
||
this.mode = newConfig.mode;
|
||
}
|
||
|
||
// 重新加载配置并初始化
|
||
try {
|
||
this.loading = true;
|
||
await this.getConfig();
|
||
await this.initDocEditor();
|
||
} catch (error) {
|
||
console.error('重新加载文档失败:', error);
|
||
this.error = error.message || '重新加载失败';
|
||
} finally {
|
||
this.loading = false;
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 重试
|
||
*/
|
||
async retry() {
|
||
this.error = null;
|
||
this.loading = true;
|
||
try {
|
||
await this.getConfig();
|
||
await this.initOnlyOffice();
|
||
} catch (error) {
|
||
console.error('重试失败:', error);
|
||
this.error = error.message || '重试失败';
|
||
} finally {
|
||
this.loading = false;
|
||
}
|
||
}
|
||
}
|
||
};
|
||
</script>
|
||
|
||
<style scoped>
|
||
.onlyoffice-container {
|
||
width: 100%;
|
||
height: 90vh;
|
||
display: flex;
|
||
flex-direction: column;
|
||
position: relative;
|
||
}
|
||
|
||
.editor-placeholder {
|
||
width: 100%;
|
||
height: 100%;
|
||
min-height: 600px;
|
||
}
|
||
|
||
.loading-state,
|
||
.error-state {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
height: 100%;
|
||
min-height: 400px;
|
||
text-align: center;
|
||
padding: 40px;
|
||
}
|
||
|
||
.loading-spinner {
|
||
width: 40px;
|
||
height: 40px;
|
||
border: 4px solid #f3f3f3;
|
||
border-top: 4px solid #3498db;
|
||
border-radius: 50%;
|
||
animation: spin 1s linear infinite;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
@keyframes spin {
|
||
0% {
|
||
transform: rotate(0deg);
|
||
}
|
||
|
||
100% {
|
||
transform: rotate(360deg);
|
||
}
|
||
}
|
||
|
||
.error-icon {
|
||
font-size: 48px;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.error-state h3 {
|
||
margin: 0 0 8px 0;
|
||
color: #e74c3c;
|
||
font-size: 18px;
|
||
}
|
||
|
||
.error-state p {
|
||
margin: 0 0 16px 0;
|
||
color: #666;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.retry-btn {
|
||
background: #3498db;
|
||
color: white;
|
||
border: none;
|
||
padding: 8px 16px;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
font-size: 14px;
|
||
transition: background 0.3s;
|
||
}
|
||
|
||
.retry-btn:hover {
|
||
background: #2980b9;
|
||
}
|
||
|
||
/* 确保 OnlyOffice iframe 正确显示 */
|
||
.editor-placeholder ::v-deep iframe {
|
||
width: 100% !important;
|
||
height: 100% !important;
|
||
border: none;
|
||
display: block;
|
||
}
|
||
</style> |