一、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` 是用于请求的服务器 URLurl: '/user',// `method` 是创建请求时使用的方法method: 'get', // 默认值// `baseURL` 将自动加在 `url` 前面,除非 `url` 是一个绝对 URL。// 它可以通过设置一个 `baseURL` 便于为 axios 实例的方法传递相对 URLbaseURL: '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, Bufferdata: {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 Authauth: {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 requestsresponseEncoding: '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/cancellationcancelToken: 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 = falsenew 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 = trueconsole.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: stringeat(): void
}// 定义一个类Bird,实现上面的Animal接口
class Bird implements Animal {name: stringconstructor(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: stringconstructor(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-inputv-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-paginationclass="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.totalthis.records = res.data.data.records}}).catch((err) => {this.$message.console.error('请求出错了:' + err.message)})},// pageSize发送变化时触发handleSizeChange(pageSize) {this.pageSize = pageSizethis.pageQuery()},// page发生变化时触发handleCurrentChange(page) {this.page = pagethis.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-inputv-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-paginationclass="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.totalthis.records = res.data.data.records}}).catch((err) => {this.$message.console.error('请求出错了:' + err.message)})},// pageSize发送变化时触发handleSizeChange(pageSize) {this.pageSize = pageSizethis.pageQuery()},// page发生变化时触发handleCurrentChange(page) {this.page = pagethis.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-optionv-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-optionv-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-optionv-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-optionv-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: '', // 分类idstatusArr:[{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,3return 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 Arraythis.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="套餐图片:"requiredprop="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 = falseprivate 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.datathis.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.imagethis.checkList = res.data.data.setmealDishesthis.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 = falsethis.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 anyprams.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 : 0prams.categoryId = this.ruleForm.idType// delete prams.dishListif (this.actionType == 'add') {delete prams.idaddSetmeal(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 anythis.imageUrl = ''}} else {this.$message.error(res.data.msg)}}).catch(err => {this.$message.error('请求出错了:' + err.message)})} else {delete prams.updateTimeeditSetmeal(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 centerno-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!: stringprivate dishType: [] = []private dishList: [] = []private allDishList: any[] = []private dishListCache: any[] = []private keyInd = 0private 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.datathis.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.datanewArr.forEach((n: any) => {n.dishId = n.idn.copies = 1// n.dishCopies = 1n.dishName = n.name})this.dishList = newArrif (!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.datanewArr.forEach((n: any) => {n.dishId = n.idn.dishName = n.name})this.dishList = newArr} else {this.$message.error(res.data.msg)}})}// 点击分类private checkTypeHandle(ind: number, id: any) {this.keyInd = indthis.getDishList(id)}// 添加菜品private checkedListHandle(value: [string]) {// TODO 实现倒序 由于value是组件内封装无法从前面添加 所有取巧处理倒序添加// 倒序展示 - 数据处理前反正 为正序this.checkedListAll.reverse()// value 是一个只包含菜品名的数组 需要从 dishList中筛选出 对应的详情// 操作添加菜品const list = this.allDishList.filter((item: any) => {let datavalue.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 allArrDateif (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