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

一、VUE基础知识

基于脚手架创建前端工程

1. 环境要求

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

安装Vue CLIVue.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-if、v-else、v-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)

javascript 复制代码
{
  // `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

html 复制代码
<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

javascript 复制代码
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

    html 复制代码
    npm i element-ui -S
  • 提供子视图组件,用于效果展示 ---P1View.vue、P2View.vue、P3View.vue
    view/container/ContainerView.vue

    javascript 复制代码
    <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,会有什么效果呢?
  1. 如果实现在访问/c时,默认就展示某个子视图组件呢?

状态管理vuex

vuex介绍

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

安装

javascript 复制代码
npm install vuex@next --save

核心概念

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

使用方式

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

②src/store/index.js

javascript 复制代码
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

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

③src/main.js

javascript 复制代码
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')

④定义和展示共享数据

javascript 复制代码
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

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

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

javascript 复制代码
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: {
  }
})
html 复制代码
<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

javascript 复制代码
npm install axios
javascript 复制代码
// 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: {
  }
})
javascript 复制代码
// 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>
javascript 复制代码
// 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?
  • 实现多个组件之间的数据共享
  • 共享数据是响应式的,实时渲染到模板
  • 可以集中管理共享数据
  1. 如何使用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,以指定版本

javascript 复制代码
npm install -g typescript

查看TS版本

javascript 复制代码
tsc -v

示例

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

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

编译:tsc + 文件名

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

思考

  1. TS为什么要增加类型支持?
  • TS属于静态类型编程语言,JS属于动态类型编程语言
  • 静态类型在编译期做类型检查,动态类型在执行期间做类型检查
  • 对于JS来说,需要等到代码执行的时候才可以发现错误(晚)
  • 对于TS来说,在代码编译的时候就可以发现错误(早)
  • 配合VSCode开发工具,TS可以提前在编写代码的同时就发现代码中的错误,减少找Bug、改Bug的时间
  1. 如何理解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
  1. 字符串类型、布尔类型、数字类型
TypeScript 复制代码
// 字符串类型
let username: string = 'itcast'

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

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

console.log(username)
console.log(age)
console.log(isTrue)
  1. 字面量类型
TypeScript 复制代码
// 字面量类型
function printText(s: string, alignment: 'left'|'right'|'center') {
    console.log(s, alignment)
}

printText('hello', 'left')
printText('hello', 'right')
  1. 复杂类型------interface

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

TypeScript 复制代码
// 定义接口
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)
  1. class类

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

TypeScript 复制代码
// 定义一个类,使用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()
  1. Class类实现interface
TypeScript 复制代码
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()
  1. class类------类的继承
TypeScript 复制代码
// 定义一个类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
  1. TypeScript文件能直接运行吗?
  • 需要将TS文件编译为JS文件才能运行
  • 编译后的JS文件中类型会擦除

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

技术选型

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

熟悉前端代码结构

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

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

  1. 重点文件/目录

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

①先运行后端服务

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

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

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

我是把node.js降级到了12.22.12

TypeScript 复制代码
npm config set strict-ssl false
TypeScript 复制代码
npm install

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

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

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

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

员工分页查询

需求分析和接口设计

业务规则

根据页码展示员工信息

每页展示10条数据

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

接口设计

代码开发

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

②初始页面

③制作页面头部效果

javascript 复制代码
    <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

TypeScript 复制代码
// 分页查询
export const getEmployeeList = (params: any) =>
  request({
    'url': `/employee/page`,
    'method': 'get',
    'params': params
  })

src/view/employee/index.vue

javascript 复制代码
<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

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

②src/view/employee/index.vue

javascript 复制代码
<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

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

④src/views/employee/addEmployee.vue

javascript 复制代码
<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,在员工管理页面中,为"修改"按钮绑定单击事件,用于跳转到修改页面

javascript 复制代码
    // 跳转到修改员工页面(组件)
    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

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

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

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

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

⑥src/views/employee/addEmployee.vue

javascript 复制代码
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

javascript 复制代码
<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

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

javascript 复制代码
<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

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

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

src/api/setMeal.js

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

src/views/setmeal/index.vue

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

javascript 复制代码
        <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

javascript 复制代码
import {getSetmealPage, enableOrDisableSetmeal, deleteSetmeal } from '@/api/setMeal'
javascript 复制代码
    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

TypeScript 复制代码
//套餐启售停售
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请求

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

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

javascript 复制代码
    // 删除套餐
    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中添加模型数据

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

javascript 复制代码
    // 删除套餐
    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

javascript 复制代码
<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

javascript 复制代码
<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

TypeScript 复制代码
// 修改数据接口
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

TypeScript 复制代码
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

相关推荐
程序员爱技术几秒前
Vue 2 + JavaScript + vue-count-to 集成案例
前端·javascript·vue.js
并不会1 小时前
常见 CSS 选择器用法
前端·css·学习·html·前端开发·css选择器
衣乌安、1 小时前
【CSS】居中样式
前端·css·css3
兔老大的胡萝卜1 小时前
ppk谈JavaScript,悟透JavaScript,精通CSS高级Web,JavaScript DOM编程艺术,高性能JavaScript pdf
前端·javascript
低代码布道师1 小时前
CSS的三个重点
前端·css
耶啵奶膘3 小时前
uniapp-是否删除
linux·前端·uni-app
王哈哈^_^4 小时前
【数据集】【YOLO】【目标检测】交通事故识别数据集 8939 张,YOLO道路事故目标检测实战训练教程!
前端·人工智能·深度学习·yolo·目标检测·计算机视觉·pyqt
cs_dn_Jie5 小时前
钉钉 H5 微应用 手机端调试
前端·javascript·vue.js·vue·钉钉
开心工作室_kaic5 小时前
ssm068海鲜自助餐厅系统+vue(论文+源码)_kaic
前端·javascript·vue.js
有梦想的刺儿5 小时前
webWorker基本用法
前端·javascript·vue.js