Web中编辑预览Word、Excel、PPT、PDF

0 阅读4分钟

最终实现的效果

http://doc.liguangtao.cn/onlyoffice-1.jpg http://doc.liguangtao.cn/onlyoffice-2.jpg http://doc.liguangtao.cn/onlyoffice-2.jpg

onlyoffice-flow.jpg

1. Docker部署onlyoffice

拉取minio镜像

docker pull minio/minio:RELEASE.2025-04-22T22-12-26Z

拉取onlyoffice镜像

docker pull onlyoffice/documentserver:7.5

docker启动minio

  • minio账号:minio 密码:QWEasd123
docker run -p 10000:9000 -p 10001:9001 \
--name minio \
-d --restart=always \
-e "MINIO_ACCESS_KEY=minio" -e "MINIO_SECRET_KEY=QWEasd123" \
-v /opt/minio/data:/data \
-v /opt/minio/config:/root/.minio minio/minio:latest server /data \
--console-address ":9000" -address ":9001"

docker启动onlyoffice

  • DO2z9oopirgOJsMBkCacx2TflPTAmRRl
  • 这个是密钥,和application.yml中的docservice.security.key保持一致
docker run -i -t -d -p 9000:80 \
--name onlyoffice \
-v /opt/onlyoffice/logs:/var/log/onlyoffice \
-v /opt/onlyoffice/data:/var/www/onlyoffice/Data \
-v /opt/onlyoffice/lib:/var/lib/onlyoffice \
-v /opt/onlyoffice/db:/var/lib/postgresql \
-e JWT_ENABLED=true --env JWT_SECRET=DO2z9oopirgOJsMBkCacx2TflPTAmRRl \
onlyoffice/documentserver:7.5

2. 在minio中创建bucket

登录minio http://192.168.1.35:10000/,账号:minio 密码:QWEasd123

创建bucket http://192.168.1.35:10000/buckets/add-bucket

这里的bucket和application.yml中的minio配置保持一致

minio-add-bucket.jpg

将bucket设置成public

minio-set-bucket.jpg

3. 搭建SpringBoot项目

jdk1.8

Maven依赖

<dependency>
  <groupId>com.onlyoffice</groupId>
  <artifactId>docs-integration-sdk</artifactId>
  <version>1.6.0</version>
</dependency>
<dependency>
  <groupId>io.minio</groupId>
  <artifactId>minio</artifactId>
  <version>8.2.1</version>
</dependency>

Config配置接口和Callback回调接口

/**
 * Author: Li
 * Description: Config配置接口和Callback回调接口
 * 文档地址:https://api.onlyoffice.com/zh-CN/docs/docs-api/usage-api/config/editor
 */
@CrossOrigin("*")
@RestController
@RequestMapping("/office")
@Slf4j
public class OfficeController {
    @Resource
    private ConfigService configService;
    @Resource
    private MinioClient minioClient;
    @Resource
    private CallbackService callbackService;
    @Value("${minio.endpoint}")
    private String endpoint;
    @Value("${minio.bucket-name}")
    private String bucketName;
    @Resource
    private JwtManager jwtManager;
    @Resource
    private OfficeServiceImpl officeService;

    /**
     * config配置接口,生成onlyoffice配置并进行jwt签名
     */ 
    @GetMapping("/config")
    public ResponseEntity<Config> getConfig(@RequestParam String fileUrl, @RequestParam Mode mode) throws UnsupportedEncodingException {
        Config config = this.configService.createConfig(fileUrl, mode, Type.DESKTOP);
        return ResponseEntity.ok(config);
    }

    /**
     * onlyfoofice回调接口,开始编辑、保存文件的时候会触发
     */
    @PostMapping("/callback")
    public String callback(final HttpServletRequest request,  // track file changes
                           @RequestParam("fileUrl") final String fileUrl,
                           @RequestBody final Callback body) {
        log.debug("回调参数{}", JSON.toJSONString(body));
        try {
//            String authorization = request.getHeader("Authorization");
//            if (StringUtils.isEmpty(authorization)) {
//                return "{\"error\":1,\"message\":\"Request payload is empty\"}";
//            }
//            String token = authorization.replace("Bearer ", "");
//            Callback callback = this.callbackService.verifyCallback(body, token);
//            this.callbackService.processCallback(callback, fileUrl);
            this.callbackService.processCallback(body, fileUrl);
        } catch (Exception e) {
            log.error("", e);
            return "{\"error\":1,\"message\":\"Request payload is empty\"}";
        }
        return "{\"error\":\"0\"}";
    }
}

获取onlyoffice文档编辑器配置信息

/**
 * Author: Li
 * Description: 获取配置信息,该配置决定了页面的按钮及菜单的显示
 * 文档地址:https://api.onlyoffice.com/zh-CN/docs/docs-api/usage-api/config/editor
 */
@Service
@Slf4j
public class ConfigServiceImpl implements ConfigService {
    @Resource
    private JwtManager jwtManager;
    @Value("${docservice.security.key}")
    private String secretKey;
    @Value("${docservice.security.enable}")
    private Boolean securityEnable;
    @Value("${docservice.callback}")
    private String callback;
    @Value("${minio.bucket-name}")
    private  String bucketName;
    @Resource
    private MinioClient minioClient;
    @Value("${minio.endpoint}")
    private String endpoint;

    public Config createConfig(final String fileUrl, final Mode mode, final Type pageType) throws UnsupportedEncodingException {
        // @TODO fileUrl可以是fileId, 在这里可以根据fileId查询数据库中具体的文件信息进行填充
        DocumentType documentType = this.getDocumentType(fileUrl);
        // 文档配置
        Document document = this.getDocument(fileUrl, pageType);
        // 编辑器配置
        EditorConfig editorConfig = this.getEditorConfig(fileUrl, mode, pageType);
        Config config = Config.builder()
                .width("100%")
                .height("100%")
                .type(pageType)
                .documentType(documentType)
                .document(document)
                .editorConfig(editorConfig)
                .build();
        // 是否开启jwt签名秘钥
        if (Boolean.TRUE.equals(this.securityEnable)) {
            String token = this.jwtManager.createToken(config, secretKey);
            config.setToken(token);
        }
        return config;
    }

    /**
     * 编辑器配置,设置文档转换服务回调地址
     */
    public EditorConfig getEditorConfig(String fileUrl, Mode mode, Type type) throws UnsupportedEncodingException {
        Permissions permissions = this.getPermissions();
        EditorConfig editorConfig = EditorConfig.builder()
                // 设置文档创建的接口地址,这里禁止在编辑器中创建文档,设置为null,文档编辑器将隐藏创建文档的按钮
                .createUrl(null)
                .lang("zh") // zh | en
                .mode(mode)
                .user(this.getUser())
                .recent(null)
                .templates(null)
                .customization(this.getCustomization())
                .plugins(null)
                .build();
        if (permissions != null && (Boolean.TRUE.equals(permissions.getEdit()) || Boolean.TRUE.equals(permissions.getFillForms()) || Boolean.TRUE.equals(permissions.getComment()) || Boolean.TRUE.equals(permissions.getReview())) && mode.equals(Mode.EDIT)) {
            //!!!编辑时的回调地址,文档转换服务将会调用该接口,完成文档保存更新
            String callbackUrl = String.format("%s?fileUrl=%s", this.callback, URLEncoder.encode(fileUrl, "UTF-8"));
            editorConfig.setCallbackUrl(callbackUrl);
        }
        return editorConfig;
    }

    /**
     * 获取文档相关的配置
     */
    public Document getDocument(String fileUrl, Type type)  {
        // 文档标题
        String documentName = this.getDocumentName(fileUrl);
        /*
         * 定义服务用来识别文档的唯一文档标识符。如果发送了已知key,则将从缓存中获取文档。
         * 每次编辑和保存文档时,都必须重新生成key。文档 url 可以用作 key,但不能使用特殊字符,长度限制为 128 个符号。
         * :::请注意, 对于连接到同一文档服务器的所有独立服务,密钥必须是唯一的。
         * 否则,服务可能会从编辑器缓存中打开其他人的文件。如果多个第三方集成商连接到同一文档服务器,他们也必须提供唯一的密钥。
         * 可以使用的关键字符: 0-9, a-z, A-Z, -._=。 最大密钥长度为 128 个字符。 :::
         *
         * !!!!这里获取minio中的文件名+最后修改时间生成hash随机字符串作为key,这样key值和文件版本对应有效利用文档转换服务中的缓存策略
         * key的生成策略非常重要!!!,如果每次都生成新的key会造成文件的缓存失效,文档转换服务将重新下载文件,可能导致文件不一致
         */
        String key;
        try {
            String objectName = fileUrl.replace(this.endpoint + "/" + this.bucketName + "/", "");
            StatObjectResponse stat = this.minioClient.statObject(StatObjectArgs.builder()
                    .bucket(this.bucketName)
                    .object(objectName)
                    .build());
            ZonedDateTime zonedDateTime = stat.lastModified();
            // 将 ZonedDateTime 转换为 Instant
            Instant instant = zonedDateTime.toInstant();
            // 获取时间戳(毫秒)
            long timestamp = instant.toEpochMilli();
            key = DigestUtils.sha256Hex(objectName + timestamp);
        }catch (Exception e) {
            key = UUID.randomUUID().toString();
            log.error("获取文件信息失败", e);
        }
        String suffix = this.getExtension(fileUrl);
        return Document.builder()
                // docx | pptx | xlsx | pdf , 获取文件后缀
                .fileType(suffix)
                .key(key)
                .title(documentName)
                // 文档下载地址,这个地址提供给文档转换服务,用于下载文档。
                .url(fileUrl)
                .info(this.getInfo())
                .permissions(this.getPermissions())
                .build();
    }

    public DocumentType getDocumentType(String fileUrl) {
        if (Pattern.matches(".*\\.docx$", fileUrl)) {
            return DocumentType.WORD;
        } else if (Pattern.matches(".*\\.pptx$", fileUrl)) {
            return DocumentType.SLIDE;
        } else if (Pattern.matches(".*\\.xlsx$", fileUrl)) {
            return DocumentType.CELL;
        } else if (Pattern.matches(".*\\.pdf$", fileUrl)) {
            // !!!这里不是写错了,而是onlyoffice默认读取pdf为word,所以这里返回WORD
            return DocumentType.WORD;
        } else {
            throw new RuntimeException("未知的文件类型");
        }
    }

    private String getDocumentName(String fileUrl) {
        String fileName = fileUrl.substring(fileUrl.lastIndexOf("/") + 1);
        if (StringUtils.isEmpty(fileName)) {
            throw new RuntimeException("文件名不能为空");
        }
        return fileName.split("\\?")[0];
    }

    private String getExtension(String fileUrl) {
        String suffix = fileUrl.substring(fileUrl.lastIndexOf(".") + 1);
        if (StringUtils.isEmpty(suffix)) {
            throw new RuntimeException("无法读取文件扩展名");
        }
        String ext = suffix.split("\\?")[0];
        if (!Pattern.matches("docx|pptx|xlsx|pdf", ext)) {
            throw new RuntimeException("文件扩展名不合法");
        }
        return ext;
    }

    /**
     * 包含文档的附加参数(文档所有者、存储文档的文件夹、上传日期、共享设置);
     */
    public Info getInfo() {
        // @TODO 查询数据库获取当前文档的信息填充
        return Info.builder()
                .owner("文档所有人")
                .favorite(false)
                // 文档上传时间
                .uploaded("20250607")
                .build();
    }

    /**
     * 获取当前用户对文件的权限
     */
    public Permissions getPermissions() {
        // @TODO 获取当前登录的用户,查询他对该文件的权限
        return Permissions.builder()
                .chat(false)
                .comment(true)
                .commentGroups(new CommentGroups())
                .copy(true)
                .download(true)
                .edit(true)
                .fillForms(true)
                .modifyContentControl(true)
                .modifyFilter(true)
                .print(true)
                .protect(false)
                .review(true)
                .reviewGroups(new ArrayList<>(0))
                .userInfoGroups(null)
                .build();
    }

    /**
     * 获取当前登录人的用户信息,作为文档编辑人显示
     */
    public User getUser() {
        // @TODO 获取当前登录人信息
        User user = User.builder()
                .id("1")
                .name("winter")
                .image("")
                .build();
        return user;
    }

    /**
     * 定义编辑器需要显示的按钮、菜单
     */
    public Customization getCustomization() {
        Goback goback = Goback.builder()
                .url("")
                .build();
        // 允许自定义编辑器界面,使其看起来像您的其他产品(如果有),并更改附加按钮、链接、更改徽标和编辑器所有者详细信息的显示或不显示
        Customization customization = Customization.builder()
                .autosave(true) // if the Autosave menu option is enabled or disabled
                .comments(true) // if the Comments menu button is displayed or hidden
                .compactHeader(false) /* if the additional action buttons are displayed
    in the upper part of the editor window header next to the logo (false) or in the toolbar (true) */
                .compactToolbar(false) // if the top toolbar type displayed is full (false) or compact (true)
                .forcesave(false)/* add the request for the forced file saving to the callback handler
    when saving the document within the document editing service */
                .help(false)  //  if the Help menu button is displayed or hidden
                .hideRightMenu(false) // if the right menu is displayed or hidden on first loading
                .hideRulers(false) // if the editor rulers are displayed or hidden
                .feedback(false)
                .goback(goback)
                .plugins(false)
                .build();

        return customization;
    }
}

处理文档转换服务回调业务

/**
 * Author: Li
 * Description: 处理文档转换服务回调业务
 */
import com.onlyoffice.service.documenteditor.callback.CallbackService;

@Slf4j
@Service
public class CallbackServiceImpl implements CallbackService {

    @Value("${docservice.security.enable}")
    private Boolean securityEnable;
    @Value("${docservice.security.key}")
    private String secretKey;
    @Resource
    private JwtManager jwtManager;
    @Value("${minio.bucket-name}")
    private String bucketName;
    @Value("${minio.endpoint}")
    private String endpoint;
    @Resource
    private MinioClient minioClient;
    @Resource
    private ObjectMapper objectMapper;

    /**
     * 校验回调数据
     *
     * @param callback      回调数据
     * @param authorization 请求头中的授权信息
     */
    @Override
    public Callback verifyCallback(Callback callback, String authorization) throws JsonProcessingException {
        if (!Boolean.TRUE.equals(this.securityEnable)) {
            return callback;
        }
        String token = callback.getToken();
        boolean fromHeader = false;
        if (StringUtils.isEmpty(token) && StringUtils.isNotEmpty(authorization)) {
            token = authorization.replace("Bearer ", "");
            fromHeader = true;
        }
        if (StringUtils.isEmpty(token)) {
            throw new SecurityException("Not found authorization token");
        }
        String payload = this.jwtManager.verify(token);
        if (fromHeader) {
            JSONObject data = new JSONObject(payload);
            JSONObject callbackFromToken = data.getJSONObject("payload");
            return this.objectMapper.readValue(callbackFromToken.toString(), Callback.class);
        } else {
            return this.objectMapper.readValue(payload, Callback.class);
        }
    }

    @Override
    public void processCallback(Callback callback, String fileUrl) throws Exception {
        switch (callback.getStatus()) {
            case EDITING:
                this.handlerEditing(callback, fileUrl);
                break;
            case SAVE:
                this.handlerSave(callback, fileUrl);
                break;
            case SAVE_CORRUPTED:
                this.handlerSaveCorrupted(callback, fileUrl);
                break;
            case CLOSED:
                this.handlerClosed(callback, fileUrl);
                break;
            case FORCESAVE:
                this.handlerForcesave(callback, fileUrl);
                break;
            case FORCESAVE_CORRUPTED:
                this.handlerForcesaveCurrupted(callback, fileUrl);
                break;
            default:
                throw new RuntimeException("Callback has no status");
        }

    }

    @Override
    public void handlerEditing(final Callback callback, final String fileUrl) {
        Action action = callback.getActions().get(0);  // get the user ID who is editing the document
        if (Type.CONNECTED.equals(action.getType())) {  // if this value is not equal to the user ID
            String userId = action.getUserid();  // get user ID
            if (!callback.getUsers().contains(userId)) {  // if this user is not specified in the body

            }
        }
    }

    @Override
    public void handlerSave(final Callback callback, final String fileUrl) {
        // objectName --> /20251227/test.docx
        String objectName = fileUrl.replace(this.endpoint + "/" + this.bucketName + "/", "");
        String downloadUrl = callback.getUrl();
        // 从文件转换服务中下载文件
        RestTemplate restTemplate = new RestTemplate();
        // 设置请求头
        HttpHeaders headers = new HttpHeaders();
        headers.set("Accept", "application/octet-stream");
        ResponseEntity<byte[]> response = restTemplate.exchange(
                downloadUrl,
                HttpMethod.GET,
                new HttpEntity<>(headers),
                byte[].class
        );
        // 将响应体转换为 InputStream
        try (InputStream inputStream = new ByteArrayInputStream(response.getBody())) {
            // 将新的文件上传到minio
            PutObjectArgs args = PutObjectArgs.builder()
                    .bucket(this.bucketName)
                    .object(objectName)
                    .stream(inputStream, inputStream.available(), -1)
                    .contentType("application/octet-stream")
                    .build();
            this.minioClient.putObject(args);
            log.debug("文档保存成功objectName:{},bucket:{}", objectName, this.bucketName);
        } catch (Exception e) {
            log.error("", e);
            throw new RuntimeException(objectName + "文档保存失败");
        }
    }

    @Override
    public void handlerForcesave(final Callback callback, final String fileUrl) {
        this.handlerSave(callback, fileUrl);
    }


    @Override
    public void handlerForcesaveCurrupted(Callback callback, String fileUrl) throws Exception {
        this.handlerForcesave(callback, fileUrl);
    }

    @Override
    public void handlerSaveCorrupted(Callback callback, String fileUrl) throws Exception {
        this.handlerSave(callback, fileUrl);
    }

    @Override
    public void handlerClosed(final Callback callback, final String fileUrl) {
    }
}

jwt签名校验,配置信息和回调数据将会使用这个类进行签名 | 校验

/**
 * Author: Li
 * Description: jwt签名校验,配置信息和回调数据将会使用这个类进行签名 | 校验
 */
@Component
public class DefaultJwtManager implements JwtManager {
    @Value("${docservice.security.enable}")
    private Boolean securityEnable;
    @Value("${docservice.security.key}")
    private String secretKey;
    @Resource
    private ObjectMapper objectMapper;

    @Override
    public String createToken(Object object) {
        Map<String, ?> payloadMap = (Map)this.objectMapper.convertValue(object, Map.class);
        return this.createToken(payloadMap, this.secretKey);
    }
    @Override
    public String createToken(Object object, String key) {
        Map<String, ?> payloadMap = (Map)this.objectMapper.convertValue(object, Map.class);
        return this.createToken(payloadMap, key);
    }
    @Override
    public String createToken(Map<String, ?> payloadMap, String key) {
        Algorithm algorithm = Algorithm.HMAC256(key);
        String token = JWT.create().withPayload(payloadMap).sign(algorithm);
        return token;
    }
    @Override
    public String verify(String token) {
        return this.verifyToken(token, this.secretKey);
    }
    @Override
    public String verifyToken(String token, String key) {
        Base64.Decoder decoder = Base64.getUrlDecoder();
        Algorithm algorithm = Algorithm.HMAC256(key);
        // 验证token
        DecodedJWT jwt = JWT.require(algorithm).acceptLeeway(1).build().verify(token);
        return new String(decoder.decode(jwt.getPayload()));
    }
}

minio配置

/**
 * Author: Li
 * Description: minio配置
 */
@Configuration
public class MinioConfig {
    @Value("${minio.endpoint}")
    private String minioEndpoint;

    @Value("${minio.access-key}")
    private String minioAccessKey;

    @Value("${minio.secret-key}")
    private String minioSecretKey;

    @Bean
    public MinioClient minioClient() {
        return MinioClient.builder()
                .endpoint(minioEndpoint)
                .credentials(minioAccessKey, minioSecretKey)
                .build();
    }
}

SpringBoot配置 application.yml

server:
  port: 9002
  servlet:
    context-path: /api

docservice:
  # 文档转换服务地址
  url: http://192.168.1.35:9000/
  security:
    enable: true
    key: DO2z9oopirgOJsMBkCacx2TflPTAmRRl
    header:
  # 回调地址,这个回调地址是当前项目部署后的接口地址,就是我们在OfficeController中定义的callback
  callback: http://192.168.1.110:9002/api/office/callback
  watermark:
    enable: true

minio:
  endpoint: http://192.168.1.35:10001
  access-key: 1xI27xoiYpAVqO2YZK3x
  secret-key: Wcjs4Wf01FxUaZfGZw6jO750j8BXUzAbypJk9epj
  bucket-name: winter

4. html测试页面

浏览器打开测试页面,查看最终效果

<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <meta
      name="viewport"
      content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no, minimal-ui"
    />
    <meta name="apple-mobile-web-app-capable" content="yes" />
    <meta name="mobile-web-app-capable" content="yes" />
    <title>ONLYOFFICE</title>
    <style>
      html {
        height: 100%;
        width: 100%;
      }

      body {
        background: #fff;
        color: #333;
        font-family: Arial, Tahoma, sans-serif;
        font-size: 12px;
        font-weight: normal;
        height: 100%;
        margin: 0;
        overflow-y: hidden;
        padding: 0;
        text-decoration: none;
      }

      .header {
        height: 60px;
        border: 1px dashed red;
      }

      .form {
        width: 100%;
        height: calc(100vh);
        /* border: 2px solid red; */
      }

      div {
        margin: 0;
        padding: 0;
      }
    </style>
    <!-- @TODO 替换成自己的 -->
    <script src="http://192.168.1.35:9000/web-apps/apps/api/documents/api.js"></script>
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
  </head>
  <body>
    <div class="form">
      <div id="editor"></div>
    </div>
  </body>
</html>
<script>
  let docEditor;
  let config;
  // @TODO 替换成自己的 
  const fileUrl = "http://192.168.1.35:10001/winter/02施工升降机原始记录(空白).docx";

  function innerAlert(message, inEditor) {
    console.log(message);
    docEditor.showMessage(message);
  }

  // 编辑器准备就绪
  function onAppReady() {
    innerAlert("Document editor ready");
  }

  // 修改文档时调用的函数。使用以下参数调用它:{"data": true} --适用于当前用户正在编辑文档时
  var onDocumentStateChange = function (event) {
    var title = document.title.replace(/\*$/g, "");
    document.title = title + (event.data ? "*" : "");
  };

  // an error or some other specific event occurs
  function onError(event) {
    if (event) {
      innerAlert(event.data);
    }
  }

  // 显示 错误 后调用的函数,当使用旧的 document.key 值打开文档进行编辑时,该值用于编辑以前的文档版本并成功保存。调用此事件时,必须使用新的 document.key 重新初始化编辑器
  function onOutdatedVersion(event) {
    location.reload(true);
  }

  // 通过 meta 命令更改文档的元信息时调用的函数。
  // 文档的名称在 data.title 参数中发送。收藏 图标高亮状态在 data.favorite 参数中发送。
  // 当用户点击 收藏 图标时,调用 setFavorite 方法更新收藏图标高亮状态信息如果未声明该方法,则收藏图标不会更改。
  function onMetaChange(event) {
    if (event.data.favorite !== undefined) {
      var favorite = !!event.data.favorite;
      var title = document.title.replace(/^\☆/g, "");
      document.title = (favorite ? "☆" : "") + title;
      docEditor.setFavorite(favorite);
    }
    innerAlert("onMetaChange: " + JSON.stringify(event.data));
  }

  function onDocumentReady() {
    try {
      const connector = docEditor.createConnector();
      connector.executeMethod("GetCurrentWord", [], (word) => {
        console.log(`[METHOD] GetCurrentWord: ${word}`);
      });
    } catch (e) {
      error(e);
    }
  }
  // 监听编辑器事件
  function initEvent() {
    config.events = {
      // 应用程序被加载到浏览器中
      onAppReady,
      // 文档被加载到文档编辑器中
      onDocumentReady,
      // 修改文档时调用的函数。使用以下参数调用它:{"data": true} --适用于当前用户正在编辑文档时
      onDocumentStateChange,
      // 发生错误或其他特定事件时调用的函数。错误消息在 data 参数中发送
      onError,
      // 显示 错误 后调用的函数,当使用旧的 document.key 值打开文档进行编辑时,该值用于编辑以前的文档版本并成功保存。调用此事件时,必须使用新的 document.key 重新初始化编辑器
      onOutdatedVersion,
      // 通过 meta 命令更改文档的元信息时调用的函数。
      onMetaChange,
    };
  }

  async function init() {
    const res = await axios({
      url: "http://localhost:9002/api/office/config",
      method: "get",
      params: {
        // 以下文件地址改成自己的minio上传的文件地址
        fileUrl,
        mode: "EDIT",
      },
    });
    config = res.data.data;
    console.log(config);
    initEvent();
    docEditor = new DocsAPI.DocEditor("editor", config);
  }
  init();
</script>

联系开发者

打赏作者