【Vue前端开发学习】_前端Vue基础快速入门_整合知识笔记及苍穹外卖前端板块开发示例

前言:本文汇总苍穹项目前端学习三大模块(vue基础知识、员工模块、套餐模块),见目录可选择对应想要查看的板块,各位开始学习吧!

组件参考文档:Element - The world's most popular Vue UI framework

目录

模块一:vue基础知识

1.VUE基础

1.1 基于脚手架创建前端工程

1.2 VUE基本使用方式

1.3 小结

2.路由 Vue-Router

2.1 Vue-Router 介绍

2.2 路由配置

2.3 嵌套路由

2.4 小结

3.状态管理vuex

3.1 vuex 介绍

3.2 使用方式

3.3 小结

4.TypeScript

4.1 介绍

4.2 TypeScript常用类型

4.3 小结

模块二:员工管理(实例)

5 前端模块开发

5.1 前端环境搭建

5.2 通过登录功能梳理前端代码

6 员工分页查询

6.1 需求分析和接口设计

6.2 代码开发

6.3 功能测试

7 启用、禁用员工账号

7.1 需求分析和接口设计

7.2 代码开发

7.3 功能测试

8 添加员工

8.1 需求分析和接口设计

8.2 代码开发

8.3 功能测试

9 添加员工

9.1 需求分析和接口设计

9.2 代码开发

9.3 功能测试

模块三:套餐管理(实例)

10 套餐分页查询

10.1 需求分析和接口设计

10.2 代码开发

10.3 功能测试

11 启售、停售套餐

11.1 需求分析和接口设计

11.2 代码开发

11.3 功能测试

12 删除套餐

12.1 需求分析和接口设计

12.2 代码开发

12.3 功能测试

13 新增套餐

13.1 需求分析和接口设计

13.2 代码开发

13.3 功能测试

完结撒花!



模块一:vue基础知识

1.VUE基础

1.1 基于脚手架创建前端工程

①使用VUE CLI创建前端工程:

  • 方式一:vue create项目名称
  • 方式二:vue ui

②项目结构:

③启动前端项目:

  • 启动:npm run serve   
  • 停止:Ctrl+C

前端项目启动后,服务器端后默认为8080,很容易和后端tomcat端口号冲突。

问题: 如何修改前端服务的端口号?

解决如下:

1.2 VUE基本使用方式

①vue组件

②文本插值

③属性绑定

④事件绑定

⑤双向绑定

⑥条件渲染

⑦axios

       Axios是一个基于promise的网络请求库,作用与浏览器和node.js中。

安装命令:

npm install axios

导入命令:

import axios from 'axios'

注意:为了解决跨域问题,可以在vue.config.js文件中配置代理:

const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
  transpileDependencies: true,
  devServer:{
	  port:7070,
    proxy: {
      '/api':{
        target:'http://localhost:8081',
        pathRewrite:{
          '^/api':''
        }
      }
    }
  }
})

1.3 小结


2.路由 Vue-Router

2.1 Vue-Router 介绍

     vue 属于单页面应用,所谓路由,就是根据浏览器路径不同,用不同的视图组件替换这个页面内容。

基于Vue CLI创建带有路由功能的前端项目:

创建命令:vue ui

打开文件夹工程:创建完带有路由功能的前端项目后,在工程中会生成一个路由文件,如下所示:

为了能够使用路由功能,在前端项目的入口文件main.js中,创建Vue实例时需要指定路由对象:

效果展示:当访问不同的路径时,就可以看到不同的视图。

2.2 路由配置

路由组成:

  • VueRouter:路由器根据路由请求在路由视图中动态渲染对应的视图组件

  • <router-link>:路由链接组件,浏览器会解析成<a>

  • <router-view>:路由视图组件,用来展示与路由路径匹配的视图组件

  • 具体配置方式:

1) 在index.js中配置路由路径和视图的对应关系

import Vue from 'vue'
import VueRouter from 'vue-router'
import HomeView from '../views/HomeView.vue'

Vue.use(VueRouter)

//维护路由表,某个路由路径对应哪个视图组件
const routes = [
  {
    path: '/',
    name: 'home',
    component: HomeView
  },
  {
    path: '/about',
    name: 'about',
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () => import(/* webpackChunkName: "about" */ '../views/AboutView.vue')
  }
]

const router = new VueRouter({
  routes
})

export default router

2) <router-link>:在App.vue中配置路由链接组件,用于生成路由视图组件之间的超链接标签

3) <router-view>:相当于一个占位符,表示视图组件展示的位置

  • 路由跳转:

1)标签式:前面提到的</router-link>标签的方式

2)编程式:this.$router.push('/about')  (通过注册事件触发)

问题:如果用户访问的路由地址不存在,该如何处理?

解决:配置一个404视图组件,当访问的路由地址不存在时,则重定向到此视图组件

2.3 嵌套路由

定义:组件内要切换内容,就需要用到嵌套路由(子路由)。

效果如下:

在App.vue视图组件中有<router-view>标签,其他视图组件可以展示在此。

ContainerView.vue组件可以展示在App.vue视图组件的<router-view>位置

安装步骤:

1)安装element-ui,命令如下:

npm i element-ui -S

导入 elementui,实现页面布局(Container 布局容器)---ContainerView.vue

<template>
    <el-container>
        <el-header>Header</el-header>
        <el-container>
            <el-aside width="200px">Aside</el-aside>
            <el-main>Main</el-main>
        </el-container>
    </el-container>
</template>

<script>
export default {

}
</script>

<style>
.el-header,
.el-footer {
    background-color: #B3C0D1;
    color: #333;
    text-align: center;
    line-height: 60px;
}

.el-aside {
    background-color: #D3DCE6;
    color: #333;
    text-align: center;
    line-height: 200px;
}

.el-main {
    background-color: #E9EEF3;
    color: #333;
    text-align: center;
    line-height: 160px;
}

body>.el-container {
    margin-bottom: 40px;
}

.el-container:nth-child(5) .el-aside,
.el-container:nth-child(6) .el-aside {
    line-height: 260px;
}

.el-container:nth-child(7) .el-aside {
    line-height: 320px;
}
</style>

2)提供子视图组件,用于效果展示 ---P1View.vue、P2View.vue、P3View.vue

<template>
  <div>
    这是P1 View
  </div>
</template>
 
<script>
export default {
 
}
</script>
 
<style>
 
</style>

3)在 src/router/index.js 中配置路由映射规则(嵌套路由配置)

  {
    path: '/c',
    component: () => import('../views/container/ContainerView.vue'),
    //嵌套路由(子路由),对应的组件会展示在当前组件内部
    children: [
      {
        path: '/c/p1',
        component: () => import('../views/container/P1View.vue')
      },
      {
        path: '/c/p2',
        component: () => import('../views/container/P2View.vue')
      },
      {
        path: '/c/p3',
        component: () => import('../views/container/P3View.vue')
      }
    ]
  }

4)在ContainerView.vue 布局容器视图中添加<router-view>,实现子视图组件展示

5)在ContainerView.vue 布局容器视图中添加<router-link>,实现路由请求

注意:

    子路由变化,切换的是【ContainerView 组件】中 <router-view></router-view> 部分的内容。

问题:

1.对于前面的案例,如果用户访问的路由是 /c,会有什么效果呢?

2.如何实现在访问 /c 时,默认就展示某个子视图组件呢?

    配置重定向,当访问/c时,直接重定向到/c/p1即可,如下配置:

2.4 小结


3.状态管理vuex

3.1 vuex 介绍

  • vuex 是一个专为 Vue.js 应用程序开发的状态(数据)管理库。
  • vuex 可以在多个组件之间共享数据,并且共享的数据是响应式的,即数据的变更能及时渲染到模板。
  • vuex采用集中式存储管理所有组件的状态。

在命令行终端安装vuex工程,命令如下:

npm install vuex@next --save

vuex中的几个核心概念:

  • state:状态对象,集中定义各个组件共享的数据(只有一种在mutations中修改数据的方式)

  • mutations:类似于一个事件,用于修改共享数据,要求必须是同步函数

  • actions:类似于mutation,可以包含异步操作,通过调用mutation改变共享数据

3.2 使用方式

本章节通过一个案例来学习vuex的使用方式,具体操作步骤如下:

1)初始化

     创建带有vuex功能的前端项目(vue ui)

注意:在创建的前端工程中,可以发现自动创建了vuex相关的文件(src/store/index.js),并且在main.js中创建Vue实例时,需要将store对象传入,代码如下:

2)State

Vuex的存储对象,它是整个应用的唯一数据源(单一状态树)。所有组件都能从中读取共享状态

/**
 在src/store/index.js文件中集中定义和管理共享数据
*/
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

//集中管理多个组件共享的数据
export default new Vuex.Store({
  //集中定义共享数据
  state: {
    name: '未登录游客'
  }
})

然后通过 插值表达式 引用,从而在视图组件中展示共享数据

3)mutations

通过在mutations属性中定义函数,修改共享数据。必须都是同步操作

export default new Vuex.Store({

  //通过当前属性中定义的函数修改共享数据,必须都是同步操作
  mutations: {
    setName(state, newName) {
      state.name = newName
    }
  }
})

绑定一个事件调用mutations属性中的该函数,从而实现修改数据。

this.$store.commit('setName','修改参数')

4)actions

 Actions 类似于 mutations,但不同的是,actions 是 异步 的。它们不会直接变更状态,而是提交 mutation,间接 变更状态

下载并引入‘axios’,然后在actions属性中定义函数,用于调用mutations。可以实现异步操

import axios from 'axios'

Vue.use(Vuex)

//集中管理多个组件共享的数据
export default new Vuex.Store({
  //通过actions调用mutation,在actions中可以进行异步操作
  actions: {
    setNameByAxios(context) {
      axios({
        url: '/api/admin/employee/login',
        method: 'post',
        data: {
          username: 'admin',
          password: '123456'
        }
      }).then(res => {
        if (res.data.code == 1) {
          //异步请求后,需要修改共享数据
          //在actions中调用mutations中定义的setName函数
          context.commit('setName', res.data.data.name)
        }
      })
    }
  }
})

绑定一个事件调用actions中定义的函数,从而实现修改数据。

this.$store.dispatch('setNameByAxios')

发送请求不能直接发送到后端,所以出现了跨域问题,因此需要配置代理转发到后端。

3.3 小结

4.TypeScript

4.1 介绍

转化过程:(类型擦除)

安装typescript:

npm install -g typescript

查看TS版本:

思考:

   TS为什么要增加类型支持?

  • TS属于静态类型编程语言,JS属于动态类型编程语言。
  • 静态类型在编译期做类型检查,动态类型在执行期做类型检查。
  • 对于JS来说,需要等到代码执行的时候才能发现错误(晚) 。
  • 对于TS来说,在代码编译的时候就可以发现错误(早) 。
  • 配合VSCode开发工具,TS可在编写代码时发现错误,减少找Bug、改Bug的时间 。

小结:

4.2 TypeScript常用类型

类型备注
字符串类型string
数字类型number
布尔类型boolean
数组类型number [], string [], boolean [] 依此类推
任意类型any相当于又回到了没有类型的时代
复杂类型type 与 interface
函数类型() => void对函数的参数和返回值进行说明
字面量类型"a"|"b"|"c"限制变量或参数的取值
class 类class Animal
  • 类型标注的位置:标注变量、标注参数、标注返回值

1)字符串类型、数字类型、布尔类型

2)字面量类型

注意:类似于Java中的枚举

3)interface类型

小技巧:可以通过在属性名后面加上? (表示当前属性为可选)

4)class类

  • class类-基本使用

注意:使用class关键字来定义类,类中可以包含属性、构造方法、普通方法

  • class类-实现接口

  • class类-类的继承

运行.js文件:

4.3 小结


模块二:员工管理(实例)

5 前端模块开发

5.1 前端环境搭建

  • 技术选型

node.js    vue    ElementUI    axios    vuex    vue-router    typescript

  • 熟悉前端代码结构(导入项目代码)

5.2 通过登录功能梳理前端代码

①先打开idea运行后端服务。

②在VScode的终端中输入命令,下载前端项目中的依赖(不需要指定安装哪些包,会自动扫描):

  • npm install  (可能会出现node版本冲突导致下载失败,需要降低版本)
  • npm install --legacy-peer-deps (可忽略版本冲突,不用降低node版本)

把nodejs的版本降级到12版本,可参考文章链接:

苍穹外卖-前端搭建-npm install失败解决方法_jeecgboot npm下载依赖不可用怎么回事-CSDN博客

如果出现安全性问题,打开cmd执行下面的命令

npm config set strict-ssl false

④修改代理转发请求的后端服务地址(保证后端服务的端口号一致)

⑤输入命令 npm run serve ,运行前端项目(前端的端口号为8888)

⑥通过登录功能梳理前端代码

  • 获得登录页面路由地址
  • 从main.ts中找到路由文件
  • 从路由文件中找到登录视图组件
  • 从登录视图组件中找到登录方法
  • 跟踪登录方法的执行过程

6 员工分页查询

6.1 需求分析和接口设计

业务规则:

根据页码展示员工信息

每页展示10条数据

分页查询可以根据需要,输入员工姓名进行查询

接口设计:

6.2 代码开发

①从路由文件router.ts中找到员工管理页面(组件)

②制作页面头部效果

    <div class="container">
      <div class="tableBar">
      <label style="margin-right: 5px">员工姓名:</label>
      <el-input
      placeholder="请输入员工姓名:"
      style="width: 15%"
      clearable
      />
      <el-button type="primary" style="margin-left: 20px">查询</el-button>
      <el-button type="primary" style="float: right"> + 添加员工</el-button>
    </div>
    </div>

注意:

  • 输入框和按钮都是使用ElementUI提供的组件
  • 对于前端的组件只需要参考ElementUI提供的文档,进行修改即可

③员工分页查询

src/api/employee.ts

//员工分页查询
export const getEmployeeList = (params: any) =>
  request({
    'url': `/employee/page`,
    'method': `GET`,
    'params': params
  })

src/view/employee/index.vue

<template>
  <div class="dashboard-container">
    <div class="container">
      <div class="tableBar">
        <label style="margin-right: 5px">员工姓名:</label>
        <el-input v-model="name" placeholder="请输入员工姓名" style="width: 15%" clearable />
        <el-button type="primary" style="margin-left: 20px" @click="pageQuery()">查询</el-button>
        <el-button type="primary" style="float: right"> + 添加员工</el-button>
      </div>

      <el-table :data="records" stripe style="width: 100%">
        <el-table-column prop="name" label="员工姓名" width="180">
        </el-table-column>
        <el-table-column prop="username" label="账号" width="180">
        </el-table-column>
        <el-table-column prop="phone" label="手机号"> </el-table-column>
        <el-table-column prop="status" label="账号状态">
          <template slot-scope="scope">
            {{ scope.row.status === 0 ? '禁用' : '启用' }}
          </template>
        </el-table-column>
        <el-table-column prop="updateTime" label="最后操作时间">
        </el-table-column>
        <el-table-column label="操作">
          <template slot-scope="scope">
            <el-button type="text">修改</el-button>
            <el-button type="text">{{
              scope.row.status === 1 ? '禁用' : '启用'
            }}</el-button>
          </template>
        </el-table-column>
      </el-table>

      <el-pagination class="pageList" @size-change="handleSizeChange" @current-change="handleCurrentChange"
        :current-page="page" :page-sizes="[10, 20, 30, 40, 50]" :page-size="pageSize"
        layout="total, sizes, prev, pager, next, jumper" :total="total">
      </el-pagination>
    </div>
  </div>
</template>

<script lang="ts">
import { getEmployeeList } from '@/api/employee'

export default {
  // 模型数据
  data() {
    return {
      name: '', // 员工姓名,对应上面的输入框
      page: 1, // 页码
      pageSize: 10, // 每页记录数
      total: 0, // 总记录数
      records: [], // 当前页要展示的数据集合
    }
  },
  // 自动调用pageQuery方法
  // 这段代码是 Vue.js 组件中的生命周期钩子函数 created()。在 Vue.js 组件中,created() 是一个生命周期钩子函数,在组件实例被创建之后立即调用。这个钩子函数通常用于在组件实例创建后执行一些初始化任务。
  created() {
    this.pageQuery()
  },

  methods: {
    // 分页查询
    pageQuery() {
      // 准备请求参数
      const params = {
        name: this.name,
        page: this.page,
        pageSize: this.pageSize,
      }

      // 发送Ajax请求,访问后端服务,获取分页数据
      getEmployeeList(params)
        .then((res) => {
          if (res.data.code === 1) {
            this.total = res.data.data.total
            this.records = res.data.data.records
          }
        })
        .catch((err) => {
          this.$message.console.error('请求出错了:' + err.message)
        })
    },
    // pageSize发送变化时触发
    handleSizeChange(pageSize) {
      this.pageSize = pageSize
      this.pageQuery()
    },
    // page发生变化时触发
    handleCurrentChange(page) {
      this.page = page
      this.pageQuery()
    },
  },
}
</script>

<style lang="scss" scoped>
.disabled-text {
  color: #bac0cd !important;
}
</style>

6.3 功能测试

7 启用、禁用员工账号

7.1 需求分析和接口设计

业务规则:

可以对状态为“启用”的员工账号进行“禁用”操作

可以对状态为“禁用”的员工账号进行“启用”操作

状态为“禁用”的员工账号不能登录系统

接口设计:

7.2 代码开发

①src/api/employee.ts

// 启用禁用员工账号
export const enableOrDisableEmployee = (params: any) =>
  request({
    'url': `/employee/status/${params.status}`,
    'method': 'post',
    'params': {id: params.id}
  })

②src/view/employee/index.vue

    // 启用禁用员工账号
    handleStartOrStop(row) {
      if(row.username === 'admin') {
        this.$message.error('admin为系统的管理员账号,不能更改帐号状态!')
        return
      }
        // alert(`id=${row.id} status=${row.status}`)
 
        // 弹出确认提示框
        this.$confirm('确认要修改当前员工账号的状态吗?', '提示', {
          confirmButtonText: '确定',
          cancelButtonText: '取消',
          type: 'warning'
        }).then(() => {
          const p = {
            id: row.id,
            status: !row.status ? 1 : 0
          }
          enableOrDisableEmployee(p).then(res => {
            if(res.data.code === 1) {
              this.$message.success('员工的账号状态修改成功!')
              this.pageQuery()
            }
          })
        })
    }

7.3 功能测试

8 添加员工

8.1 需求分析和接口设计

业务规则:

接口设计:

8.2 代码开发

添加员工操作步骤

  • 点击“添加员工”按钮,跳转到新增页面
  • 在新增员工页面录入员工相关信息
  • 点击“保存”按钮完成新增操作

①为“添加员工”按钮绑定单击事件:@click="handleAddEmp"

src/views/employee/index.vue

②提供handleAddEmp方法,进行路由跳转

③src/api/employee.ts

  // 新增员工
export const addEmployee = (params: any) =>
  request({
    'url': '/employee',
    'method': 'post',
    'data': params
  })

④src/views/employee/addEmployee.vue

<template>
  <div class="addBrand-container">
    <div class="container">
      <el-form :model="ruleForm" :rules="rules" ref="ruleForm" label-width="180px">
        <el-form-item label="账号" prop="username">
          <el-input v-model="ruleForm.username"></el-input>
        </el-form-item>
        <el-form-item label="员工姓名" prop="name">
          <el-input v-model="ruleForm.name"></el-input>
        </el-form-item>
        <el-form-item label="手机号" prop="phone">
          <el-input v-model="ruleForm.phone"></el-input>
        </el-form-item>
        <el-form-item label="性别" prop="sex">
            <el-radio v-model="ruleForm.sex" label="1">男</el-radio>
            <el-radio v-model="ruleForm.sex" label="2">女</el-radio>
        </el-form-item>
        <el-form-item label="身份证号" prop="idNumber">
          <el-input v-model="ruleForm.idNumber"></el-input>
        </el-form-item>
        <div class="subBox">
          <el-button type="primary" @click="submitForm('ruleForm',false)">保存</el-button>
          <el-button 
            v-if="this.optType === 'add'" 
            type="primary" 
            @click="submitForm('ruleForm',true)">保存并继续添加员工
          </el-button>
          <el-button @click="() => this.$router.push('/employee')">返回</el-button>
        </div>
      </el-form>
    </div>
  </div>
</template>
 
<script lang="ts">
import {addEmployee} from '@/api/employee'
export default {
  data() {
    return {
      optType: 'add',
      ruleForm: {
        name: '',
        username: '',
        sex: '1',
        phone: '',
        idNumber: ''
      },
      rules: {
        name: [
            { required: true, message: '请输入员工姓名', trigger: 'blur' }
        ],
        username: [
            { required: true, message: '请输入员工账号', trigger: 'blur' }
        ],
        phone: [
            { required: true, trigger: 'blur', validator: (rule, value, callback) => {
              if(value === '' || (!/^1(3|4|5|6|7|8)\d{9}$/.test(value))) {
                callback(new Error('请输入正确的手机号!'))
              } else {
                callback()
              }
            }}
        ],
        idNumber: [
            { required: true, trigger: 'blur', validator: (rule, value, callback) => {
              if(value === '' || (!/(^\d{15}$)|(^\d{18}$)|(^\d{17}(X|x)$)/.test(value))) {
                callback(new Error('请输入正确的身份证号!'))
              } else {
                callback()
              }
            }}
        ]
      }
    }
  },
  methods: {
    submitForm(formName, isContinue) {
      // 进行表单校验
      this.$refs[formName].validate((valid) => {
        if(valid) {
          // alert('所有表单项都符合要求')
          // 表单校验通过,发起Ajax请求,将数据提交到后端
          addEmployee(this.ruleForm).then((res) => {
            if(res.data.code === 1) {
              this.$message.success('员工添加成功!')
              if(isContinue) {  // 保存并继续添加
              this.ruleForm = {
                name: '',
                username: '',
                sex: '1',
                phone: '',
                idNumber: ''
              }
              } else {
                this.$router.push('/employee')
              }
            } else {
              this.$message.error(res.data.msg)
            }
          })
        }
      })
    }
  }
}
</script>
 
<style lang="scss" scoped>
.addBrand {
  &-container {
    margin: 30px;
    margin-top: 30px;
    .HeadLable {
      background-color: transparent;
      margin-bottom: 0px;
      padding-left: 0px;
    }
    .container {
      position: relative;
      z-index: 1;
      background: #fff;
      padding: 30px;
      border-radius: 4px;
      // min-height: 500px;
      .subBox {
        padding-top: 30px;
        text-align: center;
        border-top: solid 1px $gray-5;
      }
    }
    .idNumber {
      margin-bottom: 39px;
    }
 
    .el-form-item {
      margin-bottom: 29px;
    }
    .el-input {
      width: 293px;
    }
  }
}
</style>

8.3 功能测试

9 添加员工

9.1 需求分析和接口设计

业务规则:

编辑员工功能涉及到两个接口:

  • 根据id查询员工信息
  • 编辑员工信息

接口设计:

9.2 代码开发

修改员工操作步骤:

  • 点击“修改”按钮,跳转到修改页面
  • 在修改员工页面录入员工相关信息
  • 点击“保存”按钮完成修改操作

注意

  • 由于添加员工和修改员工的表单项非常类似,所以添加和修改操作可以共用同一个页面addEmployee.vue
  • 修改员工设计原数据回显,所以需要传递员工id作为参数

①src/views/employee/index.vue

在员工管理页面中,为“修改”按钮绑定单击事件,用于跳转到修改页面

    // 跳转到修改员工页面(组件)
    handleUpdateEmp(row) {
      if(row.username === 'admin') {
        // 如果是内置管理员账号,不允许修改
        this.$message.error('admin为系统的管理员账号,不能修改!')
        return
      }
      // 跳转到修改页面,通过地址栏传递参数
      this.$router.push({
        path: '/employee/add',
        query: {id: row.id}
      })
    }

②由于addEmployee.vue为新增和修改共用页面,需要能够区分当前操作:

  • 如果路由中传递了id参数,则当前操作为修改
  • 如果路由中没有传递id参数,则当前操作为新增

③根据id查询员工,src/api/employee.ts

  // 根据id查询员工
export const queryEmployeeById = (id: number) =>
  request({
    'url': `/employee/${id}`,
    'method': 'get'
  })

④数据回显,src/views/employee/addEmployee.vue

⑤修改员工信息,src/api/employee.ts

// 修改员工
export const updateEmployee = (params: any) =>
  request({
    'url': '/employee',
    'method': 'put',
    'data': params
  })

⑥src/views/employee/addEmployee.vue

9.3 功能测试

修改操作,回显成功!


模块三:套餐管理(实例)

10 套餐分页查询

10.1 需求分析和接口设计

业务规则:

  • 根据页码展示套餐信息
  • 每页展示10条数据
  • 分页查询时可以根据需要输入套餐名称、套餐分类、售卖状态进行查询

接口设计:(略)

  • 套餐分页查询接口
  • 分类查询接口(用于下拉框中分类数据显示)

10.2 代码开发

①从路由文件router.ts中找到套餐管理页面(组件)

②制作页面头部效果(输入框、按钮、下拉框都是ElementUI提供的组件)

<template>中,加入头部导航栏容器:

        <div class="tableBar">
          <label style="margin-right: 5px">套餐名称:</label>
          <el-input v-model="name" placeholder="请输入套餐名称" style="width: 15%" clearable />

          <label style="margin-left: 5px">套餐分类:</label>
          <el-select v-model="value" placeholder="请选择">
            <el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value">
            </el-option>
          </el-select>

          <label style="margin-left: 5px">售卖状态:</label>
          <el-select v-model="saleStatus" placeholder="请选择">
            <el-option v-for="item in saleStatusArr" :key="item.value" :label="item.label" :value="item.value">
            </el-option>
          </el-select>

          <el-button type="primary" style="margin-left: 20px" @click="pageQuery()">查询</el-button>
          <div style="float:right">
            <el-button type="danger">批量删除</el-button>
            <el-button type="info">+新建套餐</el-button>
          </div>
        </div>
      </div>

<script>中,模型数据的定义:

运行如下:

③导入查询套餐分类的JS方法,动态填充套餐分类下拉框

发送Ajax请求,返回后端查询数据:

页面组件和数据模型进行相应调整:

成功查询数据:

④为 查询 按钮绑定事件,发送Ajax请求获取分页数据

为 查询 按钮绑定事件:

 导入方法,发送Ajax请求:

在生命周期中,添加第一次加载就发送Ajax请求查询的相关逻辑代码:

⑤数据展示 (使用ElementUI中的表格组件和分页条组件)

      <el-table :data="records" stripe class="tableBox" @selection-change="handleSelectionChange">
        <el-table-column type="selection" width="25" />
        <el-table-column prop="name" label="套餐名称" />
        <el-table-column label="图片">
          <template slot-scope="scope">
            <el-image style="width: 80px; height: 40px; border: none" :src="scope.row.image"></el-image>
          </template>
        </el-table-column>
        <el-table-column prop="categoryName" label="套餐分类" />
        <el-table-column prop="price" label="套餐价" />
        <el-table-column label="售卖状态">
          <template slot-scope="scope">
            <div class="tableColumn-status" :class="{ 'stop-use': scope.row.status === 0 }">
              {{ scope.row.status === 0 ? '停售' : '启售' }}
            </div>
          </template>
        </el-table-column>
        <el-table-column prop="updateTime" label="最后操作时间" />
        <el-table-column label="操作" align="center" width="250px">
          <template slot-scope="scope">
            <el-button type="text" size="small"> 修改 </el-button>
            <el-button type="text" size="small" @click="handleStartOrStop(scope.row)">
              {{ scope.row.status == '1' ? '停售' : '启售' }}
            </el-button>
            <el-button type="text" size="small" @click="handleDelete('S', scope.row.id)"> 删除 </el-button>
          </template>
        </el-table-column>
      </el-table>

      <!-- 分页条 -->
      <el-pagination class="pageList" 
      :page-sizes="[10, 20, 30, 40]" 
      :page-size="pageSize"
      layout="total, sizes, prev, pager, next, jumper" 
      :total="total" 
      @size-change="handleSizeChange"
      @current-change="handleCurrentChange" />

⑥ 分页条 相关触发事件的实现方法:

10.3 功能测试

11 启售、停售套餐

11.1 需求分析和接口设计

业务规则:

  • 可以对状态为“启售”的套餐进行“停售“操作
  • 可以对状态为”停售“的套餐进行”启售“操作

接口设计:

11.2 代码开发

①为启售、停售按钮绑定单击事件

②导入启售停售套餐方法,发送Ajax请求:

注意:这里测试时要运行redis-server,否则会请求失败!

11.3 功能测试

12 删除套餐

12.1 需求分析和接口设计

业务规则:

  • 点击删除按钮,删除指定的一个套餐
  • 勾选需要删除的套餐,点击批量删除按钮,删除选中的一个或多个套餐

接口设计:

12.2 代码开发

①为 单个删除 按钮绑定单击事件

 ②导入删除套餐方法,发送Ajax请求

③批量删除:(使用了ElementUI的table多选组件)

在src/views/setmeal/index.vue中添加模型数据

该函数用于存储勾选的对象:

  • 批量删除 按钮绑定单击事件

完善handleDelete方法:

    // 删除套餐
    handleDelete(type: string, id: string) {
      this.$confirm('确认调整当前套餐的售卖状态, 是否继续?', '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning',
      }).then(() => {
        let param = ''
        if (type = 'B') {//批量删除
          const arr = new Array
          this.multipleSelection.forEach(element => {  //1,2,3
            arr.push(element.id)
          })
          param = arr.join(',')
        } else {// 单个删除
          param = id
        }

        deleteSetmeal(param).then(res => {
          if (res.data.code === 1) {
            this.$message.success('删除成功!')
            this.pageQuery()
          } else {
            this.$message.error(res.data.msg)
          }
        })
      })
    },
    handleSelectionChange(val) {
      this.multipleSelection = val;
    }

12.3 功能测试

13 新增套餐

13.1 需求分析和接口设计

业务规则:

接口设计:

  • 根据类型查询分类接口
  • 根据分类查询菜品接口
  • 文件上传接口
  • 新增套餐接口

13.2 代码开发

新增套餐操作步骤

①点击”新建套餐“按钮,跳转到新增页面

src/views/setmeal/index.vue

src/router.ts

②在套餐页面录入套餐相关信息,src/views/setmeal/addSetmeal.vue

<template>
  <div class="addBrand-container">
    <div class="container">
      <el-form ref="ruleForm"
               :model="ruleForm"
               :rules="rules"
               :inline="true"
               label-width="180px"
               class="demo-ruleForm">
        <div>
          <el-form-item label="套餐名称:"
                        prop="name">
            <el-input v-model="ruleForm.name"
                      placeholder="请填写套餐名称"
                      maxlength="14" />
          </el-form-item>
          <el-form-item label="套餐分类:"
                        prop="idType">
            <el-select v-model="ruleForm.idType"
                       placeholder="请选择套餐分类"
                       @change="$forceUpdate()">
              <el-option v-for="(item, index) in setMealList"
                         :key="index"
                         :label="item.name"
                         :value="item.id" />
            </el-select>
          </el-form-item>
        </div>
        <div>
          <el-form-item label="套餐价格:"
                        prop="price">
            <el-input v-model="ruleForm.price"
                      placeholder="请设置套餐价格" />
          </el-form-item>
        </div>
        <div>
          <el-form-item label="套餐菜品:"
                        required>
            <el-form-item>
              <div class="addDish">
                <span v-if="dishTable.length == 0"
                      class="addBut"
                      @click="openAddDish('new')">
                  + 添加菜品</span>
                <div v-if="dishTable.length != 0"
                     class="content">
                  <div class="addBut"
                       style="margin-bottom: 20px"
                       @click="openAddDish('change')">
                    + 添加菜品
                  </div>
                  <div class="table">
                    <el-table :data="dishTable"
                              style="width: 100%">
                      <el-table-column prop="name"
                                       label="名称"
                                       width="180"
                                       align="center" />
                      <el-table-column prop="price"
                                       label="原价"
                                       width="180"
                                       align="center">
                        <template slot-scope="scope">
                          {{ (Number(scope.row.price).toFixed(2) * 100) / 100 }}
                        </template>
                      </el-table-column>
                      <el-table-column prop="address"
                                       label="份数"
                                       align="center">
                        <template slot-scope="scope">
                          <el-input-number v-model="scope.row.copies"
                                           size="small"
                                           :min="1"
                                           :max="99"
                                           label="描述文字" />
                        </template>
                      </el-table-column>
                      <el-table-column prop="address"
                                       label="操作"
                                       width="180px;"
                                       align="center">
                        <template slot-scope="scope">
                          <el-button type="text"
                                     size="small"
                                     class="delBut non"
                                     @click="delDishHandle(scope.$index)">
                            删除
                          </el-button>
                        </template>
                      </el-table-column>
                    </el-table>
                  </div>
                </div>
              </div>
            </el-form-item>
          </el-form-item>
        </div>
        <div>
          <el-form-item label="套餐图片:"
                        required
                        prop="image">
            <image-upload :prop-image-url="imageUrl"
                          @imageChange="imageChange">
              图片大小不超过2M<br>仅能上传 PNG JPEG JPG类型图片<br>建议上传200*200或300*300尺寸的图片
            </image-upload>
          </el-form-item>
        </div>
        <div class="address">
          <el-form-item label="套餐描述:">
            <el-input v-model="ruleForm.description"
                      type="textarea"
                      :rows="3"
                      maxlength="200"
                      placeholder="套餐描述,最长200字" />
          </el-form-item>
        </div>
        <div class="subBox address">
          <el-form-item>
            <el-button @click="() => $router.back()">
              取消
            </el-button>
            <el-button type="primary"
                       :class="{ continue: actionType === 'add' }"
                       @click="submitForm('ruleForm', false)">
              保存
            </el-button>
            <el-button v-if="actionType == 'add'"
                       type="primary"
                       @click="submitForm('ruleForm', true)">
              保存并继续添加
            </el-button>
          </el-form-item>
        </div>
      </el-form>
    </div>
    <el-dialog v-if="dialogVisible"
               title="添加菜品"
               class="addDishList"
               :visible.sync="dialogVisible"  
               width="60%"
               :before-close="handleClose">
      <AddDish v-if="dialogVisible"
               ref="adddish"
               :check-list="checkList"
               :seach-key="seachKey"
               :dish-list="dishList"
               @checkList="getCheckList" />
      <span slot="footer"
            class="dialog-footer">
        <el-button @click="handleClose">取 消</el-button>
        <el-button type="primary"
                   @click="addTableList">添 加</el-button>
      </span>
    </el-dialog>
  </div>
</template>

<script lang="ts">
import { Component, Vue } from 'vue-property-decorator'
import HeadLable from '@/components/HeadLable/index.vue'
import ImageUpload from '@/components/ImgUpload/index.vue'
import AddDish from './components/AddDish.vue'
import { querySetmealById, addSetmeal, editSetmeal } from '@/api/setMeal'
import { getCategoryList } from '@/api/dish'
import { baseUrl } from '@/config.json'

@Component({
  name: 'addShop',
  components: {
    HeadLable,
    AddDish,
    ImageUpload
  }
})
export default class extends Vue {
  private value: string = ''
  private setMealList: [] = []
  private seachKey: string = ''
  private dishList: [] = []
  private imageUrl: string = ''
  private actionType: string = ''
  private dishTable: [] = []
  private dialogVisible: boolean = false
  private checkList: any[] = []
  private ruleForm = {
    name: '',
    categoryId: '',
    price: '',
    code: '',
    image: '',
    description: '',
    dishList: [],
    status: true,
    idType: ''
  }

  get rules() {
    return {
      name: {
        required: true,
        validator: (rule: any, value: string, callback: Function) => {
          if (!value) {
            callback(new Error('请输入套餐名称'))
          } else {
            const reg = /^([A-Za-z0-9\u4e00-\u9fa5]){2,20}$/
            if (!reg.test(value)) {
              callback(new Error('套餐名称输入不符,请输入2-20个字符'))
            } else {
              callback()
            }
          }
        },
        trigger: 'blur'
      },
      idType: {
        required: true,
        message: '请选择套餐分类',
        trigger: 'change'
      },
      image: {
        required: true,
        message: '菜品图片不能为空'
      },
      price: {
        required: true,
        // 'message': '请输入套餐价格',
        validator: (rules: any, value: string, callback: Function) => {
          const reg = /^([1-9]\d{0,5}|0)(\.\d{1,2})?$/
          if (!reg.test(value) || Number(value) <= 0) {
            callback(
              new Error(
                '套餐价格格式有误,请输入大于零且最多保留两位小数的金额'
              )
            )
          } else {
            callback()
          }
        },
        trigger: 'blur'
      },
      code: { required: true, message: '请输入商品码', trigger: 'blur' }
    }
  }

  created() {
    this.getDishTypeList()
    this.actionType = this.$route.query.id ? 'edit' : 'add'
    if (this.actionType == 'edit') {
      this.init()
    }
  }

  private async init() {
    querySetmealById(this.$route.query.id).then(res => {
      if (res && res.data && res.data.code === 1) {
        this.ruleForm = res.data.data
        this.ruleForm.status = res.data.data.status == '1'
        ;(this.ruleForm as any).price = res.data.data.price
        // this.imageUrl = `http://172.17.2.120:8080/common/download?name=${res.data.data.image}`
        this.imageUrl = res.data.data.image
        this.checkList = res.data.data.setmealDishes
        this.dishTable = res.data.data.setmealDishes.reverse()
        this.ruleForm.idType = res.data.data.categoryId
      } else {
        this.$message.error(res.data.msg)
      }
    })
  }
  private seachHandle() {
    this.seachKey = this.value
  }
  // 获取套餐分类
  private getDishTypeList() {
    getCategoryList({ type: 2, page: 1, pageSize: 1000 }).then(res => {
      if (res && res.data && res.data.code === 1) {
        this.setMealList = res.data.data.map((obj: any) => ({
          ...obj,
          idType: obj.id
        }))
      } else {
        this.$message.error(res.data.msg)
      }
    })
  }

  // 通过套餐ID获取菜品列表分类
  // private getDishList (id:number) {
  //   getDishListType({id}).then(res => {
  //     if (res.data.code == 200) {
  //       const { data } = res.data
  //       this.dishList = data
  //     } else {
  //       this.$message.error(res.data.desc)
  //     }
  //   })
  // }

  // 删除套餐菜品
  delDishHandle(index: any) {
    this.dishTable.splice(index, 1)
    this.checkList = this.dishTable
    // this.checkList.splice(index, 1)
  }

  // 获取添加菜品数据 - 确定加菜倒序展示
  private getCheckList(value: any) {
    this.checkList = [...value].reverse()
  }

  // 添加菜品
  openAddDish(st: string) {
    this.seachKey = ''
    this.dialogVisible = true
  }
  // 取消添加菜品
  handleClose(done: any) {
    // this.$refs.adddish.close()
    this.dialogVisible = false
    this.checkList = JSON.parse(JSON.stringify(this.dishTable))
    // this.dialogVisible = false
  }

  // 保存添加菜品列表
  public addTableList() {
    this.dishTable = JSON.parse(JSON.stringify(this.checkList))
    this.dishTable.forEach((n: any) => {
      n.copies = 1
    })
    this.dialogVisible = false
  }

  public submitForm(formName: any, st: any) {
    ;(this.$refs[formName] as any).validate((valid: any) => {
      if (valid) {
        if (this.dishTable.length === 0) {
          return this.$message.error('套餐下菜品不能为空')
        }
        if (!this.ruleForm.image) return this.$message.error('套餐图片不能为空')
        let prams = { ...this.ruleForm } as any
        prams.setmealDishes = this.dishTable.map((obj: any) => ({
          copies: obj.copies,
          dishId: obj.dishId,
          name: obj.name,
          price: obj.price
        }))
        ;(prams as any).status =
          this.actionType === 'add' ? 0 : this.ruleForm.status ? 1 : 0
        prams.categoryId = this.ruleForm.idType
        // delete prams.dishList
        if (this.actionType == 'add') {
          delete prams.id
          addSetmeal(prams)
            .then(res => {
              if (res && res.data && res.data.code === 1) {
                this.$message.success('套餐添加成功!')
                if (!st) {
                  this.$router.push({ path: '/setmeal' })
                } else {
                  ;(this as any).$refs.ruleForm.resetFields()
                  this.dishList = []
                  this.dishTable = []
                  this.ruleForm = {
                    name: '',
                    categoryId: '',
                    price: '',
                    code: '',
                    image: '',
                    description: '',
                    dishList: [],
                    status: true,
                    id: '',
                    idType: ''
                  } as any
                  this.imageUrl = ''
                }
              } else {
                this.$message.error(res.data.msg)
              }
            })
            .catch(err => {
              this.$message.error('请求出错了:' + err.message)
            })
        } else {
          delete prams.updateTime
          editSetmeal(prams)
            .then(res => {
              if (res.data.code === 1) {
                this.$message.success('套餐修改成功!')
                this.$router.push({ path: '/setmeal' })
              } else {
                // this.$message.error(res.data.desc || res.data.message)
              }
            })
            .catch(err => {
              this.$message.error('请求出错了:' + err.message)
            })
        }
      } else {
        // console.log('error submit!!')
        return false
      }
    })
  }

  imageChange(value: any) {
    this.ruleForm.image = value
  }
}
</script>
<style>
.avatar-uploader .el-icon-plus:after {
  position: absolute;
  display: inline-block;
  content: ' ' !important;
  left: calc(50% - 20px);
  top: calc(50% - 40px);
  width: 40px;
  height: 40px;
  background: url('./../../assets/icons/icon_upload@2x.png') center center
    no-repeat;
  background-size: 20px;
}
</style>
<style lang="scss">
// .el-form-item__error {
//   top: 90%;
// }
.addBrand-container {
  .avatar-uploader .el-upload {
    border: 1px dashed #d9d9d9;
    border-radius: 6px;
    cursor: pointer;
    position: relative;
    overflow: hidden;
  }

  .avatar-uploader .el-upload:hover {
    border-color: #ffc200;
  }

  .avatar-uploader-icon {
    font-size: 28px;
    color: #8c939d;
    width: 200px;
    height: 160px;
    line-height: 160px;
    text-align: center;
  }

  .avatar {
    width: 200px;
    height: 160px;
    display: block;
  }

  // .el-form--inline .el-form-item__content {
  //   width: 293px;
  // }

  .el-input {
    width: 293px;
  }

  .address {
    .el-form-item__content {
      width: 777px !important;
    }
  }
  .el-input__prefix {
    top: 2px;
  }

  .addDish {
    .el-input {
      width: 130px;
    }

    .el-input-number__increase {
      border-left: solid 1px #fbe396;
      background: #fffbf0;
    }

    .el-input-number__decrease {
      border-right: solid 1px #fbe396;
      background: #fffbf0;
    }

    input {
      border: 1px solid #fbe396;
    }

    .table {
      border: solid 1px #ebeef5;
      border-radius: 3px;

      th {
        padding: 5px 0;
      }

      td {
        padding: 7px 0;
      }
    }
  }

  .addDishList {
    .seachDish {
      position: absolute;
      top: 12px;
      right: 20px;
    }

    .el-dialog__footer {
      padding-top: 27px;
    }

    .el-dialog__body {
      padding: 0;
      border-bottom: solid 1px #efefef;
    }
    .seachDish {
      .el-input__inner {
        height: 40px;
        line-height: 40px;
      }
    }
  }
}
</style>
<style lang="scss" scoped>
.addBrand {
  &-container {
    margin: 30px;

    .container {
      position: relative;
      z-index: 1;
      background: #fff;
      padding: 30px;
      border-radius: 4px;
      min-height: 500px;

      .subBox {
        padding-top: 30px;
        text-align: center;
        border-top: solid 1px $gray-5;
      }
      .el-input {
        width: 350px;
      }
      .addDish {
        width: 777px;

        .addBut {
          background: #ffc200;
          display: inline-block;
          padding: 0px 20px;
          border-radius: 3px;
          line-height: 40px;
          cursor: pointer;
          border-radius: 4px;
          color: #333333;
          font-weight: 500;
        }

        .content {
          background: #fafafb;
          padding: 20px;
          border: solid 1px #d8dde3;
          border-radius: 3px;
        }
      }
    }
  }
}
</style>

src/views/setmeal/components/AddDish.vue

<template>
  <div class="addDish">
    <div class="leftCont">
      <div v-show="seachKey.trim() == ''"
           class="tabBut">
        <span v-for="(item, index) in dishType"
              :key="index"
              :class="{ act: index == keyInd }"
              @click="checkTypeHandle(index, item.id)">{{ item.name }}</span>
      </div>
      <div class="tabList">
        <div class="table"
             :class="{ borderNone: !dishList.length }">
          <div v-if="dishList.length == 0"
               style="padding-left: 10px">
            <Empty />
          </div>
          <el-checkbox-group v-if="dishList.length > 0"
                             v-model="checkedList"
                             @change="checkedListHandle">
            <div v-for="(item, index) in dishList"
                 :key="item.name + item.id"
                 class="items">
              <el-checkbox :key="index"
                           :label="item.name">
                <div class="item">
                  <span style="flex: 3; text-align: left">{{
                    item.dishName
                  }}</span>
                  <span>{{ item.status == 0 ? '停售' : '在售' }}</span>
                  <span>{{ (Number(item.price) ).toFixed(2)*100/100 }}</span>
                </div>
              </el-checkbox>
            </div>
          </el-checkbox-group>
        </div>
      </div>
    </div>
    <div class="ritCont">
      <div class="tit">
        已选菜品({{ checkedListAll.length }})
      </div>
      <div class="items">
        <div v-for="(item, ind) in checkedListAll"
             :key="ind"
             class="item">
          <span>{{ item.dishName || item.name }}</span>
          <span class="price">¥ {{ (Number(item.price) ).toFixed(2)*100/100 }} </span>
          <span class="del"
                @click="delCheck(item.name)">
            <img src="./../../../assets/icons/btn_clean@2x.png"
                 alt="">
          </span>
        </div>
      </div>
    </div>
  </div>
</template>

<script lang="ts">
import { Component, Prop, Vue, Watch } from 'vue-property-decorator'
// import {getDishTypeList, getDishListType} from '@/api/dish';
import { getCategoryList, queryDishList } from '@/api/dish'
import Empty from '@/components/Empty/index.vue'

@Component({
  name: 'selectInput',
  components: {
    Empty
  }
})
export default class extends Vue {
  @Prop({ default: '' }) private value!: number
  @Prop({ default: [] }) private checkList!: any[]
  @Prop({ default: '' }) private seachKey!: string
  private dishType: [] = []
  private dishList: [] = []
  private allDishList: any[] = []
  private dishListCache: any[] = []
  private keyInd = 0
  private searchValue: string = ''
  public checkedList: any[] = []
  private checkedListAll: any[] = []
  private ids: any = new Set()
  created() {
    this.init()
  }

  @Watch('seachKey')
  private seachKeyChange(value: any) {
    if (value.trim()) {
      this.getDishForName(this.seachKey)
    }
  }

  public init() {
    // 菜单列表数据获取
    this.getDishType()
    // 初始化选项
    this.checkedList = this.checkList.map((it: any) => it.name)
    // 已选项的菜品-详细信息
    this.checkedListAll = this.checkList.reverse()
  }
  // 获取套餐分类
  public getDishType() {
    getCategoryList({ type: 1 }).then(res => {
      if (res && res.data && res.data.code === 1) {
        this.dishType = res.data.data
        this.getDishList(res.data.data[0].id)
      } else {
        this.$message.error(res.data.msg)
      }
      // if (res.data.code == 200) {
      //   const { data } = res.data
      //   this.   = data
      //   this.getDishList(data[0].category_id)
      // } else {
      //   this.$message.error(res.data.desc)
      // }
    })
  }

  // 通过套餐ID获取菜品列表分类
  private getDishList(id: number) {
    queryDishList({ categoryId: id }).then(res => {
      if (res && res.data && res.data.code === 1) {
        if (res.data.data.length == 0) {
          this.dishList = []
          return
        }
        let newArr = res.data.data
        newArr.forEach((n: any) => {
          n.dishId = n.id
          n.copies = 1
          // n.dishCopies = 1
          n.dishName = n.name
        })
        this.dishList = newArr
        if (!this.ids.has(id)) {
          this.allDishList = [...this.allDishList, ...newArr]
        }
        this.ids.add(id)
      } else {
        this.$message.error(res.data.msg)
      }
    })
  }

  // 关键词收搜菜品列表分类
  private getDishForName(name: any) {
    queryDishList({ name }).then(res => {
      if (res && res.data && res.data.code === 1) {
        let newArr = res.data.data
        newArr.forEach((n: any) => {
          n.dishId = n.id
          n.dishName = n.name
        })
        this.dishList = newArr
      } else {
        this.$message.error(res.data.msg)
      }
    })
  }
  // 点击分类
  private checkTypeHandle(ind: number, id: any) {
    this.keyInd = ind
    this.getDishList(id)
  }
  // 添加菜品
  private checkedListHandle(value: [string]) {
    // TODO 实现倒序 由于value是组件内封装无法从前面添加 所有取巧处理倒序添加
    // 倒序展示 - 数据处理前反正 为正序
    this.checkedListAll.reverse()
    // value 是一个只包含菜品名的数组 需要从 dishList中筛选出 对应的详情
    // 操作添加菜品
    const list = this.allDishList.filter((item: any) => {
      let data
      value.forEach((it: any) => {
        if (item.name == it) {
          data = item
        }
      })
      return data
    })
    // 编辑的时候需要与已有菜品合并
    // 与当前请求下的选择性 然后去重就是当前的列表
    const dishListCat = [...this.checkedListAll, ...list]
    let arrData: any[] = []
    this.checkedListAll = dishListCat.filter((item: any) => {
      let allArrDate
      if (arrData.length == 0) {
        arrData.push(item.name)
        allArrDate = item
      } else {
        const st = arrData.some(it => item.name == it)
        if (!st) {
          arrData.push(item.name)
          allArrDate = item
        }
      }
      return allArrDate
    })
    // 如果是减菜 走这里
    if (value.length < arrData.length) {
      this.checkedListAll = this.checkedListAll.filter((item: any) => {
        if (value.some(it => it == item.name)) {
          return item
        }
      })
    }
    this.$emit('checkList', this.checkedListAll)
    // 数据处理完反转为倒序
    this.checkedListAll.reverse()
  }

  open(done: any) {
    this.dishListCache = JSON.parse(JSON.stringify(this.checkList))
  }

  close(done: any) {
    this.checkList = this.dishListCache
  }

  // 删除
  private delCheck(name: any) {
    const index = this.checkedList.findIndex(it => it === name)
    const indexAll = this.checkedListAll.findIndex(
      (it: any) => it.name === name
    )

    this.checkedList.splice(index, 1)
    this.checkedListAll.splice(indexAll, 1)
    this.$emit('checkList', this.checkedListAll)
  }
}
</script>
<style lang="scss">
.addDish {
  .el-checkbox__label {
    width: 100%;
  }
  .empty-box {
    margin-top: 50px;
    margin-bottom: 0px;
  }
}
</style>
<style lang="scss" scoped>
.addDish {
  padding: 0 20px;
  display: flex;
  line-height: 40px;
  .empty-box {
    img {
      width: 190px;
      height: 147px;
    }
  }

  .borderNone {
    border: none !important;
  }
  span,
  .tit {
    color: #333;
  }
  .leftCont {
    display: flex;
    border-right: solid 1px #efefef;
    width: 60%;
    padding: 15px;
    .tabBut {
      width: 110px;
      font-weight: bold;
      border-right: solid 2px #f4f4f4;
      span {
        display: block;
        text-align: center;
        // border-right: solid 2px #f4f4f4;
        cursor: pointer;
        position: relative;
      }
    }
    .act {
      border-color: $mine !important;
      color: $mine !important;
    }
    .act::after {
      content: ' ';
      display: inline-block;
      background-color: $mine;
      width: 2px;
      height: 40px;
      position: absolute;
      right: -2px;
    }
    .tabList {
      flex: 1;
      padding: 15px;
      height: 400px;
      overflow-y: scroll;
      .table {
        border: solid 1px #f4f4f4;
        border-bottom: solid 1px #f4f4f4;
        .items {
          border-bottom: solid 1px #f4f4f4;
          padding: 0 10px;
          display: flex;
          .el-checkbox,
          .el-checkbox__label {
            width: 100%;
          }
          .item {
            display: flex;
            padding-right: 20px;
            span {
              display: inline-block;
              text-align: center;
              flex: 1;
              font-weight: normal;
            }
          }
        }
      }
    }
  }
  .ritCont {
    width: 40%;
    .tit {
      margin: 0 15px;
      font-weight: bold;
    }
    .items {
      height: 338px;
      padding: 4px 15px;
      overflow: scroll;
    }
    .item {
      box-shadow: 0px 1px 4px 3px rgba(0, 0, 0, 0.03);
      display: flex;
      text-align: center;
      padding: 0 10px;
      margin-bottom: 20px;
      border-radius: 6px;
      color: #818693;
      span:first-child {
        text-align: left;
        color: #20232a;
        flex: 70%;
      }
      .price {
        display: inline-block;
        flex: 70%;
        text-align: left;
      }
      .del {
        cursor: pointer;
        img {
          position: relative;
          top: 5px;
          width: 20px;
        }
      }
    }
  }
}
</style>

src/api/setMeals.ts

import request from '@/utils/request'
/**
 *
 * 套餐管理
 *
 **/

//套餐分页查询
export const getSetmealPage = (params: any) => {
    return request({
        url: '/setmeal/page',
        method: 'GET',
        params: params
    })
}

//套餐启售停售
export const enableOrDisableSetmeal = (params: any) => {
    return request({
        url: `/setmeal/status/${params.status}`,
        method: 'POST',
        params: {id: params.id}
    })
}

//删除套餐
export const deleteSetmeal = (ids: string) => {//1,2,3
    return request({
        url: '/setmeal',
        method: 'DELETE',
        params: {ids: ids}
    })
}


  
// 修改数据接口
export const editSetmeal = (params: any) => {
    return request({
        url: '/setmeal',
        method: 'put',
        data: { ...params }
    })
}

// 新增数据接口
export const addSetmeal = (params: any) => {
    return request({
        url: '/setmeal',
        method: 'post',
        data: { ...params }
    })
}

// 查询详情接口
export const querySetmealById = (id: string | (string | null)[]) => {
    return request({
        url: `/setmeal/${id}`,
        method: 'get'
    })
}

src/api/dish.ts

import request from '@/utils/request'
/**
 *
 * 菜品管理
 *
 **/
// 查询列表接口
export const getDishPage = (params: any) => {
  return request({
    url: '/dish/page',
    method: 'get',
    params
  })
}

// 删除接口
export const deleteDish = (ids: string) => {
  return request({
    url: '/dish',
    method: 'delete',
    params: { ids }
  })
}

// 修改接口
export const editDish = (params: any) => {
  return request({
    url: '/dish',
    method: 'put',
    data: { ...params }
  })
}

// 新增接口
export const addDish = (params: any) => {
  return request({
    url: '/dish',
    method: 'post',
    data: { ...params }
  })
}

// 查询详情
export const queryDishById = (id: string | (string | null)[]) => {
  return request({
    url: `/dish/${id}`,
    method: 'get'
  })
}

// 获取菜品分类列表
export const getCategoryList = (params: any) => {
  return request({
    url: '/category/list',
    method: 'get',
    params
  })
}

// 查菜品列表的接口
export const queryDishList = (params: any) => {
  return request({
    url: '/dish/list',
    method: 'get',
    params
  })
}

// 文件down预览
export const commonDownload = (params: any) => {
  return request({
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
    },
    url: '/common/download',
    method: 'get',
    params
  })
}

// 起售停售---批量起售停售接口
export const dishStatusByStatus = (params: any) => {
  return request({
    url: `/dish/status/${params.status}`,
    method: 'post',
    params: { id: params.id }
  })
}

//菜品分类数据查询
export const dishCategoryList = (params: any) => {
  return request({
    url: `/category/list`,
    method: 'get',
    params: { ...params }
  })
}

③点击”保存“按钮完成新增操作

13.3 功能测试

完结撒花!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值