【前后端的那些事】快速上手富文本+富文本图片上传

news/2024/7/9 23:42:46 标签: vue, springboot, 富文本

文章目录

    • fullText富文本
      • 1. 后端接口
        • 1.1 定义常量
        • 1.2 定义返回实体类
        • 1.3 上传图片接口
        • 1.4 下载图片接口
      • 2. 前端代码编写
        • 2.1 安装
        • 2.2 快速使用
      • 3. 配置富文本图片上传地址
        • 3.1 配置图片上传配置
      • 4. 全部代码展示

前言:最近写项目,发现了一些很有意思的功能,想写文章,录视频把这些内容记录下。但这些功能太零碎,如果为每个功能都单独搭建一个项目,这明显不合适。于是我想,就搭建一个项目,把那些我想将的小功能全部整合到一起。实现 搭一次环境,处处使用。

本文主要实现一下两个功能

  1. 富文本
  2. 图片上传+下载

环境搭建
文章链接

已录制视频
视频链接

fullText富文本

使用wangEditor(vue3) + springboot实现富文本功能

效果图:
在这里插入图片描述

1. 后端接口

图片存储的逻辑:

  • 接收前端传递图片数据
  • 将图片下载到后端本地
  • 返回图片访问URL

图片下载的逻辑:

  • 提供下载文件的名字
  • 在后端服务器根据文件名寻找文件所在位置
  • 将文件以流数据形式导出,并通过HttpServletResponse返回

tip: 图片访问URL,本质上是访问下载文件接口URL

1.1 定义常量
/**
* 文件访问域名(请求下载的接口) 
* DOMAIN本质是访问图片下载接口
*/
private static final String DOMAIN = "http://localhost:9005/api_demo/fullText/file/download/";

/**
* 文件物理存储位置
*/
private static final String STORE_DIR = "E:\\B站视频创作\\前后端项目构建-小功能实现\\代码\\backend\\src\\main\\resources\\pict\\";

1.2 定义返回实体类
    static class Success {
        public final int errno;
        public final Object data;
        public Success(String url) {
            this.errno = 0;
            HashMap<String, String> map = new HashMap<>();
            map.put("url", url);
            this.data = map;
        }
    }

tip: 后端接口返回的图片需要按照一定的格式返回,具体可以参考文档[图片上传](菜单配置 | wangEditor)

  • 上传成功
{
    "errno": 0, // 注意:值是数字,不能是字符串
    "data": {
        "url": "xxx", // 图片 src ,必须
        "alt": "yyy", // 图片描述文字,非必须
        "href": "zzz" // 图片的链接,非必须
    }
}
  • 上传失败
{
    "errno": 1, // 只要不等于 0 就行
    "message": "失败信息"
}
1.3 上传图片接口
    /**
     * 获取后缀
     */
    public static String getFileSuffix(String fileName) {
        // 检查文件名是否为null或空
        if (fileName == null || fileName.isEmpty()) {
            return "";
        }

        // 查找最后一个点(.)的位置
        int dotIndex = fileName.lastIndexOf('.');

        // 检查是否找到点,且不是在字符串开头
        if (dotIndex > 0) {
            // 从点开始截取,直到字符串末尾
            return fileName.substring(dotIndex);
        }

        // 如果没有找到点,或点在字符串开头,则返回空字符串
        return "";
    }

    /**
     * 上传文件接口
     * @param file
     * @return
     * @throws IOException
     */
    @RequestMapping("/file/upload")
    public Object uploadPict(@RequestParam("image") MultipartFile file) throws IOException {
        // 获取文件流
        InputStream is = file.getInputStream();
        // 获取文件真实名字
        String fileName = UUID.randomUUID().toString().substring(0, 10) + getFileSuffix(file.getOriginalFilename());
        // 在服务器中存储文件
        FileUtils.copyInputStreamToFile(is, new File(STORE_DIR + fileName));
        // 返回图片url
        String url = DOMAIN + fileName;
        return new Success(url);
    }
1.4 下载图片接口
    /**
     * 文件下载接口
     * @param fileName 文件名
     * @param request
     * @param response
     */
    @GetMapping("/file/download/{fileName}")
    public void download(@PathVariable("fileName") String fileName, HttpServletRequest request, HttpServletResponse response) {
        // 获取真实的文件路径
        String filePath = STORE_DIR + fileName;
        System.out.println("++++完整路径为:"+filePath);

        try {
            // 下载文件
            // 设置响应头
            response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
            response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + fileName);

            // 读取文件内容并写入输出流
            Files.copy(Paths.get(filePath), response.getOutputStream());
            response.getOutputStream().flush();
        } catch (IOException e) {
            response.setStatus(404);
        }
    }

2. 前端代码编写

2.1 安装
pnpm install @wangeditor/editor --save

pnpm install @wangeditor/editor-for-vue@next --save
2.2 快速使用

模板

<template>
  <div style="border: 1px solid #ccc">
    <Toolbar
      style="border-bottom: 1px solid #ccc"
      :editor="editorRef"
      :mode="mode"
    />
    <Editor
      style="height: 500px; overflow-y: hidden"
      v-model="valueHtml"
      :defaultConfig="editorConfig"
      :mode="mode"
      @onCreated="handleCreated"
    />
  </div>
</template>

script

使用setup语法糖

<script setup lang="ts">
import "@wangeditor/editor/dist/css/style.css";
import { Editor, Toolbar } from "@wangeditor/editor-for-vue";
import { IEditorConfig } from "@wangeditor/editor";
import { shallowRef, ref } from "vue";

// 初始化 MENU_CONF 属性
const editorConfig: Partial<IEditorConfig> = {
  MENU_CONF: {}
};
const mode = "default";

// 编辑器实例,必须用 shallowRef,重要!
const editorRef = shallowRef();

const handleCreated = editor => {
  console.log("created", editor);
  editorRef.value = editor; // 记录 editor 实例,重要!
};

// 绑定数据
const valueHtml = ref("");

// 组件销毁时,也及时销毁编辑器,重要!
onBeforeUnmount(() => {
  const editor = editorRef.value;
  if (editor == null) return;

  editor.destroy();
});
</script>

3. 配置富文本图片上传地址

3.1 配置图片上传配置
<script>
// 配置上传地址
editorConfig.MENU_CONF["uploadImage"] = {
  // form-data fieldName ,默认值 'wangeditor-uploaded-image'
  fieldName: "image",
  server: baseUrlApi("fullText/file/upload"),
  // 小于该值就插入 base64 格式(而不上传),默认为 0
  base64LimitSize: 5 * 1024 // 5kb
};
</script>

tip: fieldName对应的是后端的文件上传接口:@RequestParam(“xxx”) MultipartFile中xxx的内容

4. 全部代码展示

  • 前端

    <script setup lang="ts">
    import "@wangeditor/editor/dist/css/style.css";
    import { Editor, Toolbar } from "@wangeditor/editor-for-vue";
    import { IEditorConfig } from "@wangeditor/editor";
    import { shallowRef, ref, onBeforeUnmount } from "vue";
    import { baseUrlApi } from "@/api/utils";
    
    // 初始化 MENU_CONF 属性
    const editorConfig: Partial<IEditorConfig> = {
      MENU_CONF: {}
    };
    const mode = "default";
    
    // 编辑器实例,必须用 shallowRef,重要!
    const editorRef = shallowRef();
    
    const handleCreated = editor => {
      console.log("created", editor);
      editorRef.value = editor; // 记录 editor 实例,重要!
    };
    
    // 绑定数据
    const valueHtml = ref("");
    
    // 组件销毁时,也及时销毁编辑器,重要!
    onBeforeUnmount(() => {
      const editor = editorRef.value;
      if (editor == null) return;
    
      editor.destroy();
    });
    
    // 配置上传地址
    editorConfig.MENU_CONF["uploadImage"] = {
      // form-data fieldName ,默认值 'wangeditor-uploaded-image'
      fieldName: "image",
      server: baseUrlApi("fullText/file/upload"),
      // 小于该值就插入 base64 格式(而不上传),默认为 0
      base64LimitSize: 5 * 1024 // 5kb
    };
    
    const handleChange = editor => {
      // TS 语法
      console.log("content", editor.getHtml());
    };
    </script>
    
    <template>
      <div style="border: 1px solid #ccc; margin-top: 10px">
        <Toolbar
          style="border-bottom: 1px solid #ccc"
          :editor="editorRef"
          :mode="mode"
        />
        <Editor
          style="height: 500px; overflow-y: hidden"
          v-model="valueHtml"
          :defaultConfig="editorConfig"
          :mode="mode"
          @onCreated="handleCreated"
          @onChange="handleChange"
        />
      </div>
    </template>
    
    <style lang="scss" scoped></style>
    
  • 后端

    @RequestMapping("/fullText")
    @RestController
    public class FullTextController {
        /**
         * 文件访问域名(请求下载的接口)
         */
        private static final String DOMAIN = "http://localhost:9005/api_demo/fullText/file/download/";
    
        /**
         * 文件物理存储位置
         */
        private static final String STORE_DIR = "E:\\B站视频创作\\前后端项目构建-小功能实现\\代码\\backend\\src\\main\\resources\\pict\\";
    
        static class Success {
            public final int errno;
            public final Object data;
            public Success(String url) {
                this.errno = 0;
                HashMap<String, String> map = new HashMap<>();
                map.put("url", url);
                this.data = map;
            }
        }
    
        /**
         * 获取后缀
         */
        public static String getFileSuffix(String fileName) {
            // 检查文件名是否为null或空
            if (fileName == null || fileName.isEmpty()) {
                return "";
            }
    
            // 查找最后一个点(.)的位置
            int dotIndex = fileName.lastIndexOf('.');
    
            // 检查是否找到点,且不是在字符串开头
            if (dotIndex > 0) {
                // 从点开始截取,直到字符串末尾
                return fileName.substring(dotIndex);
            }
    
            // 如果没有找到点,或点在字符串开头,则返回空字符串
            return "";
        }
    
        /**
         * 上传文件接口
         * @param file
         * @return
         * @throws IOException
         */
        @RequestMapping("/file/upload")
        public Object uploadPict(@RequestParam("image") MultipartFile file) throws IOException {
            // 获取文件流
            InputStream is = file.getInputStream();
            // 获取文件真实名字
            String fileName = UUID.randomUUID().toString().substring(0, 10) + getFileSuffix(file.getOriginalFilename());
            // 在服务器中存储文件
            FileUtils.copyInputStreamToFile(is, new File(STORE_DIR + fileName));
            // 返回图片url
            String url = DOMAIN + fileName;
            return new Success(url);
        }
    
        /**
         * 文件下载接口
         * @param fileName 文件名
         * @param request
         * @param response
         */
        @GetMapping("/file/download/{fileName}")
        public void download(@PathVariable("fileName") String fileName, HttpServletRequest request, HttpServletResponse response) {
            // 获取真实的文件路径
            String filePath = STORE_DIR + fileName;
            System.out.println("++++完整路径为:"+filePath);
    
            try {
                // 下载文件
                // 设置响应头
                response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
                response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + fileName);
    
                // 读取文件内容并写入输出流
                Files.copy(Paths.get(filePath), response.getOutputStream());
                response.getOutputStream().flush();
            } catch (IOException e) {
                response.setStatus(404);
            }
        }
    }
    

http://www.niftyadmin.cn/n/5321966.html

相关文章

【MySQL】导入导出SQL脚本及远程备份---超详细介绍

目录 前言&#xff1a; 一 navcat导入导出 1.1 导入 1.2 导出 二 mysqldump 导入导出 2.1 导入 2.2 导出 三 load data infile命令导入导出 3.1 导入 3.2 导出 四 远程备份 五 思维导图 前言&#xff1a; 随着当今企业发展&#xff0c;数据库的数据越来越多&…

what is BERT?

BERT Introduction Paper 参考博客 9781838821593_ColorImages.pdf (packt-cdn.com) Bidirectional Encoder Representation from Transformer 来自Transformer的双向编码器表征 基于上下文&#xff08;context-based&#xff09;的嵌入模型。 那么基于上下文&#xff08;…

爬虫之牛刀小试(四):爬取B站番剧的简介

今天爬取的是b站。 如何爬取b站中的番剧呢&#xff1f; 首先我们来到番剧索引中&#xff0c;随便点开一部动漫&#xff0c;检查代码。 每个作品对应一个链接: https://www.bilibili.com/bangumi/play/ss…&#xff08;ss后面的数字称为ss号&#xff09; 发现关于动漫的信息…

C++ STL set容器

和 map、multimap 容器不同&#xff0c;使用 set 容器存储的各个键值对&#xff0c;要求键 key 和值 value 必须相等。 举个例子&#xff0c;如下有 2 组键值对数据&#xff1a; {<a, 1>, <b, 2>, <c, 3>} {<a, a>, <b, b>, <c, c>} 显然&…

python MySQL学习

免费 MySQL Community Server 社区版本 免费 但是MySQL 不提供官方技术支持 MySQL Cluster 集群版 开源免费 可将几个 MySQL Server 封装乘一个Server 收费 MySQL Enterprise Edition 商业版 该版本是收费的 可以试用30天 官方提供技术支持 MySQL Cluster CGE 高级集群版…

JavaScript 异步编程解决方案-中篇

天下事有难易乎&#xff1f; 为之&#xff0c;则难者亦易矣&#xff1b;不为&#xff0c; 则易者亦难矣。人之为学有难易乎&#xff1f; 学之&#xff0c;则难者亦易矣&#xff1b;不学&#xff0c;则易者亦难矣。 async 函数 和promise then的规则一样 async function fun() …

我们做了个写论文解读的agent

已经2024年了&#xff0c;该出现一个论文解读AI Agent了。 尽管我们公司的主营业务不是做这块的&#xff0c;但&#xff0c;我们还是顺手做了这样一个agent&#xff0c;因为——我们公司的算法同学也需要刷论文啊喂&#xff0c; 而且我们也经常人工写论文解读嘛&#xff0c;所…

前端“数据代理”

讲数据代理之前我们先闲聊一下“同源数据”与“数据认可”。世间的一切在形成人文语言泛指的事物时&#xff0c;我们都会为其"下定义"&#xff0c;下定义之前我们都会用形形色色的条件对其限制与说明。通过这种下定义的方式我们得到了不同事物在语言上的定义&#xf…