Vue 富文本编辑器

Vue 富文本编辑器

  • [1 前端代码](#1 前端代码)
    • [1 添加依赖](#1 添加依赖)
    • [2 测试代码](#2 测试代码)
    • [3 编辑效果](#3 编辑效果)
    • [4 详情效果](#4 详情效果)
  • [2 后端代码](#2 后端代码)
    • [1 后端代码](#1 后端代码)

1 前端代码

使用的是 Vite + Vue2 + WnagEditor-Text

1 添加依赖

cmd 复制代码
"@wangeditor-next/editor": "^5.6.0"
"@wangeditor-next/editor-for-vue2": "^1.0.2"
复制代码
yarn add wangeditor-next/editor 
yarn add @wangeditor-next/editor-for-vue2
# 或者 npm install @wangeditor-next/editor --save
# 或者 npm install @wangeditor-next/editor-for-vue2 --save

yarn add wangeditor-next/editor 
yarn add @wangeditor-next/editor-for-vue
# 或者 npm install @wangeditor-next/editor --save
# 或者 npm install @wangeditor-next/editor-for-vue --save
javascript 复制代码
{
  "name": "edit",
  "private": true,
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "@wangeditor-next/editor": "^5.6.0",
    "@wangeditor-next/editor-for-vue2": "^1.0.2",
    "vue": "2.7.16"
  },
  "devDependencies": {
    "vite": "npm:rolldown-vite@7.2.5",
    "vite-plugin-vue2": "^2.0.3",
    "vue-template-compiler": "^2.7.16"
  },
  "overrides": {
    "vite": "npm:rolldown-vite@7.2.5"
  }
}

2 测试代码

cmd 复制代码
使用vite+vue项目
html 复制代码
<template>
  <div style="border: 1px solid #ccc;">
    <Toolbar style="border-bottom: 1px solid #ccc" :editor="editor" :defaultConfig="toolbarConfig" />
    <Editor style="height: 400px; overflow-y: hidden" :defaultConfig="editorConfig" v-model="html" @onChange="onChange"
      @onCreated="onCreated" />
  </div>
</template>

<script>
import { Editor, Toolbar } from '@wangeditor-next/editor-for-vue2'
import { getToken } from "@/utils/auth"

export default {
  name: 'RichText',
  components: { Editor, Toolbar },
  props: {
    value: {
      type: String,
      default: ''
    }
  },
  data() {
    return {
      editor: null,
      html: '',
      toolbarConfig: {},
      editorConfig: {
        placeholder: '请输入内容...',
        MENU_CONF: {
          // 图片上传配置(调用相同的 /common/upload 接口)
          uploadImage: {
            server: process.env.VUE_APP_BASE_API + "/common/upload",
            fieldName: 'file',
            headers: {
              Authorization: "Bearer " + getToken()
            },
            maxFileSize: 5 * 1024 * 1024, // 5M
            maxNumberOfFiles: 10,
            allowedFileTypes: ['image/*'],
            customInsert(res, insertFn) {
              if (res.code !== 200) {
                throw new Error(res.msg)
              }
              let url = process.env.VUE_APP_BASE_API + res.fileName
              insertFn(url, '', url)
            }
          },
          // 视频上传配置(调用相同的 /common/upload 接口)
          uploadVideo: {
            server: process.env.VUE_APP_BASE_API + "/common/upload",
            fieldName: 'file',
            headers: {
              Authorization: "Bearer " + getToken()
            },
            maxFileSize: 500 * 1024 * 1024, // 500M
            allowedFileTypes: ['video/*'],
            customInsert(res, insertFn) {
              if (res.code !== 200) {
                throw new Error(res.msg)
              }
              // 兼容后端返回格式,优先取 fileName
              let url = process.env.VUE_APP_BASE_API + res.fileName
              insertFn(url, '', url)
            }
          }
        },
      },
    }
  },
  watch: {
    value: {
      handler(val) {
        if (val !== this.html) {
          this.html = val
        }
      },
      immediate: true
    }
  },
  methods: {
    onCreated(editor) {
      this.editor = Object.seal(editor)
    },
    onChange(editor) {
      this.$emit('input', editor.getHtml())
    },
  },
  beforeDestroy() {
    const editor = this.editor
    if (editor == null) return
    editor.destroy()
  },
}
</script>

<style src="@wangeditor-next/editor/dist/css/style.css"></style>

3 编辑效果

4 详情效果

2 后端代码

后端测试代码使用的是RuoYi

1 后端代码

java 复制代码
package com.ruoyi.web.controller.common;

import com.ruoyi.common.config.RuoYiConfig;
import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.file.FileUploadUtils;
import com.ruoyi.common.utils.file.FileUtils;
import com.ruoyi.framework.config.ServerConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.ArrayList;
import java.util.List;

/**
 * 通用请求处理
 *
 * @author ruoyi
 */
@RestController
@RequestMapping("/common")
public class CommonController {

    private static final Logger log = LoggerFactory.getLogger(CommonController.class);

    private static final String FILE_DELIMETER = ",";

    @Autowired
    private ServerConfig serverConfig;

    /**
     * 通用下载请求
     *
     * @param fileName 文件名称
     * @param delete   是否删除
     */
    @GetMapping("/download")
    public void fileDownload(String fileName, Boolean delete, HttpServletResponse response, HttpServletRequest request) {
        try {
            if (!FileUtils.checkAllowDownload(fileName)) {
                throw new Exception(StringUtils.format("文件名称({})非法,不允许下载。 ", fileName));
            }
            String realFileName = System.currentTimeMillis() + fileName.substring(fileName.indexOf("_") + 1);
            String filePath = RuoYiConfig.getDownloadPath() + fileName;

            response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
            FileUtils.setAttachmentResponseHeader(response, realFileName);
            FileUtils.writeBytes(filePath, response.getOutputStream());
            if (delete) {
                FileUtils.deleteFile(filePath);
            }
        } catch (Exception e) {
            log.error("下载文件失败", e);
        }
    }

    /**
     * 通用上传请求(单个)
     */
    @PostMapping("/upload")
    public AjaxResult uploadFile(MultipartFile file) throws Exception {
        try {
            // 上传文件路径
            String filePath = RuoYiConfig.getUploadPath();
            // 上传并返回新文件名称
            String fileName = FileUploadUtils.upload(filePath, file);
            String url = serverConfig.getUrl() + fileName;
            AjaxResult ajax = AjaxResult.success();
            ajax.put("url", url);
            ajax.put("fileName", fileName);
            ajax.put("newFileName", FileUtils.getName(fileName));
            ajax.put("originalFilename", file.getOriginalFilename());
            return ajax;
        } catch (Exception e) {
            return AjaxResult.error(e.getMessage());
        }
    }

    /**
     * 通用上传请求(单个)
     */
    @PostMapping("/register/upload")
    public AjaxResult registerUpload(MultipartFile file) throws Exception {
        try {
            // 上传文件路径
            String filePath = RuoYiConfig.getUploadPath();
            // 上传并返回新文件名称
            String fileName = FileUploadUtils.upload(filePath, file);
            String url = serverConfig.getUrl() + fileName;
            AjaxResult ajax = AjaxResult.success();
            ajax.put("url", url);
            ajax.put("fileName", fileName);
            ajax.put("newFileName", FileUtils.getName(fileName));
            ajax.put("originalFilename", file.getOriginalFilename());
            return ajax;
        } catch (Exception e) {
            return AjaxResult.error(e.getMessage());
        }
    }

    /**
     * 通用上传请求(多个)
     */
    @PostMapping("/uploads")
    public AjaxResult uploadFiles(List<MultipartFile> files) throws Exception {
        try {
            // 上传文件路径
            String filePath = RuoYiConfig.getUploadPath();
            List<String> urls = new ArrayList<String>();
            List<String> fileNames = new ArrayList<String>();
            List<String> newFileNames = new ArrayList<String>();
            List<String> originalFilenames = new ArrayList<String>();
            for (MultipartFile file : files) {
                // 上传并返回新文件名称
                String fileName = FileUploadUtils.upload(filePath, file);
                String url = serverConfig.getUrl() + fileName;
                urls.add(url);
                fileNames.add(fileName);
                newFileNames.add(FileUtils.getName(fileName));
                originalFilenames.add(file.getOriginalFilename());
            }
            AjaxResult ajax = AjaxResult.success();
            ajax.put("urls", StringUtils.join(urls, FILE_DELIMETER));
            ajax.put("fileNames", StringUtils.join(fileNames, FILE_DELIMETER));
            ajax.put("newFileNames", StringUtils.join(newFileNames, FILE_DELIMETER));
            ajax.put("originalFilenames", StringUtils.join(originalFilenames, FILE_DELIMETER));
            return ajax;
        } catch (Exception e) {
            return AjaxResult.error(e.getMessage());
        }
    }

    /**
     * 本地资源通用下载
     */
    @GetMapping("/download/resource")
    public void resourceDownload(String resource, HttpServletRequest request, HttpServletResponse response)
            throws Exception {
        try {
            if (!FileUtils.checkAllowDownload(resource)) {
                throw new Exception(StringUtils.format("资源文件({})非法,不允许下载。 ", resource));
            }
            // 本地资源路径
            String localPath = RuoYiConfig.getProfile();
            // 数据库资源地址
            String downloadPath = localPath + StringUtils.substringAfter(resource, Constants.RESOURCE_PREFIX);
            // 下载名称
            String downloadName = StringUtils.substringAfterLast(downloadPath, "/");
            response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
            FileUtils.setAttachmentResponseHeader(response, downloadName);
            FileUtils.writeBytes(downloadPath, response.getOutputStream());
        } catch (Exception e) {
            log.error("下载文件失败", e);
        }
    }

}
相关推荐
mldong6 小时前
你的 Vue3 项目也能有钉钉同款审批流设计器:npm 装包,10 分钟画出第一条审批流
前端·vue.js
2分钟速写快排7 小时前
什么是 RAG?如何用 RAG 实现一个用户记忆?
前端·后端·ai编程
passerby60617 小时前
如何自己造一个时间处理库
前端·javascript·github
走到天涯海角8 小时前
react里面的长列表渲染优化
前端·react.js·前端框架
小羊没烦恼!9 小时前
Hello Web API系列教程——Web API与国际化
java·服务器·前端·javascript·php
北岛贰9 小时前
迷茫焦虑期,我做了一个带支付带官网的 AI 聊天虚拟恋人 App
前端·人工智能·后端
mayaairi10 小时前
Vue2 组件通讯(三):全局事件总线、PubSub、插槽与组件实例属性
前端·javascript·vue.js
kyriewen11 小时前
面试官问我:AI 都能写代码了,前端凭什么还值 25K
前端·javascript·人工智能
风骏时光牛马12 小时前
AI源码分析:拆解模型底层实现逻辑
前端
BillKu12 小时前
全局样式变量:CSS 自定义属性(--border-color)和SCSS 变量($border-color)的说明
javascript·css·scss