| | |
| | | class="native-pdf-preview" |
| | | title="PDF预览" |
| | | ></iframe> |
| | | <div v-else id="vabOnlyOffice"></div> |
| | | <template v-else> |
| | | <div :id="editorId"></div> |
| | | <div v-if="editorWarning" class="editor-warning">{{ editorWarning }}</div> |
| | | <div v-if="editorState !== 'ready'" class="editor-status"> |
| | | <template v-if="editorState === 'failed'"> |
| | | <p>{{ editorError }}</p> |
| | | <el-button type="primary" @click="setEditor(option)">重新加载</el-button> |
| | | </template> |
| | | <span v-else>正在连接文档服务并加载报告...</span> |
| | | </div> |
| | | </template> |
| | | </div> |
| | | </template> |
| | | |
| | | <script> |
| | | // OnlyOffice 文档服务地址,按环境配置;脚本在真正需要预览时才加载 |
| | | const OFFICE_API_BASE = import.meta.env.VITE_APP_ONLYOFFICE_API || "http://192.168.21.53:9001"; |
| | | |
| | | function normalizeOptions(source = {}) { |
| | | return { |
| | | url: source.url || "", |
| | | isEdit: source.isEdit === true || source.isEdit === "true", |
| | | fileType: source.fileType || "", |
| | | title: source.title || "", |
| | | lang: source.lang || "zh-CN", |
| | | isPrint: source.isPrint !== false, |
| | | user: { id: source.user_id, name: source.user_name }, |
| | | editUrl: source.editUrl || "", |
| | | key: source.key || "", |
| | | model: source.model, |
| | | token: source.token |
| | | }; |
| | | } |
| | | |
| | | // 多个页面共用编辑器,api.js 只加载一次 |
| | | let docsApiPromise = null; |
| | |
| | | docsApiPromise = new Promise((resolve, reject) => { |
| | | const script = document.createElement("script"); |
| | | script.src = OFFICE_API_BASE.replace(/\/$/, "") + "/web-apps/apps/api/documents/api.js"; |
| | | const timeout = window.setTimeout(() => { |
| | | script.remove(); |
| | | reject(new Error("OnlyOffice 文档服务连接超时")); |
| | | }, 15000); |
| | | script.onload = () => { |
| | | window.clearTimeout(timeout); |
| | | if (window.DocsAPI) { |
| | | resolve(window.DocsAPI); |
| | | } else { |
| | | reject(new Error("api.js 加载完成但未注册 DocsAPI")); |
| | | } |
| | | }; |
| | | script.onerror = () => reject(new Error("OnlyOffice 文档服务脚本加载失败:" + script.src)); |
| | | script.onerror = () => { |
| | | window.clearTimeout(timeout); |
| | | script.remove(); |
| | | reject(new Error("OnlyOffice 文档服务脚本加载失败")); |
| | | }; |
| | | document.head.appendChild(script); |
| | | }); |
| | | // 加载失败后允许下次重试 |
| | |
| | | export default { |
| | | name: "VabOnlyOffice", |
| | | props: ['options'], |
| | | emits: ['ready', 'error'], |
| | | data() { |
| | | return { |
| | | editorId: `onlyoffice-${this._uid || Math.random().toString(36).slice(2)}`, |
| | | doctype: "", |
| | | docEditor: null, |
| | | //参考vabOnlyOffice组件参数配置 |
| | | option: { |
| | | url: "", |
| | | isEdit: false, |
| | | fileType: "", |
| | | title: "", |
| | | lang: "zh-CN", |
| | | isPrint: true, |
| | | user: {}, |
| | | editUrl: "" |
| | | } |
| | | editorGeneration: 0, |
| | | editorState: 'loading', |
| | | editorError: '', |
| | | editorWarning: '', |
| | | readyTimer: null, |
| | | option: normalizeOptions(this.options || this.$route.query) |
| | | }; |
| | | }, |
| | | computed: { |
| | |
| | | return this.isPdfFile(this.option); |
| | | } |
| | | }, |
| | | created() { |
| | | if(this.options){ |
| | | const option = this.options |
| | | this.option.url = option.url |
| | | this.option.isEdit = option.isEdit === "true" ? true : false |
| | | this.option.fileType = option.fileType |
| | | this.option.title = option.title |
| | | this.option.lang = option.lang |
| | | this.option.isPrint = option.isPrint |
| | | this.option.user.id = option.user_id |
| | | this.option.user.name = option.user_name |
| | | this.option.editUrl = option.editUrl |
| | | }else{ |
| | | const option = this.$route.query |
| | | this.option.url = option.url |
| | | this.option.isEdit = option.isEdit === "true" ? true : false |
| | | this.option.fileType = option.fileType |
| | | this.option.title = option.title |
| | | this.option.lang = option.lang |
| | | this.option.isPrint = option.isPrint |
| | | this.option.user.id = option.user_id |
| | | this.option.user.name = option.user_name |
| | | this.option.editUrl = option.editUrl |
| | | } |
| | | }, |
| | | beforeUnmount() { |
| | | this.editorGeneration++; |
| | | window.clearTimeout(this.readyTimer); |
| | | this.destroyEditor(); |
| | | }, |
| | | watch: { |
| | | option: { |
| | | handler: function(n) { |
| | | this.setEditor(n); |
| | | this.doctype = this.getFileType(n.fileType); |
| | | options: { |
| | | handler(newOptions) { |
| | | this.option = normalizeOptions(newOptions || this.$route.query); |
| | | this.setEditor(this.option); |
| | | }, |
| | | deep: true |
| | | }, |
| | | '$route.query'(query) { |
| | | if (!this.options) { |
| | | this.option = normalizeOptions(query); |
| | | this.setEditor(this.option); |
| | | } |
| | | } |
| | | }, |
| | | mounted() { |
| | |
| | | methods: { |
| | | destroyEditor() { |
| | | if (this.docEditor !== null) { |
| | | this.docEditor.destroyEditor(); |
| | | try { |
| | | this.docEditor.destroyEditor(); |
| | | } catch (error) { |
| | | console.error('销毁 OnlyOffice 编辑器失败', error); |
| | | } |
| | | this.docEditor = null; |
| | | } |
| | | }, |
| | | async setEditor(option) { |
| | | const generation = ++this.editorGeneration; |
| | | window.clearTimeout(this.readyTimer); |
| | | this.destroyEditor(); |
| | | // PDF 使用浏览器原生预览;其他文档才初始化 OnlyOffice。 |
| | | if (!option || !option.url || this.isPdfFile(option)) { |
| | | return; |
| | | } |
| | | this.editorState = 'loading'; |
| | | this.editorError = ''; |
| | | this.editorWarning = ''; |
| | | // 等待 v-else 中的 OnlyOffice 容器完成渲染。 |
| | | await this.$nextTick(); |
| | | this.doctype = this.getFileType(option.fileType); |
| | | if (!this.doctype) { |
| | | this.failEditor('报告文件格式不受支持', generation); |
| | | return; |
| | | } |
| | | let config = { |
| | | document: { |
| | | //后缀 |
| | |
| | | //定制 |
| | | customization: { |
| | | autosave: true, //是否自动保存 |
| | | forcesave: true, //点击保存时立即回调业务服务 |
| | | chat: true, |
| | | comments: false, |
| | | help: false, |
| | |
| | | }, |
| | | width: "100%", |
| | | height: "100%", |
| | | token: option.token || "" |
| | | token: option.token || "", |
| | | events: { |
| | | onDocumentReady: () => { |
| | | if (generation === this.editorGeneration) { |
| | | window.clearTimeout(this.readyTimer); |
| | | this.editorState = 'ready'; |
| | | this.$emit('ready'); |
| | | } |
| | | }, |
| | | onError: (event) => { |
| | | const detail = event && event.data && (event.data.errorDescription || event.data.errorCode); |
| | | this.failEditor(detail ? `报告加载失败:${detail}` : '报告加载失败,请检查文档服务', generation); |
| | | } |
| | | } |
| | | }; |
| | | |
| | | try { |
| | | await loadDocsApi(); |
| | | } catch (error) { |
| | | console.error(error); |
| | | this.$message.error("文档服务暂时不可用,无法预览"); |
| | | this.failEditor(error.message || '文档服务暂时不可用', generation); |
| | | return; |
| | | } |
| | | if (this._isDestroyed) { |
| | | if (generation !== this.editorGeneration || !this.$el?.isConnected) { |
| | | return; |
| | | } |
| | | this.docEditor = new window.DocsAPI.DocEditor("vabOnlyOffice", config); |
| | | try { |
| | | this.readyTimer = window.setTimeout(() => { |
| | | this.failEditor('报告加载超时,请检查文档服务与报告地址', generation); |
| | | }, 30000); |
| | | this.docEditor = new window.DocsAPI.DocEditor(this.editorId, config); |
| | | } catch (error) { |
| | | window.clearTimeout(this.readyTimer); |
| | | console.error(error); |
| | | this.failEditor('文档编辑器初始化失败,请重试', generation); |
| | | } |
| | | }, |
| | | failEditor(message, generation) { |
| | | if (generation !== this.editorGeneration) return; |
| | | window.clearTimeout(this.readyTimer); |
| | | if (this.editorState === 'ready') { |
| | | this.editorWarning = String(message); |
| | | this.$emit('error', this.editorWarning); |
| | | return; |
| | | } |
| | | this.editorError = String(message); |
| | | this.editorState = 'failed'; |
| | | this.$emit('error', this.editorError); |
| | | }, |
| | | isPdfFile(option) { |
| | | if (!option) { |
| | |
| | | <style scoped> |
| | | .document-preview, |
| | | .native-pdf-preview, |
| | | #vabOnlyOffice { |
| | | .document-preview > div { |
| | | width: 100%; |
| | | height: 100%; |
| | | } |
| | | .document-preview { |
| | | position: relative; |
| | | } |
| | | .editor-status { |
| | | position: absolute; |
| | | inset: 0; |
| | | z-index: 2; |
| | | display: flex; |
| | | flex-direction: column; |
| | | justify-content: center; |
| | | align-items: center; |
| | | gap: 12px; |
| | | background: #fff; |
| | | color: #606266; |
| | | } |
| | | .editor-warning { |
| | | position: absolute; |
| | | top: 0; |
| | | left: 0; |
| | | right: 0; |
| | | z-index: 2; |
| | | height: auto !important; |
| | | padding: 8px 12px; |
| | | background: #fff3e0; |
| | | color: #a35d00; |
| | | } |
| | | .native-pdf-preview { |
| | | display: block; |
| | | border: 0; |