后台管理系统2——文件上传功能、富文本编辑器集成

1 文件上传功能

需要使用到element-plus中的文件上传功能。

1.1 后台方面

需要设计一个服务器用来接收上传的文件信息。
controller文件夹创建一个FileController类,用来实现文件的上传与下载功能。除此之外,需要在resources文件夹下创建一个files 文件用来存放上传的文件信息。

package com.example.controller;

import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import com.example.common.Result;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.util.List;

@RestController
@RequestMapping("/files")

public class FileController {
    //获取端口
    @Value("${server.port}")
    private String port;

    private  static  final String ip = "http://localhost";

    /**
     * 文件上传
     * @param file
     * @return
     * @throws IOException
     */
    //采用post接口实现文件上传
    @PostMapping("/upload")
    // 当泛型中的内容不知道如何写的时候,使用 ? 代替
    public Result<?> upload(MultipartFile file) throws IOException {
        // file 用来接收前台传过来的file 对象
        //1 获取源文件的名称
        String originalFilename = file.getOriginalFilename();
        // 1.1 由于在进行保存文件的时候,相同的文件名会存在覆盖问题,所以为了解决这个问题,在文件的前面加一个前缀,为了定义文件的唯一标识

        // 1.1.1时间戳实现
        // long l = System.currentTimeMillis();

        // 1.1.2 使用 UID,生成一串不重复的字符串
        String flag = IdUtil.fastSimpleUUID();
        //2 将文件名存到 files 文件夹下
        // 2.1 获取当前项目所在的路径 System.getProperty("user.dir")
        // 2.2 获取到files的路径 绝对路径
        String rootFilePath = System.getProperty("user.dir") + "/src/main/resources/files/" + flag + '_' + originalFilename;
        // 3 使用工具类进行文件存入 originalFilename获取字节流
        FileUtil.writeBytes(file.getBytes(),rootFilePath);
        // 4 返回结果 url
        return Result.success(ip + ":" +port + "/files/" + flag);

    }

    /**
     * 文件下载
     * @param flag
     * @param response
     */
    @GetMapping("/{flag}")
    // 获取response 对象,将获取到的所有文件与当前传过来的文件进行对比
    public  void getFiles(@PathVariable String flag,HttpServletResponse response){
        OutputStream os;  // 新建一个输出流对象
        String basePath = System.getProperty("user.dir") + "/src/main/resources/files/";  // 定于文件上传的根路径
        List<String> fileNames = FileUtil.listFileNames(basePath);  // 获取所有的文件名称
        String fileName = fileNames.stream().filter(name -> name.contains(flag)).findAny().orElse("");  // 找到跟参数一致的文件
        try {
            if (StrUtil.isNotEmpty(fileName)) {  //文件名称存在 下载文件
                response.addHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(fileName, "UTF-8"));
                response.setContentType("application/octet-stream");
                // 获取文件路径
                byte[] bytes = FileUtil.readBytes(basePath + fileName);  // 通过文件的路径读取文件字节流
                os = response.getOutputStream();   // 通过输出流返回文件
                os.write(bytes);
                os.flush();
                os.close();
            }
        } catch (Exception e) {
            System.out.println("文件下载失败");
        }
    }

}

1.2 数据库表的修改

由于在后台取出图片需要使用一个地址,所以要新增一个字段用来储存地址信息。
在这里插入图片描述
同时也需要对entity文件夹下的book类进行修改:

//表名记得改
@TableName("book")
// 自动进行生成getter setter 方法
@Data

public class Book {
    @TableId(type = IdType.AUTO)
    private Integer id;
    private String name;
    private BigDecimal price;
    private String author;
//    @JsonFormat(pattern = "yyyy-MM-dd",timezone = "GMT+8")
    //数据库中有下划线的,自动准换为驼峰写法
    private Date createTime;
    private  String cover;

}

1.3 前端方面

需要在页面加上封面信息,所以要分别在table表单中和弹窗中添加封面信息,具体代码如下:
在这里插入图片描述

<template>
  <div style="flex: 1; padding: 10px">
    <div class="content">
      <!-- 功能区域———新增按钮 -->
      <div style="margin: 10px 0" class="content_left">
        <el-button type="primary" @click="add">增加</el-button>
      </div>

      <!-- 搜索区域 -->
      <div style="margin: 10px 0" class="content_right">
        <el-input
          v-model="search"
          placeholder="请输入关键字"
          style="width: 200px"
          clearable
        />
        <el-button type="primary" style="margin-left: 5px" @click="load"
          >查询</el-button
        >
      </div>
    </div>
    <el-table :data="tableData" border style="width: 100%" stripe>
      <!-- 在进行数据显示的时候,给日期加上排序 -->
      <el-table-column prop="id" label="ID" sortable />
      <el-table-column prop="name" label="书名" />
      <el-table-column prop="price" label="单价" />
      <el-table-column prop="author" label="作者" />
      <el-table-column prop="createTime" label="出版时间" />
      <el-table-column label="封面">
        <template #default="scope">
          <el-image
            style="width: 100px; height: 100px"
            :src="scope.row.cover"
            :preview-src-list="[scope.row.cover]"
            preview-teleported="true"
            fit="fill"
          />
        </template>
      </el-table-column>

      <el-table-column fixed="right" label="操作" width="150">
        <template #default="scope">
          <el-button size="small" @click="handleEdit(scope.row)"
            >编辑</el-button
          >
          <el-popconfirm
            title="确认删除吗?"
            @confirm="handleDelete(scope.row.id)"
          >
            <template #reference>
              <el-button type="danger" size="small">删除</el-button>
            </template>
          </el-popconfirm>
        </template>
      </el-table-column>
    </el-table>

    <div style="margin: 10px 0">
      <el-pagination
        v-model:current-page="currentPage"
        v-model:page-size="pageSize"
        :page-sizes="[5, 10, 20]"
        layout="total, sizes, prev, pager, next, jumper"
        :total="total"
        @size-change="handleSizeChange"
        @current-change="handleCurrentChange"
      />
      <el-dialog v-model="dialogVisible" title="提示" width="30%">
        <el-form :model="form" label-width="120px">
          <el-form-item label="书名" style="width: 80%">
            <el-input v-model="form.name" />
          </el-form-item>
          <el-form-item label="单价" style="width: 80%">
            <el-input v-model="form.price" />
          </el-form-item>
          <el-form-item label="作者" style="width: 80%">
            <el-input v-model="form.author" />
          </el-form-item>
          <el-form-item label="出版时间" style="width: 80%">
            <el-date-picker
              v-model="form.createTime"
              type="date"
              clearable
              value-format="YYYY-MM-DD"
            />
          </el-form-item>
          <el-form-item label="封面" style="width: 80%">
            <!-- on-success 文件上传成功时的钩子 -->
            <el-upload
              ref="upload"
              action="http://localhost:9090/files/upload"
              :on-success="filesUploadSuccess"
            >
              <el-button type="primary">点击上传</el-button>
            </el-upload>
          </el-form-item>
        </el-form>
        <template #footer>
          <span class="dialog-footer">
            <el-button @click="dialogVisible = false">取消</el-button>
            <!-- 点击之后,将数据保存到后台,显示在页面上面 -->
            <el-button type="primary" @click="save"> 确定 </el-button>
          </span>
        </template>
      </el-dialog>
    </div>
  </div>
</template>

同时设置一个钩子函数,当文件成功上传之后触发的函数,通过 ** :on-success=“filesUploadSuccess”** 进行实现。主要进行修改的是添加和编辑功能,在文件上传成功之后要将原先上传记录给删除,其中需要注意的,在进行编辑操作的时候,当点击 操作 按钮的时候,可能存在当前dom结点不存在的问题,所以需要进行使用** this.$nextTick**来解决这个问题。

<script>
import request from "@/utils/request";
export default {
  name: "Book",
  components: {},
  data() {
    return {
      form: {},
      dialogVisible: false,
      currentPage: 1,
      pageSize: 10,
      total: 0,
      search: "",
      // 从后台进行获取数据
      tableData: [],
      flag: false,
    };
  },
  created() {
    this.load();
  },
  // updated() {
  //   this.load();
  // },
  methods: {
    // 编辑操作
    handleEdit(row) {
      // console.log(row);
      /** 此时可以拿到当前行的数据,然后将拿到的数据,传入到form中进行显示,为了避免v-model
       *  的影响,当在表格中进行修改数据的时候,会导致form中的数据,当点击取消之后,会影响原先的数据
       * 所以采用深拷贝的方式进行赋值
       */
      this.form = JSON.parse(JSON.stringify(row));
      //  打开弹窗
      this.dialogVisible = true;
      // 解决dom 不存在的问题。
      this.$nextTick(() => {
        this.$refs["upload"].clearFiles();
      });
    },
    // 改变页面个数触发函数
    handleSizeChange() {
      this.load();
    },
    // 改变当前页码
    handleCurrentChange() {
      this.load();
    },
    // 当点击添加按钮之后,会显示一个弹窗
    add() {
      // 打开弹窗
      this.dialogVisible = true;
      // 同时要进行清空表单里面的内容
      this.form = {};
      this.$refs["upload"].clearFiles();
    },
    //查询数据库中的数据,由于需要多次进行使用,所以先进行一下封装
    // 此时的方法并没有进行调用,所以可以再页面一进行加载,就调用
    load() {
      // 在进行后端设计的时候,get代表查询
      // get不能直接使用对象进行传参
      request
        .get("/book", {
          params: {
            // 需要进行传递参数
            pageNum: this.currentPage,
            pageSize: this.pageSize,
            search: this.search,
          },
        })
        .then((res) => {
          // console.log(res);
          this.tableData = res.data.records;
          // 总条数
          this.total = res.data.total;
        });
    },
    // 将数据保存到后台
    save() {
      // 通过 form表单中id进行判断,如果有id,表示为更新,否则为新增
      if (this.form.id) {
        // 更新 put
        request.put("/book", this.form).then((res) => {
          // console.log(res);
          if (res.code === "0") {
            this.$message({
              type: "success",
              message: "更新成功",
            });
          } else {
            this.$message({
              type: "error",
              message: "更新失败",
            });
          }
          // 判断之后,需要进行弹窗的关闭和页面的重新渲染
          this.load();
          this.dialogVisible = false;
        });
      } else {
        // 新增
        // 此时路径不能使用/api/book,要不然会自动拼接
        request.post("/book", this.form).then((res) => {
          // console.log(res);
          //成功之后的提示
          if (res.code === "0") {
            this.$message({
              type: "success",
              message: "新增成功",
            });
          } else {
            this.$message({
              type: "error",
              message: "新增失败",
            });
          } // 判断之后,需要进行弹窗的关闭和页面的重新渲染
          this.load();
          this.dialogVisible = false;
        });
      }
    },
    // 删除当前数据
    handleDelete(id) {
      // console.log(id);
      // 通过字符串拼接的方式,将id传给后端userMapper.deleteById(id);中进行删除
      request.delete("/book/" + id).then((res) => {
        if (res.code === "0") {
          this.$message({
            type: "success",
            message: "删除成功",
          });
        } else {
          this.$message({
            type: "error",
            message: "删除失败",
          });
        }
        this.load(); //重新进行表格数据的渲染
      });
    },
    // 文件上传成功之后的操作
    filesUploadSuccess(res) {
      // res 接收的是response ,从里面获取url
      console.log(res.data);
      this.form.cover = res.data;
    },
  },
};
</script>

1.4 后端跨域问题

虽然在前端已经解决跨域问题,但是在后端进行一下配置。在common文件夹下创建一个CorsConfig类,用来设置跨域方面的信息,具体代码如下:

package com.example.common;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

@Configuration
public class CorsConfig {

    // 当前跨域请求最大有效时长。这里默认1天
    private static final long MAX_AGE = 24 * 60 * 60;

    private CorsConfiguration buildConfig() {
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.addAllowedOrigin("*"); // 1 设置访问源地址
        corsConfiguration.addAllowedHeader("*"); // 2 设置访问源请求头
        corsConfiguration.addAllowedMethod("*"); // 3 设置访问源请求方法
        corsConfiguration.setMaxAge(MAX_AGE);
        return corsConfiguration;
    }

    @Bean
    public CorsFilter corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", buildConfig()); // 4 对接口配置跨域设置
        return new CorsFilter(source);
    }
}

2 富文本编辑器

2.1 使用方法

官网
在项目中进行安装依赖:

npm install @wangeditor/editor --save

2.2 在项目中的具体应用。

2.2.1 创建news表

在这里插入图片描述

2.2.2 创建实体类News

entity文件夹下创建News类:

//表名记得改
@TableName("news")
// 自动进行生成getter setter 方法
@Data

public class News {
    @TableId(type = IdType.AUTO)
    private Integer id;
    private String title;
    private String content;
    private String author;
    @JsonFormat(pattern = "yyyy-MM-dd",timezone = "GMT+8")
    private Date time;
    
}

2.2.2 创建接口NewsMapper

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.entity.News;



public interface NewsMapper extends BaseMapper<News> {
    
}


2.2.3 创建Newscontroller

import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.example.common.Result;
import com.example.entity.News;
import com.example.mapper.NewsMapper;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;

@RestController
@RequestMapping("/news")
public class NewsController {

    @Resource
    NewsMapper newsMapper;

    // 新增
    @PostMapping
    // Result<?>表示接收任何类型的数据
    public Result<?> save(@RequestBody News news){

        // 插入数据库,在里面进行使用
        newsMapper.insert(news);
        return Result.success();
    }

    // 查询,使用的是分页查询
    // 在进行网页查询的时候,需要必须传入三个参数pageNum pageSize search
    @GetMapping
    // 当前页pageNum 默认从 1 开始,,每页条数 pageSize 默认从 10 开始,,查询关键字 默认为空
    public Result<?> findPage(@RequestParam(defaultValue = "1") Integer pageNum,@RequestParam(defaultValue = "10") Integer pageSize,@RequestParam(defaultValue = "") String search){
        LambdaQueryWrapper<News> wrapper = Wrappers.<News>lambdaQuery();
        if(StrUtil.isNotBlank(search)){
            wrapper.like(News::getTitle,search);
        }
        Page<News> newsPage =  newsMapper.selectPage(new Page<>(pageNum,pageSize),wrapper);
        return Result.success(newsPage);
    }

    // 更新 使用PutMapping接口
    @PutMapping
    // Result<?>表示接收任何类型的数据
    public Result<?> update(@RequestBody News news){
        // 根据id进行更新
        newsMapper.updateById(news);
        return Result.success();
    }

    // 删除 使用DeleteMapping接口
    @DeleteMapping("/{id}")
    // "/{id}" 占位符是方式 需要通过PathVariable注解进行获取参数
    public Result<?> delete(@PathVariable Long id){
        // 根据id进行删除
        newsMapper.deleteById(id);
        return Result.success();
    }

}

2.2.4 创建News.vue组件

<template>
  <div style="flex: 1; padding: 10px">
    <div class="content">
      <!-- 功能区域———新增按钮 -->
      <div style="margin: 10px 0" class="content_left">
        <el-button type="primary" @click="add">增加</el-button>
      </div>

      <!-- 搜索区域 -->
      <div style="margin: 10px 0" class="content_right">
        <el-input
          v-model="search"
          placeholder="请输入关键字"
          style="width: 200px"
          clearable
        />
        <el-button type="primary" style="margin-left: 5px" @click="load"
          >查询</el-button
        >
      </div>
    </div>
    <el-table :data="tableData" border style="width: 100%" stripe>
      <!-- 在进行数据显示的时候,给日期加上排序 -->
      <el-table-column prop="id" label="ID" sortable />
      <el-table-column prop="title" label="标题" />
      <el-table-column prop="author" label="作者" />
      <el-table-column prop="time" label="发表时间" />
      <el-table-column fixed="right" label="操作" width="150">
        <template #default="scope">
          <el-button size="small" @click="handleEdit(scope.row)"
            >编辑</el-button
          >
          <el-popconfirm
            title="确认删除吗?"
            @confirm="handleDelete(scope.row.id)"
          >
            <template #reference>
              <el-button type="danger" size="small">删除</el-button>
            </template>
          </el-popconfirm>
        </template>
      </el-table-column>
    </el-table>

    <div style="margin: 10px 0">
      <el-pagination
        v-model:current-page="currentPage"
        v-model:page-size="pageSize"
        :page-sizes="[5, 10, 20]"
        layout="total, sizes, prev, pager, next, jumper"
        :total="total"
        @size-change="handleSizeChange"
        @current-change="handleCurrentChange"
      />
      <!-- 对话弹出框 -->
      <el-dialog v-model="dialogVisible" title="提示" width="50%">
        <!-- 定义一个表单,将要进行添加的信息显示在此位置 -->
        <!-- <span>这是一段信息</span> -->
        <!-- 此时需要进行绑定一个变量form -->
        <el-form :model="form" label-width="120px">
          <el-form-item label="标题" style="width: 50%">
            <el-input v-model="form.title" />
          </el-form-item>
          <!-- <el-form-item label="内容" style="width: 80%">
            <el-input v-model="form.content" />
          </el-form-item> -->
        </el-form>
        <template #footer>
          <span class="dialog-footer">
            <el-button @click="dialogVisible = false">取消</el-button>
            <!-- 点击之后,将数据保存到后台,显示在页面上面 -->
            <el-button type="primary" @click="save"> 确定 </el-button>
          </span>
        </template>
      </el-dialog>
    </div>
  </div>
</template>

2.2.5 创建 /news 路由

{
    // 头部加上侧边栏
    path: '/',
    name: 'Layout',
    // 重定向功能,当访问 / 时,会自动的访问 /home 的页面
    redirect: '/user',
    component: Layout,
    // 进行路由的嵌套 主体区域
    children:[
      {
        path: '/user',
        name: 'User',
        component: ()=>import("@/views/User"),
      },
      {
        path: '/person',
        name: 'Person',
        component: ()=>import("@/views/Person"),
      },
      {
        path: '/book',
        name: 'Book',
        component: ()=>import("@/views/Book"),
      },
      {
        path: '/news',
        name: 'News',
        component: ()=>import("@/views/News"),
      }
    ]
  },

2.2.6 在侧边栏进行加入

<template>
  <div>
    <!-- el-menu 存在一个属性 router ,
        功能是,当进行点击里面的内容时,会自动进行跳转
        会根据index中的路由内容进行跳转
     -->
    <!-- default-active="user" 进行控制高亮 -->
    <el-menu
      :default-active="path"
      class="el-menu-vertical-demo"
      router
      style="width: 200px; min-height: calc(100vh - 50px)"
    >
      <el-sub-menu index="1">
        <template #title>
          <span>系统管理</span>
        </template>
        <el-menu-item index="/user">用户管理</el-menu-item>
      </el-sub-menu>
      <!-- index 中的值与路由名称相对应 -->
      <el-menu-item index="/book">图书管理</el-menu-item>
      <el-menu-item index="/news">新闻管理</el-menu-item>
      <!-- <el-menu-item index="date" :route="{ path: '/' }">数据管理</el-menu-item> -->
    </el-menu>
  </div>
</template>

2.2.7 富文本编辑器的使用

在进行安装依赖的时候,由于安装的是wangEditor5版本,与wangEditor4版本使用有很大的不同,同时由于是使用vue3进行项目的编写,所以除了安装wangEditor5版本依赖之外,还需要安装vue3的依赖,代码如下:

yarn add @wangeditor/editor
# 或者 npm install @wangeditor/editor --save

yarn add @wangeditor/editor-for-vue@next
# 或者 npm install @wangeditor/editor-for-vue@next --save

在项目中的使用:

  <div>
            <Toolbar
              style="border-bottom: 1px solid #ccc"
              :editor="editorRef"
            />
            <Editor
              style="height: 500px; overflow-y: hidden"
              v-model="valueHtml"
              @onCreated="handleCreated"
            />
          </div>
<script>
import request from "@/utils/request";
import { Editor, Toolbar } from "@wangeditor/editor-for-vue";
import "@wangeditor/editor/dist/css/style.css"; // 引入 css
import { shallowRef } from "vue";
export default {
  name: "News",
  components: {
    Editor,
    Toolbar,
  },
  data() {
    return {
      form: {},
      editorRef: shallowRef(),
      valueHtml: "",
    };
  },
  methods(){
   save() {
      // 当进行保存的时候,获取到富文本框中的内容,然后将获取的内容保存到form 中的content中
      // console.log(this.valueHtml);
      this.form.content = this.valueHtml;

      // 通过 form表单中id进行判断,如果有id,表示为更新,否则为新增
      if (this.form.id) {
        // 更新 put
        request.put("/news", this.form).then((res) => {
          // console.log(res);
          if (res.code === "0") {
            this.$message({
              type: "success",
              message: "更新成功",
            });
          } else {
            this.$message({
              type: "error",
              message: "更新失败",
            });
          }
          // 判断之后,需要进行弹窗的关闭和页面的重新渲染
          this.load();
          this.dialogVisible = false;
        });
      } 
  }

2.2.8 本地图片上传功能

官网图片上传的步骤:图片上传
上传是两个需要注意的点:
(1) 服务端地址,必填,否则上传图片会报错,里面的地址就是在后端进行设置的地址,里面存在一个属性
fieldName,里面的值要跟后端的参数一样。

data(){
return {
editorConfig: {
        MENU_CONF: {
          // 图片上传
          uploadImage: {
            server: "http://localhost:9090/files/editor/upload",
            // 需要与后台的接收的名字相同,要不然会报 500 的错误
            // 设置上传参数名称
            fieldName: "file",
          },
        },
      },
     }
}

(2) 服务端 response body 格式要求如下:

 /**
     * 富文本编译器的 文件上传
     * @param file
     * @return
     * @throws IOException
     */
    //采用post接口实现文件上传
    @PostMapping("/editor/upload")
    public JSON editorUpload(MultipartFile file) throws IOException {
        // file 用来接收前台传过来的file 对象
        //1 获取源文件的名称
        String originalFilename = file.getOriginalFilename();
        String flag = IdUtil.fastSimpleUUID();
        //2 将文件名存到 files 文件夹下
        // 2.1 获取当前项目所在的路径 System.getProperty("user.dir")
        // 2.2 获取到files的路径 绝对路径
        String rootFilePath = System.getProperty("user.dir") + "/src/main/resources/files/" + flag + '_' + originalFilename;
        // 3 使用工具类进行文件存入 originalFilename获取字节流
        FileUtil.writeBytes(file.getBytes(),rootFilePath);
        // 4 返回结果 url
        String url = ip + ":" +port + "/files/" + flag;
        // 5 此时需要自定义一个对象 json
        JSONObject json = new JSONObject();
        json.set("errno",0);
        JSONObject data = new JSONObject();
        json.set("data",data);
        data.set("url",url);
        return json;

    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值