一、VUE基础知识
基于脚手架创建前端工程
1. 环境要求
安装node.js:Node.js安装与配置(详细步骤)_nodejs安装及环境配置-CSDN博客
查看node和npm的版本号
安装Vue CLI:Vue.js安装与创建默认项目(详细步骤)_nodejs安装及环境配置-CSDN博客
查看vue版本
使用Vue CLI创建前端工程
- 方式一:vue create项目名称
① 创建一个不带中文的文件夹,如下图:
② 创建工程---选择Vue 2
③ 选择npm
④ 如果中间有报错,如下:
npm ERR! code EPERM
npm ERR! syscall mkdir
npm ERR! path C:\Program Files\nodejs\node_cache\_cacache\index-v5\ee\aa
npm ERR! errno -4048
npm ERR! Error: EPERM: operation not permitted, mkdir 'C:\Program Files\nodejs\node_cache\_cacache\index-v5\ee\aa'
找到nodejs的安装目录,右击属性->安全->编辑->把所有权限都勾选上
⑤ 结果:
- 方式二:vue ui
①打开ui界面
② 点击创建
③ 填写项目信息
④ 选择vue2,创建项目
⑤结果:
项目结构
运行项目
npm run serve
命令的最后一个单词并不是固定的,与package.json下写的这一项相关,如下
如果8080端口号被占用,可以在vue.config.js中更改端口号
如果上面这种方式不起作用的,可以到项目对应文件夹用cmd试试
退出运行:Ctrl + C
vue基本使用方式
Vue组件(Vue2)
Vue的组件文件以.vue结尾,每个组件由三部分组成:结构、样式、逻辑。
示例
Vue 2:一个Vue组件的模板只能有一个根元素。这是因为Vue 2使用的是基于AST(抽象语法树)的模板编译方式,需要将模板编译为render函数,而render函数只能返回一个根节点。
Vue 3 : Vue的模板编译器进行了重大改进,支持多个根元素。Vue 3使用了基于编译器的模板编译方式,这意味着在Vue 3中,一个组件的模板可以有多个根元素,而不再需要包裹在一个单独的根元素内。
文本插值
作用:用来绑定 data 方法返回的对象属性
用法:{{}}
属性绑定
作用:为标签的属性绑定data方法中返回的属性
用法:v-bind:xxx,简写为 :xxx
事件绑定
作用:为元素绑定对应的事件
用法:v-on:xxx,简写为@xxx
双向绑定
作用:表单输入项和data方法中的属性进行绑定,任意一方改变都会同步给另一方
用法:v-model
条件渲染
作用:根据表达式的值来动态渲染页面元素
用法:v-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)
{
// `url` 是用于请求的服务器 URL
url: '/user',
// `method` 是创建请求时使用的方法
method: 'get', // 默认值
// `baseURL` 将自动加在 `url` 前面,除非 `url` 是一个绝对 URL。
// 它可以通过设置一个 `baseURL` 便于为 axios 实例的方法传递相对 URL
baseURL: 'https://some-domain.com/api/',
// `transformRequest` 允许在向服务器发送前,修改请求数据
// 它只能用于 'PUT', 'POST' 和 'PATCH' 这几个请求方法
// 数组中最后一个函数必须返回一个字符串, 一个Buffer实例,ArrayBuffer,FormData,或 Stream
// 你可以修改请求头。
transformRequest: [function (data, headers) {
// 对发送的 data 进行任意转换处理
return data;
}],
// `transformResponse` 在传递给 then/catch 前,允许修改响应数据
transformResponse: [function (data) {
// 对接收的 data 进行任意转换处理
return data;
}],
// 自定义请求头
headers: {'X-Requested-With': 'XMLHttpRequest'},
// `params` 是与请求一起发送的 URL 参数
// 必须是一个简单对象或 URLSearchParams 对象
params: {
ID: 12345
},
// `paramsSerializer`是可选方法,主要用于序列化`params`
// (e.g. https://www.npmjs.com/package/qs, http://api.jquery.com/jquery.param/)
paramsSerializer: function (params) {
return Qs.stringify(params, {arrayFormat: 'brackets'})
},
// `data` 是作为请求体被发送的数据
// 仅适用 'PUT', 'POST', 'DELETE 和 'PATCH' 请求方法
// 在没有设置 `transformRequest` 时,则必须是以下类型之一:
// - string, plain object, ArrayBuffer, ArrayBufferView, URLSearchParams
// - 浏览器专属: FormData, File, Blob
// - Node 专属: Stream, Buffer
data: {
firstName: 'Fred'
},
// 发送请求体数据的可选语法
// 请求方式 post
// 只有 value 会被发送,key 则不会
data: 'Country=Brasil&City=Belo Horizonte',
// `timeout` 指定请求超时的毫秒数。
// 如果请求时间超过 `timeout` 的值,则请求会被中断
timeout: 1000, // 默认值是 `0` (永不超时)
// `withCredentials` 表示跨域请求时是否需要使用凭证
withCredentials: false, // default
// `adapter` 允许自定义处理请求,这使测试更加容易。
// 返回一个 promise 并提供一个有效的响应 (参见 lib/adapters/README.md)。
adapter: function (config) {
/* ... */
},
// `auth` HTTP Basic Auth
auth: {
username: 'janedoe',
password: 's00pers3cret'
},
// `responseType` 表示浏览器将要响应的数据类型
// 选项包括: 'arraybuffer', 'document', 'json', 'text', 'stream'
// 浏览器专属:'blob'
responseType: 'json', // 默认值
// `responseEncoding` 表示用于解码响应的编码 (Node.js 专属)
// 注意:忽略 `responseType` 的值为 'stream',或者是客户端请求
// Note: Ignored for `responseType` of 'stream' or client-side requests
responseEncoding: 'utf8', // 默认值
// `xsrfCookieName` 是 xsrf token 的值,被用作 cookie 的名称
xsrfCookieName: 'XSRF-TOKEN', // 默认值
// `xsrfHeaderName` 是带有 xsrf token 值的http 请求头名称
xsrfHeaderName: 'X-XSRF-TOKEN', // 默认值
// `onUploadProgress` 允许为上传处理进度事件
// 浏览器专属
onUploadProgress: function (progressEvent) {
// 处理原生进度事件
},
// `onDownloadProgress` 允许为下载处理进度事件
// 浏览器专属
onDownloadProgress: function (progressEvent) {
// 处理原生进度事件
},
// `maxContentLength` 定义了node.js中允许的HTTP响应内容的最大字节数
maxContentLength: 2000,
// `maxBodyLength`(仅Node)定义允许的http请求内容的最大字节数
maxBodyLength: 2000,
// `validateStatus` 定义了对于给定的 HTTP状态码是 resolve 还是 reject promise。
// 如果 `validateStatus` 返回 `true` (或者设置为 `null` 或 `undefined`),
// 则promise 将会 resolved,否则是 rejected。
validateStatus: function (status) {
return status >= 200 && status < 300; // 默认值
},
// `maxRedirects` 定义了在node.js中要遵循的最大重定向数。
// 如果设置为0,则不会进行重定向
maxRedirects: 5, // 默认值
// `socketPath` 定义了在node.js中使用的UNIX套接字。
// e.g. '/var/run/docker.sock' 发送请求到 docker 守护进程。
// 只能指定 `socketPath` 或 `proxy` 。
// 若都指定,这使用 `socketPath` 。
socketPath: null, // default
// `httpAgent` and `httpsAgent` define a custom agent to be used when performing http
// and https requests, respectively, in node.js. This allows options to be added like
// `keepAlive` that are not enabled by default.
httpAgent: new http.Agent({ keepAlive: true }),
httpsAgent: new https.Agent({ keepAlive: true }),
// `proxy` 定义了代理服务器的主机名,端口和协议。
// 您可以使用常规的`http_proxy` 和 `https_proxy` 环境变量。
// 使用 `false` 可以禁用代理功能,同时环境变量也会被忽略。
// `auth`表示应使用HTTP Basic auth连接到代理,并且提供凭据。
// 这将设置一个 `Proxy-Authorization` 请求头,它会覆盖 `headers` 中已存在的自定义 `Proxy-Authorization` 请求头。
// 如果代理服务器使用 HTTPS,则必须设置 protocol 为`https`
proxy: {
protocol: 'https',
host: '127.0.0.1',
port: 9000,
auth: {
username: 'mikeymike',
password: 'rapunz3l'
}
},
// see https://axios-http.com/zh/docs/cancellation
cancelToken: new CancelToken(function (cancel) {
}),
// `decompress` indicates whether or not the response body should be decompressed
// automatically. If set to `true` will also remove the 'content-encoding' header
// from the responses objects of all decompressed responses
// - Node only (XHR cannot turn off decompression)
decompress: true // 默认值
}
示例——配置代理
记得要先运行后端服务,启动redis
HelloWorld.vue
<template>
<div class="hello">
<div><input type="button" value="发送POST请求" @click="handleSendPOST"/></div>
<div><input type="button" value="发送GET请求" @click="handleSendGET"/></div>
<div><input type="button" value="统一请求方式" @click="handleSend"/></div>
</div>
</template>
<script>
import axiox from 'axios'
export default {
name: 'HelloWorld',
props: {
msg: String
},
methods: {
handleSendPOST() {
// 通过axios发送异域POST方式的http请求
axiox.post('/api/admin/employee/login', {
username: 'admin',
password: '123456'
}).then(res => {
console.log(res.data)
}).catch(error => {
console.log(error.response)
})
},
handleSendGET() {
// 通过axios发送GET方式请求
axiox.get('/api/admin/shop/status', {
headers: {
token: 'eyJhbGciOiJIUzI1NiJ9.eyJlbXBJZCI6MSwiZXhwIjoxNzE0MzIyNDAyfQ.gMfQXajaBTKnMuz19_BsmhWLGWov24rqZDLcPLwZCSA'
}
}).then(res => {
console.log(res.data)
})
},
handleSend() {
// 使用axios提供的统一调用方式发送请求
axiox({
url: '/api/admin/employee/login',
method: 'post',
data: { // data表示通过请求体传参
username: 'admin',
password: '123456'
}
}).then(res => {
console.log(res.data.data.token)
axiox({
url: '/api/admin/shop/status',
method: 'get',
headers: {
token: res.data.data.token
}
})
})
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h3 {
margin: 40px 0 0;
}
ul {
list-style-type: none;
padding: 0;
}
li {
display: inline-block;
margin: 0 10px;
}
a {
color: #42b983;
}
</style>
vue.config.js
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true,
devServer:{
port:8082,
proxy: {
'/api' : {
target: 'http://localhost:8081',
pathRewrite: {
'^/api' : ''
}
}
}
}
})
结果
二、VUE进阶(router、vuex、typescript)
路由 Vue-Router
Vue-Router介绍
vue属于单页面应用,所谓的路由,就是根据浏览器路径不同,用不同的视图组件替换这个页面内容。
vue应用中如何实现路由?
- 通过vue-router实现路由功能,需要安装js库(npm install vue-router)
基于Vue CLI创建带有路由功能的前端项目
命令:vue ui
①包管理器选择:npm
②预设选择:手动
③功能添加:Router
④配置版本选择:2.x,linter config选择:ESLint with error prevention only
⑤选择创建项目,不保存预设
⑥查看创建结果
⑦运行项目
路由配置
路由组成
VueRouter:路由器,根据路由请求在路由视图中动态渲染对应的视图组件
<router-link>:路由链接组件,浏览器会解析成<a>
<router-view>:路由视图组件,用来展示与路由匹配的视图组件
路由跳转
- 标签式<router-link>
- 编程式
如果请求的路径不存在,应该如何处理?
①当上面的路径都匹配不到时,重定向到最后一项
嵌套路由
嵌套路由:组件内要切换内容,就需要用到嵌套路由(子路由)
实现步骤:
- 安装并导入elementui,实现页面布局(Container布局容器)---ContainerView.vue
npm i element-ui -S
- 提供子视图组件,用于效果展示 ---P1View.vue、P2View.vue、P3View.vue
view/container/ContainerView.vue<template> <el-container> <el-header>Header</el-header> <el-container> <el-aside width="200px">Aside</el-aside> <el-main>Main</el-main> </el-container> </el-container> </template> <script> export default {}; </script> <style> .el-header, .el-footer { background-color: #B3C0D1; color: #333; text-align: center; line-height: 60px; } .el-aside { background-color: #D3DCE6; color: #333; text-align: center; line-height: 200px; } .el-main { background-color: #E9EEF3; color: #333; text-align: center; line-height: 160px; } body > .el-container { margin-bottom: 40px; } .el-container:nth-child(5) .el-aside, .el-container:nth-child(6) .el-aside { line-height: 260px; } .el-container:nth-child(7) .el-aside { line-height: 320px; } </style>
- 在src/router/index.js中配置路由映射规则(嵌套路由配置)
- 在布局容器视图中添加<router-view>,实现子视图组件展示
- 在布局容器视图中添加<router-link>,实现路由请求
注意事项:子路由变化,切换的是【ContainerView组件】中‘<router-view></router-view>’部分的内容。
思考
1. 对于前面的案例,如果用户访问的路由是/c,会有什么效果呢?
2. 如果实现在访问/c时,默认就展示某个子视图组件呢?
状态管理vuex
vuex介绍
- vuex是一个专为Vue.js应用程序开发的状态管理库
- vuex可以在多个组件之间共享数据,并且共享的数据是响应式的,即数据的变更能及时渲染到模板
- vuex采用集中式存储管理所有组件的状态
安装
npm install vuex@next --save
核心概念
- state:状态对象,集中定义各个组件共享的数据
- mutations:类似于一个事件,用于修改共享数据,要求必须是同步函数
- actions:类似于mutation,可以包含异步操作,通过调用mutation来改变共享数据
使用方式
①创建带有vuex功能的脚手架工程
②src/store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
// 集中管理多个组件共享的数据
export default new Vuex.Store({
state: {
},
getters: {
},
mutations: {
},
actions: {
},
modules: {
}
})
③src/main.js
import Vue from 'vue'
import App from './App.vue'
import store from './store'
Vue.config.productionTip = false
new Vue({
// 使用vuex功能
store,
render: h => h(App)
}).$mount('#app')
④定义和展示共享数据
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
// 集中管理多个组件共享的数据
export default new Vuex.Store({
// 集中定义共享数据
state: {
name: '未登录游客'
},
getters: {
},
mutations: {
},
actions: {
},
modules: {
}
})
⑤在mutations中定义函数,修改共享数据
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
// 集中管理多个组件共享的数据
export default new Vuex.Store({
// 集中定义共享数据
state: {
name: '未登录游客'
},
getters: {
},
// 修改共享数据只能通过mutation实现,必须是同步操作
mutations: {
setName(state, newName) {
state.name = newName
}
},
// 通过actions可以调用mutations,在action中可以进行异步操作
actions: {
},
modules: {
}
})
<template>
<div id="app">
欢迎您,{{$store.state.name}}
<input type = "button" value = "通过mutations修改共享数据" @click="handleUpdate"/>
<img alt="Vue logo" src="./assets/logo.png">
<HelloWorld msg="Welcome to Your Vue.js App"/>
</div>
</template>
<script>
import HelloWorld from './components/HelloWorld.vue'
export default {
name: 'App',
components: {
HelloWorld
},
methods: {
handleUpdate() {
// mutations中定义的函数不能直接调用,必须通过这种方式来调用
// setName为mutations中定义的函数名称,lisi为需要传递的参数
this.$store.commit('setName', 'lisi')
}
}
}
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
④在actions中定义函数,用于调用mutation
先安装axios
npm install axios
// src/store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
import axios from 'axios'
Vue.use(Vuex)
// 集中管理多个组件共享的数据
export default new Vuex.Store({
// 集中定义共享数据
state: {
name: '未登录游客'
},
getters: {
},
// 修改共享数据只能通过mutation实现,必须是同步操作
mutations: {
setName(state, newName) {
state.name = newName
}
},
// 通过actions可以调用mutations,在action中可以进行异步操作
actions: {
setNameByAxios(context) {
axios ({
url: '/api/admin/employee/login',
method: 'post',
data: {
username: 'admin',
password: '123456'
}
}).then(res => {
if(res.data.code == 1) {
// 异步请求后,需要修改共享数据
// 调用mutation中定义的setName函数
context.commit('setName', res.data.data.name)
}
})
}
},
modules: {
}
})
// App.vue
<template>
<div id="app">
欢迎您,{{$store.state.name}}
<input type = "button" value = "通过mutations修改共享数据" @click="handleUpdate"/>
<input type = "button" value = "调用actions中定义的函数" @click="handleCallAction"/>
<img alt="Vue logo" src="./assets/logo.png">
<HelloWorld msg="Welcome to Your Vue.js App"/>
</div>
</template>
<script>
import HelloWorld from './components/HelloWorld.vue'
export default {
name: 'App',
components: {
HelloWorld
},
methods: {
handleUpdate() {
// mutations中定义的函数不能直接调用,必须通过这种方式来调用
// setName为mutations中定义的函数名称,lisi为需要传递的参数
this.$store.commit('setName', 'lisi')
},
handleCallAction() {
// 调用actions中定义的函数,setNameByAxios为函数名
this.$store.dispatch('setNameByAxios')
}
}
}
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
// vue.config.js
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true,
devServer: {
port:8082,
proxy: {
'/api': {
target: 'http://location:8081',
pathRewrite: {
'^/api': ''
}
}
}
}
})
思考
1. 如何理解vuex?
- 实现多个组件之间的数据共享
- 共享数据是响应式的,实时渲染到模板
- 可以集中管理共享数据
2. 如何使用vuex?
- 在store对象的state属性中定义共享数据
- 在store对象的mutations属性中定义修改共享数据的函数
- 在store对象的actions属性中定义调用mutation的函数,可以进行异或操作
- mutations中的函数不能直接调用,只能通过store对象的commit方法调用
- actions中定义的函数不能直接调用,只能通过store对象的dispatch方法调用
TypeScript
TypeScript介绍
- TypeScript(简称:TS)是微软推出的开源语言
- TypeScript是JavaScript的超集(JS有的TS都有)
- TypeScript = Type + JavaScript(在JS基础上增加了类型支持)
- TypeScript文件扩展名为ts
- TypeScript可编译成标准的JavaScript,并且在编译时进行类型检查
安装typescript(全局安装)
如果安装失败,以管理员身份运行命令行窗口,可以在安装命令后加上 @5.0.2,以指定版本
npm install -g typescript
查看TS版本
tsc -v
示例
// 通过ts代码,指定函数的参数类型为string
function hello(msg:string) {
console.log(msg)
}
// 传入的参数类型为number
hello(123)
编译:tsc + 文件名
改正后(传参为:'123')
思考
1. TS为什么要增加类型支持?
- TS属于静态类型编程语言,JS属于动态类型编程语言
- 静态类型在编译期做类型检查,动态类型在执行期间做类型检查
- 对于JS来说,需要等到代码执行的时候才可以发现错误(晚)
- 对于TS来说,在代码编译的时候就可以发现错误(早)
- 配合VSCode开发工具,TS可以提前在编写代码的同时就发现代码中的错误,减少找Bug、改Bug的时间
2. 如何理解TypeScript?
- 是JavaScript的超集,兼容JavaScript
- 扩展了JavaScript的语法,文件扩展名为ts
- 可以编译成标准的JavaScript,并且可以在编译时进行类型检查
- 全局安装npm install -g typescript
- 视图tsc命令将ts文件编译成js文件
- 使用node命令运行js文件
TypeScript常用类型
类型 | 例 | 备注 |
字符串类型 | string | |
数字类型 | number | |
布尔类型 | boolean | |
数组类型 | number[], string[], boolean[]依此类推 | |
任意类型 | any | 相当于又回到了没有类型的时代 |
复杂类型 | type与interface | |
函数类型 | () => void | 对函数的参数和返回值进行说明 |
字面量类型 | "a"|"b"|"c" | 限制变量或参数的取值 |
class类 | class Animal |
类型标注的位置
- 标注变量
- 标注参数
- 标注返回值
项目示例
1. 创建项目时勾选上TypeScript、Router、Vuex
2. 字符串类型、布尔类型、数字类型
// 字符串类型
let username: string = 'itcast'
// 数字类型
let age: number = 20
// 布尔类型
let isTrue: boolean = true
console.log(username)
console.log(age)
console.log(isTrue)
3. 字面量类型
// 字面量类型
function printText(s: string, alignment: 'left'|'right'|'center') {
console.log(s, alignment)
}
printText('hello', 'left')
printText('hello', 'right')
4. 复杂类型——interface
小技巧:可以通过在属性名后面加上?,表示当前属性为可选
// 定义接口
interface Cat {
name: string,
age: number
}
// 定义变量为Cat类型
const c1: Cat = {name: '小白', age: 1}
// const c2: Cat = {name: '小白'} // 错误:缺少age属性
// const c3: Cat = {name: '小白', age: 1, sex: '公'} // 错误:多了sex属性
console.log(c1)
5. class类
注意:使用class关键字来定义类,类中可以包含属性、构造方法、普通方法
// 定义一个类,使用class关键字
class User {
name: string; // 属性
constructor(name: string) {
// 构造方法
this.name = name
}
// 方法
study() {
console.log(this.name + '正在学习')
}
}
// 使用User类型
const user = new User('张三')
// 输出类中的属性
console.log(user.name)
// 调用类中的方法
user.study()
6. Class类实现interface
interface Animal {
name: string
eat(): void
}
// 定义一个类Bird,实现上面的Animal接口
class Bird implements Animal {
name: string
constructor(name: string) {
this.name = name
}
eat(): void {
console.log(this.name + ' eat')
}
}
// 创建类型为Bird的对象
const b1 = new Bird('杜鹃')
console.log(b1.name)
b1.eat()
7. class类——类的继承
// 定义一个类Bird,实现上面的Animal接口
class Bird implements Animal {
name: string
constructor(name: string) {
this.name = name
}
eat(): void {
console.log(this.name + ' eat')
}
}
// 定义Parrot类,并且继承Bird类
class Parrot extends Bird {
say():void {
console.log(this.name + ' say hello')
}
}
const myParrot = new Parrot('Polly')
myParrot.say()
myParrot.eat()
小结
1.TypeScript的常用类型有哪些?
- string、number、boolean
- 字面量、void
- interface、class
2. TypeScript文件能直接运行吗?
- 需要将TS文件编译为JS文件才能运行
- 编译后的JS文件中类型会擦除
三、苍穹外卖前端项目环境搭建、员工管理
技术选型
- node.js
- vue
- ElementUI
- axios
- vuex
- vue-router
- typescript
熟悉前端代码结构
1. 代码导入:直接导入课程资料中提供的前端工程,在此基础上开发即可
在苍穹外卖前端课程->资料->day02->资料->苍穹外卖前端初始工程
2. 重点文件/目录
3. 通过登录功能梳理前端代码
①先运行后端服务
②下载前端中的依赖(不需要指定安装哪些包,会自动扫描):npm install
③把nodejs的版本降级到12版本,如果出现安全性问题,代开cmd执行下面的命令
可以参考这篇文章:node.js安装配置详细介绍以及nodejs版本降级_nodejs低版本-CSDN博客
我是把node.js降级到了12.22.12
npm config set strict-ssl false
npm install
④修改后端服务的地址(如果前面课程中修改了后端服务的端口号)
⑤npm run serve,前端的端口号为8888
⑥通过登录功能梳理前端代码
- 获得登录页面路由地址
- 从main.ts中找到路由文件
- 从路由文件中找到登录视图组件
- 从登录视图组件中找到登录方法
- 跟踪登录方法的执行过程
员工分页查询
需求分析和接口设计
业务规则
根据页码展示员工信息
每页展示10条数据
分页查询可以根据需要,输入员工姓名进行查询
接口设计
代码开发
①从路由文件router.ts中找到员工管理页面(组件)
②初始页面
③制作页面头部效果
<div class="container">
<div class="tableBar">
<label style="margin-right: 5px">员工姓名:</label>
<el-input
placeholder="请输入员工姓名"
style="width: 15%"
clearable
/>
<el-button type="primary" style="margin-left: 20px">查询</el-button>
<el-button type="primary" style="float: right"> + 添加员工</el-button>
</div>
</div>
注意
- 输入框和按钮都是使用ElementUI提供的组件
- 对于前端的组件只需要参考ElementUI提供的文档,进行修改即可
链接:Element - The world's most popular Vue UI framework
④员工分页查询
src/api/employee.ts
// 分页查询
export const getEmployeeList = (params: any) =>
request({
'url': `/employee/page`,
'method': 'get',
'params': params
})
src/view/employee/index.vue
<template>
<div class="dashboard-container">
<div class="container">
<div class="tableBar">
<label style="margin-right: 5px">员工姓名:</label>
<el-input
v-model="name"
placeholder="请输入员工姓名"
style="width: 15%"
clearable
/>
<el-button type="primary" style="margin-left: 20px" @click="pageQuery()">查询</el-button>
<el-button type="primary" style="float: right"> + 添加员工</el-button>
</div>
<el-table :data="records" stripe style="width: 100%">
<el-table-column prop="name" label="员工姓名" width="180">
</el-table-column>
<el-table-column prop="username" label="账号" width="180">
</el-table-column>
<el-table-column prop="phone" label="手机号"> </el-table-column>
<el-table-column prop="status" label="账号状态">
<template slot-scope="scope">
{{ scope.row.status === 0 ? '禁用' : '启用' }}
</template>
</el-table-column>
<el-table-column prop="updateTime" label="最后操作时间">
</el-table-column>
<el-table-column label="操作">
<template slot-scope="scope">
<el-button type="text">修改</el-button>
<el-button type="text">{{
scope.row.status === 1 ? '禁用' : '启用'
}}</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
class="pageList"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="page"
:page-sizes="[10, 20, 30, 40, 50]"
:page-size="pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="total">
</el-pagination>
</div>
</div>
</template>
<script lang="ts">
import { getEmployeeList } from '@/api/employee'
export default {
// 模型数据
data() {
return {
name: '', // 员工姓名,对应上面的输入框
page: 1, // 页码
pageSize: 10, // 每页记录数
total: 0, // 总记录数
records: [], // 当前页要展示的数据集合
}
},
// 自动调用pageQuery方法
// 这段代码是 Vue.js 组件中的生命周期钩子函数 created()。在 Vue.js 组件中,created() 是一个生命周期钩子函数,在组件实例被创建之后立即调用。这个钩子函数通常用于在组件实例创建后执行一些初始化任务。
created() {
this.pageQuery()
},
methods: {
// 分页查询
pageQuery() {
// 准备请求参数
const params = {
name: this.name,
page: this.page,
pageSize: this.pageSize,
}
// 发送Ajax请求,访问后端服务,获取分页数据
getEmployeeList(params)
.then((res) => {
if (res.data.code === 1) {
this.total = res.data.data.total
this.records = res.data.data.records
}
})
.catch((err) => {
this.$message.console.error('请求出错了:' + err.message)
})
},
// pageSize发送变化时触发
handleSizeChange(pageSize) {
this.pageSize = pageSize
this.pageQuery()
},
// page发生变化时触发
handleCurrentChange(page) {
this.page = page
this.pageQuery()
},
},
}
</script>
<style lang="scss" scoped>
.disabled-text {
color: #bac0cd !important;
}
</style>
功能测试
启用、禁用员工账号
需求分析和接口设计
业务规则
可以对状态为“启用”的员工账号进行“禁用”操作
可以对状态为“禁用”的员工账号进行“启用”操作
状态为“禁用”的员工账号不能登录系统
接口设计
代码开发
①src/api/employee.ts
// 启用禁用员工账号
export const enableOrDisableEmployee = (params: any) =>
request({
'url': `/employee/status/${params.status}`,
'method': 'post',
'params': {id: params.id}
})
②src/view/employee/index.vue
<template>
<div class="dashboard-container">
<div class="container">
<div class="tableBar">
<label style="margin-right: 5px">员工姓名:</label>
<el-input
v-model="name"
placeholder="请输入员工姓名"
style="width: 15%"
clearable
/>
<el-button type="primary" style="margin-left: 20px" @click="pageQuery()">查询</el-button>
<el-button type="primary" style="float: right"> + 添加员工</el-button>
</div>
<el-table :data="records" stripe style="width: 100%">
<el-table-column prop="name" label="员工姓名" width="180">
</el-table-column>
<el-table-column prop="username" label="账号" width="180">
</el-table-column>
<el-table-column prop="phone" label="手机号"> </el-table-column>
<el-table-column prop="status" label="账号状态">
<template slot-scope="scope">
{{ scope.row.status === 0 ? '禁用' : '启用' }}
</template>
</el-table-column>
<el-table-column prop="updateTime" label="最后操作时间">
</el-table-column>
<el-table-column label="操作">
<template slot-scope="scope">
<el-button type="text">修改</el-button>
<el-button type="text" @click="handleStartOrStop(scope.row)">{{scope.row.status === 1 ? '禁用' : '启用'}}</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
class="pageList"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="page"
:page-sizes="[10, 20, 30, 40, 50]"
:page-size="pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="total">
</el-pagination>
</div>
</div>
</template>
<script lang="ts">
import { getEmployeeList, enableOrDisableEmployee} from '@/api/employee'
export default {
// 模型数据
data() {
return {
name: '', // 员工姓名,对应上面的输入框
page: 1, // 页码
pageSize: 10, // 每页记录数
total: 0, // 总记录数
records: [], // 当前页要展示的数据集合
}
},
// 自动调用pageQuery方法
created() {
this.pageQuery()
},
methods: {
// 分页查询
pageQuery() {
// 准备请求参数
const params = {
name: this.name,
page: this.page,
pageSize: this.pageSize,
}
// 发送Ajax请求,访问后端服务,获取分页数据
getEmployeeList(params)
.then((res) => {
if (res.data.code === 1) {
this.total = res.data.data.total
this.records = res.data.data.records
}
})
.catch((err) => {
this.$message.console.error('请求出错了:' + err.message)
})
},
// pageSize发送变化时触发
handleSizeChange(pageSize) {
this.pageSize = pageSize
this.pageQuery()
},
// page发生变化时触发
handleCurrentChange(page) {
this.page = page
this.pageQuery()
},
// 启用禁用员工账号
handleStartOrStop(row) {
if(row.username === 'admin') {
this.$message.error('admin为系统的管理员账号,不能更改帐号状态!')
return
}
// alert(`id=${row.id} status=${row.status}`)
// 弹出确认提示框
this.$confirm('确认要修改当前员工账号的状态吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
const p = {
id: row.id,
status: !row.status ? 1 : 0
}
enableOrDisableEmployee(p).then(res => {
if(res.data.code === 1) {
this.$message.success('员工的账号状态修改成功!')
this.pageQuery()
}
})
})
}
},
}
</script>
<style lang="scss" scoped>
.disabled-text {
color: #bac0cd !important;
}
</style>
功能测试
添加员工
需求分析和接口设计
产品原型
接口设计
代码开发
添加员工操作步骤
- 点击“添加员工”按钮,跳转到新增页面
- 在新增员工页面录入员工相关信息
- 点击“保存”按钮完成新增操作
①为“添加员工”按钮绑定单击事件:src/views/employee/index.vue
②提供handleAddEmp方法,进行路由跳转
③src/api/employee.ts
// 新增员工
export const addEmployee = (params: any) =>
request({
'url': '/employee',
'method': 'post',
'data': params
})
④src/views/employee/addEmployee.vue
<template>
<div class="addBrand-container">
<div class="container">
<el-form :model="ruleForm" :rules="rules" ref="ruleForm" label-width="180px">
<el-form-item label="账号" prop="username">
<el-input v-model="ruleForm.username"></el-input>
</el-form-item>
<el-form-item label="员工姓名" prop="name">
<el-input v-model="ruleForm.name"></el-input>
</el-form-item>
<el-form-item label="手机号" prop="phone">
<el-input v-model="ruleForm.phone"></el-input>
</el-form-item>
<el-form-item label="性别" prop="sex">
<el-radio v-model="ruleForm.sex" label="1">男</el-radio>
<el-radio v-model="ruleForm.sex" label="2">女</el-radio>
</el-form-item>
<el-form-item label="身份证号" prop="idNumber">
<el-input v-model="ruleForm.idNumber"></el-input>
</el-form-item>
<div class="subBox">
<el-button type="primary" @click="submitForm('ruleForm',false)">保存</el-button>
<el-button
v-if="this.optType === 'add'"
type="primary"
@click="submitForm('ruleForm',true)">保存并继续添加员工
</el-button>
<el-button @click="() => this.$router.push('/employee')">返回</el-button>
</div>
</el-form>
</div>
</div>
</template>
<script lang="ts">
import {addEmployee} from '@/api/employee'
export default {
data() {
return {
optType: 'add',
ruleForm: {
name: '',
username: '',
sex: '1',
phone: '',
idNumber: ''
},
rules: {
name: [
{ required: true, message: '请输入员工姓名', trigger: 'blur' }
],
username: [
{ required: true, message: '请输入员工账号', trigger: 'blur' }
],
phone: [
{ required: true, trigger: 'blur', validator: (rule, value, callback) => {
if(value === '' || (!/^1(3|4|5|6|7|8)\d{9}$/.test(value))) {
callback(new Error('请输入正确的手机号!'))
} else {
callback()
}
}}
],
idNumber: [
{ required: true, trigger: 'blur', validator: (rule, value, callback) => {
if(value === '' || (!/(^\d{15}$)|(^\d{18}$)|(^\d{17}(X|x)$)/.test(value))) {
callback(new Error('请输入正确的身份证号!'))
} else {
callback()
}
}}
]
}
}
},
methods: {
submitForm(formName, isContinue) {
// 进行表单校验
this.$refs[formName].validate((valid) => {
if(valid) {
// alert('所有表单项都符合要求')
// 表单校验通过,发起Ajax请求,将数据提交到后端
addEmployee(this.ruleForm).then((res) => {
if(res.data.code === 1) {
this.$message.success('员工添加成功!')
if(isContinue) { // 保存并继续添加
this.ruleForm = {
name: '',
username: '',
sex: '1',
phone: '',
idNumber: ''
}
} else {
this.$router.push('/employee')
}
} else {
this.$message.error(res.data.msg)
}
})
}
})
}
}
}
</script>
<style lang="scss" scoped>
.addBrand {
&-container {
margin: 30px;
margin-top: 30px;
.HeadLable {
background-color: transparent;
margin-bottom: 0px;
padding-left: 0px;
}
.container {
position: relative;
z-index: 1;
background: #fff;
padding: 30px;
border-radius: 4px;
// min-height: 500px;
.subBox {
padding-top: 30px;
text-align: center;
border-top: solid 1px $gray-5;
}
}
.idNumber {
margin-bottom: 39px;
}
.el-form-item {
margin-bottom: 29px;
}
.el-input {
width: 293px;
}
}
}
</style>
功能测试
修改员工
需求分析和接口设计
产品原型
编辑员工功能涉及到两个接口:
- 根据id查询员工信息
- 编辑员工信息
代码开发
修改员工操作步骤:
- 点击“修改”按钮,跳转到修改页面
- 在修改员工页面录入员工相关信息
- 点击“保存”按钮完成修改操作
注意
- 由于添加员工和修改员工的表单项非常类似,所以添加和修改操作可以共用同一个页面addEmployee.vue
- 修改员工设计原数据回显,所以需要传递员工id作为参数
①src/views/employee/index.vue,在员工管理页面中,为“修改”按钮绑定单击事件,用于跳转到修改页面
// 跳转到修改员工页面(组件)
handleUpdateEmp(row) {
if(row.username === 'admin') {
// 如果是内置管理员账号,不允许修改
this.$message.error('admin为系统的管理员账号,不能修改!')
return
}
// 跳转到修改页面,通过地址栏传递参数
this.$router.push({
path: '/employee/add',
query: {id: row.id}
})
}
②由于addEmployee.vue为新增和修改共用页面,需要能够区分当前操作:
- 如果路由中传递了id参数,则当前操作为修改
- 如果路由中没有传递id参数,则当前操作为新增
③根据id查询员工,src/api/employee.ts
// 根据id查询员工
export const queryEmployeeById = (id: number) =>
request({
'url': `/employee/${id}`,
'method': 'get'
})
④数据回显,src/views/employee/addEmployee.vue
⑤修改员工信息,src/api/employee.ts
// 修改员工
export const updateEmployee = (params: any) =>
request({
'url': '/employee',
'method': 'put',
'data': params
})
⑥src/views/employee/addEmployee.vue
import { addEmployee, queryEmployeeById, updateEmployee} from '@/api/employee'
export default {
data() {
return {
optType: '', // 当前新增的类型为新增或者修改
ruleForm: {
name: '',
username: '',
sex: '1',
phone: '',
idNumber: '',
},
rules: {
name: [{ required: true, message: '请输入员工姓名', trigger: 'blur' }],
username: [
{ required: true, message: '请输入员工账号', trigger: 'blur' },
],
phone: [
{
required: true,
trigger: 'blur',
validator: (rule, value, callback) => {
if (value === '' || !/^1(3|4|5|6|7|8)\d{9}$/.test(value)) {
callback(new Error('请输入正确的手机号!'))
} else {
callback()
}
},
},
],
idNumber: [
{
required: true,
trigger: 'blur',
validator: (rule, value, callback) => {
if (
value === '' ||
!/(^\d{15}$)|(^\d{18}$)|(^\d{17}(X|x)$)/.test(value)
) {
callback(new Error('请输入正确的身份证号!'))
} else {
callback()
}
},
},
],
},
}
},
// 页面加载完成执行的代码
created() {
// 获取路由参数{id},如果有则为修改操作,否则为新增操作
this.optType = this.$route.query.id ? 'update' : 'add'
if (this.optType === 'update') {
// 修改操作,需要根据id查询员工信息用于页面回显
queryEmployeeById(this.$route.query.id).then((res) => {
if (res.data.code === 1) {
this.ruleForm = res.data.data
}
})
}
},
methods: {
submitForm(formName, isContinue) {
// 进行表单校验
this.$refs[formName].validate((valid) => {
if (valid) {
// alert('所有表单项都符合要求')
// 表单校验通过,发起Ajax请求,将数据提交到后端
if (this.optType === 'add') {
// 新增操作
addEmployee(this.ruleForm).then((res) => {
if (res.data.code === 1) {
this.$message.success('员工添加成功!')
if (isContinue) {
// 保存并继续添加
this.ruleForm = {
name: '',
username: '',
sex: '1',
phone: '',
idNumber: '',
}
} else {
this.$router.push('/employee')
}
} else {
this.$message.error(res.data.msg)
}
})
} else {
// 修改操作
updateEmployee(this.ruleForm).then(res => {
if(res.data.code == 1) {
this.$message.success('员工信息修改成功!')
this.$router.push('/employee')
}
})
}
}
})
},
},
}
功能测试
四、套餐管理
套餐分页查询
需求分析和接口设计
产品原型
业务规则
- 根据页码展示套餐信息
- 每页展示10条数据
- 分页查询时可以根据需要输入套餐名称、套餐分类、售卖状态进行查询
接口设计
- 套餐分页查询接口
- 分类查询接口(用于下拉框中分类数据显示)
代码开发
①从路由文件router.ts中找到套餐管理页面(组件)
②制作页面头部效果,src/views/setmeal/index.vue
<template>
<div class="dashboard-container">
<div class="container">
<div class="tableBar">
<div class="tableBar">
<label style="margin-right: 5px">套餐名称:</label>
<el-input v-model="name" placeholder="请输入套餐名称" style="width: 15%" clearable/>
<label style="margin-left: 5px">套餐分类:</label>
<el-select v-model="value" placeholder="请选择">
<el-option
v-for="item in options"
:key="item.value"
:label="item.label"
:value="item.value">
</el-option>
</el-select>
<label style="margin-left: 5px">售卖状态:</label>
<el-select v-model="saleStatus" placeholder="请选择">
<el-option
v-for="item in saleStatusArr"
:key="item.value"
:label="item.label"
:value="item.value">
</el-option>
</el-select>
<el-button type="primary" style="margin-left: 20px" @click="pageQuery()">查询</el-button>
<div style="float:right">
<el-button type="danger">批量删除</el-button>
<el-button type="info">+新建套餐</el-button>
</div>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
export default {
// 模型数据
data() {
return {
name: '', // 套餐名称,对应上面的输入框
page: 1, // 页码
pageSize: 10, // 每页记录数
total: 0, // 总记录数
records: [], // 当前页要展示的数据集合
options: [{
value: '选项1',
label: '黄金糕'
}, {
value: '选项2',
label: '双皮奶'
}, {
value: '选项3',
label: '蚵仔煎'
}, {
value: '选项4',
label: '龙须面'
}, {
value: '选项5',
label: '北京烤鸭'
}],
value: '',
saleStatusArr:[{
value: '1',
label: '起售'
}, {
value: '0',
label: '停售'
}],
saleStatus: ''
}
},
}
</script>
<style lang="scss">
.el-table-column--selection .cell {
padding-left: 10px;
}
</style>
<style lang="scss" scoped>
.dashboard {
&-container {
margin: 30px;
.container {
background: #fff;
position: relative;
z-index: 1;
padding: 30px 28px;
border-radius: 4px;
.tableBar {
margin-bottom: 20px;
.tableLab {
float: right;
span {
cursor: pointer;
display: inline-block;
font-size: 14px;
padding: 0 20px;
color: $gray-2;
}
}
}
.tableBox {
width: 100%;
border: 1px solid $gray-5;
border-bottom: 0;
}
.pageList {
text-align: center;
margin-top: 30px;
}
//查询黑色按钮样式
.normal-btn {
background: #333333;
color: white;
margin-left: 20px;
}
}
}
}
</style>
注意
- 输入框、按钮、下拉框都是使用ElementUI提供的组件
- 对于前端的组件只需要参考ElementUI提供的文档,进行修改即可
③导入查询套餐分类的JS方法,动态填充套餐分类下拉框,src/views/setmeal/index.vue
完整代码(做了一些小调整)
<template>
<div class="dashboard-container">
<div class="container">
<div class="tableBar">
<div class="tableBar">
<label style="margin-right: 5px">套餐名称:</label>
<el-input v-model="name" placeholder="请输入套餐名称" style="width: 15%" clearable/>
<label style="margin-left: 5px">套餐分类:</label>
<el-select v-model="categoryId" placeholder="请选择">
<el-option
v-for="item in options"
:key="item.id"
:label="item.name"
:value="item.id">
</el-option>
</el-select>
<label style="margin-left: 5px">售卖状态:</label>
<el-select v-model="status" placeholder="请选择">
<el-option
v-for="item in statusArr"
:key="item.value"
:label="item.label"
:value="item.value">
</el-option>
</el-select>
<el-button type="primary" style="margin-left: 20px" @click="pageQuery()">查询</el-button>
<div style="float:right">
<el-button type="danger">批量删除</el-button>
<el-button type="info">+新建套餐</el-button>
</div>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import {getCategoryByType} from '@/api/category'
export default {
// 模型数据
data() {
return {
name: '', // 套餐名称,对应上面的输入框
page: 1, // 页码
pageSize: 10, // 每页记录数
total: 0, // 总记录数
records: [], // 当前页要展示的数据集合
options: [],
categoryId: '', // 分类id
statusArr:[{
value: '1',
label: '起售'
}, {
value: '0',
label: '停售'
}],
status: '' // 售卖状态
}
},
created() {
// 查询套餐分类,用于填充查询页面的下拉框
getCategoryByType({type:2}).then(res => {
if(res.data.code == 1) {
this.options = res.data.data
}
})
}
}
</script>
<style lang="scss">
.el-table-column--selection .cell {
padding-left: 10px;
}
</style>
<style lang="scss" scoped>
.dashboard {
&-container {
margin: 30px;
.container {
background: #fff;
position: relative;
z-index: 1;
padding: 30px 28px;
border-radius: 4px;
.tableBar {
margin-bottom: 20px;
.tableLab {
float: right;
span {
cursor: pointer;
display: inline-block;
font-size: 14px;
padding: 0 20px;
color: $gray-2;
}
}
}
.tableBox {
width: 100%;
border: 1px solid $gray-5;
border-bottom: 0;
}
.pageList {
text-align: center;
margin-top: 30px;
}
//查询黑色按钮样式
.normal-btn {
background: #333333;
color: white;
margin-left: 20px;
}
}
}
}
</style>
src/api/category.ts
// 根据类型查询分类:1为菜品分类 2为套餐分类
export const getCategoryByType = (params: any) => {
return request({
url: `/category/list`,
method: 'get',
params: params
})
}
④为查询按钮绑定事件,发送Ajax请求获取分页数据
src/api/setMeal.js
//套餐分页查询
export const getSetmealPage = (params: any) => {
return request({
url: '/setmeal/page',
method: 'GET',
params: params
})
}
src/views/setmeal/index.vue
⑤分页查询,src/views/setmeal/index.vue
<el-table :data="records" stripe class="tableBox" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="25" />
<el-table-column prop="name" label="套餐名称" />
<el-table-column label="图片">
<template slot-scope="scope">
<el-image style="width: 80px; height: 40px; border: none" :src="scope.row.image"></el-image>
</template>
</el-table-column>
<el-table-column prop="categoryName" label="套餐分类" />
<el-table-column prop="price" label="套餐价"/>
<el-table-column label="售卖状态">
<template slot-scope="scope">
<div class="tableColumn-status" :class="{ 'stop-use': scope.row.status === 0 }">
{{ scope.row.status === 0 ? '停售' : '启售' }}
</div>
</template>
</el-table-column>
<el-table-column prop="updateTime" label="最后操作时间" />
<el-table-column label="操作" align="center" width="250px">
<template slot-scope="scope">
<el-button type="text" size="small"> 修改 </el-button>
<el-button type="text" size="small" @click="handleStartOrStop(scope.row)">
{{ scope.row.status == '1' ? '停售' : '启售' }}
</el-button>
<el-button type="text" size="small" @click="handleDelete('S',scope.row.id)"> 删除 </el-button>
</template>
</el-table-column>
</el-table>
<el-pagination class="pageList"
:page-sizes="[10, 20, 30, 40]"
:page-size="pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange" />
功能测试
启售停售套餐
需求分析和接口设计
产品原型
业务规则
- 可以对状态为“启售”的套餐进行“停售:操作
- 可以对状态为”停售“的套餐进行”启售“操作
接口设计
代码开发
①为启售、停售按钮绑定单击事件,src/views/setmeal/index.vue
import {getSetmealPage, enableOrDisableSetmeal, deleteSetmeal } from '@/api/setMeal'
handleStartOrStop(row) {
// alert(`id=${row.id} status=${row.status}`)
this.$confirm('确认调整该套餐的售卖状态?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(() => {
enableOrDisableSetmeal({ id: row.id, status: !row.status ? 1 : 0 })
.then((res) => {
if (res.status === 200) {
this.$message.success('套餐售卖状态更改成功!')
this.pageQuery()
}
})
.catch((err) => {
this.$message.error('请求出错了:' + err.message)
})
})
}
src/api/setMeal.ts
//套餐启售停售
export const enableOrDisableSetmeal = (params: any) => {
return request({
url: `/setmeal/status/${params.status}`,
method: 'POST',
params: {id: params.id}
})
}
注意:这里测试时要运行redis-server,否则会出现下面的错误
功能测试
删除套餐
需求分析和设计
产品原型
业务规则
- 点击删除按钮,删除指定的一个套餐
- 勾选需要删除的套餐,点击批量删除按钮,删除选中的一个或多个套餐
接口设计
代码开发
①在src/api/setMeal.ts中封装删除套餐方法,发送Ajax请求
//删除套餐
export const deleteSetmeal = (ids: string) => {//1,2,3
return request({
url: '/setmeal',
method: 'DELETE',
params: {ids: ids}
})
}
②在src/views/setmeal/index.vue书写删除按钮单击事件
// 删除套餐
handleDelete(type:string, id:string) {
deleteSetmeal(id).then(res => {
if(res.data.code === 1) {
this.$message.success('删除成功!')
this.pageQuery()
} else {
this.$message.error(res.data.msg)
}
})
}
③批量删除
在src/views/setmeal/index.vue中添加模型数据
为批量删除按钮绑定单击事件
// 删除套餐
handleDelete(type:string, id:string) {
this.$confirm('确认删除当前指定的套餐,是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(() => {
let param = ''
if(type == 'B') {
// 批量删除
// alert(this.multipleSelection.length)
const arr = new Array
this.multipleSelection.forEach(element => {
arr.push(element.id)
})
param = arr.join(',')
} else {
// 单一删除
param = id
}
deleteSetmeal(param).then(res => {
if(res.data.code === 1) {
this.$message.success('删除成功!')
this.pageQuery()
} else {
this.$message.error(res.data.msg)
}
})
})
},
功能测试
新增套餐
需求分析和接口设计
产品原型
接口设计
- 根据类型查询分类接口
- 根据分类查询菜品接口
- 文件上传接口
- 新增套餐接口
代码解读
新增套餐操作步骤
①点击”新建套餐“按钮,跳转到新增页面,src/views/setmeal/index.vue
src/router.ts
②在套餐页面录入套餐相关信息,src/views/setmeal/addSetmeal.vue
<template>
<div class="addBrand-container">
<div class="container">
<el-form ref="ruleForm"
:model="ruleForm"
:rules="rules"
:inline="true"
label-width="180px"
class="demo-ruleForm">
<div>
<el-form-item label="套餐名称:"
prop="name">
<el-input v-model="ruleForm.name"
placeholder="请填写套餐名称"
maxlength="14" />
</el-form-item>
<el-form-item label="套餐分类:"
prop="idType">
<el-select v-model="ruleForm.idType"
placeholder="请选择套餐分类"
@change="$forceUpdate()">
<el-option v-for="(item, index) in setMealList"
:key="index"
:label="item.name"
:value="item.id" />
</el-select>
</el-form-item>
</div>
<div>
<el-form-item label="套餐价格:"
prop="price">
<el-input v-model="ruleForm.price"
placeholder="请设置套餐价格" />
</el-form-item>
</div>
<div>
<el-form-item label="套餐菜品:"
required>
<el-form-item>
<div class="addDish">
<span v-if="dishTable.length == 0"
class="addBut"
@click="openAddDish('new')">
+ 添加菜品</span>
<div v-if="dishTable.length != 0"
class="content">
<div class="addBut"
style="margin-bottom: 20px"
@click="openAddDish('change')">
+ 添加菜品
</div>
<div class="table">
<el-table :data="dishTable"
style="width: 100%">
<el-table-column prop="name"
label="名称"
width="180"
align="center" />
<el-table-column prop="price"
label="原价"
width="180"
align="center">
<template slot-scope="scope">
{{ (Number(scope.row.price).toFixed(2) * 100) / 100 }}
</template>
</el-table-column>
<el-table-column prop="address"
label="份数"
align="center">
<template slot-scope="scope">
<el-input-number v-model="scope.row.copies"
size="small"
:min="1"
:max="99"
label="描述文字" />
</template>
</el-table-column>
<el-table-column prop="address"
label="操作"
width="180px;"
align="center">
<template slot-scope="scope">
<el-button type="text"
size="small"
class="delBut non"
@click="delDishHandle(scope.$index)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
</div>
</div>
</div>
</el-form-item>
</el-form-item>
</div>
<div>
<el-form-item label="套餐图片:"
required
prop="image">
<image-upload :prop-image-url="imageUrl"
@imageChange="imageChange">
图片大小不超过2M<br>仅能上传 PNG JPEG JPG类型图片<br>建议上传200*200或300*300尺寸的图片
</image-upload>
</el-form-item>
</div>
<div class="address">
<el-form-item label="套餐描述:">
<el-input v-model="ruleForm.description"
type="textarea"
:rows="3"
maxlength="200"
placeholder="套餐描述,最长200字" />
</el-form-item>
</div>
<div class="subBox address">
<el-form-item>
<el-button @click="() => $router.back()">
取消
</el-button>
<el-button type="primary"
:class="{ continue: actionType === 'add' }"
@click="submitForm('ruleForm', false)">
保存
</el-button>
<el-button v-if="actionType == 'add'"
type="primary"
@click="submitForm('ruleForm', true)">
保存并继续添加
</el-button>
</el-form-item>
</div>
</el-form>
</div>
<el-dialog v-if="dialogVisible"
title="添加菜品"
class="addDishList"
:visible.sync="dialogVisible"
width="60%"
:before-close="handleClose">
<AddDish v-if="dialogVisible"
ref="adddish"
:check-list="checkList"
:seach-key="seachKey"
:dish-list="dishList"
@checkList="getCheckList" />
<span slot="footer"
class="dialog-footer">
<el-button @click="handleClose">取 消</el-button>
<el-button type="primary"
@click="addTableList">添 加</el-button>
</span>
</el-dialog>
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator'
import HeadLable from '@/components/HeadLable/index.vue'
import ImageUpload from '@/components/ImgUpload/index.vue'
import AddDish from './components/AddDish.vue'
import { querySetmealById, addSetmeal, editSetmeal } from '@/api/setMeal'
import { getCategoryList } from '@/api/dish'
import { baseUrl } from '@/config.json'
@Component({
name: 'addShop',
components: {
HeadLable,
AddDish,
ImageUpload
}
})
export default class extends Vue {
private value: string = ''
private setMealList: [] = []
private seachKey: string = ''
private dishList: [] = []
private imageUrl: string = ''
private actionType: string = ''
private dishTable: [] = []
private dialogVisible: boolean = false
private checkList: any[] = []
private ruleForm = {
name: '',
categoryId: '',
price: '',
code: '',
image: '',
description: '',
dishList: [],
status: true,
idType: ''
}
get rules() {
return {
name: {
required: true,
validator: (rule: any, value: string, callback: Function) => {
if (!value) {
callback(new Error('请输入套餐名称'))
} else {
const reg = /^([A-Za-z0-9\u4e00-\u9fa5]){2,20}$/
if (!reg.test(value)) {
callback(new Error('套餐名称输入不符,请输入2-20个字符'))
} else {
callback()
}
}
},
trigger: 'blur'
},
idType: {
required: true,
message: '请选择套餐分类',
trigger: 'change'
},
image: {
required: true,
message: '菜品图片不能为空'
},
price: {
required: true,
// 'message': '请输入套餐价格',
validator: (rules: any, value: string, callback: Function) => {
const reg = /^([1-9]\d{0,5}|0)(\.\d{1,2})?$/
if (!reg.test(value) || Number(value) <= 0) {
callback(
new Error(
'套餐价格格式有误,请输入大于零且最多保留两位小数的金额'
)
)
} else {
callback()
}
},
trigger: 'blur'
},
code: { required: true, message: '请输入商品码', trigger: 'blur' }
}
}
created() {
this.getDishTypeList()
this.actionType = this.$route.query.id ? 'edit' : 'add'
if (this.actionType == 'edit') {
this.init()
}
}
private async init() {
querySetmealById(this.$route.query.id).then(res => {
if (res && res.data && res.data.code === 1) {
this.ruleForm = res.data.data
this.ruleForm.status = res.data.data.status == '1'
;(this.ruleForm as any).price = res.data.data.price
// this.imageUrl = `http://172.17.2.120:8080/common/download?name=${res.data.data.image}`
this.imageUrl = res.data.data.image
this.checkList = res.data.data.setmealDishes
this.dishTable = res.data.data.setmealDishes.reverse()
this.ruleForm.idType = res.data.data.categoryId
} else {
this.$message.error(res.data.msg)
}
})
}
private seachHandle() {
this.seachKey = this.value
}
// 获取套餐分类
private getDishTypeList() {
getCategoryList({ type: 2, page: 1, pageSize: 1000 }).then(res => {
if (res && res.data && res.data.code === 1) {
this.setMealList = res.data.data.map((obj: any) => ({
...obj,
idType: obj.id
}))
} else {
this.$message.error(res.data.msg)
}
})
}
// 通过套餐ID获取菜品列表分类
// private getDishList (id:number) {
// getDishListType({id}).then(res => {
// if (res.data.code == 200) {
// const { data } = res.data
// this.dishList = data
// } else {
// this.$message.error(res.data.desc)
// }
// })
// }
// 删除套餐菜品
delDishHandle(index: any) {
this.dishTable.splice(index, 1)
this.checkList = this.dishTable
// this.checkList.splice(index, 1)
}
// 获取添加菜品数据 - 确定加菜倒序展示
private getCheckList(value: any) {
this.checkList = [...value].reverse()
}
// 添加菜品
openAddDish(st: string) {
this.seachKey = ''
this.dialogVisible = true
}
// 取消添加菜品
handleClose(done: any) {
// this.$refs.adddish.close()
this.dialogVisible = false
this.checkList = JSON.parse(JSON.stringify(this.dishTable))
// this.dialogVisible = false
}
// 保存添加菜品列表
public addTableList() {
this.dishTable = JSON.parse(JSON.stringify(this.checkList))
this.dishTable.forEach((n: any) => {
n.copies = 1
})
this.dialogVisible = false
}
public submitForm(formName: any, st: any) {
;(this.$refs[formName] as any).validate((valid: any) => {
if (valid) {
if (this.dishTable.length === 0) {
return this.$message.error('套餐下菜品不能为空')
}
if (!this.ruleForm.image) return this.$message.error('套餐图片不能为空')
let prams = { ...this.ruleForm } as any
prams.setmealDishes = this.dishTable.map((obj: any) => ({
copies: obj.copies,
dishId: obj.dishId,
name: obj.name,
price: obj.price
}))
;(prams as any).status =
this.actionType === 'add' ? 0 : this.ruleForm.status ? 1 : 0
prams.categoryId = this.ruleForm.idType
// delete prams.dishList
if (this.actionType == 'add') {
delete prams.id
addSetmeal(prams)
.then(res => {
if (res && res.data && res.data.code === 1) {
this.$message.success('套餐添加成功!')
if (!st) {
this.$router.push({ path: '/setmeal' })
} else {
;(this as any).$refs.ruleForm.resetFields()
this.dishList = []
this.dishTable = []
this.ruleForm = {
name: '',
categoryId: '',
price: '',
code: '',
image: '',
description: '',
dishList: [],
status: true,
id: '',
idType: ''
} as any
this.imageUrl = ''
}
} else {
this.$message.error(res.data.msg)
}
})
.catch(err => {
this.$message.error('请求出错了:' + err.message)
})
} else {
delete prams.updateTime
editSetmeal(prams)
.then(res => {
if (res.data.code === 1) {
this.$message.success('套餐修改成功!')
this.$router.push({ path: '/setmeal' })
} else {
// this.$message.error(res.data.desc || res.data.message)
}
})
.catch(err => {
this.$message.error('请求出错了:' + err.message)
})
}
} else {
// console.log('error submit!!')
return false
}
})
}
imageChange(value: any) {
this.ruleForm.image = value
}
}
</script>
<style>
.avatar-uploader .el-icon-plus:after {
position: absolute;
display: inline-block;
content: ' ' !important;
left: calc(50% - 20px);
top: calc(50% - 40px);
width: 40px;
height: 40px;
background: url('./../../assets/icons/icon_upload@2x.png') center center
no-repeat;
background-size: 20px;
}
</style>
<style lang="scss">
// .el-form-item__error {
// top: 90%;
// }
.addBrand-container {
.avatar-uploader .el-upload {
border: 1px dashed #d9d9d9;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
}
.avatar-uploader .el-upload:hover {
border-color: #ffc200;
}
.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 200px;
height: 160px;
line-height: 160px;
text-align: center;
}
.avatar {
width: 200px;
height: 160px;
display: block;
}
// .el-form--inline .el-form-item__content {
// width: 293px;
// }
.el-input {
width: 293px;
}
.address {
.el-form-item__content {
width: 777px !important;
}
}
.el-input__prefix {
top: 2px;
}
.addDish {
.el-input {
width: 130px;
}
.el-input-number__increase {
border-left: solid 1px #fbe396;
background: #fffbf0;
}
.el-input-number__decrease {
border-right: solid 1px #fbe396;
background: #fffbf0;
}
input {
border: 1px solid #fbe396;
}
.table {
border: solid 1px #ebeef5;
border-radius: 3px;
th {
padding: 5px 0;
}
td {
padding: 7px 0;
}
}
}
.addDishList {
.seachDish {
position: absolute;
top: 12px;
right: 20px;
}
.el-dialog__footer {
padding-top: 27px;
}
.el-dialog__body {
padding: 0;
border-bottom: solid 1px #efefef;
}
.seachDish {
.el-input__inner {
height: 40px;
line-height: 40px;
}
}
}
}
</style>
<style lang="scss" scoped>
.addBrand {
&-container {
margin: 30px;
.container {
position: relative;
z-index: 1;
background: #fff;
padding: 30px;
border-radius: 4px;
min-height: 500px;
.subBox {
padding-top: 30px;
text-align: center;
border-top: solid 1px $gray-5;
}
.el-input {
width: 350px;
}
.addDish {
width: 777px;
.addBut {
background: #ffc200;
display: inline-block;
padding: 0px 20px;
border-radius: 3px;
line-height: 40px;
cursor: pointer;
border-radius: 4px;
color: #333333;
font-weight: 500;
}
.content {
background: #fafafb;
padding: 20px;
border: solid 1px #d8dde3;
border-radius: 3px;
}
}
}
}
}
</style>
src/views/setmeal/components/AddDish.vue
<template>
<div class="addDish">
<div class="leftCont">
<div v-show="seachKey.trim() == ''"
class="tabBut">
<span v-for="(item, index) in dishType"
:key="index"
:class="{ act: index == keyInd }"
@click="checkTypeHandle(index, item.id)">{{ item.name }}</span>
</div>
<div class="tabList">
<div class="table"
:class="{ borderNone: !dishList.length }">
<div v-if="dishList.length == 0"
style="padding-left: 10px">
<Empty />
</div>
<el-checkbox-group v-if="dishList.length > 0"
v-model="checkedList"
@change="checkedListHandle">
<div v-for="(item, index) in dishList"
:key="item.name + item.id"
class="items">
<el-checkbox :key="index"
:label="item.name">
<div class="item">
<span style="flex: 3; text-align: left">{{
item.dishName
}}</span>
<span>{{ item.status == 0 ? '停售' : '在售' }}</span>
<span>{{ (Number(item.price) ).toFixed(2)*100/100 }}</span>
</div>
</el-checkbox>
</div>
</el-checkbox-group>
</div>
</div>
</div>
<div class="ritCont">
<div class="tit">
已选菜品({{ checkedListAll.length }})
</div>
<div class="items">
<div v-for="(item, ind) in checkedListAll"
:key="ind"
class="item">
<span>{{ item.dishName || item.name }}</span>
<span class="price">¥ {{ (Number(item.price) ).toFixed(2)*100/100 }} </span>
<span class="del"
@click="delCheck(item.name)">
<img src="./../../../assets/icons/btn_clean@2x.png"
alt="">
</span>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue, Watch } from 'vue-property-decorator'
// import {getDishTypeList, getDishListType} from '@/api/dish';
import { getCategoryList, queryDishList } from '@/api/dish'
import Empty from '@/components/Empty/index.vue'
@Component({
name: 'selectInput',
components: {
Empty
}
})
export default class extends Vue {
@Prop({ default: '' }) private value!: number
@Prop({ default: [] }) private checkList!: any[]
@Prop({ default: '' }) private seachKey!: string
private dishType: [] = []
private dishList: [] = []
private allDishList: any[] = []
private dishListCache: any[] = []
private keyInd = 0
private searchValue: string = ''
public checkedList: any[] = []
private checkedListAll: any[] = []
private ids: any = new Set()
created() {
this.init()
}
@Watch('seachKey')
private seachKeyChange(value: any) {
if (value.trim()) {
this.getDishForName(this.seachKey)
}
}
public init() {
// 菜单列表数据获取
this.getDishType()
// 初始化选项
this.checkedList = this.checkList.map((it: any) => it.name)
// 已选项的菜品-详细信息
this.checkedListAll = this.checkList.reverse()
}
// 获取套餐分类
public getDishType() {
getCategoryList({ type: 1 }).then(res => {
if (res && res.data && res.data.code === 1) {
this.dishType = res.data.data
this.getDishList(res.data.data[0].id)
} else {
this.$message.error(res.data.msg)
}
// if (res.data.code == 200) {
// const { data } = res.data
// this. = data
// this.getDishList(data[0].category_id)
// } else {
// this.$message.error(res.data.desc)
// }
})
}
// 通过套餐ID获取菜品列表分类
private getDishList(id: number) {
queryDishList({ categoryId: id }).then(res => {
if (res && res.data && res.data.code === 1) {
if (res.data.data.length == 0) {
this.dishList = []
return
}
let newArr = res.data.data
newArr.forEach((n: any) => {
n.dishId = n.id
n.copies = 1
// n.dishCopies = 1
n.dishName = n.name
})
this.dishList = newArr
if (!this.ids.has(id)) {
this.allDishList = [...this.allDishList, ...newArr]
}
this.ids.add(id)
} else {
this.$message.error(res.data.msg)
}
})
}
// 关键词收搜菜品列表分类
private getDishForName(name: any) {
queryDishList({ name }).then(res => {
if (res && res.data && res.data.code === 1) {
let newArr = res.data.data
newArr.forEach((n: any) => {
n.dishId = n.id
n.dishName = n.name
})
this.dishList = newArr
} else {
this.$message.error(res.data.msg)
}
})
}
// 点击分类
private checkTypeHandle(ind: number, id: any) {
this.keyInd = ind
this.getDishList(id)
}
// 添加菜品
private checkedListHandle(value: [string]) {
// TODO 实现倒序 由于value是组件内封装无法从前面添加 所有取巧处理倒序添加
// 倒序展示 - 数据处理前反正 为正序
this.checkedListAll.reverse()
// value 是一个只包含菜品名的数组 需要从 dishList中筛选出 对应的详情
// 操作添加菜品
const list = this.allDishList.filter((item: any) => {
let data
value.forEach((it: any) => {
if (item.name == it) {
data = item
}
})
return data
})
// 编辑的时候需要与已有菜品合并
// 与当前请求下的选择性 然后去重就是当前的列表
const dishListCat = [...this.checkedListAll, ...list]
let arrData: any[] = []
this.checkedListAll = dishListCat.filter((item: any) => {
let allArrDate
if (arrData.length == 0) {
arrData.push(item.name)
allArrDate = item
} else {
const st = arrData.some(it => item.name == it)
if (!st) {
arrData.push(item.name)
allArrDate = item
}
}
return allArrDate
})
// 如果是减菜 走这里
if (value.length < arrData.length) {
this.checkedListAll = this.checkedListAll.filter((item: any) => {
if (value.some(it => it == item.name)) {
return item
}
})
}
this.$emit('checkList', this.checkedListAll)
// 数据处理完反转为倒序
this.checkedListAll.reverse()
}
open(done: any) {
this.dishListCache = JSON.parse(JSON.stringify(this.checkList))
}
close(done: any) {
this.checkList = this.dishListCache
}
// 删除
private delCheck(name: any) {
const index = this.checkedList.findIndex(it => it === name)
const indexAll = this.checkedListAll.findIndex(
(it: any) => it.name === name
)
this.checkedList.splice(index, 1)
this.checkedListAll.splice(indexAll, 1)
this.$emit('checkList', this.checkedListAll)
}
}
</script>
<style lang="scss">
.addDish {
.el-checkbox__label {
width: 100%;
}
.empty-box {
margin-top: 50px;
margin-bottom: 0px;
}
}
</style>
<style lang="scss" scoped>
.addDish {
padding: 0 20px;
display: flex;
line-height: 40px;
.empty-box {
img {
width: 190px;
height: 147px;
}
}
.borderNone {
border: none !important;
}
span,
.tit {
color: #333;
}
.leftCont {
display: flex;
border-right: solid 1px #efefef;
width: 60%;
padding: 15px;
.tabBut {
width: 110px;
font-weight: bold;
border-right: solid 2px #f4f4f4;
span {
display: block;
text-align: center;
// border-right: solid 2px #f4f4f4;
cursor: pointer;
position: relative;
}
}
.act {
border-color: $mine !important;
color: $mine !important;
}
.act::after {
content: ' ';
display: inline-block;
background-color: $mine;
width: 2px;
height: 40px;
position: absolute;
right: -2px;
}
.tabList {
flex: 1;
padding: 15px;
height: 400px;
overflow-y: scroll;
.table {
border: solid 1px #f4f4f4;
border-bottom: solid 1px #f4f4f4;
.items {
border-bottom: solid 1px #f4f4f4;
padding: 0 10px;
display: flex;
.el-checkbox,
.el-checkbox__label {
width: 100%;
}
.item {
display: flex;
padding-right: 20px;
span {
display: inline-block;
text-align: center;
flex: 1;
font-weight: normal;
}
}
}
}
}
}
.ritCont {
width: 40%;
.tit {
margin: 0 15px;
font-weight: bold;
}
.items {
height: 338px;
padding: 4px 15px;
overflow: scroll;
}
.item {
box-shadow: 0px 1px 4px 3px rgba(0, 0, 0, 0.03);
display: flex;
text-align: center;
padding: 0 10px;
margin-bottom: 20px;
border-radius: 6px;
color: #818693;
span:first-child {
text-align: left;
color: #20232a;
flex: 70%;
}
.price {
display: inline-block;
flex: 70%;
text-align: left;
}
.del {
cursor: pointer;
img {
position: relative;
top: 5px;
width: 20px;
}
}
}
}
}
</style>
src/api/setMeals.ts
// 修改数据接口
export const editSetmeal = (params: any) => {
return request({
url: '/setmeal',
method: 'put',
data: { ...params }
})
}
// 新增数据接口
export const addSetmeal = (params: any) => {
return request({
url: '/setmeal',
method: 'post',
data: { ...params }
})
}
// 查询详情接口
export const querySetmealById = (id: string | (string | null)[]) => {
return request({
url: `/setmeal/${id}`,
method: 'get'
})
}
src/api/dish.ts
import request from '@/utils/request'
/**
*
* 菜品管理
*
**/
// 查询列表接口
export const getDishPage = (params: any) => {
return request({
url: '/dish/page',
method: 'get',
params
})
}
// 删除接口
export const deleteDish = (ids: string) => {
return request({
url: '/dish',
method: 'delete',
params: { ids }
})
}
// 修改接口
export const editDish = (params: any) => {
return request({
url: '/dish',
method: 'put',
data: { ...params }
})
}
// 新增接口
export const addDish = (params: any) => {
return request({
url: '/dish',
method: 'post',
data: { ...params }
})
}
// 查询详情
export const queryDishById = (id: string | (string | null)[]) => {
return request({
url: `/dish/${id}`,
method: 'get'
})
}
// 获取菜品分类列表
export const getCategoryList = (params: any) => {
return request({
url: '/category/list',
method: 'get',
params
})
}
// 查菜品列表的接口
export const queryDishList = (params: any) => {
return request({
url: '/dish/list',
method: 'get',
params
})
}
// 文件down预览
export const commonDownload = (params: any) => {
return request({
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
},
url: '/common/download',
method: 'get',
params
})
}
// 起售停售---批量起售停售接口
export const dishStatusByStatus = (params: any) => {
return request({
url: `/dish/status/${params.status}`,
method: 'post',
params: { id: params.id }
})
}
//菜品分类数据查询
export const dishCategoryList = (params: any) => {
return request({
url: `/category/list`,
method: 'get',
params: { ...params }
})
}
③点击”保存“按钮完成新增操作
功能测试
完结!!!
前端完整源码:https://pan.baidu.com/s/1JAI65SyP8qIIeLxh2U923g?pwd=ewap
后端完整源码:https://pan.baidu.com/s/1hHnA-H_xOFiVEeIVi92A3Q?pwd=0k80