Springboot+Vue2整合onlyoffice实现文档在线协同编辑
目录
- docker部署onlyoffice镜像
- vue2整合onlyoffice
- springboot回调接口配置
1.docker部署onlyoffice
# 使用docker拉取并启动onlyoffice镜像docker run -itd--name onlyoffice -p10086:80 -eJWT_ENABLED=true -eJWT_SECRET=mlzhilu_secret onlyoffice/documentserver:8.0
注意:
- 自7.2版本以后,onlyoffice默认开启jwt,可以手动设置JWT_ENABLED=false以关闭jwt校验,但是关闭jwt校验可能导致链接不安全,本文默认使用8.0版本
- 如果启用jwt校验的话,需要手动设置secret,否则onlyoffice会随机生成secret,这种情况下就需要等容器启动后到容器内部的
/etc/onlyoffice/documentserver/local.json
文件中查看 - documentserver服务默认使用http链接,因此内部端口为80,同时也支持https链接,内部端口443,如需开启https,需要手动添加相应环境变量并生成SSL证书(请自行了解)
2.vue2整合onlyoffice
- (1)新建vue页面
onlyoffice/index.vue
# onlyoffice/index.vue文件内容
<template>
<div style="overflow: scroll;height: calc(100vh - 84px);">
<div :id="onlineEditorId"></div>
</div>
</template>
<script>
import {getOnlyOfficeConfig} from "@/api/documents/menu";
export default {
name: "OnlineEditor",
data() {
return {
// 文档ID
documentId: '',
// 文档版本号
versionId: '',
// 打开文件的方式,true-编辑模式,false-预览模式
isEdit: true,
docEditor: null,
onlineEditorId: 'onlineEditor',
}
},
watch: {
documentId: {
handler: function () {
this.loadScript();
this.initEditor();
},
deep: true,
},
},
activated() {
if (this.documentId) {
this.loadScript();
this.initEditor();
}
},
created() {
// 从路由中获取参数
const documentId = this.$route.query.documentId;
const versionId = this.$route.query.versionId;
const isEdit = this.$route.query.isEdit;
if (versionId) this.versionId = versionId;
if (isEdit) this.isEdit = isEdit;
if (documentId) {
this.documentId = documentId;
this.onlineEditorId += this.documentId;
this.loadScript();
this.initEditor();
}
},
methods: {
// 动态加载onlyoffice api脚本
async loadScript(){
const scriptId = "script_"+this.documentId;
if (document.getElementById(scriptId)===null){
const script = document.createElement('script')
script.id = scriptId;
script.src = "http://10.49.47.24:10086/web-apps/apps/api/documents/api.js"
script.type = "text/javascript"
document.head.appendChild(script);
}
},
// 初始化onlyoffice编辑器
async initEditor(){
const scriptId = "script_"+this.documentId;
if (document.getElementById(scriptId)===null){
await this.loadScript();
}
// 保证每次刷新页面时重新加载onlyoffice对象,避免缓存问题
if (this.docEditor){
this.docEditor.destroyEditor();
this.docEditor = null;
}
const param = {
documentId: this.documentId,
versionId: this.versionId,
isEdit: this.isEdit
}
// 从后端获取onlyoffice配置,避免配置被修改
await getOnlyOfficeConfig(param).then(res=>{
let data = res.data;
this.docEditor = new window.DocsAPI.DocEditor(this.onlineEditorId, data);
})
},
},
// 关闭页面销毁onlyoffice对象
beforeDestroy() {
if (this.docEditor){
this.docEditor.destroyEditor();
this.docEditor = null;
}
}
}
</script>
<style scoped lang="scss">
</style>
- (2)父组件页面路由调用
# 通过点击时间出发路由跳转,并传递参数
handleEdit(item){const route =this.$router.resolve({path:"/components/edit/office",query:{documentId: item.id,isEdit:true},});// 在新窗口打开页面
window.open(route.href,"_blank");},
3.SpringBoot回调接口配置
为了保证onlyoffice配置不被修改,我这里将onlyoffice配置信息通过后端接口的形式获取,这里将onlyoffice配置信息配置在SpringBoot的配置文件中,如果不需要的话可以将这些配置直接写在前端的js代码中。
- (1) 在配置文件(如:application.yml或application.properties)中添加如下配置
# onlyoffice配置only-office:secret: devops_20240521
config:document:# 文档下载接口,这个接口需要在springboot后端中实现url: http://10.49.47.24:10010/dev-api/documents/only/office/download
permissions:# 是否可以编辑edit:trueprint:falsedownload:true# 是否可以填写表格,如果将mode参数设置为edit,则填写表单仅对文档编辑器可用。 默认值与edit或review参数的值一致。fillForms:false# 跟踪变化review:trueeditorConfig:# onlyoffice回调接口,这个接口也需要在springboot后端中实现callbackUrl: http://10.49.47.24:10010/dev-api/documents/only/office/callbackToSaveFile
lang: zh-CN
coEditing:mode: fast,change:true# 定制化配置customization:forcesave:trueautosave:falsecomments:truecompactHeader:falsecompactToolbar:falsecompatibleFeatures:falsecustomer:address: 中国北京市海淀区
info: xxxxx文档在线写作平台
logo: https://example.com/logo-big.png
logoDark: https://example.com/dark-logo-big.png
mail: [email protected]
name: xxxxx平台
phone:123456789www: www.example.com
features:# 是否开启拼写检查spellcheck:mode:truechange:trueregion: zh-CN
type: desktop
- (2)OnlyOfficeConfig配置类
# OnlyOfficeConfig.java内容
/**
* onlyOffice配置
* 这里的配置会从 application.yml或application.properties 中读取
*/@Data@AllArgsConstructor@NoArgsConstructor@Configuration@ConfigurationProperties(prefix ="only-office")publicclassOnlyOfficeConfigimplementsSerializable{privatestaticfinallong serialVersionUID =1L;privateString secret;privateConfig config;@Data@AllArgsConstructor@NoArgsConstructorpublicstaticclassConfigimplementsSerializable{privatestaticfinallong serialVersionUID =1L;privateDocument document;privateEditorConfig editorConfig;privateString type;privateString token;privateString documentType;privateString height ="100%";privateString width ="100%";@Data@AllArgsConstructor@NoArgsConstructorpublicstaticclassDocumentimplementsSerializable{privatestaticfinallong serialVersionUID =1L;privateString title;privateString fileType;privateString key;privateString url;privatePermissions permissions;@Data@AllArgsConstructor@NoArgsConstructorpublicstaticclassPermissionsimplementsSerializable{privatestaticfinallong serialVersionUID =1L;privateBoolean edit;privateBoolean print;privateBoolean download;privateBoolean fillForms;privateBoolean review;}}@Data@AllArgsConstructor@NoArgsConstructorpublicstaticclassEditorConfigimplementsSerializable{privatestaticfinallong serialVersionUID =1L;privateString callbackUrl;privateString lang;privateCoEditing coEditing;privateCustomization customization;privateString region;privateUser user;publicUsergetUser(){returnStringUtils.isNull(user)?newUser():user;}@Data@AllArgsConstructor@NoArgsConstructorpublicstaticclassCoEditingimplementsSerializable{privatestaticfinallong serialVersionUID =1L;privateString mode;privateBoolean change;}@Data@AllArgsConstructor@NoArgsConstructorpublicstaticclassCustomizationimplementsSerializable{privatestaticfinallong serialVersionUID =1L;privateBoolean forcesave;privateBoolean autosave;privateBoolean comments;privateBoolean compactHeader;privateBoolean compactToolbar;privateBoolean compatibleFeatures;privateCustomer customer;privateFeatures features;@Data@AllArgsConstructor@NoArgsConstructorpublicstaticclassCustomerimplementsSerializable{privatestaticfinallong serialVersionUID =1L;privateString address;privateString info;privateString logo;privateString logoDark;privateString mail;privateString name;privateString phone;privateString www;}@Data@AllArgsConstructor@NoArgsConstructorpublicstaticclassFeaturesimplementsSerializable{privatestaticfinallong serialVersionUID =1L;privateSpellcheck spellcheck;@Data@AllArgsConstructor@NoArgsConstructorpublicstaticclassSpellcheckimplementsSerializable{privatestaticfinallong serialVersionUID =1L;privateBoolean mode;privateBoolean change;}}}@Data@AllArgsConstructor@NoArgsConstructorpublicstaticclassUserimplementsSerializable{privatestaticfinallong serialVersionUID =1L;privateString id;privateString name;privateString image;privateString group;}}}}
- (3)Controller接口
这里需要注意的是:在对onlyoffice配置进行jwt加密时需要用到一个依赖
prime-jwt
,坐标如下:
<dependency><groupId>com.inversoft</groupId><artifactId>prime-jwt</artifactId><version>1.3.1</version></dependency>
# OnlyOfficeController.java内容
/**
* onlyoffice接口类
*/@RestController@RequestMapping("/only/office")publicclassOnlyOfficeController{@ResourceprivateOnlyOfficeConfig onlyOfficeConfig;@ResourceprivateOnlyOfficeServiceImpl onlyOfficeService;privatestaticfinalHashMap<String,List<String>> extensionMap =newHashMap<>();// 初始化扩展名映射static{
extensionMap.put("word",Arrays.asList("doc","docm","docx","docxf","dot","dotm","dotx","epub","fb2","fodt","htm","html","mht","mhtml","odt","oform","ott","rtf","stw","sxw","txt","wps","wpt","xml"));
extensionMap.put("cell",Arrays.asList("csv","et","ett","fods","ods","ots","sxc","xls","xlsb","xlsm","xlsx","xlt","xltm","xltx","xml"));
extensionMap.put("slide",Arrays.asList("dps","dpt","fodp","odp","otp","pot","potm","potx","pps","ppsm","ppsx","ppt","pptm","pptx","sxi"));
extensionMap.put("pdf",Arrays.asList("djvu","oxps","pdf","xps"));}/**
* onlyoffice回调接口,这个接口的内容基本不需要修改,
* 只需要修改 onlyOfficeService.handleCallbackResponse(callBackResponse);
* 及其方法中的业务逻辑即可
*/@PostMapping(value ="/callbackToSaveFile")publicvoidcallbackToSaveFile(HttpServletRequest request,HttpServletResponse response)throwsIOException{PrintWriter writer = response.getWriter();Scanner scanner =newScanner(request.getInputStream()).useDelimiter("\\A");String body = scanner.hasNext()? scanner.next():"";CallBackResponse callBackResponse =JSONObject.parseObject(body,CallBackResponse.class);// 只需要修改这行代码及其业务逻辑即可
onlyOfficeService.handleCallbackResponse(callBackResponse);
writer.write("{\"error\":0}");}/**
* 文档下载接口
*/@GetMapping("/download")publicvoidofficeDownload(@RequestParam("documentId")@NotNullString documentId,@RequestParam(value ="versionId", required =false)String versionId,HttpServletResponse response){
onlyOfficeService.downloadFile(documentId, versionId, response);}/**
* 获取onlyoffice配置接口
*/@GetMapping("/config")publicAjaxResultgetOnlyOfficeConfig(String documentId,String versionId,Boolean isEdit){DevelopDocumentVo developDocumentVo = developDocumentService.selectDevelopDocumentById(documentId);if(StringUtils.isNull(developDocumentVo))returnerror("文件不存在");String fileName = developDocumentVo.getFileName();OnlyOfficeConfig.Config config = onlyOfficeConfig.getConfig();OnlyOfficeConfig.Config.Document document = config.getDocument();OnlyOfficeConfig.Config configuration =newOnlyOfficeConfig.Config();OnlyOfficeConfig.Config.Document documentConfig =newOnlyOfficeConfig.Config.Document();
documentConfig.setKey(documentId);// 编辑模式if(StringUtils.isNotNull(isEdit)&&isEdit){
documentConfig.setTitle(fileName);}else{// 预览模式
documentConfig.setTitle(StringUtils.format("{}({})", fileName,"预览模式"));}
documentConfig.setFileType(this.getExtension(fileName));OnlyOfficeConfig.Config.Document.Permissions permissions = config.getDocument().getPermissions();if(StringUtils.isNotNull(isEdit)){
permissions.setEdit(isEdit);
permissions.setReview(false);}
documentConfig.setPermissions(permissions);String documentUrl =StringUtils.isEmpty(versionId)?StringUtils.format("{}?documentId={}", document.getUrl(), documentId):StringUtils.format("{}?documentId={}&versionId={}", document.getUrl(), documentId, versionId);
documentConfig.setUrl(documentUrl);Long userId =SecurityUtils.getUserId();SysUser sysUser =SecurityUtils.getLoginUser().getSysUser();OnlyOfficeConfig.Config.EditorConfig editorConfig = config.getEditorConfig();OnlyOfficeConfig.Config.EditorConfig.User user = editorConfig.getUser();
user.setId(String.valueOf(userId));
user.setName(sysUser.getNickName());
user.setImage(sysUser.getAvatar());
editorConfig.setUser(user);
configuration.setEditorConfig(editorConfig);
configuration.setDocumentType(this.getDocumentType(fileName));
configuration.setDocument(documentConfig);String secret = onlyOfficeConfig.getSecret();HashMap<String,Object> claims =newHashMap<>();
claims.put("document", documentConfig);
claims.put("editorConfig", editorConfig);
claims.put("documentType",this.getDocumentType(fileName));
claims.put("type", configuration.getType());Signer signer =HMACSigner.newSHA256Signer(secret);JWT jwt =newJWT();for(String key : claims.keySet()){
jwt.addClaim(key, claims.get(key));}String token =JWT.getEncoder().encode(jwt, signer);
configuration.setToken(token);
configuration.setType(config.getType());returnsuccess(configuration);}}
- (4)CallBackResponse实体类
# CallBackResponse.java内容
/**
* onlyOffice回调响应参数实体
* 数据格式:
* {
* "key": "1797934023043756034",
* "status": 6,
* "url": "http://10.x.xx.42:10020/cache/files/data/179793402xxx6034_5182/output.docx/output.docx?md5=w6_C_mPuu6uWt7jsYURmWg&expires=1717572948&WOPISrc=179793402xxx6034&filename=output.docx",
* "changesurl": "http://10.x.xx.42:10020/cache/files/data/179793xxxx3756034_5182/changes.zip/changes.zip?md5=8lYUI4TD1s2bW-pzs_akgQ&expires=1717572948&WOPISrc=1797934023xxx56034&filename=changes.zip",
* "history": {
* "serverVersion": "8.0.1",
* "changes": [
* {
* "created": "2024-06-05 07:20:01",
* "user": {
* "id": "2",
* "name": "mlzhilu"
* }
* },
* {
* "created": "2024-06-05 07:20:44",
* "user": {
* "id": "1",
* "name": "超级管理员"
* }
* }
* ]
* },
* "users": [
* "1"
* ],
* "actions": [
* {
* "type": 2,
* "userid": "1"
* }
* ],
* "lastsave": "2024-06-05T07:20:45.000Z",
* "forcesavetype": 1,
* "token": "eyJhbGciOiJIU......-53bhhSRg",
* "filetype": "docx"
* }
*/@Data@AllArgsConstructor@NoArgsConstructor@Accessors(chain =true)publicclassCallBackResponse{privateString key;privateint status;privateString url;@JsonProperty("changesurl")privateString changesUrl;privateHistory history;privateList<String> users;privateList<Map<String,Object>> actions;@JsonProperty("lastsave")privateDate lastSave;@JsonProperty("forcesavetype")privateint forceSaveType;privateString token;privateString filetype;// History 内部类@Data@AllArgsConstructor@NoArgsConstructor@Accessors(chain =true)publicstaticclassHistory{privateString serverVersion;privateList<Change> changes;// Change 内部类@Data@AllArgsConstructor@NoArgsConstructor@Accessors(chain =true)publicstaticclassChange{privateDate created;privateUser user;// User 内部类@Data@AllArgsConstructor@NoArgsConstructor@Accessors(chain =true)publicstaticclassUser{privateString id;privateString name;}}}}
- (5)ServiceImpl接口
# OnlyOfficeServiceImpl.java内容
@ServicepublicclassOnlyOfficeServiceImpl{// 文档关闭标志位(2和3均表示文档关闭)// 强制保存文档标志位(6和7均表示强制保存文档)privatefinalstaticList<Integer>DOCUMENT_SAVE_STATUS_LIST=Arrays.asList(2,3,6,7);publicvoidhandleCallbackResponse(CallBackResponse callBackResponse){String documentId = callBackResponse.getKey();int status = callBackResponse.getStatus();String url = callBackResponse.getUrl();List<String> users = callBackResponse.getUsers();//保存文档逻辑if(DOCUMENT_SAVE_STATUS_LIST.contains(status)&&StringUtils.isNotEmpty(url)&&!users.isEmpty()&&StringUtils.isNotEmpty(documentId)){// TODO 这里主要是根据onlyoffice服务器中响应的临时文件下载链接,去下载文件并做一些自己的业务处理}}/*
* 文档下载业务
* 这个接口中文档需要通过HttpServletResponse返回文件
*/publicvoiddownloadFile(String id,String versionId,HttpServletResponse response){// TODO 这里主要是根据文档ID和文档版本ID提供文档下载的功能,并且需要保证下载文档时是以文档流的形式下载的}}
引用
onlyoffice官方文档
版权归原作者 乌拉队长 所有, 如有侵权,请联系我们删除。