一、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.vuenpm 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.jsimport 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.jsconst { 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文件扩展名为tsTypeScript可编译成标准的JavaScript,并且在编译时进行类型检查安装typescript(全局安装)
如果安装失败,以管理员身份运行命令行窗口,可以在安装命令后加上 @5.0.2,以指定版本
npm install -g typescript
查看TS版本
tsc -v
示例
// 通过ts代码,指定函数的参数类型为stringfunction hello(msg:string) { console.log(msg)}// 传入的参数类型为numberhello(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: 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字面量、voidinterface、class2. TypeScript文件能直接运行吗?
需要将TS文件编译为JS文件才能运行编译后的JS文件中类型会擦除
三、苍穹外卖前端项目环境搭建、员工管理
技术选型
node.jsvueElementUIaxiosvuexvue-routertypescript
熟悉前端代码结构
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 } })}
③点击”保存“按钮完成新增操作
功能测试
完结!!!