基于SpringCloud的前后端分离的文章管理系统

发布时间 2023-06-15 11:06:56作者: Aiyinsiqi

基于SpringCloud的文章管理系统

技术栈

SpringCloud |微服务| 注册中心使用的Nacos | Dism配置DNS映射 | SpringBoot |Mybatis-Plus | vue3 | Avue | element-ui | token | redis | MySQL | 构建工具Maven | Wangeditor | 测试工具Apifox

印象深刻的问题

Vue3 和 Vue2 的区别 Vue3实现父子传参

一些 debug的经验:不会debug的程序员不是好程序员

动态路由的第二种配置方式

通过远端数据去对接前台接口,url以网关配置的路由起始,如 /drp 如 /admin

客户前台与后端间一层接口 前端与后端 间一层接口(api)

借助框架工具进行开发时,仔细阅读原文档十分甚至九分重要,因为你不知道作者用了什么奇奇怪怪的封装

利用mapper.xml 完成 数据库字段 到 后端实体类字段名 的 映射

利用 VO 和 DTO 更加规范的处理 实体类中需要的数据(十分甚至九分优雅)

  • DTO(Data Transfer Object)和VO(Value Object)是在软件开发中常用的设计模式或概念,用于处理数据传输和封装数据。

  • DTO是一种用于数据传输的对象,用于在不同的层或模块之间传递数据。它的主要作用是将数据从一个地方传输到另一个地方,可以在不同的系统组件或服务之间传递数据,通常用于在客户端和服务器之间传输数据。DTO对象通常是无状态的,只包含数据字段和对应的getter和setter方法,不包含业务逻辑。

  • VO(Value Object)是一种用于封装数据的对象,通常用于表示一个不可变的值或领域对象。VO的主要作用是将一组相关的数据封装在一个对象中,并提供相应的操作方法。VO对象通常包含多个属性,并提供访问这些属性的方法,但不提供修改属性的方法,以保持对象的不可变性。

  • DTO和VO之间的区别在于它们的使用场景和目的。DTO主要用于数据传输,用于在不同层或模块之间传递数据,而VO主要用于封装数据和行为,用于表示一个不可变的值或领域对象。DTO通常是为了方便数据传输和解耦而设计的,而VO则更注重数据的封装和语义表示。

因为是通过网关9999统一转发的,所以用9999和8080都可以

 

一、实验任务:

完成内容管理系统后台部分的基本功能操作。要求jdbc连接数据库,数据库自行设计。具体功能如下:

1、文章分类的管理,包括文章分类的列表显示(含删除、修改、查询的功能)、添加。

2、文章的管理,包括文章的列表显示(含删除、修改、查询的功能)、添加。

3、用户的管理,包括用户的列表显示(含删除、修改、查询的功能)、添加。

用户登录及退出、修改个人信息功能。

二、数据库设计:

article

image-20230615101133427

type

image-20230615101206331

type_article_middle

image-20230615101307487

menu

image-20230615101446508

user_role

image-20230615101530930

user

image-20230615101614929

三、程序界面截图:

image-20230615101842985

image-20230615101935307

image-20230615102023784

image-20230615102044132

image-20230615102103396

image-20230615102206304

四、程序代码(主要的后台代码):

ArticleController
/*
*   Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: lengleng (wangiegie@gmail.com)
*/

package com.pig4cloud.pig.article.controller;

import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.pig4cloud.pig.article.dto.ArticleDTO;
import com.pig4cloud.pig.article.entity.TypeArticleMiddle;
import com.pig4cloud.pig.article.service.TypeArticleMiddleService;
import com.pig4cloud.pig.common.core.constant.CommonConstants;
import com.pig4cloud.pig.common.core.util.R;
import com.pig4cloud.pig.common.log.annotation.SysLog;
import com.pig4cloud.pig.article.service.ArticleService;
import org.springframework.beans.BeanUtils;
import org.springframework.security.access.prepost.PreAuthorize;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpHeaders;
import org.springframework.web.bind.annotation.*;

import java.util.ArrayList;
import java.util.List;


/**
* @author pig code generator
* @date 2023-05-28 18:08:52
*/
@RestController
@RequiredArgsConstructor
@RequestMapping("/article")
@Tag(name = "管理")
@SecurityRequirement(name = HttpHeaders.AUTHORIZATION)
public class ArticleController {

/**
* 自动注入
*
* @Qualifier 找到指定名称的Bean 自动注入 如果没找到 默认为null
* @Resource 找到指定名称的Bean 自动注入 如果没找到 报错createBean
* @Autowired 自动找到当前类型的Bean 自动注入 Spring
* SpringContextHolder.getBean("1111");
*/
private final ArticleService articleService;
private final TypeArticleMiddleService typeArticleMiddleService;

/**
* 分页查询
*
* @param page   分页对象
* @param article
* @return
*/
@Operation(summary = "分页查询", description = "分页查询")
@GetMapping("/page")
@PreAuthorize("@pms.hasPermission('article_article_get')")
public R getArticlePage(Page page, ArticleDTO article) {
page = articleService.page(page, Wrappers.query(article)); //查询所有文章列表
List records = page.getRecords();//获取记录 //获取了文章表的所有数据
List<ArticleDTO> articleDTOS = new ArrayList<>();

//TODO
//处理返回对象

records.forEach(articleDTO -> {
ArticleDTO articles = new ArticleDTO();
BeanUtils.copyProperties(articleDTO, articles);
//TODO select type_id from biaoming where article_id= huoqudaode ID 参考 https://blog.csdn.net/alan_liuyue/article/details/121162237
List<TypeArticleMiddle> typeArticleMiddleTypeIds =
typeArticleMiddleService.list(Wrappers.<TypeArticleMiddle>query()
.select("type_id").eq("article_id", articles.getId()));
//类型ID列表
List<Integer> re = new ArrayList<>();
//处理查询到的信息
typeArticleMiddleTypeIds.forEach(typeId -> {
re.add(typeId.getTypeId());
});
articles.setTypeIds(re);
articleDTOS.add(articles);
});
page.setRecords(articleDTOS);
return R.ok(page);
}


/**
* 通过id查询
*
* @param id id
* @return R
*/
@Operation(summary = "通过id查询", description = "通过id查询")
@GetMapping("/{id}")
@PreAuthorize("@pms.hasPermission('article_article_get')")
public R getById(@PathVariable("id") Integer id) {
return R.ok(articleService.getById(id));
}

/**
* 新增
*
* @param article
* @return R
*/
//(下面的方法由 前端的api请求调用)(在Vue组件中调用后端的新增接口)(在前端的Vue.js中,使用Axios来发送HTTP请求与后端进行通信)
@Operation(summary = "新增", description = "新增") //这是一个 Swagger 注解,用于描述该方法的作用和描述。
@SysLog("新增")  //这是一个自定义注解,用于记录操作日志,标记了该方法进行了新增操作。
@PostMapping  //这是一个 Spring MVC 注解,指示该方法处理 HTTP POST 请求。
@PreAuthorize("@pms.hasPermission('article_article_add')")  //这是一个 Spring Security 注解,用于进行权限校验。在这里,它检查当前用户是否具有执行该方法的权限。 对应前端
public R save(@RequestBody ArticleDTO article) {  //方法声明:接受一个 ArticleDTO 对象作为请求体,并返回一个类型为 R 的对象。 (R的代码是由架构师实现的)
//保存文章 (2023/6/4突然想到: 这个时候 文章主体就已经有了,那么什么时候 写入 文章主题到 article里面,完全是在前端完成么   soga! 应该就是这样了)
articleService.save(article);  //调用 articleService 的 save 方法,将传入的 article 对象保存到数据库中。 (此方法依然是由架构师实现)
//获取前端传过来的文章类别ID
List<Integer> typeIds = article.getTypeIds();  //从 article 对象中获取前端传来的文章类别ID,存储在一个 List<Integer> 类型的变量 typeIds 中
//创建批量保存的文章与类别关系的列表
List<TypeArticleMiddle> typeArticleMiddles = new ArrayList<>();  //创建了一个空的 List<TypeArticleMiddle> 类型的变量 typeArticleMiddles,用于存储文章与类别关系的列表。
// 组合id,文章id,类别id
//循环处理前端传过来的ID
typeIds.forEach(typeId -> {  //遍历 typeIds 列表中的每个元素   (注:一定要记着这事:当前的article 对应有多个id)(迭代的结果是得到当前文章的类别列表)
TypeArticleMiddle typeArticleMiddle = new TypeArticleMiddle();
typeArticleMiddle.setArticleId(article.getId()); //将 article 对象的 ID 设置为 typeArticleMiddle 对象的 文章id 属性。
typeArticleMiddle.setTypeId(typeId);   //当前迭代的 typeId 设置为 typeArticleMiddle 对象的类别ID属性
typeArticleMiddles.add(typeArticleMiddle);   //将一个对象(文章id和类别id的其中一种)放入到中间表
});
//批量保存类别与文章的关系
typeArticleMiddleService.saveBatch(typeArticleMiddles);   //调用 typeArticleMiddleService 的 saveBatch 方法,将 typeArticleMiddles 列表中的数据保存到数据库中。
// 同样是架构师需要操心的事情

return R.ok();  //还是架构师写的(public static <T> R<T> ok() {return restResult(null, CommonConstants.SUCCESS, null);})
  // 返回一个表示成功状态的 R 对象。
//这是一个用于新增文章的方法。它接受一个 ArticleDTO 对象作为请求体,并保存到数据库中。
// 同时,它记录了操作日志,并进行了权限校验。
// 在保存文章的同时,还将文章与类别之间的关系保存到数据库中。
// 最后,返回一个表示成功状态的对象。
}

/**
* 修改
*
* @param article
* @return R
*/
@Operation(summary = "修改", description = "修改")
@SysLog("修改")
@PutMapping
@PreAuthorize("@pms.hasPermission('article_article_edit')")
public R updateById(@RequestBody ArticleDTO article) {

/**
* 1.获取到前端传过来的ID集合 article。getTyprIds
* 2。删除原本的数据
* 3。批量添加新的数据
* typeArticleMiddleService.remove(Wrappers.<TypeArticleMiddle>query().eq("article_id",article.getId()));
*/
//获取到前端传过来的ID集合 article。getTyprIds
List<Integer> typeIds = article.getTypeIds();
//删除原本的数据
typeArticleMiddleService.remove(Wrappers.<TypeArticleMiddle>query().eq("article_id", article.getId()));

//
List<TypeArticleMiddle> typeArticleMiddles = new ArrayList<>();
typeIds.forEach(typeId -> {
TypeArticleMiddle typeArticleMiddle = new TypeArticleMiddle();
typeArticleMiddle.setArticleId(article.getId());
typeArticleMiddle.setTypeId(typeId);
typeArticleMiddles.add(typeArticleMiddle);
});
typeArticleMiddleService.saveBatch(typeArticleMiddles);


return R.ok(articleService.updateById(article));
}

/**
* 通过id删除
*
* @param id id
* @return R
*/
@Operation(summary = "通过id删除", description = "通过id删除")
@SysLog("通过id删除")
@DeleteMapping("/{id}")
@PreAuthorize("@pms.hasPermission('article_article_del')")
public R removeById(@PathVariable Integer id) {
return R.ok(articleService.removeById(id));
}

}
ArticleDTO
package com.pig4cloud.pig.article.dto;

import com.baomidou.mybatisplus.annotation.TableField;
import com.pig4cloud.pig.article.entity.Article;
import lombok.Data;

import java.util.List;

@Data
public class ArticleDTO extends Article {
/**
* 文章类别ID
*/
@TableField(exist = false)
private List<Integer> typeIds;

}
Article(Entity)
/*
*   Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: lengleng (wangiegie@gmail.com)
*/
package com.pig4cloud.pig.article.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.pig4cloud.pig.common.mybatis.base.BaseEntity;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;

/**
*
*
* @author pig code generator
* @date 2023-05-28 18:08:52
*/
@Data
@TableName("article")
@EqualsAndHashCode(callSuper = true)
@Schema(description = "")
public class Article extends BaseEntity {

   /**
    * 主键
    */
   @TableId(type = IdType.ASSIGN_ID)
   @Schema(description ="主键")
   private Integer id;

   /**
    * 文章标题
    */
   @Schema(description ="文章标题")
   private String articleTitle;

   /**
    * 作者姓名
    */
   @Schema(description ="作者姓名")
   private String author;

   /**
    * 简介
    */
   @Schema(description ="简介")
   private String brief;

   /**
    * 主体
    */
   @Schema(description ="主体")
   private String body;


}
mapper
/*
*   Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: lengleng (wangiegie@gmail.com)
*/

package com.pig4cloud.pig.article.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.pig4cloud.pig.article.entity.Article;
import org.apache.ibatis.annotations.Mapper;

/**
*
*
* @author pig code generator
* @date 2023-05-28 18:08:52
*/
@Mapper
public interface ArticleMapper extends BaseMapper<Article> {

}
mapper.xml
<?xml version="1.0" encoding="UTF-8"?>

<!--
 ~
 ~      Copyright (c) 2018-2025, lengleng All rights reserved.
 ~
 ~  Redistribution and use in source and binary forms, with or without
 ~  modification, are permitted provided that the following conditions are met:
 ~
 ~ Redistributions of source code must retain the above copyright notice,
 ~  this list of conditions and the following disclaimer.
 ~  Redistributions in binary form must reproduce the above copyright
 ~  notice, this list of conditions and the following disclaimer in the
 ~  documentation and/or other materials provided with the distribution.
 ~  Neither the name of the pig4cloud.com developer nor the names of its
 ~  contributors may be used to endorse or promote products derived from
 ~  this software without specific prior written permission.
 ~  Author: lengleng (wangiegie@gmail.com)
 ~
 -->

<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.pig4cloud.pig.article.mapper.ArticleMapper">

 <resultMap id="articleMap" type="com.pig4cloud.pig.article.entity.Article">
   <id property="id" column="id"/>
   <result property="articleTitle" column="article_title"/>
   <result property="author" column="author"/>
   <result property="brief" column="brief"/>
   <result property="body" column="body"/>
 </resultMap>
</mapper>
application.yml
server:
 port: 4002

spring:
 application:
   name: @artifactId@
 cloud:
   nacos:
     username: @nacos.username@
     password: @nacos.password@
     discovery:
       server-addr: ${NACOS_HOST:pig-register}:${NACOS_PORT:8848}
     config:
       server-addr: ${spring.cloud.nacos.discovery.server-addr}
 config:
   import:
     - nacos:application-@profiles.active@.yml
     - nacos:${spring.application.name}-@profiles.active@.yml


服务启动类
/*
* Copyright (c) 2020 pig4cloud Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*     http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.pig4cloud.pig.article;

import com.pig4cloud.pig.common.feign.annotation.EnablePigFeignClients;
import com.pig4cloud.pig.common.security.annotation.EnablePigResourceServer;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

/**
* @author Aiyinsiqi
* @date 2023年06月1日 文章管理系统
*/

//@EnablePigDoc
@EnablePigResourceServer
@EnablePigFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class PigArticleApplication {

public static void main(String[] args) {
SpringApplication.run(PigArticleApplication.class, args);
}


}

贴一部分前端代码

article/index.vue
<!--
 -   Copyright (c) 2018-2025, lengleng All rights reserved.
 -
 - Redistribution and use in source and binary forms, with or without
 - modification, are permitted provided that the following conditions are met:
 -
 - Redistributions of source code must retain the above copyright notice,
 - this list of conditions and the following disclaimer.
 - Redistributions in binary form must reproduce the above copyright
 - notice, this list of conditions and the following disclaimer in the
 - documentation and/or other materials provided with the distribution.
 - Neither the name of the pig4cloud.com developer nor the names of its
 - contributors may be used to endorse or promote products derived from
 - this software without specific prior written permission.
 - Author: lengleng (wangiegie@gmail.com)
 -->
 <!-- <avue-crud>:这是另一个自定义的组件标签,表示一个可增删改查的CRUD(Create, Read, Update, Delete)组件。在这里,将该标签作为basic-container组件的子组件。

   ref="crud":给avue-crud组件添加了一个引用名为"crud",用于在Vue实例中通过$refs来访问该组件的实例。 -->

   
<template>
<div class="execution">  <!-- 定义一个包裹组件内容的div元素,其中class="execution"是为该div元素添加了一个CSS类。 -->
   <basic-container>    <!-- 这是一个自定义的组件标签,表示一个基本容器组件。在这里,将该标签作为外部容器包裹子组件。 -->
     
     <avue-crud ref="crud"
                v-model:page="page"
                :data="tableData"
                :permission="permissionList"
                :table-loading="tableLoading"
                :option="tableOption"
                @on-load="getList"
                @search-change="searchChange"
                @refresh-change="refreshChange"
                @size-change="sizeChange"
                @current-change="currentChange"
                @row-update="handleUpdate"
                @row-save="handleSave"
                @row-del="rowDel">

         <template #menu="{row,index,size}"> <!-- jquery 的知识     $refs.crud.rowEdit(row,index) --><!--danger 是啥 GP-->
             <el-button type="primary"
                 :size="size"
                 icon="el-icon-edit"
                 @click="toEditRow(row)">编辑</el-button>
             <el-button type="danger"
                 :size="size"
                 icon="el-icon-delete"
                 @click="rowDel(row, index)">删除</el-button>
         </template>
         <template #menu-left="{}">
             <el-button type="success"
                icon="el-icon-plus"
                @click="toAddIssue">发布新文章</el-button>
         </template>
     </avue-crud>
   </basic-container>
 </div>
</template>

<script>
import {fetchList, getObj, addObj, putObj, delObj} from '@/api/article'
import {tableOption} from '@/const/crud/article'
import {mapGetters} from 'vuex'



export default {
 name: 'article',
 data() {
   return {
     searchForm: {},
     tableData: [],
     page: {
       total: 0, // 总页数
       currentPage: 1, // 当前页数
       pageSize: 20 // 每页显示多少条
    },
     tableLoading: false,
     tableOption: tableOption
  }
},
 computed: {
   ...mapGetters(['permissions']),
   permissionList() {
     return {
       addBtn: this.validData(this.permissions.article_article_add, false),  //对应后端的ArticleController中的新增接口的Spring Security 注解,用于进行权限校验
       delBtn: this.validData(this.permissions.article_article_del, false),
       editBtn: this.validData(this.permissions.article_article_edit, false)
    };
  }
},
 methods: {
   toAddIssue() {
     localStorage.setItem("K", "ADD");
     this.$router.push({
       path: '/article/article/issue'  
    })
  },
   //修改行数据
   toEditRow(row) {
     //添加一个标识   标识为修改
     localStorage.setItem("K", "EDIT")
     this.tableLoading = true
     setTimeout(() => {
       const article = JSON.stringify(row)
       localStorage.setItem("article", article)
       this.$router.push({
         path: '/article/article/issue'  //相对路径可以么?
      })
    }, 100);
  },

   getList(page, params) {   //GPT还没问 2023/6/5 2:36
     this.tableLoading = true
     fetchList(Object.assign({
       current: page.currentPage,
       size: page.pageSize
    }, params, this.searchForm)).then(response => {
       this.tableData = response.data.data.records
       this.page.total = response.data.data.total
       this.tableLoading = false
    }).catch(() => {
       this.tableLoading = false
    })
  },
   rowDel: function (row, index) {
     this.$confirm('是否确认删除ID为' + row.id, '提示', {
       confirmButtonText: '确定',
       cancelButtonText: '取消',
       type: 'warning'
    }).then(function () {
       return delObj(row.id)
    }).then(data => {
       this.$message.success('删除成功')
       this.getList(this.page)
    }).catch(cancelorerror => {
    })
  },
   handleUpdate: function (row, index, done, loading) {
     putObj(row).then(data => {
       this.$message.success('修改成功')
       done()
       this.getList(this.page)
    }).catch(() => {
       loading();
    });
  },

   /**
    * 位于前端的views文件夹的 index.vue文件
    * import {fetchList, getObj, addObj, putObj, delObj} from '@/api/article' 引入了api下的addObj方法
    * addObj 是一个封装了Axios发送网络请求的方法。
    */
   handleSave: function (row, done, loading) {
     addObj(row).then(data => {            //2023/6/4 17:52 调用 addObj 函数,并使用 .then() 和 .catch() 方法处理 Promise 的结果。如果 Promise 成功,将执行 then 中的回调函数;如果 Promise 失败,将执行 catch 中的回调函数。
       this.$message.success('添加成功')  //使用 $message 对象(一个消息提示框)显示成功消息。
       done()  //执行 done 方法,可能是一个回调函数,表示保存操作已完成。(这个回调函数在哪里?)
       this.getList(this.page)  //为了更新列表数据,以便在保存成功后刷新列表
    }).catch(() => {
       loading();  //执行 loading 方法,可能是一个回调函数,用于显示加载状态或进行其他加载相关操作。
    });
  },
   /**
    * 代码中的this指什么?
    */





   sizeChange(pageSize) {
     this.page.pageSize = pageSize
  },
   currentChange(current) {
     this.page.currentPage = current
  },
   searchChange(form, done) {
     this.searchForm = form
     this.page.currentPage = 1
     this.getList(this.page, form)
     done()
  },
   refreshChange() {
     this.getList(this.page)
  }
}
}

</script>
article/issue.vue
<template>
 <div class="execution">
   <basic-container v-loading="loading">
     <avue-form ref="form" v-model="value" :option="formOptions">  <!--2023/6/4 重点:formOptions 在哪里???? const -->
     </avue-form>
       <div style="user-select: none; width: 82.57%; font-size: 13px; margin: 0 0 10px 10px">
        文章主体:
       </div>
 
       <!-- v-model="value" :editor="editorRef" -->
   <div style="border: 1px solid #ccc">   <!--哇超!!!之前一直没有编辑器的原因是把 编辑器放到了 template里 我也不清楚为什么?反正加上就没了,yi-->
       <Toolbar
         style="border-bottom: 1px solid #ccc"
         :editor="editorRef"
         :defaultConfig="toolbarConfig"
         
         :mode="mode"
       />
       <Editor
         style="height: 500px; overflow-y: hidden;"
         v-model="valueHtml"
         :defaultConfig="editorConfig"
         :mode="mode"
         @onCreated="handleCreated"
      />
  </div>
 

 

<!-- <div style="display: flex; justify-content: right;">
     <el-button @click="addArticle" type="primary" plain>提交</el-button>
 </div>-->
  <el-button style="margin: 20px 0 0 20px" @click="addArticle" type="primary">{{ btnText }}</el-button>
</basic-container>
</div>
</template>

<script>
import '@wangeditor/editor/dist/css/style.css' // 引入 css
import {formOptions} from "@/const/crud/issue";
import { getCurrentInstance, onBeforeUnmount, reactive, ref, shallowRef, onMounted } from 'vue'
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
import { method } from 'lodash';
import { data, html, value } from 'dom7';
import {fetchList, getObj, addObj, putObj, delObj} from '@/api/article'
import { ElMessage } from 'element-plus'
import {useRouter} from 'vue-router'

//嘿嘿 2023/6/5 0:57 增添了新的Message组件 在Components中。
// import Message from '@/components/Message'

export default {
 components: { Editor, Toolbar }, //components 对象用于引入 Editor 和 Toolbar 组件
 setup(props) {
   const { proxy } = getCurrentInstance()  //用于获取当前组件实例对象
   const router=useRouter()   //获取 Vue Router 实例。

   // 编辑器实例,必须用 shallowRef
   const editorRef = shallowRef()  //shallowRef 函数用于创建一个浅层响应式引用
   // 内容 HTML
   let valueHtml = ref('<p></p>') //ref创建一个响应式应用
 
   const toolbarConfig = {}
   const editorConfig = { placeholder: '请输入内容...' }
   let value=ref('')
   let html=ref('')
   let btnText=ref('')

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

   //handleCreated 函数是创建编辑器实例时的事件回调函数
   const handleCreated = (editor) => {
     const toolbarConfig = {}
    // const editor = editorRef.value;
     editorRef.value = editor; // 记录 editor 实例,重要! //有问题 6/5 3:02
     const data = localStorage.getItem('article');
     const metd = localStorage.getItem('K');   //metd是 method的缩写

     localStorage.removeItem("article")
     localStorage.removeItem("K")
     if (metd === "EDIT"){
       //debugger
       btnText.value = "保存";
       method.value = "EDIT";
       // 将字符串转换成对象
       // value._rawValue = JSON.parse(data);      
       // valueHtml = ref(value._rawValue.body); //TODO 6/5 3:05   TODO
       // value = JSON.parse(JSON.stringify(data))
       //value = JSON.parse(data);
       // proxy.$nextTick(()=>{
       //   this.editor = new editorRef('#editorRef')
       // })

       value.value = JSON.parse(data);
       console.log(value.value.body);
       valueHtml.value = value.value.body;
       // console.log(valueHtml);
      // getObj(parseInt(value.id));
      // editorRef.value = valueHtml;
       
      // modelValue = JSON.parse(value).body;
    } else {
       btnText.value = "发布";
       method.value = "ADD";
    }
     
    value.editor = Object.seal(editor);

  };

//final 的意义就是 该变量的地址不能改变(例如 不能new一个新对象给它) 赋的值无所谓
   function addArticle(){
     //editorRef.value.getHtml() 获取Html
     // console.log()
     value._rawValue.body=editorRef.value.getHtml()
   
    // this.loading = true;
     //文章校验
     //校验主体
     if (value._rawValue.body==='' || value._rawValue.body.trim()==='' || value._rawValue.body==='<p><br></p>' || value._rawValue.body===null){
      // value._rawValue.$message.error('请编写文章主体');
     // Message({type:'error', text:'请编写文章主体'})
      // this.loading = false;
      ElMessage({showClose: true, message: '请编写文章主体',type: 'error'})
       return false;
    }

     //校验标题
     if (value._rawValue.articleTitle==='' || value._rawValue.articleTitle.trim()==='' || value._rawValue.articleTitle==='<p><br></p>' || value._rawValue.articleTitle===null){
     ElMessage({showClose: true, message: '请填写文章标题',type: 'error'})
       return false;
    }

     //校验作者姓名
     if (value._rawValue.author==="蒋介石" || value._rawValue.author==="大陆仔"){
       ElMessage({showClose: true, message: '作者名非法!',type: 'error'})
     // this.loading = false;
       return false;
    }

     //校验文章简介
     if (value._rawValue.brief==='' || value._rawValue.brief.trim()==='' || value._rawValue.brief==='<p><br></p>' || value._rawValue.brief===null){
       ElMessage({showClose: true, message: '请编写文章简介',type: 'error'})
     // this.loading = false;
       return false;
    }


     if (method.value === "ADD") {    //此处学到的知识:vue3 中一定要加上 value
       //debugger
       addObj(value._rawValue).then(resp => {
         router.push("/article/article/index")
      })
    } else {
       putObj(value._rawValue).then(resp => {
         router.push("/article/article/index")
      })
    }
     //文章提交
     /*addObj(value._rawValue).then(resp=>{
      router.push("/article/article/index")
    })*/
     // if (this.method === "ADD") {
     //   addObj(value._rawValue).then(resp => {
     //     this.$router.push({
     //       path: '/article/article/index'
     //     })
     //   })
     // } else {
     //   putObj(value._rawValue).then(resp => {
     //     this.$router.push({
     //       path: '/article/article'
     //     })
     //   })
     // }
  }

   return {
     editorRef,
     btnText,
     valueHtml,
     mode: 'default', // 或 'simple'
     toolbarConfig,
     editorConfig,
     loading:false,
     handleCreated,
     formOptions,
     value,
     addArticle
  }
   
},
 computed: {
   editorConfig() {
     return editorConfig
  },
   toolbarConfig() {
     return toolbarConfig
  },
   formOptions() {
     return formOptions
  }
},
 
}
</script>    
api
/*
*   Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: lengleng (wangiegie@gmail.com)
*/

import request from '@/router/axios'

export function fetchList(query) {
 return request({
   url: '/article/article/page',
   method: 'get',
   params: query
})
}


/**
* 2023/6/4 17:30
* 下方代码解释
* 位于 前端的api文件夹的article.js文件
* 该函数的作用是向服务器发送一个 POST 请求,创建一篇文章。请求的 URL 是 /article/article,请求的数据是通过参数 obj 提供的。
*
* 要确保在实际运行中,服务器正确配置,能够接收并处理该 POST 请求,并根据传递的数据 obj 来创建相应的资源(文章)。
*/
export function addObj(obj) {
 return request({           //return request({...}):返回一个 request 函数的调用结果。request 是一个封装了发送网络请求的方法,例如使用 Axios 库发送请求。
   url: '/article/article', //指定请求的 URL 为 /article/article,即向服务器发送 POST 请求以创建一篇文章。  
                             // 这个url 在后台Controller类中唯一标识!!!!!! (是不是所谓的前后端接口的对接就是在这里实现的??)
   method: 'post',   //指定请求方法为 POST,表示在服务器上创建资源。
   data: obj    //将参数 obj 作为请求的数据发送给服务器。通常,obj 是一个包含了要添加到文章的相关信息的对象。
})
}

export function getObj(id) {
 return request({
   url: '/article/article/' + id,
   method: 'get'
})
}

export function delObj(id) {
 return request({
   url: '/article/article/' + id,
   method: 'delete'
})
}

export function putObj(obj) {
 return request({
   url: '/article/article',
   method: 'put',
   data: obj
})
}
crud
export const tableOption = {
   border: true,
   index: true,
   indexLabel: '序号',
   stripe: true,
   menuAlign: 'center',
   align: 'center',
   addBtn:false,
   editBtn:false,
   delBtn:false,
   searchMenuSpan: 6,
   column: [{
       type: 'input',
       label: '主键',
       prop: 'id',
       addDisplay: false,
       editDisabled: true,
       hide: true,
       readonly: true,
       display: false
  }, {type: 'input', label: '文章标题', prop: 'articleTitle'}, {
       type: 'input',
       label: '作者姓名',
       prop: 'author'
  }, {type: 'input', label: '简介', prop: 'brief'}, {type: 'input', label: '主体', prop: 'body', hide: true}, {
       type: 'select',
       label: '文章类别',
       cascader: [],
       span: 24,
       display: true,
       props: {label: 'typeName', value: 'id', desc: 'desc'},
       prop: 'typeIds',
       dicUrl: '/article/type/list',
       dicMethod: 'get',
       dataType: 'array',
       detail: false,
       filterable: true,
       clearable: true,
       multiple: true
  }]
}

五、实验总结

对vue3中 ref的一些理解

在Vue中,ref 是一个用于在模板中引用 DOM 元素或组件实例的特殊属性。

  • 它可以在 Vue 组件中创建一个响应式的引用,使得我们能够在 JavaScript 中访问到模板中的 DOM 元素或组件实例。

  • 使用 ref 的一般语法是在模板中给元素或组件添加 ref 属性,并为其指定一个名称。例如:

    <template>
     <div>
       <h1 ref="myElement">Hello, Vue!</h1>
     </div>
    </template>
  • 在上述示例中,ref 属性被添加到 <h1> 元素上,并指定名称为 "myElement"。

  • 在组件实例的 JavaScript 代码中,可以通过访问 $refs 对象来获取具有 ref 属性的元素或组件实例。例如:

    <script>
    export default {
     mounted() {
       const element = this.$refs.myElement;
       console.log(element); // 输出引用的 DOM 元素
    }
    };
    </script>
  • 在上述示例中,mounted 钩子函数中访问了 $refs.myElement,它返回了被引用的 <h1> 元素。这样,我们就可以在 JavaScript 中操作该元素。

  • 需要注意的是,$refs 是一个非响应式的对象,这意味着如果在模板中使用了 v-ifv-for 等指令导致元素的存在状态发生变化,$refs 对象的内容不会自动更新。另外,$refs 仅在组件渲染完成后才会填充,因此应确保在合适的生命周期钩子函数(如 mountedupdated)中使用 $refs

  • 总而言之,ref 提供了一种在 Vue 中访问模板中 DOM 元素或组件实例的方法,并且通过 $refs 对象可以获取到引用的元素或组件实例的引用,从而在 JavaScript 中进行操作。