《苍穹外卖》前端课程知识点记录

一、VUE基础知识

基于脚手架创建前端工程

1. 环境要求

安装node.js:Node.js安装与配置(详细步骤)_nodejs安装及环境配置-CSDN博客
查看node和npm的版本号

安装Vue CLI:Vue.js安装与创建默认项目(详细步骤)_nodejs安装及环境配置-CSDN博客

查看vue版本

使用Vue CLI创建前端工程

  • 方式一:vue create项目名称

  ① 创建一个不带中文的文件夹,如下图:


② 创建工程---选择Vue 2

③ 选择npm


④ 如果中间有报错,如下:
npm ERR! code EPERM
npm ERR! syscall mkdir
npm ERR! path C:\Program Files\nodejs\node_cache\_cacache\index-v5\ee\aa
npm ERR! errno -4048
npm ERR! Error: EPERM: operation not permitted, mkdir 'C:\Program Files\nodejs\node_cache\_cacache\index-v5\ee\aa'

找到nodejs的安装目录,右击属性->安全->编辑->把所有权限都勾选上


⑤ 结果:

  • 方式二:vue ui

①打开ui界面

② 点击创建

③ 填写项目信息

④ 选择vue2,创建项目

⑤结果:

项目结构

运行项目

npm run serve

命令的最后一个单词并不是固定的,与package.json下写的这一项相关,如下

如果8080端口号被占用,可以在vue.config.js中更改端口号

如果上面这种方式不起作用的,可以到项目对应文件夹用cmd试试

退出运行:Ctrl + C

vue基本使用方式

Vue组件(Vue2)

Vue的组件文件以.vue结尾,每个组件由三部分组成:结构、样式、逻辑。

示例

Vue 2:一个Vue组件的模板只能有一个根元素。这是因为Vue 2使用的是基于AST(抽象语法树)的模板编译方式,需要将模板编译为render函数,而render函数只能返回一个根节点。

Vue 3 : Vue的模板编译器进行了重大改进,支持多个根元素。Vue 3使用了基于编译器的模板编译方式,这意味着在Vue 3中,一个组件的模板可以有多个根元素,而不再需要包裹在一个单独的根元素内。

文本插值

作用:用来绑定 data 方法返回的对象属性

用法:{{}}

属性绑定

作用:为标签的属性绑定data方法中返回的属性

用法:v-bind:xxx,简写为 :xxx

事件绑定

作用:为元素绑定对应的事件

用法:v-on:xxx,简写为@xxx

双向绑定

作用:表单输入项和data方法中的属性进行绑定,任意一方改变都会同步给另一方

用法:v-model

条件渲染

作用:根据表达式的值来动态渲染页面元素

用法:v-ifv-elsev-else-if

axios

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

安装命令:npm install axios

导入命令:import axios from 'axios'

axios的API列表:

请求备注
axios.get(url[, config])
axios.delete(url[, config])
axios.head(url[, config])
axios.options(url[, config])
axios.post(url[, data[, config]])
axios.put(url, data[, config]])
axios.patch(url[, data[, config]])

参数说明:

  • url:请求路径
  • data:请求体数据,最常见的是JSON格式数据
  • config:配置对象,可以设置查询参数、请求体信息

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

 axios统一使用方式:axios(config)

请求配置

网址:请求配置 | Axios中文文档 | Axios中文网 (axios-http.cn)

{
  // `url` 是用于请求的服务器 URL
  url: '/user',

  // `method` 是创建请求时使用的方法
  method: 'get', // 默认值

  // `baseURL` 将自动加在 `url` 前面,除非 `url` 是一个绝对 URL。
  // 它可以通过设置一个 `baseURL` 便于为 axios 实例的方法传递相对 URL
  baseURL: 'https://some-domain.com/api/',

  // `transformRequest` 允许在向服务器发送前,修改请求数据
  // 它只能用于 'PUT', 'POST' 和 'PATCH' 这几个请求方法
  // 数组中最后一个函数必须返回一个字符串, 一个Buffer实例,ArrayBuffer,FormData,或 Stream
  // 你可以修改请求头。
  transformRequest: [function (data, headers) {
    // 对发送的 data 进行任意转换处理

    return data;
  }],

  // `transformResponse` 在传递给 then/catch 前,允许修改响应数据
  transformResponse: [function (data) {
    // 对接收的 data 进行任意转换处理

    return data;
  }],

  // 自定义请求头
  headers: {'X-Requested-With': 'XMLHttpRequest'},

  // `params` 是与请求一起发送的 URL 参数
  // 必须是一个简单对象或 URLSearchParams 对象
  params: {
    ID: 12345
  },

  // `paramsSerializer`是可选方法,主要用于序列化`params`
  // (e.g. https://www.npmjs.com/package/qs, http://api.jquery.com/jquery.param/)
  paramsSerializer: function (params) {
    return Qs.stringify(params, {arrayFormat: 'brackets'})
  },

  // `data` 是作为请求体被发送的数据
  // 仅适用 'PUT', 'POST', 'DELETE 和 'PATCH' 请求方法
  // 在没有设置 `transformRequest` 时,则必须是以下类型之一:
  // - string, plain object, ArrayBuffer, ArrayBufferView, URLSearchParams
  // - 浏览器专属: FormData, File, Blob
  // - Node 专属: Stream, Buffer
  data: {
    firstName: 'Fred'
  },
  
  // 发送请求体数据的可选语法
  // 请求方式 post
  // 只有 value 会被发送,key 则不会
  data: 'Country=Brasil&City=Belo Horizonte',

  // `timeout` 指定请求超时的毫秒数。
  // 如果请求时间超过 `timeout` 的值,则请求会被中断
  timeout: 1000, // 默认值是 `0` (永不超时)

  // `withCredentials` 表示跨域请求时是否需要使用凭证
  withCredentials: false, // default

  // `adapter` 允许自定义处理请求,这使测试更加容易。
  // 返回一个 promise 并提供一个有效的响应 (参见 lib/adapters/README.md)。
  adapter: function (config) {
    /* ... */
  },

  // `auth` HTTP Basic Auth
  auth: {
    username: 'janedoe',
    password: 's00pers3cret'
  },

  // `responseType` 表示浏览器将要响应的数据类型
  // 选项包括: 'arraybuffer', 'document', 'json', 'text', 'stream'
  // 浏览器专属:'blob'
  responseType: 'json', // 默认值

  // `responseEncoding` 表示用于解码响应的编码 (Node.js 专属)
  // 注意:忽略 `responseType` 的值为 'stream',或者是客户端请求
  // Note: Ignored for `responseType` of 'stream' or client-side requests
  responseEncoding: 'utf8', // 默认值

  // `xsrfCookieName` 是 xsrf token 的值,被用作 cookie 的名称
  xsrfCookieName: 'XSRF-TOKEN', // 默认值

  // `xsrfHeaderName` 是带有 xsrf token 值的http 请求头名称
  xsrfHeaderName: 'X-XSRF-TOKEN', // 默认值

  // `onUploadProgress` 允许为上传处理进度事件
  // 浏览器专属
  onUploadProgress: function (progressEvent) {
    // 处理原生进度事件
  },

  // `onDownloadProgress` 允许为下载处理进度事件
  // 浏览器专属
  onDownloadProgress: function (progressEvent) {
    // 处理原生进度事件
  },

  // `maxContentLength` 定义了node.js中允许的HTTP响应内容的最大字节数
  maxContentLength: 2000,

  // `maxBodyLength`(仅Node)定义允许的http请求内容的最大字节数
  maxBodyLength: 2000,

  // `validateStatus` 定义了对于给定的 HTTP状态码是 resolve 还是 reject promise。
  // 如果 `validateStatus` 返回 `true` (或者设置为 `null` 或 `undefined`),
  // 则promise 将会 resolved,否则是 rejected。
  validateStatus: function (status) {
    return status >= 200 && status < 300; // 默认值
  },

  // `maxRedirects` 定义了在node.js中要遵循的最大重定向数。
  // 如果设置为0,则不会进行重定向
  maxRedirects: 5, // 默认值

  // `socketPath` 定义了在node.js中使用的UNIX套接字。
  // e.g. '/var/run/docker.sock' 发送请求到 docker 守护进程。
  // 只能指定 `socketPath` 或 `proxy` 。
  // 若都指定,这使用 `socketPath` 。
  socketPath: null, // default

  // `httpAgent` and `httpsAgent` define a custom agent to be used when performing http
  // and https requests, respectively, in node.js. This allows options to be added like
  // `keepAlive` that are not enabled by default.
  httpAgent: new http.Agent({ keepAlive: true }),
  httpsAgent: new https.Agent({ keepAlive: true }),

  // `proxy` 定义了代理服务器的主机名,端口和协议。
  // 您可以使用常规的`http_proxy` 和 `https_proxy` 环境变量。
  // 使用 `false` 可以禁用代理功能,同时环境变量也会被忽略。
  // `auth`表示应使用HTTP Basic auth连接到代理,并且提供凭据。
  // 这将设置一个 `Proxy-Authorization` 请求头,它会覆盖 `headers` 中已存在的自定义 `Proxy-Authorization` 请求头。
  // 如果代理服务器使用 HTTPS,则必须设置 protocol 为`https`
  proxy: {
    protocol: 'https',
    host: '127.0.0.1',
    port: 9000,
    auth: {
      username: 'mikeymike',
      password: 'rapunz3l'
    }
  },

  // see https://axios-http.com/zh/docs/cancellation
  cancelToken: new CancelToken(function (cancel) {
  }),

  // `decompress` indicates whether or not the response body should be decompressed 
  // automatically. If set to `true` will also remove the 'content-encoding' header 
  // from the responses objects of all decompressed responses
  // - Node only (XHR cannot turn off decompression)
  decompress: true // 默认值

}

示例——配置代理

记得要先运行后端服务,启动redis

HelloWorld.vue

<template>
  <div class="hello">

    <div><input type="button" value="发送POST请求" @click="handleSendPOST"/></div>
    <div><input type="button" value="发送GET请求" @click="handleSendGET"/></div>

    <div><input type="button" value="统一请求方式" @click="handleSend"/></div>

  </div>
</template>

<script>
import axiox from 'axios'
export default {
  name: 'HelloWorld',
  props: {
    msg: String
  },
  methods: {
    handleSendPOST() {
      // 通过axios发送异域POST方式的http请求
      axiox.post('/api/admin/employee/login', {
        username: 'admin',
        password: '123456'
      }).then(res => {
        console.log(res.data)
      }).catch(error => {
        console.log(error.response)
      })
    },
    handleSendGET() {
      // 通过axios发送GET方式请求
      axiox.get('/api/admin/shop/status', {
        headers: {
          token: 'eyJhbGciOiJIUzI1NiJ9.eyJlbXBJZCI6MSwiZXhwIjoxNzE0MzIyNDAyfQ.gMfQXajaBTKnMuz19_BsmhWLGWov24rqZDLcPLwZCSA'
        }
      }).then(res => {
        console.log(res.data)
      })
    },
    handleSend() {
      // 使用axios提供的统一调用方式发送请求
      axiox({
        url: '/api/admin/employee/login',
        method: 'post',
        data: {  // data表示通过请求体传参
          username: 'admin',
          password: '123456'
        }
      }).then(res => {
        console.log(res.data.data.token)
        axiox({
          url: '/api/admin/shop/status',
          method: 'get',
          headers: {
            token: res.data.data.token
          }
        })
      })
    }
  }
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h3 {
  margin: 40px 0 0;
}
ul {
  list-style-type: none;
  padding: 0;
}
li {
  display: inline-block;
  margin: 0 10px;
}
a {
  color: #42b983;
}
</style>

vue.config.js

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

结果

二、VUE进阶(router、vuex、typescript)

路由 Vue-Router

Vue-Router介绍

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

vue应用中如何实现路由?

  • 通过vue-router实现路由功能,需要安装js库(npm install vue-router)

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

命令:vue ui

①包管理器选择:npm

②预设选择:手动

③功能添加:Router

④配置版本选择:2.x,linter config选择:ESLint with error prevention only

⑤选择创建项目,不保存预设

⑥查看创建结果

⑦运行项目

路由配置

路由组成

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

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

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

路由跳转

  • 标签式<router-link>
  • 编程式

如果请求的路径不存在,应该如何处理?

①当上面的路径都匹配不到时,重定向到最后一项

嵌套路由

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

实现步骤:

  • 安装并导入elementui,实现页面布局(Container布局容器)---ContainerView.vue
    npm i element-ui -S

     
  • 提供子视图组件,用于效果展示 ---P1View.vue、P2View.vue、P3View.vue
    view/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>
  • 在src/router/index.js中配置路由映射规则(嵌套路由配置)
  • 在布局容器视图中添加<router-view>,实现子视图组件展示
  • 在布局容器视图中添加<router-link>,实现路由请求

注意事项:子路由变化,切换的是【ContainerView组件】中‘<router-view></router-view>’部分的内容。

思考

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

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

状态管理vuex

vuex介绍

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

安装

npm install vuex@next --save

核心概念

  • state:状态对象,集中定义各个组件共享的数据
  • mutations:类似于一个事件,用于修改共享数据,要求必须是同步函数
  • actions:类似于mutation,可以包含异步操作,通过调用mutation来改变共享数据

使用方式

①创建带有vuex功能的脚手架工程

②src/store/index.js

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

// 集中管理多个组件共享的数据
export default new Vuex.Store({
  state: {
  },
  getters: {
  },
  mutations: {
  },
  actions: {
  },
  modules: {
  }
})

③src/main.js

import Vue from 'vue'
import App from './App.vue'
import store from './store'

Vue.config.productionTip = false

new Vue({
  // 使用vuex功能
  store,
  render: h => h(App)
}).$mount('#app')

④定义和展示共享数据

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

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

⑤在mutations中定义函数,修改共享数据

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

// 集中管理多个组件共享的数据
export default new Vuex.Store({
  // 集中定义共享数据
  state: {
    name: '未登录游客'
  },
  getters: {
  },
  // 修改共享数据只能通过mutation实现,必须是同步操作
  mutations: {
    setName(state, newName) {
      state.name = newName
    }
  },
  // 通过actions可以调用mutations,在action中可以进行异步操作
  actions: {
  },
  modules: {
  }
})
<template>
  <div id="app">
    欢迎您,{{$store.state.name}}

    <input type = "button" value = "通过mutations修改共享数据" @click="handleUpdate"/>
    <img alt="Vue logo" src="./assets/logo.png">
    <HelloWorld msg="Welcome to Your Vue.js App"/>
  </div>
</template>

<script>
import HelloWorld from './components/HelloWorld.vue'

export default {
  name: 'App',
  components: {
    HelloWorld
  },
  methods: {
    handleUpdate() {
      // mutations中定义的函数不能直接调用,必须通过这种方式来调用
      // setName为mutations中定义的函数名称,lisi为需要传递的参数
      this.$store.commit('setName', 'lisi')
    }
  }
}
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

④在actions中定义函数,用于调用mutation

先安装axios

npm install axios
// src/store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
import axios from 'axios'

Vue.use(Vuex)

// 集中管理多个组件共享的数据
export default new Vuex.Store({
  // 集中定义共享数据
  state: {
    name: '未登录游客'
  },
  getters: {
  },
  // 修改共享数据只能通过mutation实现,必须是同步操作
  mutations: {
    setName(state, newName) {
      state.name = newName
    }
  },
  // 通过actions可以调用mutations,在action中可以进行异步操作
  actions: {
    setNameByAxios(context) {
      axios ({
        url: '/api/admin/employee/login',
        method: 'post',
        data: {
          username: 'admin',
          password: '123456'
        }
      }).then(res => {
        if(res.data.code == 1) {
          // 异步请求后,需要修改共享数据
          // 调用mutation中定义的setName函数
          context.commit('setName', res.data.data.name)
        }
      })
    }
  },
  modules: {
  }
})
// App.vue
<template>
  <div id="app">
    欢迎您,{{$store.state.name}}

    <input type = "button" value = "通过mutations修改共享数据" @click="handleUpdate"/>
    <input type = "button" value = "调用actions中定义的函数" @click="handleCallAction"/>
    <img alt="Vue logo" src="./assets/logo.png">
    <HelloWorld msg="Welcome to Your Vue.js App"/>
  </div>
</template>

<script>
import HelloWorld from './components/HelloWorld.vue'

export default {
  name: 'App',
  components: {
    HelloWorld
  },
  methods: {
    handleUpdate() {
      // mutations中定义的函数不能直接调用,必须通过这种方式来调用
      // setName为mutations中定义的函数名称,lisi为需要传递的参数
      this.$store.commit('setName', 'lisi')
    },
    handleCallAction() {
      // 调用actions中定义的函数,setNameByAxios为函数名
      this.$store.dispatch('setNameByAxios')
    }
  }
}
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>
// vue.config.js
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
  transpileDependencies: true,
  devServer: {
    port:8082,
    proxy: {
      '/api': {
        target: 'http://location:8081',
        pathRewrite: {
          '^/api': ''
        }
      }
    }
  }
})

思考

1. 如何理解vuex?

  • 实现多个组件之间的数据共享
  • 共享数据是响应式的,实时渲染到模板
  • 可以集中管理共享数据

2. 如何使用vuex?

  • 在store对象的state属性中定义共享数据
  • 在store对象的mutations属性中定义修改共享数据的函数
  • 在store对象的actions属性中定义调用mutation的函数,可以进行异或操作
  • mutations中的函数不能直接调用,只能通过store对象的commit方法调用
  • actions中定义的函数不能直接调用,只能通过store对象的dispatch方法调用

TypeScript

TypeScript介绍

  • TypeScript(简称:TS)是微软推出的开源语言
  • TypeScript是JavaScript的超集(JS有的TS都有)
  • TypeScript = Type + JavaScript(在JS基础上增加了类型支持)
  • TypeScript文件扩展名为ts
  • TypeScript可编译成标准的JavaScript,并且在编译时进行类型检查

安装typescript(全局安装)

如果安装失败,以管理员身份运行命令行窗口,可以在安装命令后加上 @5.0.2,以指定版本

npm install -g typescript

查看TS版本

tsc -v

示例

// 通过ts代码,指定函数的参数类型为string
function hello(msg:string) {
        console.log(msg)
}

// 传入的参数类型为number
hello(123)

编译:tsc + 文件名

改正后(传参为:'123')

思考

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

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

2. 如何理解TypeScript?

  • 是JavaScript的超集,兼容JavaScript
  • 扩展了JavaScript的语法,文件扩展名为ts
  • 可以编译成标准的JavaScript,并且可以在编译时进行类型检查
  • 全局安装npm install -g typescript
  • 视图tsc命令将ts文件编译成js文件
  • 使用node命令运行js文件

TypeScript常用类型

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

类型标注的位置

  • 标注变量
  • 标注参数
  • 标注返回值

项目示例

1. 创建项目时勾选上TypeScript、Router、Vuex

2. 字符串类型、布尔类型、数字类型

// 字符串类型
let username: string = 'itcast'

// 数字类型
let age: number = 20

// 布尔类型
let isTrue: boolean = true

console.log(username)
console.log(age)
console.log(isTrue)

3. 字面量类型

// 字面量类型
function printText(s: string, alignment: 'left'|'right'|'center') {
    console.log(s, alignment)
}

printText('hello', 'left')
printText('hello', 'right')

4. 复杂类型——interface

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

// 定义接口
interface Cat {
    name: string,
    age: number
}


// 定义变量为Cat类型
const c1: Cat = {name: '小白', age: 1}
// const c2: Cat = {name: '小白'}  // 错误:缺少age属性
// const c3: Cat = {name: '小白', age: 1, sex: '公'}  // 错误:多了sex属性

console.log(c1)

5. class类

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

// 定义一个类,使用class关键字
class User {
    name: string;  // 属性
    constructor(name: string) {
        // 构造方法
        this.name = name
    }
    // 方法
    study() {
        console.log(this.name + '正在学习')
    }
}

// 使用User类型
const user = new User('张三')
// 输出类中的属性
console.log(user.name)
// 调用类中的方法
user.study()

6. Class类实现interface

interface Animal {
    name: string
    eat(): void
}

// 定义一个类Bird,实现上面的Animal接口
class Bird implements Animal {
    name: string
    constructor(name: string) {
        this.name = name
    }
    eat(): void {
        console.log(this.name + ' eat')
    }
}

// 创建类型为Bird的对象
const b1 = new Bird('杜鹃')
console.log(b1.name)
b1.eat()

7. class类——类的继承


// 定义一个类Bird,实现上面的Animal接口
class Bird implements Animal {
    name: string
    constructor(name: string) {
        this.name = name
    }
    eat(): void {
        console.log(this.name + ' eat')
    }
}


// 定义Parrot类,并且继承Bird类
class Parrot extends Bird {
    say():void {
        console.log(this.name + ' say hello')
    }
}
const myParrot = new Parrot('Polly')
myParrot.say()
myParrot.eat()

小结

1.TypeScript的常用类型有哪些?

  • string、number、boolean
  • 字面量、void
  • interface、class

2. TypeScript文件能直接运行吗?

  • 需要将TS文件编译为JS文件才能运行
  • 编译后的JS文件中类型会擦除

三、苍穹外卖前端项目环境搭建、员工管理

技术选型

  • node.js
  • vue
  • ElementUI
  • axios
  • vuex
  • vue-router
  • typescript

熟悉前端代码结构

1. 代码导入:直接导入课程资料中提供的前端工程,在此基础上开发即可

在苍穹外卖前端课程->资料->day02->资料->苍穹外卖前端初始工程

2. 重点文件/目录

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

①先运行后端服务

②下载前端中的依赖(不需要指定安装哪些包,会自动扫描):npm install

把nodejs的版本降级到12版本,如果出现安全性问题,代开cmd执行下面的命令

可以参考这篇文章:node.js安装配置详细介绍以及nodejs版本降级_nodejs低版本-CSDN博客

我是把node.js降级到了12.22.12

npm config set strict-ssl false


npm install

④修改后端服务的地址(如果前面课程中修改了后端服务的端口号)

⑤npm run serve,前端的端口号为8888

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

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

员工分页查询

需求分析和接口设计

业务规则

根据页码展示员工信息

每页展示10条数据

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

接口设计

代码开发

①从路由文件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提供的文档,进行修改即可

链接:Element - The world's most popular Vue UI framework

④员工分页查询

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>

功能测试

启用、禁用员工账号

需求分析和接口设计

业务规则

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

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

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

接口设计

代码开发

①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

<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" @click="handleStartOrStop(scope.row)">{{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, enableOrDisableEmployee} from '@/api/employee'

export default {
  // 模型数据
  data() {
    return {
      name: '', // 员工姓名,对应上面的输入框
      page: 1, // 页码
      pageSize: 10, // 每页记录数
      total: 0, // 总记录数
      records: [], // 当前页要展示的数据集合
    }
  },
  // 自动调用pageQuery方法
  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()
    },
    // 启用禁用员工账号
    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()
            }
          })
        })
    }
  },
}
</script>

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

功能测试

添加员工

需求分析和接口设计

产品原型

接口设计

代码开发

添加员工操作步骤

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

①为“添加员工”按钮绑定单击事件: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>

功能测试

修改员工

需求分析和接口设计

产品原型

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

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

代码开发

修改员工操作步骤:

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

注意

  • 由于添加员工和修改员工的表单项非常类似,所以添加和修改操作可以共用同一个页面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

import { addEmployee, queryEmployeeById, updateEmployee} from '@/api/employee'
export default {
  data() {
    return {
      optType: '', // 当前新增的类型为新增或者修改
      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()
              }
            },
          },
        ],
      },
    }
  },
  // 页面加载完成执行的代码
  created() {
    // 获取路由参数{id},如果有则为修改操作,否则为新增操作
    this.optType = this.$route.query.id ? 'update' : 'add'
    if (this.optType === 'update') {
      // 修改操作,需要根据id查询员工信息用于页面回显
      queryEmployeeById(this.$route.query.id).then((res) => {
        if (res.data.code === 1) {
          this.ruleForm = res.data.data
        }
      })
    }
  },
  methods: {
    submitForm(formName, isContinue) {
      // 进行表单校验
      this.$refs[formName].validate((valid) => {
        if (valid) {
          // alert('所有表单项都符合要求')
          // 表单校验通过,发起Ajax请求,将数据提交到后端

          if (this.optType === 'add') {
            // 新增操作
            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)
              }
            })
          } else {
            // 修改操作
            updateEmployee(this.ruleForm).then(res => {
              if(res.data.code == 1) {
                this.$message.success('员工信息修改成功!')
                this.$router.push('/employee')
              }
            })
          }
        }
      })
    },
  },
}

功能测试

四、套餐管理

套餐分页查询

需求分析和接口设计

产品原型

业务规则

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

接口设计

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

代码开发

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

②制作页面头部效果,src/views/setmeal/index.vue

<template>
  <div class="dashboard-container">
    <div class="container">
      <div class="tableBar">
        <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>

    </div>
  </div>
</template>

<script lang="ts">
export default {
  // 模型数据
  data() {
    return {
      name: '', // 套餐名称,对应上面的输入框
      page: 1, // 页码
      pageSize: 10, // 每页记录数
      total: 0, // 总记录数
      records: [], // 当前页要展示的数据集合
      options: [{
          value: '选项1',
          label: '黄金糕'
        }, {
          value: '选项2',
          label: '双皮奶'
        }, {
          value: '选项3',
          label: '蚵仔煎'
        }, {
          value: '选项4',
          label: '龙须面'
        }, {
          value: '选项5',
          label: '北京烤鸭'
        }],
        value: '',
        saleStatusArr:[{
          value: '1',
          label: '起售'
        }, {
          value: '0',
          label: '停售'
        }],
        saleStatus: ''
    }
  },
}
</script>
<style lang="scss">
.el-table-column--selection .cell {
  padding-left: 10px;
}
</style>
<style lang="scss" scoped>
.dashboard {
  &-container {
    margin: 30px;

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

      .tableBar {
        margin-bottom: 20px;
        .tableLab {
          float: right;
          span {
            cursor: pointer;
            display: inline-block;
            font-size: 14px;
            padding: 0 20px;
            color: $gray-2;
          }
        }
      }

      .tableBox {
        width: 100%;
        border: 1px solid $gray-5;
        border-bottom: 0;
      }

      .pageList {
        text-align: center;
        margin-top: 30px;
      }
      //查询黑色按钮样式
      .normal-btn {
        background: #333333;
        color: white;
        margin-left: 20px;
      }
    }
  }
}
</style>

注意

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

③导入查询套餐分类的JS方法,动态填充套餐分类下拉框,src/views/setmeal/index.vue

完整代码(做了一些小调整)

<template>
  <div class="dashboard-container">
    <div class="container">
      <div class="tableBar">
        <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="categoryId" placeholder="请选择">
            <el-option
              v-for="item in options"
              :key="item.id"
              :label="item.name"
              :value="item.id">
            </el-option>
          </el-select>

          <label style="margin-left: 5px">售卖状态:</label>
          <el-select v-model="status" placeholder="请选择">
            <el-option
              v-for="item in statusArr"
              :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>

    </div>
  </div>
</template>

<script lang="ts">
import {getCategoryByType} from '@/api/category'
export default {
  // 模型数据
  data() {
    return {
      name: '', // 套餐名称,对应上面的输入框
      page: 1, // 页码
      pageSize: 10, // 每页记录数
      total: 0, // 总记录数
      records: [], // 当前页要展示的数据集合
      options: [],
      categoryId: '',  // 分类id
      statusArr:[{
        value: '1',
        label: '起售'
      }, {
        value: '0',
        label: '停售'
      }],
      status: ''  // 售卖状态
    }
  },
  created() {
    // 查询套餐分类,用于填充查询页面的下拉框
    getCategoryByType({type:2}).then(res => {
      if(res.data.code == 1) {
        this.options = res.data.data
      }
    })
  }
}
</script>
<style lang="scss">
.el-table-column--selection .cell {
  padding-left: 10px;
}
</style>
<style lang="scss" scoped>
.dashboard {
  &-container {
    margin: 30px;

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

      .tableBar {
        margin-bottom: 20px;
        .tableLab {
          float: right;
          span {
            cursor: pointer;
            display: inline-block;
            font-size: 14px;
            padding: 0 20px;
            color: $gray-2;
          }
        }
      }

      .tableBox {
        width: 100%;
        border: 1px solid $gray-5;
        border-bottom: 0;
      }

      .pageList {
        text-align: center;
        margin-top: 30px;
      }
      //查询黑色按钮样式
      .normal-btn {
        background: #333333;
        color: white;
        margin-left: 20px;
      }
    }
  }
}
</style>

src/api/category.ts

// 根据类型查询分类:1为菜品分类 2为套餐分类
export const getCategoryByType = (params: any) => {
  return request({
    url: `/category/list`,
    method: 'get',
    params: params
  })
}

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

src/api/setMeal.js

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

src/views/setmeal/index.vue

⑤分页查询,src/views/setmeal/index.vue

        <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" />

功能测试

启售停售套餐

需求分析和接口设计

产品原型

业务规则

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

接口设计

代码开发

①为启售、停售按钮绑定单击事件,src/views/setmeal/index.vue

import {getSetmealPage, enableOrDisableSetmeal, deleteSetmeal } from '@/api/setMeal'
    handleStartOrStop(row) {
      // alert(`id=${row.id} status=${row.status}`) 
       this.$confirm('确认调整该套餐的售卖状态?', '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning',
      }).then(() => {
        enableOrDisableSetmeal({ id: row.id, status: !row.status ? 1 : 0 })
          .then((res) => {
            if (res.status === 200) {
              this.$message.success('套餐售卖状态更改成功!')
              this.pageQuery()
            }
          })
          .catch((err) => {
            this.$message.error('请求出错了:' + err.message)
          })
      })
    }

src/api/setMeal.ts

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

注意:这里测试时要运行redis-server,否则会出现下面的错误

功能测试

删除套餐

需求分析和设计

产品原型

业务规则

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

接口设计

代码开发

①在src/api/setMeal.ts中封装删除套餐方法,发送Ajax请求

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

②在src/views/setmeal/index.vue书写删除按钮单击事件

    // 删除套餐
    handleDelete(type:string, id:string) {
        deleteSetmeal(id).then(res => {
          if(res.data.code === 1) {
            this.$message.success('删除成功!')
            this.pageQuery()
          } else {
            this.$message.error(res.data.msg)
          }
        })
    }

③批量删除

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

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

    // 删除套餐
    handleDelete(type:string, id:string) {

      this.$confirm('确认删除当前指定的套餐,是否继续?', '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning',
      }).then(() => {
        let param = ''
        if(type == 'B') {
          // 批量删除
          // alert(this.multipleSelection.length)
          const arr = new Array
          this.multipleSelection.forEach(element => {
            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)
          }
        })
      })
    },

功能测试

新增套餐

需求分析和接口设计

产品原型

接口设计

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

代码解读

新增套餐操作步骤

①点击”新建套餐“按钮,跳转到新增页面,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

// 修改数据接口
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 }
  })
}

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

功能测试

完结!!!

前端完整源码:https://pan.baidu.com/s/1JAI65SyP8qIIeLxh2U923g?pwd=ewap 

后端完整源码:https://pan.baidu.com/s/1hHnA-H_xOFiVEeIVi92A3Q?pwd=0k80 
 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/592352.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

DHCPv4_CLIENT_ALLOCATING_06: 发送DHCPDISCOVER消息 - 在没有收到DHCPOFFER消息时超时并重新发送

测试目的&#xff1a; 验证DOIP客户端在未收到DHCP服务器的DHCOFFER消息时&#xff0c;能够正确地超时并重传DHCPDISCOVER消息。 描述&#xff1a; 在DOIP网络环境中&#xff0c;当客户端&#xff08;DUT&#xff09;启动并尝试获取IP地址时&#xff0c;它首先发送DHCPDISCO…

IoTDB 入门教程 基础篇⑨——TsFile导入导出工具

文章目录 一、前文二、准备2.1 准备导出服务器2.2 准备导入服务器 三、导出3.1 导出命令3.2 执行命令3.3 tsfile文件 四、导入4.1 上传tsfile文件4.2 导入命令4.3 执行命令 五、查询六、参考 一、前文 IoTDB入门教程——导读 数据库备份与迁移是数据库运维中的核心任务&#xf…

获取淘宝商品销量数据接口

淘宝爬虫商品销量数据采集通常涉及以下几个步骤&#xff1a; 1、确定采集目标&#xff1a;需要明确要采集的商品类别、筛选条件&#xff08;如天猫、价格区间&#xff09;、销量和金额等数据。例如&#xff0c;如果您想了解“小鱼零食”的销量和金额&#xff0c;您需要设定好价…

设计模式之前端控制器模式

想象一下&#xff0c;你的Java Web应用是个交响乐团&#xff0c;每个功能模块是乐手&#xff0c;而用户请求就像是一首首待演绎的曲目。在这场音乐盛宴中&#xff0c;谁来保证演出的流畅与协调&#xff1f;答案就是——前端控制器模式&#xff01;它如同乐队的指挥&#xff0c;…

用LangChain打造一个可以管理日程的智能助手

存储设计定义工具创建llm提示词模板创建Agent执行总结 众所周知&#xff0c;GPT可以认为是一个离线的软件的&#xff0c;对于一些实时性有要求的功能是完全不行&#xff0c;比如实时信息检索&#xff0c;再比如我们今天要实现个一个日程管理的功能&#xff0c;这个功能你纯依赖…

短视频素材去哪里找免费?短视频素材从哪儿下载?

在这个数字内容为王的时代&#xff0c;视频已经成为沟通信息和吸引观众的强大工具。无论是在市场营销、教育还是娱乐领域&#xff0c;高质量的视频素材都是制作引人注目内容的关键。以下列出的网站提供多样的视频素材&#xff0c;帮助您增强视觉叙述&#xff0c;并在竞争激烈的…

2022 HITCON -- fourchain-kernel

前言 很久没碰内核利用相关的东西了&#xff0c;这个题目都调了我两天&#xff08;&#xff1a;所以还是得熟能生巧啊 题目分析 内核版本&#xff1a;v5.10&#xff0c;所以不存在 cg 隔离、可以使用 userfaultfdkaslr、smap、smep 开启CONFIG_SLAB_FREELIST_RANDOM 和 CONF…

Java项目:基于SSM框架实现的学院党员管理系统高校党员管理系统(ssm+B/S架构+源码+数据库+毕业论文+开题)

一、项目简介 本项目是一套基于SSM框架实现的学院党员管理系统 包含&#xff1a;项目源码、数据库脚本等&#xff0c;该项目附带全部源码可作为毕设使用。 项目都经过严格调试&#xff0c;eclipse或者idea 确保可以运行&#xff01; 该系统功能完善、界面美观、操作简单、功能齐…

2024年3月Scratch图形化编程等级考试(二级)真题试卷

2024年3月Scratch图形化编程等级考试&#xff08;二级&#xff09;真题试卷 选择题 第 1 题 默认小猫角色&#xff0c;Scratch运行程序后&#xff0c;舞台上出现的图形是&#xff1f;&#xff08; &#xff09; A. B. C. D. 第 2 题 下列哪个Scratch选项可以使虫子移到…

Dynamics 365: 从0到1了解如何创建Custom API(1) - 在Power Apps中创建

今天介绍一下如果创建Custom API&#xff0c;我们首先需要知道它和action有什么区别&#xff0c;什么时候使用Custom API或者Action? Custom API和Action的区别 Create your own messages (Microsoft Dataverse) - Power Apps | Microsoft Learn 什么时候使用Custom API或者…

3.11设计模式——Visitor 访问者模式(行为型)

意图 表示一个作用于某对象结构中的各元素的操作。它允许在不改变各元素的类的前提下定义作用于这些元素的新操作。 结构 Visitor&#xff08;访问者&#xff09;为该对象结构中ConcreteElement&#xff08;具体元素&#xff09;的每一个类声明一个Visit操作&#xff0c;该操…

将java项目上传到GitHub步骤

文章目录 GitHub 作用github如何修改默认分支为master手把手教你把项目上传github上github怎么删除仓库或项目执行到push时报错的解决办法github怎么修改仓库语言 GitHub 作用 GitHub 是一个存放软件代码的网站&#xff0c;主要用于软件开发者存储和管理其项目源代码&#xff…

C++入门系列-类对象模型this指针

&#x1f308;个人主页&#xff1a;羽晨同学 &#x1f4ab;个人格言:“成为自己未来的主人~” 类对象模型 如何计算类对象的大小 class A { public:void printA(){cout << _a << endl;} private:char _a; }; 算算看&#xff0c;这个类的大小是多少 我们知道…

Unity 性能优化之Profiler窗口(二)怎么看懂这个分析器

提示&#xff1a;仅供参考&#xff0c;有误之处&#xff0c;麻烦大佬指出&#xff0c;不胜感激&#xff01; 文章目录 前言一、Profiler打开方式二、Profile简介添加没有的模块1.点击Profiler Modules&#xff08;分析器模块&#xff09;2.勾选GPU即可 自定义模块1.点击Profile…

JS 笔记9 认识JavaScript

相关内容&#xff1a;JS对象、属性、常用事件处理过程、运算符、if...else、for、…… <script type"text/javascript"></script> type属性用来指定MIME(Multipurpose Internet Mail Extension)类型&#xff0c;主要是告诉浏览器目前使用的是哪一种Scri…

SpringBoot实现Config下自动关联.xml、.properties配置信息的实例教程

本篇文章主要讲解在SpringBoot实现Config下自动关联.xml、.properties配置信息的实例教程。 日期&#xff1a;2024年5月4日 作者&#xff1a;任聪聪 .properties文件调用方法 步骤一、打开我们的 .properties 创建一个demo参数如下图&#xff1a; 步骤二、创建一个config的包&…

正点原子[第二期]Linux之ARM(MX6U)裸机篇学习笔记-8.2-链接脚本

前言&#xff1a; 本文是根据哔哩哔哩网站上“正点原子[第二期]Linux之ARM&#xff08;MX6U&#xff09;裸机篇”视频的学习笔记&#xff0c;在这里会记录下正点原子 I.MX6ULL 开发板的配套视频教程所作的实验和学习笔记内容。本文大量引用了正点原子教学视频和链接中的内容。…

用python画一个正八边形

1 问题 使用turtle库的turtle.fd()函数和turtle.seth()函数绘制一个边长100的正八边形。 2 方法 1、利用for循环解决如何画出图形中相同的八条边的问题。 2、再利用turtle.fd()函数和turtle.seth()函数画出完整的图形。 代码清单 1 import turtleturtle.pensize(2)d0for i in r…

“科技让广告更精彩”四川迈瑞斯文化传媒有限公司 行业领先的一站式媒体采购供应平台

国际数字影像产业园与园区企业一同推动数字影像技术的创新与发展&#xff0c;为数字影像产业注入新的活力。其中&#xff0c;四川迈瑞斯文化传媒有限公司&#xff08;906&#xff09;作为数字媒体行业的优秀企业&#xff0c;坚持“科技让广告更精彩”的理念&#xff0c;致力于为…

K8S哲学 - statefulSet 灰度发布

kubectl get - 获取资源及配置文件 kubectl get resource 【resourceName -oyaml】 kubectl create - 指定镜像创建或者 指定文件创建 kubectl create resource 【resourceName】 --imagemyImage 【-f my.yaml】 kubectl delete kubectl describe resource resourc…