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

一、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

相关推荐
0wioiw04 分钟前
Flutter基础(前端教程④-组件拼接)
前端·flutter
花生侠29 分钟前
记录:前端项目使用pnpm+husky(v9)+commitlint,提交代码格式化校验
前端
一涯36 分钟前
Cursor操作面板改为垂直
前端
我要让全世界知道我很低调43 分钟前
记一次 Vite 下的白屏优化
前端·css
1undefined21 小时前
element中的Table改造成虚拟列表,并封装成hooks
前端·javascript·vue.js
蓝倾1 小时前
淘宝批量获取商品SKU实战案例
前端·后端·api
comelong1 小时前
Docker容器启动postgres端口映射失败问题
前端
花海如潮淹1 小时前
硬件产品研发管理工具实战指南
前端·python
用户3802258598241 小时前
vue3源码解析:依赖收集
前端·vue.js