likes
comments
collection
share

vue基于onlyoffice实现DOC、DOCX、XLSX、PPT、PDF文档编辑预览

作者站长头像
站长
· 阅读数 655

vue基于onlyoffice实现DOC、DOCX、XLSX、PPT、PDF文档编辑预览

背景

大佬:帮我看看这个复杂exel有什么好的插件预览不?现在用的太丑了。类似这种扔来个陈年老博客... 我:好吧,我看看,于是开启了...我的文档躺坑之路

方案一

kkfileview

太老太丑了直接放弃,还是后端部署的

方案二

  1. Luckysheet
  2. luckysheet-vue3-vite

demo运行一放exel,看起来效果不错,多个sheet、图片、等都支持样式还可以。注意:其实还是有些错乱的,不过可接受范围内。然后屁颠屁颠汇报去了,过几天业务回复:我想在线编辑后给我保存...我瞬间炸裂,于是基于原demo又研究了一下导出exel(想着只要导出个blob传给后端覆盖原文件就可以了嘛,那么简单),不过我错了,原来导出依赖exceljs的具体看demo中的export文件,生成的样式和原来样式压根不一样还要自己处理如图片公式等,导出来的excel和原来的基本不可看,最终这个方案直接放弃了。

方案三

微软在线文档 https://view.officeapps.live.com/op/view.aspx?src=链接

缺点

  • 文件一定是对外公开的(这个直接就pass了)
  • 文件有暴露风险
  • 只能预览

最终方案

开源的onlyoffice 社区版和其他版本功能对比

  • docker部署简单
  • 支持DOC、DOCX、ODT、RTF、TXT、PDF、HTML、EPUB、XPS、DjVu、XLS、XLSX、ODS、CSV、PPT
  • 文件放本地,编辑后回调文件url(需要开发)
  • 可以协同编辑

准备工作

  • 安装docker
# 关闭防火墙
systemctl stop firewalld
systemctl disable firewalld

# 关闭selinux
sed -i 's/enforcing/disabled/' /etc/selinux/config  # 永久
setenforce 0  # 临时

# 关闭swap
swapoff -a  # 临时
sed -ri 's/.*swap.*/#&/' /etc/fstab    # 永久

wget https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo -O /etc/yum.repos.d/docker-ce.repo
yum -y install docker-ce
cat > /etc/docker/daemon.json << EOF
{
"registry-mirrors": ["https://b9pmyelo.mirror.aliyuncs.com"]
}
EOF
systemctl enable docker && systemctl start docker
docker run -i -t -d -p 8089:80 --restart=always onlyoffice/documentserver

注意默认现在我使用7.2版本默认是开启jwt 的,所以打开回提示打开显示文档安全令牌未正确形成,就需要关闭jwt校验,如果不想麻烦后端开发先暂时去掉,后面决定用以后再加上,这个是需要开发的。signature参考,下面是关闭的方法:

## 更改/etc/onlyoffice/documentserver/local.json
docker exec -it [容器id] bin/bash
cd /etc/onlyoffice/documentserver
sed -i 's/true/false/g' local.json
supervisorctl restart all

使用方法

拓展vue2 使用方式

  • loadScript
 const loadScript = async (url, id) => new Promise((resolve, reject) => {
    try {
        if (document.getElementById(id)) {
            if (window.DocsAPI) return resolve(null);

            const intervalHandler = setInterval(() => {
                if (!window.DocsAPI) return;

                clearInterval(intervalHandler);

                return resolve(null);
            }, 500);
        } else {
            const script = document.createElement('script');
            script.setAttribute('type', 'text/javascript');
            script.setAttribute('id', id);

            script.onload = resolve;
            script.onerror = reject;

            script.src = url;
            script.async = true;

            document.body.appendChild(script);
        }
    } catch (e) {
        console.error(e);
    }
});

export default loadScript;

  • 组件
<template>
    <div :class="s.view">
        <div :id="id"></div>
    </div>
</template>

<script>
import loadScript from './loadScript.js';

export default {
    name: 'DocumentEditor',
    props: {
        id: {
            type: String,
            default: '',
        },
        documentServerUrl: {
            type: String,
            default: '',
        },
        config: {
            type: Object,
            default: () => {},
        },
        document_fileType: {
            type: String,
            default: '',
        },
        document_title: {
            type: String,
            default: '',
        },
        documentType: {
            type: String,
            default: '',
        },
        editorConfig_lang: {
            type: String,
            default: '',
        },
        height: {
            type: String,
            default: '',
        },
        type: {
            type: String,
            default: '',
        },
        width: {
            type: String,
            default: '',
        },
        events_onAppReady: {
            type: Function,
            default: () => {},
        },
        events_onDocumentStateChange: {
            type: Function,
            default: () => {},
        },
        events_onMetaChange: {
            type: Function,
            default: () => {},
        },
        events_onDocumentReady: {
            type: Function,
            default: () => {},
        },
        events_onInfo: {
            type: Function,
            default: () => {},
        },
        events_onWarning: {
            type: Function,
            default: () => {},
        },
        events_onError: {
            type: Function,
            default: () => {},
        },
        events_onRequestSharingSettings: {
            type: Function,
            default: () => {},
        },
        events_onRequestRename: {
            type: Function,
            default: () => {},
        },
        events_onMakeActionLink: {
            type: Function,
            default: () => {},
        },
        events_onRequestInsertImage: {
            type: Function,
            default: () => {},
        },
        events_onRequestSaveAs: {
            type: Function,
            default: () => {},
        },
        events_onRequestMailMergeRecipients: {
            type: Function,
            default: () => {},
        },
        events_onRequestCompareFile: {
            type: Function,
            default: () => {},
        },
        events_onRequestEditRights: {
            type: Function,
            default: () => {},
        },
        events_onRequestHistory: {
            type: Function,
            default: () => {},
        },
        events_onRequestHistoryClose: {
            type: Function,
            default: () => {},
        },
        events_onRequestHistoryData: {
            type: Function,
            default: () => {},
        },
        events_onRequestRestore: {
            type: Function,
            default: () => {},
        },
    },
    data() {
        return {};
    },
    watch: {
        config: {
            handler() {
                this.onChangeProps();
            },
            deep: true,
        },
        document_fileType() {
            this.onChangeProps();
        },
        document_title() {
            this.onChangeProps();
        },
        documentType() {
            this.onChangeProps();
        },
        editorConfig_lang() {
            this.onChangeProps();
        },
        height() {
            this.onChangeProps();
        },
        type() {
            this.onChangeProps();
        },
        width() {
            this.onChangeProps();
        },
    },
    mounted() {
        let url = this.documentServerUrl;
        if (!url.endsWith('/')) {
            url += '/';
        }
        const docApiUrl = `${url}web-apps/apps/api/documents/api.js`;
        loadScript(docApiUrl, 'onlyoffice-api-script')
            .then(() => this.onLoad())
            .catch((err) => console.error(err));
    },
    beforeDestroy() {
        const id = this.id || '';
        if (window?.DocEditor?.instances[id]) {
            window.DocEditor.instances[id].destroyEditor();
            window.DocEditor.instances[id] = undefined;
        }
    },
    methods: {
        onLoad() {
            try {
                const id = this.id || '';
                if (!window.DocsAPI) throw new Error('DocsAPI is not defined');
                if (window?.DocEditor?.instances[id]) {
                    console.log('Skip loading. Instance already exists', id);
                    return;
                }
                if (!window?.DocEditor?.instances) {
                    window.DocEditor = { instances: {} };
                }
                const initConfig = {
                    document: {
                        fileType: this.document_fileType,
                        title: this.document_title,
                    },
                    documentType: this.documentType,
                    editorConfig: {
                        lang: this.editorConfig_lang,
                    },
                    events: {
                        onAppReady: this.onAppReady,
                        onDocumentStateChange: this.events_onDocumentStateChange,
                        onMetaChange: this.events_onMetaChange,
                        onDocumentReady: this.events_onDocumentReady,
                        onInfo: this.events_onInfo,
                        onWarning: this.events_onWarning,
                        onError: this.events_onError,
                        onRequestSharingSettings: this.events_onRequestSharingSettings,
                        onRequestRename: this.events_onRequestRename,
                        onMakeActionLink: this.events_onMakeActionLink,
                        onRequestInsertImage: this.events_onRequestInsertImage,
                        onRequestSaveAs: this.events_onRequestSaveAs,
                        onRequestMailMergeRecipients: this.events_onRequestMailMergeRecipients,
                        onRequestCompareFile: this.events_onRequestCompareFile,
                        onRequestEditRights: this.events_onRequestEditRights,
                        onRequestHistory: this.events_onRequestHistory,
                        onRequestHistoryClose: this.events_onRequestHistoryClose,
                        onRequestHistoryData: this.events_onRequestHistoryData,
                        onRequestRestore: this.events_onRequestRestore,
                    },
                    height: this.height,
                    type: this.type,
                    width: this.width,
                    ...(this.config || {}),
                };
                const editor = window.DocsAPI.DocEditor(id, initConfig);
                window.DocEditor.instances[id] = editor;
            } catch (err) {
                console.error(err);
                this.events_onError(err);
            }
        },
        onAppReady() {
            const id = this.id || '';
            this.events_onAppReady(window.DocEditor.instances[id]);
        },
        onChangeProps() {
            const id = this.id || '';
            if (window?.DocEditor?.instances[id]) {
                window.DocEditor.instances[id].destroyEditor();
                window.DocEditor.instances[id] = undefined;

                console.log('Important props have been changed. Load new Editor.');
                this.onLoad();
            }
        },
    },
};
</script>

<style lang="scss" module="s">
.view {
    width: 100%;
    height: 100%;
    iframe {
        width: 100%;
        height: 100%;
    }
}
</style>

配置 总结

配置参考

config.editorConfig.callbackUrl

  • callbackUrl 后端回调地址 具体开发参考 返回{\"error\":0}很关键
  • 后端回调地址,如果填回调地址后端没有实现会就会提示:这份文件无法保存。请检查连接设置或联系你的管理员。
  • java 代码实现参考
public class WebEditor : IHttpHandler
{
    public void ProcessRequest(HttpContext context)
    {
        string body;
        using (var reader = new StreamReader(context.Request.InputStream))
            body = reader.ReadToEnd();

        var fileData = new JavaScriptSerializer().Deserialize<Dictionary<string, object>>(body);
        if ((int) fileData["status"] == 2)
        {
            var req = WebRequest.Create((string) fileData["url"]);

            using (var stream = req.GetResponse().GetResponseStream())
            using (var fs = File.Open(PATH_FOR_SAVE, FileMode.Create))
            {
                var buffer = new byte[4096];
                int readed;
                while ((readed = stream.Read(buffer, 0, 4096)) != 0)
                    fs.Write(buffer, 0, readed);
            }
        }
        context.Response.Write("{\"error\":0}");
    }
}
  • config.editorConfig.autosave 编辑后是否自动保存
  • config.editorConfig.forcesave 回影响回调,true 一保存就会回调
  • document.permissions.edit false预览文件,true可编辑 拓展:如果是浏览器传过来的参数可以用这个方式转化false: JSON.parse('false')

总结

最后分享的躺坑之路就到这里啦!!!具体实现参考onlyoffice官网很详细的

转载自:https://juejin.cn/post/7166197009291378702
评论
请登录