liding
8 小时以前 49cdc0884a9a4381cd968cffff65e2a332bc675a
src/components/Onlyoffice/onlyoffice.vue
@@ -8,13 +8,39 @@
      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;
@@ -26,14 +52,23 @@
    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);
    });
    // 加载失败后允许下次重试
@@ -47,21 +82,18 @@
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: {
@@ -69,41 +101,24 @@
      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() {
@@ -114,19 +129,32 @@
  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: {
          //后缀
@@ -149,6 +177,7 @@
          //定制
          customization: {
            autosave: true, //是否自动保存
            forcesave: true, //点击保存时立即回调业务服务
            chat: true,
            comments: false,
            help: false,
@@ -164,20 +193,54 @@
        },
        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) {
@@ -253,10 +316,36 @@
<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;