基于SpringBoot+Mysql+Bootstrap+VUE+微信小程序实现的乐器培训机构系统

前言:可作为毕设学习使用,有相关场景需要可在此基础上快速搭建成型系统。目前有些东西还不太全,比如支付接口(这个的话需要可以进行具体对接)、课程的付费模式等,但是其他的整体流程已经开发好了。小程序端、小程序服务端、小程序后台、数据端、前后端的交互等,都已经可以使用。这篇博客分享,主要是为了帮助想独立开发小程序的朋友,以及一些需要定制类似系统的作者做参考。

一、项目介绍

微信小程序端:

    1、小程序端包含首页、产品、我的三个主模块。
    2、首页模块实现机构的课程列表、课程详情及课程学习,产品模块实现产品搜索、产品分类和详情,个人模块实现我的足迹、个人信息和我的课程。

小程序服务端:

    1、实现对小程序功能的接口及业务逻辑支撑。
    2、对接数据端处理。

Web后台管理端:

    1、实现对不同角色的权限管理,管理员以及普通用户操作权限分离。
    2、Web端管理实现对会员的管理。
    3、实现对课程的管理(课程录入、课程分类、章节管理等)
    4、Web端管理员实现对商场的关键词设置。
    5、实现对商品的管理(商品上架、品牌管理、类目管理、库存管理等)
    6、实现推广管理、一键运营,轻松设置广告banner。


二、相关技术

    1、小程序采用小程序原生语言开发。
    2、小程序管理后台系统采用目前流行的前后端分离模式,前端使用Vue+ElementUI搭建后台管理界面。
    3、小程序服务端已经管理后台均采用Springboot+Mybatis实现。
    4、权限处理使用了Shiro框架实现。
    5、使用了lombok、SelectOneByExamplePlugin/ExampleEnhancedPlugin等插件。
    6、对于系统一些商品、课程的上传存储,目前实现了对接对地存储、阿里云对象存储、腾讯云对象存储、七牛云对象存储四种方式。


三、系统结构


在这里插入图片描述


四、界面展示

小程序端:

首页(课程展示):在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
产品模块(产品展示、购买):
在这里插入图片描述
在这里插入图片描述
我的模块:
在这里插入图片描述
Web后台管理端:

管理登录页面:
在这里插入图片描述
所有模块菜单:

在这里插入图片描述
用户管理:
在这里插入图片描述
课程管理:

在这里插入图片描述
在这里插入图片描述
商场关键词设置:

在这里插入图片描述
商品管理:

在这里插入图片描述
推广管理:

在这里插入图片描述


四、部分关键代码

本系统前后陆陆续续用了2个多月,也是在边学习新技术,边上手做东西,真的是实际开发成品才会遇到一个又一个技术难关、一个又一个自己知识的盲区,还好坚持下来了~~

小程序端,说两个页面的布局吧,一个是主页面的,一个是购物车页面。

主页面 index.wxml:

<!--index.wxml-->
<view class="container">
<!-- 搜索 -->
<view class="search">
    <navigator url="/pages/search/search?source=course" class="input">
      <image class="icon"></image>
      <text class="txt">课程搜索, 共{{courseCount}}门课程</text>
    </navigator>
  </view>
  <!-- 活动轮播 -->
  <swiper class="banner" indicator-dots="true" autoplay="true" interval="3000" duration="1000">
    <swiper-item wx:for="{{banner}}" wx:key="id">
      <navigator url="{{item.link}}">
        <image src="{{item.url}}" background-size="cover"></image>
      </navigator>
    </swiper-item>
  </swiper>
  <!--快捷入口 -->
  <view class="fast_column">
      <navigator bindtap="openPage" class="item" data-url="/pages/courseList/courseList?type=hot">
        <!-- <image src="{{item.iconUrl}}" background-size="cover"></image>
        <text>{{item.name}}</text> -->
        <image src="/static/images/shortCut/hot_course.png" background-size="cover"></image>
        <text>热门课程</text>
      </navigator>
      <navigator bindtap="openPage" class="item" data-url="/pages/musicianStyle/index">
      <!-- <image src="{{item.iconUrl}}" background-size="cover"></image>
      <text>{{item.name}}</text> -->
       <image src="/static/images/shortCut/musician_style.png" background-size="cover"></image>
       <text>乐手风采</text>
     </navigator>
     <navigator class="item">
      <!-- <image src="{{item.iconUrl}}" background-size="cover"></image>
      <text>{{item.name}}</text> -->
       <image src="/static/images/shortCut/about_me.png" background-size="cover"></image>
      <text>关于我们</text>
    </navigator>
  </view>
  <!-- 体系课程 -->
  <view class="a-section a-type">
    <view class="h">
      <navigator url="">
        <text class="text"> — 体系课程系列 — </text>
      </navigator>
    </view>
    <view class="b">
      <view class="item item-1" wx:for="{{sysCatList}}" wx:key="id">
        <navigator url="/pages/courseList/courseList?id={{item.id}}">
          <view class="wrap">
            <image class="img" src="{{item.iconUrl}}" mode="aspectFill"></image>
            <view class="mt">
              <text class="name">{{item.name}}</text>
            </view>
          </view>
        </navigator>
      </view>
    </view>
    <view class="h">
      <navigator url="" >
        <text class="text"> — 特色课程系列 — </text>
      </navigator>
    </view>
    <view class="b">
      <view class="item item-1" wx:for="{{feaCatList}}" wx:key="id">
        <navigator url="/pages/courseList/courseList?id={{item.id}}">
          <view class="wrap">
            <image class="img" src="{{item.iconUrl}}" mode="aspectFill"></image>
            <view class="mt">
              <text class="brand">{{item.name}}</text>
            </view>
          </view>
        </navigator>
      </view>
  </view>
  </view>
</view>

购物车页面:cart.wxml

<view class="container">
  <view class="no-login" wx:if="{{!hasLogin}}">
    <view class="c">
      <image src="http://nos.netease.com/mailpub/hxm/yanxuan-wap/p/20150730/style/img/icon-normal/noCart-a8fe3f12e5.png" />
      <text>还没有登录</text>
      <button style="background-color:#A9A9A9" bindtap="goLogin">去登录</button>
    </view>
  </view>
  <view class='login' wx:else>
    <view class="service-policy">
      <view class="item">30天无忧退货</view>
      <view class="item">48小时快速退款</view>
      <view class="item">满88元免邮费</view>
    </view>
    <view class="no-cart" wx:if="{{cartGoods.length <= 0}}">
      <view class="c">
        <image src="http://nos.netease.com/mailpub/hxm/yanxuan-wap/p/20150730/style/img/icon-normal/noCart-a8fe3f12e5.png" />
        <text>去添加点什么吧</text>
      </view>
    </view>
    <view class="cart-view" wx:else>
      <view class="list">
        <view class="group-item">
          <view class="goods">
            <view class="item {{isEditCart ? 'edit' : ''}}" wx:for="{{cartGoods}}" wx:key="id">
              <view class="checkbox {{item.checked ? 'checked' : ''}}" bindtap="checkedItem" data-item-index="{{index}}"></view>
              <view class="cart-goods">
                <image class="img" src="{{item.picUrl}}"></image>
                <view class="info">
                  <view class="t">
                    <text class="name">{{item.goodsName}}</text>
                    <text class="num">x{{item.number}}</text>
                  </view>
                  <view class="attr">{{ isEditCart ? '已选择:' : ''}}{{item.goodsSpecificationValues||''}}</view>
                  <view class="b">
                    <text class="price">¥{{item.price}}</text>
                    <view class="selnum">
                      <view class="cut" bindtap="cutNumber" data-item-index="{{index}}">-</view>
                      <input value="{{item.number}}" class="number" disabled="true" type="number" />
                      <view class="add" bindtap="addNumber" data-item-index="{{index}}">+</view>
                    </view>
                  </view>
                </view>
              </view>
            </view>
          </view>
        </view>

      </view>
      <view class="cart-bottom">
        <view class="checkbox {{checkedAllStatus ? 'checked' : ''}}" bindtap="checkedAll">全选({{cartTotal.checkedGoodsCount}})</view>
        <view class="total">{{!isEditCart ? '¥'+cartTotal.checkedGoodsAmount : ''}}</view>
        <view class='action_btn_area'>
          <view class="{{!isEditCart ? 'edit' : 'sure'}}" bindtap="editCart">{{!isEditCart ? '编辑' : '完成'}}</view>
          <view class="delete" bindtap="deleteCart" wx:if="{{isEditCart}}">删除({{cartTotal.checkedGoodsCount}})</view>
          <view class="checkout" bindtap="checkoutOrder" wx:if="{{!isEditCart}}">下单</view>
          <!-- </view>  -->
        </view>
      </view>
    </view>
  </view>
</view>

小程序服务端:

首页Api:MiniHomeApi.java

package com.code2life.course.mini.api;

import com.code2life.course.core.util.ResponseUtil;
import com.code2life.course.db.domain.MallCourseCategory;
import com.code2life.course.db.service.MallAdService;
import com.code2life.course.db.service.MallCourseCategoryService;
import com.code2life.course.db.service.MallCourseChapterService;
import com.code2life.course.db.service.MallCourseService;
import com.code2life.course.mini.service.HomeCacheManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.FutureTask;

/**
 * @author: wen
 * @date: 2019/11/20 10:50
 * @description: 小程序首页服务
 */
@RestController
@RequestMapping("/mini/home")
@Validated
public class MiniHomeApi {

	@Autowired
	private MallCourseCategoryService categoryService;
	@Autowired
	private MallCourseService courseService;
	@Autowired
	private MallCourseChapterService chapterService;
	@Autowired
	private MallAdService adService;

	@GetMapping("/index")
	public Object index(){

		ExecutorService executorService = Executors.newFixedThreadPool(5);

		Callable<List> bannerListCallable = () -> adService.queryIndex();

		//体系课程
		Callable<List> systemListCallable = null;
		List<MallCourseCategory> systemList = categoryService.findL1ByMark("system");
		if(systemList!=null && !systemList.isEmpty()){
			Integer id = systemList.get(0).getId();
			systemListCallable = () -> categoryService.queryByPid(id);
		}else {
			systemListCallable = () -> new ArrayList();
		}

		//特色课程
		Callable<List> featureListCallable;
		List<MallCourseCategory> featureList = categoryService.findL1ByMark("feature");
		if(featureList!=null && !featureList.isEmpty()){
			Integer id = featureList.get(0).getId();
			featureListCallable = () -> categoryService.queryByPid(id);
		}else {
			featureListCallable = () -> new ArrayList();
		}

		FutureTask<List> bannerTask = new FutureTask<>(bannerListCallable);
		FutureTask<List> sysCatTask = new FutureTask<>(systemListCallable);
		FutureTask<List> feaCatTask = new FutureTask<>(featureListCallable);

		executorService.submit(bannerTask);
		executorService.submit(sysCatTask);
		executorService.submit(feaCatTask);

		Map<String, Object> entity = new HashMap<>();
		try {

			entity.put("banner", bannerTask.get());
			entity.put("sysCatList", sysCatTask.get());
			entity.put("feaCatList", feaCatTask.get());
			//缓存数据
			HomeCacheManager.loadData(HomeCacheManager.INDEX, entity);
		}
		catch (Exception e) {
			e.printStackTrace();
		}finally {
			executorService.shutdown();
		}
		return ResponseUtil.ok(entity);
	}
}

购物车订单处理API:MiniCourseOrderApi.java

package com.code2life.course.mini.api;

import com.code2life.course.mini.annotation.LoginUser;
import com.code2life.course.mini.service.MiniCourserOrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * @author: wen
 * @date: 2019/11/29 17:55
 * @description:
 */

@RestController
@RequestMapping("/mini/course/order")
@Validated
public class MiniCourseOrderApi {

	@Autowired
	private MiniCourserOrderService orderService;

	/**
	 * 订单提交
	 * @param userId
	 * @param body
	 * @return
	 */
	@PostMapping("submit")
	public Object submit(@LoginUser Integer userId, @RequestBody String body) {
		return orderService.submit(userId, body);
	}

	/**
	 * 付款订单的预支付会话标识
	 *
	 * @param userId 用户ID
	 * @param body   订单信息,{ orderId:xxx }
	 * @return 支付订单ID
	 */
	@PostMapping("prepay")
	public Object prepay(@LoginUser Integer userId, @RequestBody String body, HttpServletRequest request) {
		return orderService.prepay(userId, body, request);
	}

	/**
	 * 微信付款成功或失败回调接口
	 * <p>
	 *  TODO
	 *  注意,这里pay-notify是示例地址,建议开发者应该设立一个隐蔽的回调地址
	 *
	 * @param request 请求内容
	 * @param response 响应内容
	 * @return 操作结果
	 */
	@PostMapping("pay-notify")
	public Object payNotify(HttpServletRequest request, HttpServletResponse response) {
		return orderService.payNotify(request, response);
	}
}

Web后台管理端:

后台管理系统在线课程相关API:MallCourseApi.java

package com.code2life.course.admin.api;

import com.code2life.course.admin.dto.CourseAllinone;
import com.code2life.course.admin.service.AdminCourseService;
import com.code2life.course.core.util.ResponseUtil;
import com.code2life.course.core.validator.Order;
import com.code2life.course.core.validator.Sort;
import com.code2life.course.db.domain.MallCourse;
import io.swagger.annotations.Api;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;


/**
 * @author: wen
 * @date: 2019/11/14 23:26
 * @description:
 */
@RestController
@RequestMapping("/mall/course")
@Validated
@Api(value = "后台管理系统在线课程相关API")
public class MallCourseApi {

	private final Log logger = LogFactory.getLog(MallCourseApi.class);

	@Autowired
	private AdminCourseService courseService;

	@GetMapping("/list")
	public Object list(String courseSn, String name,
					   @RequestParam(defaultValue = "1") Integer page,
					   @RequestParam(defaultValue = "10") Integer limit,
					   @Sort @RequestParam(defaultValue = "add_time") String sort,
					   @Order @RequestParam(defaultValue = "desc") String order) {
		return courseService.list(courseSn, name, page, limit, sort, order);
	}

	@PostMapping("/delete")
	public Object delete(@RequestBody MallCourse course) {
		courseService.delete(course);
		return ResponseUtil.vueOk();
	}

	@PostMapping("/add")
	public Object create(@RequestBody CourseAllinone allinone) {
		return courseService.add(allinone);
	}

	@GetMapping("/getRelatedList")
	public Object getListCat(){
		return courseService.relatedList();
	}

	@GetMapping("/getCourseById")
	public Object getChapterNeed(Integer id) {
		return courseService.getChapterNeed(id);
	}

	@GetMapping("/courseOption")
	public Object courseOption(){
		return courseService.courseOption();
	}

}

后台管理系统商品相关API:MallGoodsApi.java

package com.code2life.course.admin.api;

import com.code2life.course.admin.dto.GoodsAllinone;
import com.code2life.course.admin.service.AdminGoodsService;
import com.code2life.course.core.validator.Order;
import com.code2life.course.core.validator.Sort;
import com.code2life.course.db.domain.MallGoods;
import io.swagger.annotations.Api;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.validation.constraints.NotNull;

/**
 * @author: wen
 * @date: 2019/11/23 19:53
 * @description:
 */
@RestController
@RequestMapping("/mall/goods")
@Validated
@Api(value = "后台管理系统商品相关API")
public class MallGoodsApi {

	@Autowired
	private AdminGoodsService goodsService;

	@GetMapping("/list")
	public Object list(String goodsSn, String GoodsName,
					   @RequestParam(defaultValue = "1") Integer page,
					   @RequestParam(defaultValue = "10") Integer limit,
					   @Sort @RequestParam(defaultValue = "add_time") String sort,
					   @Order @RequestParam(defaultValue = "desc") String order) {
		return goodsService.list(goodsSn, GoodsName, page, limit, sort, order);
	}

	@GetMapping("/catAndBrand")
	public Object list2() {
		return goodsService.list2();
	}

	@PostMapping("/add")
	public Object add(@RequestBody GoodsAllinone goodsAllinone) {
		return goodsService.add(goodsAllinone);
	}

	@PostMapping("/update")
	public Object update(@RequestBody GoodsAllinone goodsAllinone) {
		return goodsService.update(goodsAllinone);
	}

	@PostMapping("/delete")
	public Object delete(@RequestBody MallGoods goods) {
		return goodsService.delete(goods);
	}

	@GetMapping("/detail")
	public Object detail(@NotNull Integer id) {
		return goodsService.detail(id);

	}

}

Shiro登录系统验证:SysAuthorizingRealm.java

package com.code2life.course.admin.shiro;

import com.code2life.course.admin.util.Permission;
import com.code2life.course.admin.util.PermissionUtil;
import com.code2life.course.admin.vo.PermissionVo;
import com.code2life.course.core.util.bcrypt.BCryptPasswordEncoder;
import com.code2life.course.db.domain.SysRole;
import com.code2life.course.db.domain.SysUser;
import com.code2life.course.db.service.SysRoleService;
import com.code2life.course.db.service.SysUserService;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * @author: wen
 * @date: 2019/11/5 15:14
 * @description: Shiro登录系统验证
 */
public class SysAuthorizingRealm extends AuthorizingRealm {

	@Autowired
	private SysUserService sysUserService;
	@Autowired
	private SysRoleService sysRoleService;

	@Autowired
	private ApplicationContext context;
	private List<PermissionVo> allPermissions = null;
	private Set<String> allPermissionsString = null;

	private List<PermissionVo> getSystemPermissions() {
		final String basicPackage = "com.code2life.course.admin";
		if (allPermissions == null) {
			List<Permission> permissions = PermissionUtil.listPermission(context, basicPackage);
			allPermissions = PermissionUtil.listPermVo(permissions);
			allPermissionsString = PermissionUtil.listPermissionString(permissions);
		}
		return allPermissions;
	}


	@Override
	protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals){
		System.out.println("------------用户授权开始------------");
		if (principals == null) {
			throw new AuthorizationException("PrincipalCollection method argument cannot be null.");
		}
		SysUser user = (SysUser) getAvailablePrincipal(principals);
		//存放角色对象
		List<SysRole> roles = new ArrayList<>();
		//存放角色名称
		Set<String> roleNames = new HashSet<String>();
		//存放用户的权限信息
		Set<String> permissions = new HashSet<String>();
		roles = sysRoleService.findByUsercode(user.getUsercode());
		for(SysRole role : roles){
			roleNames.add(role.getName());
			//判断是否有超级权限,有需要转换成所有权限
			if(sysRoleService.checkSuperPermission(role.getId())){
				getSystemPermissions();
			}
		}
		//拥有超级权限
		if(allPermissionsString!=null && !allPermissionsString.isEmpty()) {
			permissions = allPermissionsString;
		}else{
			//查询拥有角色的权限
			for(SysRole role : roles) {
				Set<String> perms = sysRoleService.queryPermissionByRoleId(role.getId());
				permissions.addAll(perms);
			}
		}
		//通过用户账号获取用户的所有资源,并把资源存入info中
		SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
		info.addRoles(roleNames);
		info.addStringPermissions(permissions);
		System.out.println("------------用户授权结束------------");

		return info;
	}

	@Override
	protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
		System.out.println("------------用户认证开始------------");
		//将AuthenticationToken转换为UsernamePasswordToken,token中存储着用户输入的账号和密码
		UsernamePasswordToken upToken = (UsernamePasswordToken)token;
		//获取用户账号和密码
		String usercode = upToken.getUsername();
		String password = new String(upToken.getPassword());

		if (StringUtils.isEmpty(usercode)) {
			throw new AccountException("用户名不能为空");
		}
		if (StringUtils.isEmpty(password)) {
			throw new AccountException("密码不能为空");
		}
		List<SysUser> list = sysUserService.findByUsercode(usercode);
		if(list==null || list.isEmpty()){
			//账号不存在
			throw new UnknownAccountException("账号不存在");
		}
		Assert.state(list.size() < 2, "同一个用户名存在两个账户");
		SysUser user = list.get(0);
		BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
		if (!encoder.matches(password, user.getPassword())) {
			throw new UnknownAccountException("找不到用户(" + usercode + ")的帐号信息");
		}

		SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, password, getName());
		System.out.println("------------用户认证结束------------");
		return info;
	}
}

Web后台系统前端Vue部分:

后台课程管理首页:index.vue

<template>
  <div class="app-container">
    <!-- 查询和其他操作 -->
    <div class="filter-container">
      <el-input v-model="listQuery.courseSn" clearable class="filter-item" style="width: 200px;" placeholder="请输入编号"/>
      <el-input v-model="listQuery.name" clearable class="filter-item" style="width: 200px;" placeholder="支持名称模糊搜索"/>
      <el-button class="filter-item" type="primary" icon="el-icon-search" @click="handleFilter">搜索</el-button>
      <el-button class="filter-item" type="primary" icon="el-icon-edit" @click="handleCreate">添加</el-button>
    </div>
    <!-- 查询结果 -->
    <el-table v-loading="listLoading" :data="list" element-loading-text="正在查询中。。。" border fit highlight-current-row row-key="id">
    <el-table-column type="expand">
      <template slot-scope="props">
        <el-form label-position="left" class="table-expand">
          <el-form-item label="宣传画廊">
            <img v-for="pic in props.row.gallery" :key="pic" :src="pic" class="gallery">
          </el-form-item>
          <el-form-item label="商品介绍">
            <span>{{ props.row.brief }}</span>
          </el-form-item>
          <el-form-item label="关键字">
            <span>{{ props.row.keywords }}</span>
          </el-form-item>
          <el-form-item label="类目ID">
            <span>{{ props.row.categoryId }}</span>
          </el-form-item>
        </el-form>
      </template>
    </el-table-column>

    <el-table-column align="center" label="课程编号" prop="courseSn"/>

    <el-table-column align="center" min-width="100" label="名称" prop="name"/>

    <el-table-column align="center" property="iconUrl" label="图片">
      <template slot-scope="scope">
        <img :src="scope.row.iconUrl" width="40">
      </template>
    </el-table-column>

    <el-table-column align="center" label="详情" prop="detail">
      <template slot-scope="scope">
        <el-dialog :visible.sync="detailDialogVisible" title="课程详情">
          <div v-html="courseDetail"/>
        </el-dialog>
        <el-button type="primary" size="mini" @click="showDetail(scope.row.detail)">查看</el-button>
      </template>
    </el-table-column>

    <el-table-column align="center" label="课程价格" prop="cPrice"/>

    <el-table-column align="center" label="是否新品" prop="isNew">
      <template slot-scope="scope">
        <el-tag :type="scope.row.isNew ? 'success' : 'error' ">{{ scope.row.isNew ? '新品' : '非新品' }}</el-tag>
      </template>
    </el-table-column>

    <el-table-column align="center" label="是否热品" prop="isHot">
      <template slot-scope="scope">
        <el-tag :type="scope.row.isHot ? 'success' : 'error' ">{{ scope.row.isHot ? '热门' : '普通' }}</el-tag>
      </template>
    </el-table-column>

    <el-table-column align="center" label="操作" width="300" class-name="small-padding fixed-width">
      <template slot-scope="scope">
        <el-button type="primary" size="small" @click="handleChapter(scope.row)">章节录入</el-button>
        <el-button type="primary" size="mini" @click="handleUpdate(scope.row)">编辑</el-button>
        <el-button type="danger" size="mini" @click="handleDelete(scope.row)">删除</el-button>
      </template>
    </el-table-column>
    </el-table>

    <pagination v-show="total>0" :total="total" :page.sync="listQuery.page" :limit.sync="listQuery.limit" @pagination="getList" />

    <el-tooltip placement="top" content="返回顶部">
      <back-to-top :visibility-height="100" />
    </el-tooltip>
  </div>

</template>
<script>
import { listCourse, deleteCourse } from '@/api/course'
import BackToTop from '@/components/BackToTop'
import Pagination from '@/components/Pagination' // Secondary package based on el-pagination
export default {
  name: 'Course',
  components: { BackToTop, Pagination },
  data() {
    return {
      list: [],
      total: 0,
      listLoading: true,
      listQuery: {
        page: 1,
        limit: 10,
        courseSn: undefined,
        name: undefined,
        sort: 'add_time',
        order: 'desc'
      },
      courseDetail: '',
      detailDialogVisible: false
    }
  },
  created() {
    this.getList()
  },
  methods: {
    getList() {
      this.listLoading = true
      listCourse(this.listQuery).then(response => {
        this.list = response.data.list
        this.total = response.data.total
        this.listLoading = false
      }).catch(() => {
        this.list = []
        this.total = 0
        this.listLoading = false
      })
    },
    handleFilter() {
      this.listQuery.page = 1
      this.getList()
    },
    showDetail(detail) {
      this.courseDetail = detail
      this.detailDialogVisible = true
    },
    handleCreate() {
      this.$router.push({ path: '/course-management/createCourse' })
    },
    handleChapter(row) {
      const data = row
      this.$router.push({ name: 'ChapterAdd', params: { id: data.id }})
    },
    handleUpdate(row) {
      this.$notify.warning({
        title: '警告',
        message: '该功能暂未开放',
        duration: 2000
      })
      // this.$router.push({ path: '/goods/edit', query: { id: row.id }})
    },
    handleDelete(row) {
      deleteCourse(row).then(response => {
        this.$notify.success({
          title: '成功',
          message: '删除成功'
        })
        const index = this.list.indexOf(row)
        this.list.splice(index, 1)
      }).catch(response => {
        this.$notify.error({
          title: '失败',
          message: response.msg
        })
      })
    }
  }
}
</script>

<style scoped>
  .table-expand {
    font-size: 0;
  }
  .table-expand label {
    width: 100px;
    color: #99a9bf;
  }
  .table-expand .el-form-item {
    margin-right: 0;
    margin-bottom: 0;
  }
  .gallery {
    width: 80px;
    margin-right: 10px;
  }
</style>

Vue菜单路由:index.js

import Vue from 'vue'
import Router from 'vue-router'

Vue.use(Router)

/* Layout */
import Layout from '@/layout'

/* Router Modules */
import sysSettingRouter from './modules/sysSetting'
import courserRouter from './modules/course'
import goodsRouter from './modules/goods'
import mallUserRouter from './modules/mallUser'
import promotionRouter from './modules/promotion'
import mallSettingRouter from './modules/mallSetting'
/**
 * Note: sub-menu only appear when route children.length >= 1
 * Detail see: https://panjiachen.github.io/vue-element-admin-site/guide/essentials/router-and-nav.html
 *
 * hidden: true                   if set true, item will not show in the sidebar(default is false)
 * alwaysShow: true               if set true, will always show the root menu
 *                                if not set alwaysShow, when item has more than one children route,
 *                                it will becomes nested mode, otherwise not show the root menu
 * redirect: noredirect           if `redirect:noredirect` will no redirect in the breadcrumb
 * name:'router-name'             the name is used by <keep-alive> (must set!!!)
 * meta : {
    roles: ['admin','editor']    control the page roles (you can set multiple roles)
    title: 'title'               the name show in sidebar and breadcrumb (recommend set)
    icon: 'svg-name'             the icon show in the sidebar
    noCache: true                if set true, the page will no be cached(default is false)
    affix: true                  if set true, the tag will affix in the tags-view
    breadcrumb: false            if set false, the item will hidden in breadcrumb(default is true)
    activeMenu: '/example/list'  if set path, the sidebar will highlight the path you set
  }
 */

/**
 * constantRoutes
 * a base page that does not have permission requirements
 * all roles can be accessed
 */
export const constantRoutes = [
  {
    path: '/redirect',
    component: Layout,
    hidden: true,
    children: [
      {
        path: '/redirect/:path*',
        component: () => import('@/views/redirect/index')
      }
    ]
  },
  {
    path: '/login',
    component: () => import('@/views/login/index'),
    hidden: true
  },
  {
    path: '/auth-redirect',
    component: () => import('@/views/login/auth-redirect'),
    hidden: true
  },
  {
    path: '/404',
    component: () => import('@/views/error-page/404'),
    hidden: true
  },
  {
    path: '/401',
    component: () => import('@/views/error-page/401'),
    hidden: true
  },
  {
    path: '',
    component: Layout,
    redirect: 'dashboard',
    children: [
      {
        path: 'dashboard',
        component: () => import('@/views/dashboard/index'),
        name: 'Dashboard',
        meta: { title: 'dashboard', icon: 'dashboard', noCache: true, affix: true }
      }
    ]
  },
  // {
  //   path: '/documentation',
  //   component: Layout,
  //   children: [
  //     {
  //       path: 'index',
  //       component: () => import('@/views/documentation/index'),
  //       name: 'Documentation',
  //       meta: { title: 'documentation', icon: 'documentation', affix: true }
  //     }
  //   ]
  // },
  {
    path: '/guide',
    component: Layout,
    redirect: '/guide/index',
    children: [
      {
        path: 'index',
        component: () => import('@/views/guide/index'),
        name: 'Guide',
        meta: { title: 'guide', icon: 'guide', noCache: true }
      }
    ]
  }
]

/**
 * asyncRoutes
 * the routes that need to be dynamically loaded based on user roles
 */
export const asyncRoutes = [
  courserRouter,
  goodsRouter,
  promotionRouter,
  mallSettingRouter,
  mallUserRouter,
  sysSettingRouter,
{ path: '*', redirect: '/404', hidden: true }
]

const createRouter = () => new Router({
  // mode: 'history', // require service support
  scrollBehavior: () => ({ y: 0 }),
  routes: constantRoutes
})

const router = createRouter()

// Detail see: https://github.com/vuejs/vue-router/issues/1234#issuecomment-357941465
export function resetRouter() {
  const newRouter = createRouter()
  router.matcher = newRouter.matcher // reset router
}

export default router

五、写在最后

本博客属于个人原创,转载请注明出处;
本系统还要很多不足的地方,由于时间有限,后续会继续更新功能;
如果感觉兴趣,可以扫码关注咨询,获取源码。
扫码关注公众号

Logo

前往低代码交流专区

更多推荐