SSM单体项目:拉勾教育后台管理系统(下)

任务一:SSM项目前端开发

一、Vue回顾

1.项目结构说明

  • 我们使用脚手架快速构建Vue项目,项目结构如下图

image-20211225203814682

|--- edu-boss  项目名称
    |--- node_modules 存放依赖包的目录
    |--- public 静态资源管理目录
    |--- src  组件源码目录(我们写的代码)
        |--- assets 存放静态图片资源(CSS也可以放在这里)
        |--- components 存放基础组件,可复用
        |--- router 存放了项目路由文件
        |--- services 存放请求后台的 JS文件,
        |--- store  保存组件之间的共享数据
        |--- utils  管理公用的JS文件
        |--- views  放置的为公共组件(各个主要页面)
        |--- App.vue app.vue可以当做是网站首页,是一个vue项目的主组件,页面入口文件
        |--- main.js 打包运行的入口文件,引入了vue模块和app.vue组件以及路由route
     |--- babel.config.js babel配置文件, 对源代码进行转码(把es6=>es5)
     |--- package.json 项目及工具的依赖配置文件
     |--- paxkage-lock.json 依赖配置文件
     |--- README.md 项目说明
     |--- vue.config.js 自定义配置文件

2. Views 目录说明

image-20211225203923861

CourseManage: 课程管理
AdvertiseManage: 广告管理
PermissionManage: 权限管理
CommentManage:公共
Users.vue: 用户管理
Login.vue: 登录

3.vue组件化开发

每一个*.vue 文件都可以看做是一个组件.

组件的组成部分

  • template : 组件的HTML部分
  • script: 组件的JS脚本 (使用ES6语法编写)
  • style: 组件的CSS样式
<!--  1.template 代表html结构, template中的内容必须有且只有一个根元素编写页面静态部分 就是 view部分 -->
<template>
    <div>
        测试页面...
    </div>
</template>
<!-- 2.编写vue.js代码 -->
<script>
    //可以导入其组件
    // import Header from '../components/header.vue' 
    
    //默认写法, 输出该组件
    export default {
        name:"Home", // 组件名称,用于以后路由跳转
        data() {// 当前组件中需要使用的数据
            return {}
        },
      methods: {}
    }
</script>
<!-- 编写当前组件的样式代码 -->
<style scoped>
    /* 页面样式  加上scoped 表示样式就只在当前组件有效*/
</style>

二、课程模块回顾

1.课程数据展示

在开始编写前端代码之前, 导入最新的数据库脚本文件

image-20211225204130800

1.1 功能分析

  • Course.vue 组件,完成课程数据的展示和条件查询

image-20211225204206051

image-20211225204220637

1.2 JS代码编写

  1. 定义数据部分
//数据部分
  data() {
    //查询条件
    const filter = {
      courseName: "",
      status: ""
    };
    return {
      filter,
      courses: [],
      loading: false
    };
  },
  //钩子函数
  created() {
    this.loadCourses();
  },
  1. 根据接口文档,编写查询课程数据方法
//方法一: 加载课程数据
    loadCourses() {
      this.loading = true;
      const data = {};
      //查询条件
      if (this.filter.courseName) data.courseName = this.filter.courseName;
      if (this.filter.status) data.status = this.filter.status;
      console.log(data);
      //发送请求
      return axios
        .post("/course/findAllCourse", data)
        .then(resp => {
          console.log(resp.data.content);
          this.courses = resp.data.content;
          this.loading = false;
        })
        .catch(error => {
          this.$message.error("数据获取失败! ! !");
        });
    },
  1. 条件查询( 访问的是同一个接口 )
<el-button @click="handleFilter()">查询</el-button>
	//条件查询
    handleFilter() {
      this.loadCourses();
    },

2. 新建课程

2.1 功能分析

  • 点击新建,由路由导航到 CourseItem.vue
<el-button type="primary" icon="el-icon-plus" @click="handleAdd">新建课程</el-button>
//新建课程 路由跳转
handleAdd() {
    this.$router.push({ name: "CourseItem", params: { courseId: "new" } });
},
  • router.js
{
        path: "/courses/:courseId",
        name: "CourseItem",
        meta: { requireAuth: true, title: "课程详情" },
        component: () =>
          import(
            /* webpackChunkName: 'courses' */ 
"../views/CourseManage/CourseItem.vue"
          )
      },

image-20211225204844523

2.2 JS代码编写

<el-button type="primary" @click="handleSave">保存</el-button>
//保存课程信息
    handleSave() {
      this.$refs.form.validate(valid => {
        if (!valid) return false;
        axios
        .post("/course/saveOrUpdateCourse", this.course)
          .then(res => {
            this.$router.back();
          })
          .catch(error => {
            this.$message.error("保存课程信息失败! ! !");
          });
      });
    },

3.课程图片上传

3.1 功能分析

  • 在SSM前端项目中,图片上传功能使用的是公共的通用组件 UploadImage.vue.

image-20211225205012126

  • 在CourseItem.vue,引入了该组件
import UploadImage from "@/components/UploadImage.vue";
<!-- 使用图片上传组件,完成图片上传 -->
<el-form-item label="课程封面" prop="courseImgUrl">
  <upload-image
    :content="course.courseImgUrl && [course.courseImgUrl]"
    :get-urls="getCourseImgUrl"
    uploadUrl="/course/courseUpload"
    ref="courseCoverRef"
    max="10M"
    tipInfo="建议尺寸:230*300px,JPG、PNG格式,图片小于10M"
  ></upload-image>
</el-form-item>

3.2 案例演示

为了让同学们更好的理解 图片上传组件的使用,我们创建一个Vue项目来演示图片上传组件的使用方式.

  1. 导入准备好的Vue 基础项目

image-20211225205123341

  1. 在components目录下创建一个 UploadImage.vue组件

image-20211225205137292

  1. 查看ElementUI文档,复制代码到 UploadImage.vue

    https://element.eleme.cn/#/zh-CN/component/upload

<template>
  <div>
    <el-upload
      action="https://jsonplaceholder.typicode.com/posts/"
      list-type="picture-card"
      :on-preview="handlePictureCardPreview"
      :on-remove="handleRemove"
    >
      <i class="el-icon-plus"></i>
    </el-upload>
    <el-dialog :visible.sync="dialogVisible">
      <img width="100%" :src="dialogImageUrl" alt />
    </el-dialog>
  </div>
</template>
<script>
export default {
  data() {
    return {
      dialogImageUrl: "",
      dialogVisible: false
    };
  },
  methods: {
    handleRemove(file, fileList) {
      console.log(file, fileList);
    },
    handlePictureCardPreview(file) {
      this.dialogImageUrl = file.url;
      this.dialogVisible = true;
    }
  }
};
</script>
<style scoped>
</style>
  1. 配置路由
 //布局路由
  {
    path: "/index",
    name: "index",
    component: Index,
    //添加子路由,使用 children属性 来表示子路由
    children: [
      //图片上传子路由
      {
        path: "/upload",
        name: "upload",
        component: UploadImage,
      },
    ],
  },
  1. 在Index.vue 导航菜单位置添加一个图片上传选项
<el-menu-item-group>
     <!-- 修改 index的路由地址 -->
     <el-menu-item index="/upload">
        <i class="el-icon-menu"></i>图片上传
     </el-menu-item>
 </el-menu-item-group>
  1. 访问页面进行测试

3.3 属性说明

image-20211225205502786

3.4 组件的引入

怎么将一个组件引入另一个组件 ? 接下来我们来演示一下 引入图片组件.

  1. 创建一个TestUplopad.vue组件
<template>
  <div>
    <!-- 使用组件,注意使用短横线连接 -->
    <upload-image></upload-image>
  </div>
</template>
<script>
//1.导入组件
import UploadImage from "@/components/UploadImage";
export default {
  //2.注册组件
  components: {
    UploadImage
  }
};
</script>
<style scoped>
</style>

3.5 组件的传参

UploadImage.vue

/*
    组件传参
      uploadUrl:图片上传路径,
      getUrl: 函数
   */
  props: ["uploadUrl", "getUrl"],
  
  data() {
    return {
      uploadAction: this.uploadUrl
    };
  },
      
  //上传成功后的回调函数
  uploadSuccess(res, file) {
    this.getUrl(file);
  }
<el-upload
      :action="uploadAction"
      list-type="picture-card"
      :on-preview="handlePictureCardPreview"
      :on-remove="handleRemove"
      :on-success="uploadSuccess"
    >

TestUpload.vue

<template>
  <div>
    <!-- 使用组件,注意使用短横线连接 ,向父组件传递了两个参数
        uploadUrl: 图片上传地址
        :get-url:传递了一个函数
    -->
    <upload-image 
    uploadUrl="https://jsonplaceholder.typicode.com/posts/" 
    :get-url="show">
    </upload-image>
  </div>
</template>
 methods: {
    show(file) {
      console.log(file.name);
    }
  }

3.6 课程模块图片上传

CourseItem.vue

引入图片上传组件,并使用

<el-form-item label="课程封面" prop="courseImgUrl">
  <!-- 使用图片上传组件,完成图片上传 -->
  <upload-image
    :content="course.courseImgUrl && [course.courseImgUrl]"
    :get-urls="getCourseImgUrl"
    uploadUrl="/course/courseUpload"
    ref="courseCoverRef"
    max="10M"
    tipInfo="建议尺寸:230*300px,JPG、PNG格式,图片小于10M"
  ></upload-image>
</el-form-item>
import UploadImage from "@/components/UploadImage.vue";
export default {
  name: "CourseItem",
  title: "营销信息",
  components: { Editor, UploadImage },
}

4. 修改课程

  1. 点击编辑携带当前数据的id,导航到CourseItem.vue
<el-button size="mini" @click="handleNavigate('CourseItem', scope.row.id)">编辑</el-button>
//课程编辑&内容管理路由
    handleNavigate(name, id) {
      this.$router.push({ name, params: { courseId: id } });
    },
  1. 在CourseItem组件的钩子函数中,会进行判断,如果是修改会先获取对应课程数据,进行回显
//钩子函数
  created() {
    //获取课程id
    const id = this.$route.params.courseId;
    if (!id) return this.redirectToError();
    //判断是新建还是修改
    if (id === "new") {
      this.pathTitle = "新增课程";
      this.$breadcrumbs = [
        { name: "Courses", text: "课程管理" },
        { text: "新增课程" }
      ];
    } else {
      this.$breadcrumbs = [
        { name: "Courses", text: "课程管理" },
        { text: "营销信息" }
      ];
      this.loadCourse(id);
    }
  },
//回显课程信息
    loadCourse(id) {
      this.loading = true;
      return axios
        .get("/course/findCourseById?id=" + id)
        .then(resp => {
          console.log(resp);
          this.pathTitle = resp.data.content.courseName;
          this.course = Object.assign(this.course, resp.data.content);
          this.course.id = id;
          this.loading = false;
        })
        .catch(error => {
          this.$message.error("回显数据失败! !");
        });
    },
  1. 修改课程与添加课程走的都是同一个后台接口,区别是修改操作必须要携带ID

5. 课程状态管理

点击上架或者下架完成课程状态的切换.

<el-button size="mini" type="danger" v-if="scope.row.status === 1"
            @click="handleToggleStatus(scope.row)">下架</el-button>
<el-button size="mini" type="success" v-else-if="scope.row.status === 0"
            @click="handleToggleStatus(scope.row)">上架</el-button>
//切换课程状态
    handleToggleStatus(item) {
      //设置最新状态
      const toggledStatus = 1 - item.status;
      //请求后台接口
      axios
        .get("/course/updateCourseStatus", {
          params: {
            status: toggledStatus,
            id: item.id
          }
        })
        .then(res => {
          debugger;
          //设置最新的值
          item.status = toggledStatus;
          console.log(item);
          //重新加载页面
          window.location.reload;
        })
        .catch(error => {
          this.$message.error("状态修改失败! ! !");
        });
    },

6. 课程内容管理

6.1 获取课程内容数据

课程内容数据包括章节与课时信息, 根据课程ID 查询课程包含的章节与课时信息

<el-button size="mini" @click="handleNavigate('CourseSections', scope.row.id)">
内容管理
</el-button>
created() {
    //1.显示当前页面在网站中的位置
    this.$breadcrumbs = [
      { name: "Courses", text: "课程管理" },
      { text: "课程结构" }
    ];
    //2.从路由中获取传递的参数 课程id
    const id = this.$route.params.courseId;
    if (!id) return this.redirectToError();
    this.loading = true;
    //3.加载课程信息
    this.loadCourse(id);
    //4.加载课程内容
    this.loadSections(id);
  },
 //加载课程信息
    loadCourse(id) {
      axios
        .get("/courseContent/findCourseByCourseId?courseId=" + id)
        .then(res => {
          const course = res.data.content;
          //将数据保存到章节表单对象中
          this.addSectionForm.courseId = course.id;
          this.addSectionForm.courseName = course.courseName;
          //将数据保存到课时表单对象中
          this.addLessonForm.courseId = course.id;
          this.addLessonForm.courseName = course.courseName;
        })
        .catch(error => {
          this.$message.error("数据获取失败! ! !");
        });
    },
 //加载课程内容(树形结构)
    loadSections(courseId) {
      this.loading = true;
      axios
        .get("/courseContent/findSectionAndLesson?courseId=" + courseId)
        .then(res => {
          this.sections = res.data.content;
          console.log(res.data.content);
          this.loading = false;
        })
        .catch(error => {
          this.$message.error("数据获取失败! ! !");
        });
    },

6.2 章节管理

  • 新建章节
<el-button type="primary" icon="el-icon-plus" @click="handleShowAddSection">添加章节</el-button>

新增章节,需要回显章节对应的课程名称

//显示新增章节表单
handleShowAddSection() {
    this.addSectionForm = {
        courseId: this.addSectionForm.courseId,
        courseName: this.addSectionForm.courseName
    };
    this.showAddSection = true;
},
  • 修改章节
<el-button size="small" @click.stop="handleEditSection(data)">编辑</elbutton>
//编辑章节(回显)
handleEditSection(section) {
  this.addSectionForm = Object.assign(this.addSectionForm, section);
  this.showAddSection = true;
},
  • 添加与修改章节访问的都是同一个接口
//添加&修改章节
handleAddSection() {
axios
  .post("/courseContent/saveOrUpdateSection", this.addSectionForm)
  .then(res => {
    this.showAddSection = false;
    //重新加载列表
    return this.loadSections(this.addSectionForm.courseId);
    })
  .then(() => {
    //重置表单内容
    this.addSectionForm.sectionName = "";
    this.addSectionForm.description = "";
    this.addSectionForm.orderNum = 0;
    this.reload();
  })
  .catch(error => {
    this.showAddSection = false;
    this.$message.error("操作执行失败! ! !");
  });
},
  • 章节状态

章节状态有3种

//状态信息
const statusMapping = {
    0: "已隐藏",
    1: "待更新",
    2: "已更新"
};

选择状态,点击确定修改状态

<el-button type="primary" @click="handleToggleStatus">确 定</el-button>
//修改章节状态
handleToggleStatus() {
  //判断要修改的状态
  if (this.toggleStatusForm.data.sectionName) {
    //修改章节状态
    axios
      .get("/courseContent/updateSectionStatus", {
        params: {
          id: this.toggleStatusForm.id,
          status: this.toggleStatusForm.status
        }
      })
      .then(resp => {
        this.toggleStatusForm.data.status = this.toggleStatusForm.status;
        this.toggleStatusForm = {};
        this.showStatusForm = false;
        this.reload();
      })
      .catch(error => {
        this.showStatusForm = false;
        this.$message.error("修改状态失败! ! !");
      });
  } else {
    //修改课时状态
  }
},

6.3 课时管理

课时管理 包括 课时新增、课时修改、课时状态管理. 与章节管理基本相同.

三、广告模块

1 广告位管理

1.1 广告位展示

  • AdvertiseSpaces.vue 组件,为广告位页面

image-20211225210446244

JS部分

data() {
    return {
      list: null,
      listLoading: false
    };
  },
created() {
    //加载广告位数据
    this.loadPromotionSpace();
},
//方法1: 加载广告位信息
    loadPromotionSpace() {
      this.listLoading = true;
      axios
        .get("/PromotionSpace/findAllPromotionSpace")
        .then(res => {
          this.list = res.data.content;
          this.listLoading = false;
        })
        .catch(err => {
          this.$message("加载数据失败! ! !");
        });
    },

1.2 添加广告位

1)点击 按钮,通过路由导航到指定组件

<el-button size="mini" class="btn-add" @click="handleAdd()">添加广告位</el-button>
/添加广告位跳转
handleAdd() {
    this.$router.push({ path: "/addAdvertiseSpace" });
},

2) 查看路由 router.js, 跳转到的是 AddAdvertiseSpace.vue

{
    path: "addAdvertiseSpace",
    name: "AddAdvertiseSpace",
    component: () => import("../views/AdvertiseManage/AddAdvertiseSpace"),
    meta: { requireAuth: true, title: "添加广告位" }
},

3) 查看AddAdvertiseSpace.vue

<template>
  //显示组件,并传递了参数 isEdit="false" , 表示是新增操作
  <home-advertise-detail :isEdit="false"></home-advertise-detail>
</template>
<script>
//引入了AdvertiseSpaceDetail组件
import HomeAdvertiseDetail from "./AdvertiseSpaceDetail";
export default {
  name: "addHomeAdvertise",
  title: "添加广告位",
  components: { HomeAdvertiseDetail }
};
</script>

4)真正显示的组件是 AdvertiseSpaceDetail.vue

  • 首先判断要进行 新增还是修改操作, 根据isEdit ,true 为修改,false为新增
//钩子函数
  created() {
    //判断是添加还是修改操作
    if (this.isEdit) {
      //修改
      const id = this.$route.query.id;
      this.loadPromotionSpace(id);
    } else {
      //新增
      this.homeAdvertise = {};
    }
  },

新增

//方法1: 保存广告位信息
    handleSave() {
      this.$refs.form.validate(valid => {
        if (!valid) return false;
        //请求后台
        axios
          .post(
            "/PromotionSpace/saveOrUpdatePromotionSpace",
            this.homeAdvertise
            )
          .then(res => {
            //返回上个页面
            this.$router.back();
          })
          .catch(err => {
            this.$message("数据处理失败! !");
          });
      });
    },

1.3 修改广告位

需要请求后台接口,进行广告位信息回显

//方法2: 回显广告位信息
    loadPromotionSpace(id) {
      return axios
        .get("/PromotionSpace/findPromotionSpaceById?id=" + id)
        .then(res => {
          Object.assign(this.homeAdvertise, res.data.content);
          this.homeAdvertise.id = id;
        })
        .catch(err => {
          this.$message("数据处理失败! !");
        });
    }

2 广告管理

2.1 ElementUI 分页组件

  • Advertises.vue 组件,为广告列表页面

image-20211225210732084

  • 广告列表的展示,使用到了分页组件, 接下来通过一个案例演示一下分页插件的使用.

image-20211225210746676

https://element.eleme.cn/#/zh-CN/component/pagination

2.1.1快速使用

  1. 在测试项目中,创建一个PageList.vue ,复制代码如下
<template>
  <div>
    <div class="block">
      <span class="demonstration">完整功能</span>
      <el-pagination
        @size-change="handleSizeChange"
        @current-change="handleCurrentChange"
        :current-page="currentPage4"
        :page-sizes="[100, 200, 300, 400]"
        :page-size="100"
        layout="total, sizes, prev, pager, next, jumper"
        :total="400"
      ></el-pagination>
    </div>
  </div>
</template>
<script>
export default {
  methods: {
    handleSizeChange(val) {
      console.log(`每页 ${val} 条`);
    },
    handleCurrentChange(val) {
      console.log(`当前页: ${val}`);
    }
  },
  data() {
    return {
      currentPage4: 4
    };
  }
};
</script>
  1. 属性介绍

image-20211225210853294

分析:

  • page-size 与 current-page 是需要前端传给后端的数据
  • total 和 列表数据 是需要后端返回给前端的.
  1. 事件介绍

image-20211225210917957

  1. 案例演示

a.复制下面代码到 PageList

<template>
	<div class="app-container">
    <div class="table-container">
      <el-table ref="homeAdvertiseTable" :data="list" style="width: 100%;" 
border>
        <el-table-column label="id" width="220" align="center">
          <template slot-scope="scope">{{scope.row.id}}</template>
        </el-table-column>
        <el-table-column label="广告名称" align="center" width="320">
          <template slot-scope="scope">{{scope.row.name}}</template>
        </el-table-column>
        <el-table-column label="广告图片" width="420" align="center">
          <template slot-scope="scope">
            <img style="height: 80px" :src="scope.row.img" />
          </template>
        </el-table-column>
      </el-table>
    </div>
    <div class="pagination-container">
      <el-pagination
        background
        @size-change="handlePageSizeChange"
        @current-change="handleCurrentPageChange"
        layout="total, sizes,prev, pager, next,jumper"
        :current-page="page"
        :page-sizes="[5,10, 20]"
        :page-size="size"
        :total="total"
      ></el-pagination>
    </div>
  </div>
</template>

b.编写JS部分代码

<script>
export default {
  data() {
    return {
      total: 0, //总条数
      size: 5, //每页显示条数
      page: 1, //当前页
      list: [] //广告数据
    };
  },
  created() {
    this.loadList();
  },
  methods: {
    //加载广告数据
    loadList() {
      return this.axios
        .get("http://localhost:8080/ssm-web/PromotionAd/findAllPromotionAd", {
          params: {
            currentPage: this.page,
            pageSize: this.size
          }
          })
        .then(res => {
          this.list = res.data.content.list;
          this.total = res.data.content.total;
          this.listLoading = false;
        })
        .catch(error => {
          this.$message.error("数据获取失败! ! !");
        });
    },
    //每页显示条数发生变化
    handlePageSizeChange(size) {
      this.size = size;
      this.loadList();
    },
    //当前页发生变化
    handleCurrentPageChange(page) {
      this.page = page;
      this.loadList();
    }
  }
};
</script>

2.2 广告列表展示

1.需求分析
我们已经解决了分页问题,接下来再看一下广告页面要展示哪些内容:

image-20211225211042683

  • 广告列表的展示数据来源于两张表:
    • promotion_ad 广告表
    • promotion_space 广告位表

2.功能实现

a.数据部分

//数据部分
  data() {
    return {
      typeMap: {}, //保存广告位对象信息
      total: 0, //总条数
      size: 5, //每页显示条数
      page: 1, //当前页
      list: [], //广告数据
      listLoading: false
    };
  },

b. 钩子函数

created() {
    //获取广告列表数据
    this.loadPromotionAd();
    //获取广告位置数据
    this.loadPromotionSpace();
  },

c.函数部分

//方法1; 获取广告列表数据
    loadPromotionAd() {
      this.listLoading = true;
      return axios
        .get("/PromotionAd/findAllPromotionAd", {
          params: {
            currentPage: this.page,
            pageSize: this.size
          }
        })
        .then(res => {
          this.list = res.data.content.list;
          this.total = res.data.content.total;
          this.listLoading = false;
        })
        .catch(err => {});
    },
        
    //方法2: 获取广告位置数据
    loadPromotionSpace() {
      this.listLoading = true;
      return axios.get("/PromotionSpace/findAllPromotionSpace").then(res => {
        //使用map进行遍历
        res.data.content.map(item => {
          //将数据保存到 typeMap  key就是id,value就是 广告位对象
          this.typeMap[item.id] = item;
        });
        this.listLoading = false;
      });
    },
    //方法3: 获取广告位置名称
    getSpaceName(spaceId) {
      if (!spaceId) {
        return "";
      }
      return this.typeMap[spaceId] && this.typeMap[spaceId].name;
    },

2.3 广告状态修改

  • 需求分析: 点击按钮实现 状态修改, 0 下线,1 上线

image-20211225211233277

  • 功能实现

页面部分,使用的是 el-switch 组件

active-value: switch:打开时的值

inactive-value : switch 关闭时的值

<!-- 上线与下线 -->
<el-table-column label="上线/下线" width="120" align="center">
    <template slot-scope="scope">
        <el-switch
                   @change="handleUpdateStatus(scope.row)"
                   :active-value="1"
                   :inactive-value="0"
                   v-model="scope.row.status"></el-switch>
    </template>
</el-table-column>
//方法4: 修改状态
    handleUpdateStatus(row) {
      this.$confirm("是否要修改上线/下线状态?", "提示", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning"
      }).then(() => {
        //请求后台
        axios
          .get("/PromotionAd/updatePromotionAdStatus", {
            params: {
              id: row.id,
              status: row.status
            }
          })
          .then(res => {
            this.loadPromotionAd();
          })
          .catch(err => {
            this.$message("修改状态失败! ! !");
          });
      });
    },

2.4 广告新增&修改

  1. 需求分析

a.点击添加广告,触发事件

<el-button size="mini" class="btn-add" @click="handleAdd()">添加广告</el-button>

b.路由导航到指定组件

//跳转到新增
handleAdd() {
    this.$router.push({ path: "/addAdvertise" });
},

c.查看路由信息,跳转到的是 AddAdvertise.vue组件

{
    path: "addAdvertise",
    name: "AddAdvertise",
    component: () => import("../views/AdvertiseManage/AddAdvertise"),
    meta: { requireAuth: true, title: "添加广告" }
},

d. AddAdvertise.vue组件

在AddAdvertise组件中,引入了 AdvertiseDetail组件,真正的操作是在这个组件中完成的

:isEdit="false" : false表示是新增操作

<template>
  <home-advertise-detail :isEdit="false"></home-advertise-detail>
</template>
<script>
import HomeAdvertiseDetail from './AdvertiseDetail'
export default {
  name: 'addHomeAdvertise',
  title: '添加广告',
  components: { HomeAdvertiseDetail }
}
</script>

e.AdvertiseDetail.vue 组件
该组件是进行 新增和修改广告的页面.

image-20211225211526974

2.功能实现
数据部分

data() {
    return {
      homeAdvertise, //广告表单对象
      typeOptions: [] //广告位下拉列表
    };
  },

钩子函数

created() {
    //判断是新增还是修改
    if (this.isEdit) {
      //修改
      const id = this.$route.query.id;
      this.loadPromotion(id);
    } else {
      //新增
      this.homeAdvertise = {};
    }
    this.loadPromotionSpace();
  },

方法

//方法1: 获取广告位置数据
    loadPromotionSpace() {
      return axios.get("/PromotionSpace/findAllPromotionSpace").then(res => {
        //使用map函数进行遍历,获取广告位id 与 name,保存到typeOptions
        this.typeOptions = res.data.content.map(item => {
          return { label: item.name, value: item.id };
        });
      });
      },
        
    //方法2: 保存广告信息
    handleSave() {
      this.$refs.form.validate(valid => {
        if (!valid) return false;
        axios
          .post("/PromotionAd/saveOrUpdatePromotionAd", this.homeAdvertise)
          .then(res => {
            //返回上个页面 并刷新
            this.$router.back();
          })
          .catch(err => {});
      });
    },
        
    //方法3: 修改回显广告信息
    loadPromotion(id) {
      return axios
        .get("/PromotionAd/findPromotionAdById?id=" + id)
        .then(res => {
          Object.assign(this.homeAdvertise, res.data.content);
          this.homeAdvertise.id = id;
        })
        .catch(err => {});
    },

四、用户管理

1 分页&条件查询用户数据

查询条件:

1. 用户手机号
2. 注册时间,包含开始日期和结束日期

image-20211225211754775

1.1 日期选择器组件

在查询条件中使用了 ElementUI中的日期选择器,我们一起来简单学习一下日期选择器的使用.
https://element.eleme.cn/#/zh-CN/component/date-picker#mo-ren-xian-shi-ri-qi

image-20211225211813906

  1. 在测试项目中,创建一个 TestDate.vue组件,复制代码到页面
<template>
  <div>
    <div class="block">
      <span class="demonstration">带快捷选项</span>
      <el-date-picker
        v-model="dateTime"
        type="daterange"
        align="right"
        unlink-panels
        range-separator="至"
        start-placeholder="开始日期"
        end-placeholder="结束日期"
        :picker-options="pickerOptions"
      ></el-date-picker>
      <el-button type="primary" @click="getDate">查询</el-button>
    </div>
  </div>
</template>
<script>
export default {
  data() {
    return {
      pickerOptions: {
        shortcuts: [
          {
            text: "最近一周",
            onClick(picker) {
              const end = new Date();
              const start = new Date();
              start.setTime(start.getTime() - 3600 * 1000 * 24 * 7);
              picker.$emit("pick", [start, end]);
            }
          },
          {
            text: "最近一个月",
            onClick(picker) {
              const end = new Date();
              const start = new Date();
              start.setTime(start.getTime() - 3600 * 1000 * 24 * 30);
              picker.$emit("pick", [start, end]);
            }
          },
          {
            text: "最近三个月",
            onClick(picker) {
              const end = new Date();
              const start = new Date();
              start.setTime(start.getTime() - 3600 * 1000 * 24 * 90);
              picker.$emit("pick", [start, end]);
            }
          }
        ]
      },
      dateTime: ""
    };
  },
  methods: {
    getDate() {
      const params = {};
      params.startCreateTime = this.dateTime[0];
      params.startCreateTime.setHours(0);
      params.startCreateTime.setMinutes(0);
      params.startCreateTime.setSeconds(0);
      params.endCreateTime = this.dateTime[1];
      params.endCreateTime.setHours(23);
      params.endCreateTime.setMinutes(59);
      params.endCreateTime.setSeconds(59);
      console.log(params);
    }
  }
};
</script>

1.2 功能实现

数据部分

//数据部分
return {
    pickerOptions,//日期选择器选项设置
    total: 0, //总条数
    size: 10, //每页显示条数
    page: 1, //当前页
    filter,
    users: [],
    loading: false,
    allocAdminId: "",
    allocDialogVisible: false,
    allocRoleIds: [],
    allRoleList: []
};

JS部分

 created() {
      //初始化用户数据
      this.loadUsers();
    }
    //方法1: 加载用户数据
    loadUsers() {
      this.loading = true;
      //设置参数
      const params = { currentPage: this.page, pageSize: this.size };
      //过滤条件
      if (this.filter.username) params.username = this.filter.username;
      //设置日期参数
      if (this.filter.resTime) {
        params.startCreateTime = this.filter.resTime[0];
        params.startCreateTime.setHours(0);
        params.startCreateTime.setMinutes(0);
        params.startCreateTime.setSeconds(0);
        params.endCreateTime = this.filter.resTime[1];
        params.endCreateTime.setHours(23);
        params.endCreateTime.setMinutes(59);
        params.endCreateTime.setSeconds(59);
      }
      //请求后台接口
      return axios
        .post("/user/findAllUserByPage", params)
        .then(res => {
          this.users = res.data.content.list; //用户数据
       this.total = res.data.content.total;
          this.loading = false;
        })
        .catch(err => {
          this.$message("获取数据失败! ! !");
        });
    },   

2 用户状态设置

状态按钮

<el-button size="mini" type="text" @click="handleToggleStatus(scope.row)">
            {{ scope.row.status == "ENABLE" ? "禁用" : "启用" }}</el-button>

JS部分

//修改用户状态
handleToggleStatus(item) {
    return axios
        .get("/user/updateUserStatus", {
        params: {
            id: item.id,
            status: item.status
        }
    })
        .then(res => {
        debugger;
        console.log(res.data.content);
        item.status = res.data.content;
    })
        .catch(err => {
        this.$message.error("状态修改失败! ! !");
    });
},

五、权限管理

1 角色管理

1.1 展示&查询角色列表

  • 角色组件是 Roles.vue ,在该组件中对角色信息进行管理

image-20211225212102227

  • 需求分析

image-20211225212119752

  • 功能实现

数据部分

data() {
    return {
      listQuery: { name: "" },
      list: null,
      listLoading: false,
      dialogVisible: false,
      role: Object.assign({}, defaultRole),
      isEdit: false
    };
  },

钩子函数,调用loadRoles,获取角色数据

created() {
        //获取角色列表
        this.loadRoles();
      },
 //获取角色数据
    loadRoles() {
      return axios
        .post("/role/findAllRole", this.listQuery)
        .then(res => {
          this.list = res.data.content;
          this.listLoading = false;
        })
        .catch(err => {});
    },

请求携带的参数是: listQuery

<el-input v-model="listQuery.name" class="input-width" placeholder="角色名称" clearable></el-input>
//条件查询
handleSearchList() {
    this.loadRoles();
},

1.2 添加&修改角色

  1. 页面部分
<el-button size="mini" class="btn-add" @click="handleAdd()" style="margin-left: 20px">添加角色</el-button>
  1. 打开添加角色窗口的方法
//添加角色弹窗
handleAdd() {
    this.dialogVisible = true; //打开对话框
    this.isEdit = false; //false 修改操作
    this.role = Object.assign({}, defaultRole);
},

3.添加角色对话框,使用v-model 进行双向数据绑定.

<!-- 添加&修改 角色对话框 -->
<el-dialog :title="isEdit?'编辑角色':'添加角色'" :visible.sync="dialogVisible" 
width="40%">
    <el-form :model="role" label-width="150px" size="small">
        <el-form-item label="角色名称:">
            <el-input v-model="role.name" style="width: 250px"></el-input>
        </el-form-item>
        <el-form-item label="角色编码:">
            <el-input v-model="role.code" style="width: 250px"></el-input>
        </el-form-item>
        <el-form-item label="描述:">
            <el-input v-model="role.description" type="textarea" :rows="5" 
style="width: 250px"></el-input>
        </el-form-item>
    </el-form>
    <span slot="footer" class="dialog-footer">
        <el-button @click="dialogVisible = false" size="small">取 消</el-button>
        <el-button type="primary" @click="handleSave()" size="small">确 定</el-button>
    </span>
</el-dialog>
  1. 添加角色方法
//添加&修改角色
handleSave() {
    axios
        .post("/role/saveOrUpdateRole", this.role)
        .then(res => {
        this.dialogVisible = false;
        this.loadRoles();
    })
        .catch(error => {
        this.$message.error("保存课程信息失败! ! !");
    });
},
  1. 修改角色的方法

修改按钮,点击传递当前行数据对象

<el-button size="mini" type="text" @click="handleUpdate( scope.row)">编辑</el-button>

显示对话框,回显数据

//修改角色弹窗
handleUpdate( row) {
    this.dialogVisible = true;
    this.isEdit = true;
    //回显数据
    this.role = Object.assign({}, row);
},

修改角色,还是调用的handleSave 方法

1.3 删除角色

<el-button size="mini" type="text" @click="handleDelete(scope.row)">删除</el-button>

这里使用到了ElementIUI中的 MessageBox 弹框
https://element.eleme.cn/#/zh-CN/component/message-box#options

handleDelete(row) {
    this.$confirm("是否要删除该角色?", "提示", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning"
    }).then(() => {
        axios("/role/deleteRole?id=" + row.id)
            .then(res => {
            this.loadRoles();
        })
            .catch(err => {
            this.$message.error("操作失败! ! !");
        });
    });
},

1.4 为角色分配菜单

1.需求分析

  • 为角色分配菜单,一个角色可以拥有多个菜单权限

  • 一个菜单权限也可以被多个角色拥有

image-20211225212554773

  • 角色与菜单之间的关系 是多对多

image-20211225212618208

  • 点击分配菜单,页面展示效果

image-20211225212635485

  • 前端要实现的效果
    • 第一步: 获取到所有的菜单数据,在树形控件中进行展示
    • 第二步: 将当前角色拥有的菜单权限,勾选上
  1. 菜单展示功能实现
  1. 分配菜单按钮,点击传递当前行数据
<el-button size="mini" type="text" 
              @click="handleSelectMenu(scope.row)">分配菜单</el-button>
  1. 路由导航到 allocMenu
//为角色分配菜单
handleSelectMenu(row) {
    this.$router.push({ path: "/allocMenu", query: { roleId: row.id } });
},
  1. routes.js
{
      path: "allocMenu",
      name: "AllocMenu",
      component: () =>
      import(
      /* webpackChunkName: 'allocMenu' */ "../views/PermissionManage/AllocMenu"
      ),
      meta: { requireAuth: true, title: "角色菜单管理" }
  },
  1. 在AllocMenu.vue组件中完成 为角色分配菜单操作

image-20211225212823404

  1. 数据部分
data() {
    return {
      menuTreeList: [], //菜单数据
      checkedMenuId: [], //被选中的菜单
      //树形结构子节点设置
      defaultProps: {
        children: "subMenuList",
        label: "name"
      },
      roleId: null
    };
  },
  1. 钩子函数
//钩子函数
  created() {
    //获取路由携带的id
    this.roleId = this.$route.query.roleId;
    //获取菜单列表
    this.treeList();
    //获取角色所拥有的菜单信息
    this.getRoleMenu(this.roleId);
  },
  //方法1: 获取菜单列表,使用树形控件展示
    treeList() {
      axios.get("/role/findAllMenu").then(res => {
        console.log(res.data.content);
        //获取树形控件所需数据
        this.menuTreeList = res.data.content.parentMenuList;
      });
    },
        
    //方法2: 获取当前角色所拥有菜单列表id
    getRoleMenu(roleId) {
      axios.get("/role/findMenuByRoleId?roleId=" + roleId).then(res => {
        console.log(res.data.content);
        //将已有菜单权限设置为选中
        this.$refs.tree.setCheckedKeys(res.data.content);
      });
    },

3.分配菜单功能实现
分配菜单按钮

<div style="margin-top: 20px" align="center">
    <el-button type="primary" @click="handleSave()">保存</el-button>
    <el-button @click="handleClear()">清空</el-button>
</div>

方法

//方法3: 修改角色所拥有的菜单列表
    handleSave() {
      //debugger;
      //获取所有被选中的节点
      const checkedNodes = this.$refs.tree.getCheckedNodes();
      //定义常量 保存被选中的菜单id
      const checkedMenuIds = [];
      if (checkedNodes != null && checkedNodes.length > 0) {
        //遍历获取节点对象
        for (let i = 0; i < checkedNodes.length; i++) {
          const checkedNode = checkedNodes[i];
          
          //保存菜单列表id
          checkedMenuIds.push(checkedNode.id);
          //判断: 当前节点为子节点 && 其父ID在数组没有出现过,就保存这个父Id
          if (
            checkedNode.parentId !== -1 &&
            checkedMenuIds.filter(item =>checkedNode.parentId).length === 0
          ) {
            checkedMenuIds.push(checkedNode.parentId);
          }
        }
      }
      this.$confirm("是否分配菜单?", "提示", {
      confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning"
      }).then(() => {
        //准备参数
        const params = {
          roleId: this.roleId, //角色ID
          menuIdList: checkedMenuIds //当前角色拥有的菜单权限ID
        };
        //请求后台
        axios
          .post("/role/RoleContextMenu", params)
          .then(res => {
            this.$router.back();
          })
          .catch(err => {
            this.$message.error("权限分配失败! ! !");
          });
      });
    },

2 菜单管理

菜单组件是 Menus.vue ,在该组件中对菜单信息进行管理

image-20211225213042448

2.1 展示菜单列表

需求分析: 菜单列表的展示是带有分页的.

image-20211225213107240

  • 功能实现
  1. 数据部分
data() {
    return {
      total: 0, //总条数
      size: 10, //每页显示条数
      page: 1, //当前页
      list: [], //广告数据
      listLoading: true,
      parentId: 0 //菜单父id
    };
  },
  1. 钩子函数
created() {
    //获取菜单列表
    this.loadMenuList();
  },
//方法1: 加载菜单列表数据
    loadMenuList() {
      this.listLoading = true;
      return axios
        .get("/menu/findAllMenu", {
          params: {
            currentPage: this.page,
            pageSize: this.size
          }
        })
        .then(res => {
          this.list = res.data.content.list;
          this.total = res.data.content.total;
          this.listLoading = false;
        })
        .catch(error => {
          this.$message.error("数据获取失败! ! !");
        });
    },

2.2 新增&修改菜单

  1. 路由跳转流程

a.新增按钮, 点击跳转

<el-button class="btn-add" @click="handleAddMenu()" size="mini">添加菜单</el-button>
//新增菜单跳转
handleAddMenu() {
    this.$router.push("/addMenu");
},

b.AddMenu.vue 组件中引入了MenuDetail

<template>
  <menu-detail :is-edit='false'></menu-detail>
</template>
<script>
import MenuDetail from './MenuDetail'
export default {
  name: 'addMenu',
  title: '添加菜单',
  components: { MenuDetail }
}
</script>

c.MenuDetail.vue 中完成菜单的新增与修改操作

image-20211225213317273

2.需求分析
在打开新增菜单页面后, 需要展示一个下拉框,下拉框中的数据是所有的顶级父菜单.

image-20211225213333420

3.功能实现

  1. 数据部分
data() {
    return {
      menu, //菜单对象
      selectMenuList: [], //下拉列表数据
      rules
    };
  },
  1. 钩子函数
    在钩子函数中会进行判断,如果是修改操作,就根据ID 查询当前菜单信息,以及父菜单信息
    如果是新增操作,则只查询父类菜单信息即可
created() {
    if (this.isEdit) {
      //修改,回显菜单信息
      const id = this.$route.query.id;
      //获取当前菜单和父菜单信息
      this.findMenuInfoById(id);
      } else {
      //新增
      this.menu = {};
      //获取父类菜单信息
      this.findMenuInfoById(-1);
    }
  },
//方法1: 添加或修改 下拉父菜单回显
    findMenuInfoById(id) {
      axios
        .get("/menu/findMenuInfoById?id=" + id)
        .then(res => {
          debugger;
          console.log(res.data);
          //判断不为null,修改操作需要回显
          if (res.data.content.menuInfo != null) {
            this.menu = res.data.content.menuInfo;
          }
          //获取到父菜单信息,保存到selectMenuList
          this.selectMenuList = res.data.content.parentMenuList.map(item => {
            return { id: item.id, title: item.name };
          });
          //-1 显示 无上级菜单 (unshift向数组的开头添加一个元素)
          this.selectMenuList.unshift({ id: -1, title: "无上级菜单" });
        })
        .catch(err => {
          this.$message.error("数据获取失败! ! !");
        });
    },
  1. 点击保存
<el-button type="primary" @click="handleSave()">提交</el-button>
//保存菜单
    handleSave() {
      this.$refs.form.validate(valid => {
        if (!valid) return false;
        axios
          .post("/menu/saveOrUpdateMenu", this.menu)
          .then(res => {
            this.$router.back();
          })
          .catch(error => {
            this.$message.error("保存课程信息失败! ! !");
          });
      });
    }

3 资源管理

资源组件是 Resources.vue ,在该组件中对资源信息进行管理.

image-20211225213518492

3.1 展示&查询资源列表

  1. 展示资源数据 带有分页

image-20211225213534571

  1. 查询资源数据,查询条件有三个
  • 资源名称
  • 资源路径
  • 资源分类信息: 下拉列表

image-20211225213557207

  1. 数据部分
//查询条件
const listQuery = {
  currentPage: 1,
  pageSize: 5,
  name: null,
  url: null,
  categoryId: null
};
//资源对象
const defaultResource = {
  id: null,
  name: null,
  url: null,
  categoryId: null,
  description: ""
};
data() {
    return {
      listQuery,//查询条件
      total: 0,
      list: [], //资源数据
      cateList: [], //资源分类数据
      listLoading: false,
      dialogVisible: false,
      resource: Object.assign({}, defaultResource),
      isEdit: false,
      categoryOptions: [],
      defaultCategoryId: null
    };
  },
  1. 钩子函数
  • 在钩子函数中,需要获取资源 ,以及资源分类的数据
//钩子函数
created() {
    //获取资源数据
    this.getResourceList();
    //获取资源分类数据
    this.getResourceCateList();
},
  • getResourceList() 方法获取的是资源信息
//方法1: 获取资源数据
getResourceList() {
  this.listLoading = true;
  axios
    .post("/resource/findAllResource", this.listQuery)
    .then(res => {
      this.list = res.data.content.list;
      this.total = res.data.content.total;
      this.listLoading = false;
    })
    .catch(err => {
      this.$message.error("数据获取失败! ! !");
    });
},
  • getResourceCateList() 方法获取的是资源分类信息,在下拉框中展示
//方法2: 获取资源分类数据
getResourceCateList() {
  axios
    .get("/ResourceCategory/findAllResourceCategory")
    .then(res => {
      this.cateList = res.data.content;
      //遍历获取资源分类
      for (let i = 0; i < this.cateList.length; i++) {
        const cate = this.cateList[i];
        //将资源分类名与id保存到 categoryOptions中,供下拉列表展示
        this.categoryOptions.push({ label: cate.name, value: cate.id });
      }
      this.defaultCategoryId = this.cateList[0].id;
    })
    .catch(err => {
      this.$message.error("数据获取失败! ! !");
    });
},
  • 查询
<el-button style="float:right" type="primary" @click="handleSearchList()" size="small">查询搜索</el-button>
//查询条件对象
const listQuery = {
  currentPage: 1,
  pageSize: 5,
  name: null,
  url: null,
  categoryId: null
};
//查询按钮
handleSearchList() {
    this.getResourceList();
},

3.2 新增&修改资源

  1. 添加按钮
<el-button size="mini" class="btn-add" @click="handleAdd()" style="margin-left: 
20px">添加</el-button>
  1. 显示添加资源表单的对话框
//添加资源回显
handleAdd() {
    this.dialogVisible = true; //显示表单
    this.isEdit = false; //新增为false
    this.resource = Object.assign({}, defaultResource); //资源对象
    this.resource.categoryId = this.defaultCategoryId; //保存默认分类id
},

image-20211225213907268

  1. 资源分类信息使用下拉菜单进行展示:

v-model的值为当前被选中的el-option的 value 属性值

image-20211225213926080

<el-form-item label="资源分类:">
    <el-select v-model="resource.categoryId" placeholder="全部" clearable 
style="width: 250px">
    <el-option
        v-for="item in categoryOptions"
        :key="item.value"
        :label="item.label"
        :value="item.value"
        ></el-option>
    </el-select>
</el-form-item>
  1. 点击保存
<el-button type="primary" @click="handleSave()" size="small">确 定</el-button>
//添加&修改资源
handleSave() {
  axios
    .post("/resource/saveOrUpdateResource", this.resource)
    .then(res => {
      this.dialogVisible = false;
      this.getResourceList();
    })
    .catch(error => {
      this.$message.error("操作失败! ! !");
    });
},
  1. 修改操作, 参数是当前行数据
<el-button size="mini" type="text" @click="handleUpdate(scope.row)">编辑</el-button> 
  1. 回显操作
//编辑资源 回显
handleUpdate(row) {
  debugger;
  this.dialogVisible = true;
  this.isEdit = true;
  this.resource = Object.assign({}, row);
},

六、用户权限控制

1 用户登录

1.1 流程分析

  1. 用户登录界面,需要输入手机号密码

image-20211225214721952

  1. 登录组件 login.vue

image-20211225214740007

  • 登录按钮
<el-button type="primary" :loading="loading" @click="submit('login-form')">{{ loading ? 'Loading...' : '登录' }}</el-button>
  • 提交表的方法
 //提交登录表单
    submit(ref) {
      //校验
      this.$refs[ref].validate(valid => {
        if (!valid) return false;

        this.error = null;
        this.loading = true;

        //发送登录请求
        this.$store.dispatch("createToken", this.model)
          .then(res => {
            if (res.state !== 1) {
              this.error = {
                title: "Error occurred",
                message: "Abnormal, please try again later!"
              };
            }
            this.$router.replace({ path: this.$route.query.redirect || "/" });
            this.loading = false;
          })
          .catch(err => {
            this.loading = false;
          });
      });
    }
  }
  • this.$store.dispatch("createToken", this.model)
    • 这段代码的意思是调用 store仓库的actions.js中的createToken方法

image-20211225214759489

  • 发送登录请求,进行登录的代码
  /**
   * 创建新的客户端令牌
   */
  createToken: async ({ commit }, { username, password }) => {
    //请求后台登录接口
    const res = await TokenService.userLogin({
      phone: username.trim(),
      password: password.trim()
    });

    console.log(res);

    //判断结果不等于1,登录失败
    if (res.state !== 1) {
      return Promise.resolve(res);
    }

    //获取到content
    const result = res.content;

    //将token保存
    commit(CHANGE_SESSION, {
      accessToken: result.access_token
    });
      
    return res;
  },
  • TokenService
import { TokenService, UserService } from "../services";

image-20211225214830448

//登录请求 async ES6语法, 作用: 发送异步请求
export const userLogin =  async (data) => {
 //await 表示等待接收返回的数据
 return await PostRequest(`${process.env.VUE_APP_API_FAKE}/user/login${Serialize(data)}`)
}

2 动态获取用户菜单

2.1 流程分析

  1. 在我们登录成功后, 会立即发送第二个请求, 来获取用户的菜单权限列表

image-20211225214851254

  1. 在actions.js 中完成请求后台接口 获取数据的操作
  /**
   * 获取当前登录用户权限
   */
  getUserPermissions: async ({ commit }) => {
    //1.请求后台 获取当前用户的权限
    const res = await UserService.getUserPermissions();

    //2.判断
    if (!res.success) {
      //获取失败直接返回 false
      return res.success;
    }

    //3.获取数据成功,取出菜单 与 资源列表
    const { menuList, resourceList } = res.content;

    //4.下面的代码 就是在生成树形结构的菜单
    let menus = [];
    const formatMenu = treeData => {
      if (treeData.length > 0) {
        return treeData.map(item => formatMenu(item));
      }
      const result = {};

      //shown等于表示可以显示,将内容保存
      if (treeData.shown == 1) {
        result.id = treeData.id;
        result.text = treeData.name;
        result.label = treeData.name;
        result.name = treeData.href;
        result.icon = treeData.icon;
        result.shown = treeData.shown;
      } else {
        return "";
      }

      //获取子节点
      if (treeData.subMenuList) {
        result.children = [];
        treeData.subMenuList.forEach(item => {
          formatMenu(item) && result.children.push(formatMenu(item));
        });

        if (result.children.length === 0) {
          delete result.children;
        }
      }
      return result;
    };

    const memusMap = {};

    const splapMenu = treeData => {
      if (treeData.length > 0) {
        return treeData.map(item => splapMenu(item));
      }
      const result = {};
      result.id = treeData.id;
      result.text = treeData.name;
      result.label = treeData.name;
      result.name = treeData.href;
      result.icon = treeData.icon;
      result.shown = treeData.shown;
      result.name && (memusMap[result.name] = result);

      if (treeData.subMenuList) {
        result.children = [];
        treeData.subMenuList.forEach(item => {
          result.children.push(splapMenu(item));
        });
      }
      return result;
    };

    splapMenu(menuList);

    menus = formatMenu(menuList);
    commit(CHANGE_SIDERBAR_MENU, menus);
    return { menus, resourceList, menuList, memusMap };
  },

3 验证Token

3.1 导航守卫

  • 在执行路由之前先执行的一些钩子函数,比如验证用户是否有权限之类的操作,就需要使用.
  1. authorize.js 中配置了导航守卫,来对用户的登录进行限制
  // 导航守卫 to要访问的url, from从哪个路径跳转过来, next() 放行
  router.beforeHooks.unshift((to, from, next) => {
    //不需要验证直接放行
    if (!to.meta.requireAuth) return next();

    //需要验证token,调用 store中的checkToken方法
    store.dispatch("checkToken").then(valid => {
      //判断是否存在token
      if (valid) {
        //发送请求到后台,在后台再次判断token是否存在
        store.dispatch("getUserPermissions").then(res => {
        
          if (!res) {
            //失效 清除token
            store.dispatch("deleteToken");

            //跳转到登录页面
            return next({ name: "ToLogin" });
          }

          //token正确, 导航到对应的页面
          const { memusMap } = res;
          if (memusMap.Courses && to.name === "Home") {
            return next();
          } else if (memusMap[to.name]) {
            return next();
          } else if (Object.keys(memusMap).length > 0) {
            return next({ name: memusMap[Object.keys(memusMap)[0]].name });
          } else {
            next({ name: "PermissionDenied" });
          }
        });
        return next();
      }
      // unauthorized
      console.log("Unauthorized");

      //用户没有登录 跳转到登录页面
      next({ name: "Login", query: { redirect: to.fullPath } });
    });
  });
  1. 在actions.js 中检查token是否可用
checkToken: async ({ commit, getters }) => {
    //取出token
    const token = getters.session.accessToken;

    if (!token) {
      //不可用
      return Promise.resolve(false);
    }

    return Promise.resolve(true);
  },

4 用户角色分配

4.1 流程分析

  • 点击分配角色按钮

image-20211225214914807

<el-button size="mini" type="text" @click="handleSelectRole(scope.row)">分配角色
</el-button>
  • 分配角色对话框

image-20211225214926971

4.2 代码部分

  • 显示对话框
	//分配角色
    handleSelectRole(row) {
      //保存用户ID
      this.allocAdminId = row.id;

      //获取角色列表
      this.getRoleList();

      //获取当前用户拥有的角色
      this.getUserRoleById(row.id);

      //打开对话框
      this.allocDialogVisible = true;
    },
  • 获取角色列表,在下拉菜单中展示
 getRoleList(id) {
      return axios
        .post("/role/findAllRole", this.listQuery)
        .then(res => {
          this.allRoleList = res.data.content.map(item => {
            return { id: item.id, name: item.name };
          });
        })
        .catch(err => {});
    },
  • 获取当前用户拥有的角色,回显默认选中
 getUserRoleById(id) {
      axios.get("/user/findUserRoleById?id=" + id).then(res => {
        const allocRoleList = res.data.content;
        this.allocRoleIds = [];
        if (allocRoleList != null && allocRoleList.length > 0) {
          for (let i = 0; i < allocRoleList.length; i++) {
            this.allocRoleIds.push(allocRoleList[i].id);
          }
        }
      });
    },
  • 为用户分配角色
 handleAllocRole() {
      const params = {
        userId: this.allocAdminId,
        roleIdList: this.allocRoleIds
      };

      axios.post("/user/userContextRole", params).then(res => {
        this.allocDialogVisible = false;
      });
    },

任务二: nginx

1 什么是nginx?

​ Nginx(发音同 engine x)是一款轻量级的Web 服务器/反向代理服务器及电子邮件(IMAP/POP3)代理服务器,并在一个BSD-like 协议下发行。由俄罗斯的程序设计师Igor Sysoev(伊戈尔·西索夫)所开发,供俄国大型的入口网站及搜索引擎Rambler(漫步者)(俄文:Рамблер)使用。其特点是占有内存少,并发能力强,事实上nginx的并发能力确实在同类型的网页服务器中表现较好,中国大陆使用nginx网站用户有:新浪、网易、 腾讯等。

优点:

  1. 占用内存少,并发能力强
  2. Nginx专为性能优化而开发, 在高连接并发的情况下,能够支持高达 50,000 个并发连接数的响应.
  3. Nginx支持热部署, 可以在不间断服务的情况下,对软件版本进行升级.

2 应用场景

  1. http服务器: Nginx是一个http服务可以独立提供http服务。可以做网页静态服务器。

  2. 虚拟主机: 可以实现在一台服务器虚拟出多个网站。例如个人网站使用的虚拟主机。

  3. 反向代理,负载均衡 : 当网站的访问量达到一定程度后,单台服务器不能满足用户的请求时,需要用多台服务器集群可以使用nginx做反向代理。并且多台服务器可以平均分担负载,不会因为某台服务器负载高宕机而某台服务器闲置的情况。

3 Nginx安装

下载nginx, 官方网站:http://nginx.org/

我们使用的版本是1.17.8版本。

image-20211225215026056

Nginx在Linux下安装,只提供了源代码,所以我们需要进行编译.

3.1 安装环境配置

  1. 因为Nginx是C语言编写的,所以需要配置C语言编译环境 (一定要在联网状态下安装)
需要安装gcc的环境。执行命令: 
yum install gcc-c++

注意: 如果执行命令出现这样的提示:

image-20211225215038133

解决办法:

问题是 yum在锁定状态中,强制关掉yum进程即可
rm -f /var/run/yum.pid
  1. 第三方的开发包, 在编译之前需要安装这些第三方包。
  • PCRE

    • nginx的http模块使用pcre来解析正则表达式,所以需要在linux上安装pcre库
    安装命令:
    yum install -y pcre pcre-devel
    
  • zlib

    • nginx使用zlib对http包的内容进行gzip,所以需要在linux上安装zlib库。
    安装命令:
    yum install -y zlib zlib-devel
    
  • openssl

    • OpenSSL 是一个强大的安全套接字层密码库,nginx不仅支持http协议,还支持https,所以需要在linux安装openssl库。
    安装命令:
    yum install -y openssl openssl-devel
    

3.2 安装Nginx 步骤

  1. 将Nginx的源码包上传到 Linux

  2. 解压Nginx

tar -xvf nginx-1.17.8.tar 

image-20211225215057960

  1. 进入到解压之后的目录 nginx-1.17.8

image-20211225215108351

  1. 执行命令 configure,生成 Mikefile 文件
./configure \
--prefix=/usr/local/nginx \
--pid-path=/var/run/nginx/nginx.pid \
--lock-path=/var/lock/nginx.lock \
--error-log-path=/var/log/nginx/error.log \
--http-log-path=/var/log/nginx/access.log \
--with-http_gzip_static_module \
--http-client-body-temp-path=/var/temp/nginx/client \
--http-proxy-temp-path=/var/temp/nginx/proxy \
--http-fastcgi-temp-path=/var/temp/nginx/fastcgi \
--http-uwsgi-temp-path=/var/temp/nginx/uwsgi \
--http-scgi-temp-path=/var/temp/nginx/scgi

执行命令后, 生成了MakeFile文件

image-20211225215122756

  1. 创建临时文件目录
mkdir /var/temp/nginx/client -p
  1. 执行make命令,进行编译
make
  1. 安装
make install

image-20211225215135265

2.3.3 启动并访问 Nginx

  1. 进入到nginx 安装目录
cd /usr/local/nginx/

image-20211225215149820

  1. 进入到 sbin目录,执行 nginx 命令
./nginx 启动
./nginx -s stop 关闭
ps aux | grep nginx 查看进程

image-20211225215207567

  1. 通过浏览器进行访问 ,默认端口 80 (注意:是否关闭防火墙。)

image-20211225215217289

4 配置虚拟主机

虚拟主机指的是,在一台服务器中,我们使用Nginx,来配置多个网站.

如何区分不同的网站:

  1. 端口不同
  2. 域名不同

4.1 通过端口区分不同的虚拟主机

Nginx配置文件
  1. Nginx配置文件的位置
cd /usr/local/nginx/conf
nginx.conf 就是Nginx的配置文件
  1. Nginx核心配置文件说明
worker_processes  1; #work的进程数,默认为1
#配置 影响nginx服务器与用户的网络连接
events {
    worker_connections  1024; #单个work 最大并发连接数
}

# http块是配置最频繁的部分 可以嵌套多个server,配置代理,缓存,日志定义等绝大多数功能
http {
	# 引入mime类型定义文件
    include       mime.types;
    default_type  application/octet-stream;
    sendfile        on;
    keepalive_timeout  65; # 超时时间
	
	#server 配置虚拟主机的相关参数 可以有多个,一个server就是一个虚拟主机
    server {
		# 监听的端口
        listen       80; 
		#监听地址
        server_name  localhost;         

		# 默认请求配置
        location / {
            root   html; # 默认网站根目录
            index  index.html index.htm; # 欢迎页
        }

		# 错误提示页面
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }
    }
}
使用Notpad,连接Linux

​ 使用notepad++来连接linux,好处是使用notepad++来编辑linux中文件的批量文字,会比直接在linux中操作方便快捷很多.

  1. Notepad 插件中安装NppFTP

image-20211225215231291

  1. 打开NppFTP

image-20211225215239789

  1. 选择设置

image-20211225215300125

  1. 配置连接信息

image-20211225215314242

  1. 连接

image-20211225215327271

配置nginx.conf
  1. 使用Notpad 在nginx.conf 中添加一个 新的server
http {
    include       mime.types;
    default_type  application/octet-stream;


    sendfile        on;
    #tcp_nopush     on;

    #keepalive_timeout  0;
    keepalive_timeout  65;

    #gzip  on;

    server {
        listen       80;
        server_name  localhost;

        location / {
            root   html;
            index  index.html index.htm;
        }
    }
	
	# 配置新的server
	server {
        listen       81; # 修改端口
        server_name  localhost;

        location / {
            root   html81; # 重新制定一个目录
            index  index.html index.htm;
        }
    }

}
  1. 复制一份 html目录
cp -r html html81

image-20211225215345573

  1. 重新加载配置文件
sbin/nginx -s reload
  1. 访问
http://192.168.52.100 访问第一个server

http://192.168.52.100:81/ 访问第二个server

4.2 通过域名区分不同的虚拟主机

什么是域名

网址就是域名,是一个网站的地址, 由域名提供商提供,一般需要购买.

www.baidu.com

www.taobao.com

www.jd.com

image-20211225215357708

域名级别
  • 一级域名
    • 比如 .com .org .cn
  • 二级域名
    • 二级域名是在一级域名前加一级
    • 二级域名: baidu.com , zhihu.com
  • 三级域名
    • www.baidu.com
    • image.baidu.com
域名绑定
  • 一个域名对应一个ip地址,一个ip地址可以被多个域名绑定。
  • 通过 DNS服务器去解析域名
配置域名映射
  1. 本地测试可以修改hosts文件。修改window的hosts文件:(C:\Windows\System32\drivers\etc)
  • 可以配置域名和ip的映射关系,如果hosts文件中配置了域名和ip的对应关系,不需要走dns服务器。
配置一下nginx的映射
192.168.52.100 www.ng.com

image-20211225215413572

  1. 使用SwitchHosts,修改hosts
  • 解压

image-20211225215424377

  • 右键 以管理员身份运行

image-20211225215433922

  • 配置IP与域名的映射

image-20211225215450365

配置nginx.conf
#通过域名区分虚拟主机
	server {
        listen       80;
        server_name  www.t1.com;

        location / {
            root   html-t1;
            index  index.html index.htm;
        }
    }
	
	server {
        listen       80;
        server_name  www.t2.com;

        location / {
            root   html-t2;
            index  index.html index.htm;
        }
    }
  • 创建 html-t1和 html-t2 目录
cp -r html html-t1
cp -r html html-t2

image-20211225215503735

  • 修改一下index.html 中,刷新
sbin/nginx -s reload
  • 访问

image-20211225215529914

虽然只有一台服务器,但是这台服务器上运行着多个网站,访问不同的域名 就可访问到不同的网站内容

5 反向代理

5.1 什么是代理

​ 代理其实就是一个中介,A和B本来可以直连,中间插入一个C,C就是中介。刚开始的时候,代理多数是帮助内网client访问外网server用的.

客户机在发送请求时,不会直接发送给目的主机,而是先发送给代理服务器,代理服务接受客户机请求之后,再向主机发出,并接收目的主机返回的数据再发送给客户机。

5.2 正向代理

​ 比如我们国内访问谷歌,直接访问访问不到,我们可以通过一个正向代理服务器,先将请求发送到到代理服,代理服务器能够访问谷歌,这样由代理去谷歌取到返回数据,再返回给我们,这样我们就能访问谷歌了

正向代理代理的是客户端, 服务端不知道实际发起请求的客户端.

image-20211225215542862

5.3 反向代理

 反向代理和正向代理的区别就是:正向代理代理客户端,反向代理代理服务器。

​ 反向代理是指用代理服务器接收客户端的请求,然后将请求转发给网站内部应用服务器,并将从服务器上得到的结果返回给客户端.

image-20211225215556858

5.4 Nginx实现反向代理

Nginx作为反向代理服务器安装在服务端,Nginx的功能就是把请求转发给后面的应用服务器.

image-20211225215607425

  • 配置步骤

    • 第一步:简单的使用2个tomcat实例模拟两台http服务器,分别将tomcat的端口改为8080和8081

    image-20211225215630005

    • 第二步:启动两个tomcat。
    ./bin/startup.sh 
    访问两个tomcat
    http://192.168.52.100:8080/
    http://192.168.52.100:8081/ 
    
    • 第三步:反向代理服务器的配置
    	#反向代理配置 
    	#upstream中的server是真正处理请求的应用服务器地址
    	upstream lagou1{
    		#用server定义HTTP地址
    		server 192.168.52.100:8080;
    	}
    	
    	
    	server {
            listen       80;
            server_name  www.lagou1.com;
            location / {
            	# 利用 proxy_ pass可以将请求代理到upstream命名的HTTP服务
                proxy_pass http://lagou1;  #转发到的地址
                index  index.html index.htm;
            }
        }
    	
    	upstream lagou2{
    		#用server定义HTTP地址
    		server 192.168.52.100:8081;
    	}
    	
    	
    	server {
            listen       80;
            server_name  www.lagou2.com;
            location / {
                proxy_pass http://lagou2; 
                index  index.html index.htm;
            }
      }
    
    • 第四步:nginx重新加载配置文件
    nginx -s reload
    
  • 第五步:配置域名, 在hosts文件中添加域名和ip的映射关系

    image-20211225215647741

    192.168.52.100 www.lagou1.com
    192.168.52.100 www.lagou2.com
    

通过浏览器输入域名, 访问Nginx代理服务器, Nginx根据域名将请求转发给对应的目标服务器,作为用户我们看到的是服务器的响应结果页面,在整个过程中目标服务器相对于客户端是不可见的,服务端向外暴露的就是Nginx的地址.

6 负载均衡

6.1 什么是负载均衡

​ 当一个请求发送过来的时候,Nginx作为反向代理服务器,会根据请求找到后面的目标服务器去处理请求,这就是反向代理. 那么, 如果目标服务器有多台的话,找哪一个服务器去处理当前请求呢 ? 这个合理分配请求到服务器的过程就叫做负载均衡.

image-20211225215703280

6.2 为什么用负载均衡

​ 当系统面临大量用户访问,负载过高的时候,通常会使用增加服务器数量来进行横向扩展, 负载均衡主要是为了分担访问量,将请求合理分发给不同的服务器, 避免临时的网络堵塞

6.3 负载均衡策略

6.3.1 轮询
  • 默认策略, 每个请求按照时间顺序逐一分配到不同的服务器,如果某一个服务器下线,能自动剔除

  • 配置方式

    负载均衡

    upstream lagouServer{

    用server定义 HTTP地址

    server 192.168.52.100:8081;
    server 192.168.52.100:8082;
    }

    server {
    listen 80;
    server_name www.lagouNB.com;

    location / {
    	# 利用 proxy_ pass可以将请求代理到upstream命名的HTTP服务
        proxy_pass   http://lagouServer;
        index  index.html index.htm;
    }
    

    }

    负载均衡

    upstream lagouServer{

    用server定义 HTTP地址

    server 192.168.52.100:8081;
    server 192.168.52.100:8082;
    }

    server {
    listen 80;
    server_name www.lagouNB.com;

    location / {
    	# 利用 proxy_ pass可以将请求代理到upstream命名的HTTP服务
        proxy_pass   http://lagouServer;
        index  index.html index.htm;
    }
    

    }

6.3.2 weight
  • 可以根据服务器的实际情况调整服务器权重。权重越高分配的请求越多,权重越低,请求越少。默认是都是1.

    负载均衡

    upstream lagouServer{
    # 用server定义 HTTP地址
    server 192.168.52.100:8081 weight=1;
    server 192.168.52.100:8082 weight=10;
    }

任务三:项目部署与发布

1 后台项目部署

1.1 Linux环境准备

  1. 需要安装的软件

  1. 关闭防火墙

  2. 使用SQLYog连接Linux上的MySQL, 导入SQL脚本 创建项目所需的数据库

image-20211225215745385

1.2 项目打包发布

​ 在平常开发的过程中,不同的环境中项目的相关配置也会有相关的不同,我们在不同的环境中部署就要手动修改为对应环境的配置,这样太麻烦了以及这样也会很容易出错。

接下来我们就通过maven的相关配置来在打包时指定各个环境对应配置文件

1. 修改ssm_dao 子模块
  • 修改后的目录结构

image-20211225215756539

2. 第一步: 创建配置文件

在项目的src/main/resources 下面创建filter目录, 再创建 development.properties , product.properties 两个文件

  • development是开发配置内容。
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql:///ssm_lagou_edu?characterEncoding=UTF-8
jdbc.username=root
jdbc.password=123456
  • product是正式配置内容
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://192.168.52.100:3306/ssm_lagou_edu?characterEncoding=UTF-8
jdbc.username=JiuYuan
jdbc.password=JiuYuan@123
3. 第二步:配置jdbc.properties 文件

jdbc.properties中的内容不再写死,而是从上面两个文件中获取

image-20211225215812978

jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=${jdbc.url}
jdbc.username=${jdbc.username}
jdbc.password=${jdbc.password}

注意:${jdbc.url} 直接对应上面配置的development.properties或product.properties文件中的名称。

4. 第三步: 配置dao模块的的 pom.xml文件

添加如下配置

 <profiles>
        <profile>
            <id>dev</id>
            <properties>
                <!-- 测试环境 -->
                <env>development</env>
            </properties>
            <activation>
                <activeByDefault>true</activeByDefault>
            </activation>
        </profile>
        <profile>
            <id>prod</id>
            <properties>
                <!-- 正式环境 -->
                <env>product</env>
            </properties>
        </profile>
    </profiles>

    <build>
        <finalName>web</finalName>
        <filters>
            <filter>src/main/resources/filter/${env}.properties</filter>
        </filters>
        <resources>
            <resource>
                <directory>src/main/resources</directory>
                <excludes>
                    <exclude>filter/*.properties</exclude>

                </excludes>
                <filtering>true</filtering>
            </resource>
        </resources>
    </build>
5. profile说明
  • profile可以让我们定义一系列的配置信息,然后指定其激活条件。这样我们就可以定义多个profile,然后每个profile对应不同的激活条件和配置信息,从而达到不同环境使用不同配置信息的效果
  • 默认启用的是dev环境配置:
 <profiles>
        <profile>
            <id>dev</id>
            <properties>
                <!-- 测试环境 -->
                <env>development</env>
            </properties>
            <activation>
                <activeByDefault>true</activeByDefault>
            </activation>
        </profile>
        <profile>
            <id>prod</id>
            <properties>
                <!-- 正式环境 -->
                <env>product</env>
            </properties>
        </profile>
    </profiles>
  • 指定数据库配置文件路径,此路径可以自定义:
<filters>
	<filter>src/main/resources/filter/${env}.properties</filter>
</filters>
  • 指定资源目录路径
<resources>
    <resource>
        <directory>src/main/resources</directory>
        <!-- 资源根目录排除各环境的配置 -->
        <excludes>
        	<exclude>filter/*.properties</exclude>
        </excludes>
        <filtering>true</filtering>
    </resource>
</resources>
6.第四步: 打包
  • 命令打包
打本地包 mvn -Pdev install 或者mvn install(因为本例activeByDefault配的为true)
打产品包 mvn -Pprod install

结果:src/main/resources/config/jdbc.properties根据 mvn -P 参数决定值
  • 使用idea打包
7.打包后的文件
  • 使用生产环境的配置文件,进行打包image-20211225215839218

image-20211225215849327

  • 打开这个war包,我们会发现 其他子模块都已经被打成jar包,放到了lib文件夹下

image-20211225215859458

8. 发布
  • 修改一下项目名称

image-20211225215918302

  • 上传到tomcat中,启动测试

image-20211225215929712

  • 在部署tomcat的 webapps目录下创建一个 upload文件夹,保存图片
mkdir upload
  • 访问
http://192.168.52.100:8080/ssm-web/user/login?phone=18211111111&password=123456
  • 获取到响应的JSON, 发布成功

image-20211225215941242

2 前端项目部署

2.1 修改配置文件

  • 生产环境配置文件,配置后台URL

image-20211225215951330

VUE_APP_NAME = Edu Boss
VUE_APP_TITLE = Lagou Edu Boss (Dev)

VUE_APP_STORAGE_PREFIX = lagou_edu_boss_dev

#VUE_APP_API_FAKE = /front
VUE_APP_API_FAKE = http://192.168.52.100:8080/ssm-web

#VUE_APP_API_BASE = /boss
VUE_APP_API_BASE = http://192.168.52.100:8080/ssm-web
  • 自定义配置文件,配置打包相关信息

image-20211225220000168

将下面内容拷贝到 vue.config.js

module.exports = {
  publicPath: process.env.NODE_ENV === "production" ? "/edu-boss/" : "/",
  indexPath: "index.html",
  assetsDir: "static",
  lintOnSave: process.env.NODE_ENV !== "production",
  productionSourceMap: false,
  devServer: {
    open: true,
    port: 8081
  }
};

2.2 打包测试操作

  • 打包命令
npm run build
  • 在项目下会生成一个 dist 目录

image-20211225220010744

  • 在本地tomcat的webapps目录下,创建一个edu-boss文件夹,将dist目录中的文件拷贝到里面

image-20211225220020082

  1. 测试: 启动本地tomcat ,访问前端项目 路径为:
http://localhost:8081/edu-boss/

2.3 发布前端项目

  1. 解压一个新的tomcat, 修改端口号
#解压
tar xvf apache-tomcat-8.5.50.tar 
#改名
mv apache-tomcat-8.5.50 ui-tomcat

#修改端口号
cd ui-tomcat/conf/
vim server.xml 

image-20211225220038755

  1. 上传前端项目到 webapps
//上传 edu-boss.zip ,并解压
unzip edu-boss.zip 

//删除edu-boss.zip
rm -rf edu-boss.zip
  1. 运行前端项目,并访问
./bin/startup.sh 

//动态查看日志
tail -f logs/catalina.out 

//访问
http://192.168.52.100:8081/edu-boss/

3 修改tomcat默认访问项目

  • 使用notpad打开前端tomcat的配置文件 server.xml, 找到 Host 标签

image-20211225220050215

  1. 在Host标签内加入
<Context path="" docBase="edu-boss" reloadable="true" debug="0" privileged="true">
</Context>

  1. 重新启动 并访问前端项目,这个时候只需要直接访问 8081即可
http://192.168.52.100:8081/

4 配置反向代理

  1. 使用notpad打开nginx的配置文件 nginx.conf

image-20211225220100952

  1. 配置反向代理
#配置ssm项目 反向代理
	upstream lagouedu{
		server 192.168.52.100:8081;
	}
	
	server {
        listen       80;
        server_name  www.edu-boss.com;

        location / {
			
            proxy_pass http://lagouedu;  #转发的地址
            index  index.html index.htm;
        }
    }
  1. 修改本地hosts, 配置域名映射

image-20211225220111566

  1. 访问 www.edu-boss.com

image-20211225220123085

posted on 2021-12-25 22:04  寒露凝珠  阅读(95)  评论(0编辑  收藏  举报

导航