《苍穹外卖》前端课程知识点记录
一、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
odejs
ode_cache\_cacache\index-v5\ee\aa
npm ERR! errno -4048
npm ERR! Error: EPERM: operation not permitted, mkdir ‘C:\Program Files
odejs
ode_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: 'http://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. http://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 http://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
h3 {
margin: 40px 0 0;
}
ul {
list-style-type: none;
padding: 0;
}
li {
display: inline-block;
margin: 0 10px;
}
a {
color: #42b983;
}
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:路由器,根据路由请求在路由视图中动态渲染对应的视图组件
:路由链接组件,浏览器会解析成
:路由视图组件,用来展示与路由匹配的视图组件
路由跳转
- 标签式
- 编程式
如果请求的路径不存在,应该如何处理?
①当上面的路径都匹配不到时,重定向到最后一项
嵌套路由
嵌套路由:组件内要切换内容,就需要用到嵌套路由(子路由)
实现步骤:
- 安装并导入elementui,实现页面布局(Container布局容器)—ContainerView.vue
npm i element-ui -S
- 提供子视图组件,用于效果展示 —P1View.vue、P2View.vue、P3View.vue
view/container/ContainerView.vueHeader Aside Main .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; }
- 在src/router/index.js中配置路由映射规则(嵌套路由配置)
- 在布局容器视图中添加,实现子视图组件展示
- 在布局容器视图中添加,实现路由请求
注意事项:子路由变化,切换的是【ContainerView组件】中‘’部分的内容。
思考
1. 对于前面的案例,如果用户访问的路由是/c,会有什么效果呢?
2. 如果实现在访问/c时,默认就展示某个子视图组件呢?
状态管理vuex
vuex介绍
- vuex是一个专为Vue.js应用程序开发的状态管理库
- vuex可以在多个组件之间共享数据,并且共享的数据是响应式的,即数据的变更能及时渲染到模板
- vuex采用集中式存储管理所有组件的状态
安装
npm install vuex@next --save
核心概念
- state:状态对象,集中定义各个组件共享的数据
- mutations:类似于一个事件,用于修改共享数据,要求必须是同步函数
- actions:类似于mutation,可以包含异步操作,通过调用mutation来改变共享数据
使用方式
①创建带有vuex功能的脚手架工程
②src/store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
// 集中管理多个组件共享的数据
export default new Vuex.Store({
state: {
},
getters: {
},
mutations: {
},
actions: {
},
modules: {
}
})
③src/main.js
import Vue from 'vue'
import App from './App.vue'
import store from './store'
Vue.config.productionTip = false
new Vue({
// 使用vuex功能
store,
render: h => h(App)
}).$mount('#app')
④定义和展示共享数据
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
// 集中管理多个组件共享的数据
export default new Vuex.Store({
// 集中定义共享数据
state: {
name: '未登录游客'
},
getters: {
},
mutations: {
},
actions: {
},
modules: {
}
})
⑤在mutations中定义函数,修改共享数据
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
// 集中管理多个组件共享的数据
export default new Vuex.Store({
// 集中定义共享数据
state: {
name: '未登录游客'
},
getters: {
},
// 修改共享数据只能通过mutation实现,必须是同步操作
mutations: {
setName(state, newName) {
state.name = newName
}
},
// 通过actions可以调用mutations,在action中可以进行异步操作
actions: {
},
modules: {
}
})
欢迎您,{{$store.state.name}}
#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;
}
④在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
欢迎您,{{$store.state.name}}
#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;
}
// vue.config.js
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true,
devServer: {
port:8082,
proxy: {
'/api': {
target: 'http://location:8081',
pathRewrite: {
'^/api': ''
}
}
}
}
})
思考
1. 如何理解vuex?
- 实现多个组件之间的数据共享
- 共享数据是响应式的,实时渲染到模板
- 可以集中管理共享数据
2. 如何使用vuex?
- 在store对象的state属性中定义共享数据
- 在store对象的mutations属性中定义修改共享数据的函数
- 在store对象的actions属性中定义调用mutation的函数,可以进行异或操作
- mutations中的函数不能直接调用,只能通过store对象的commit方法调用
- actions中定义的函数不能直接调用,只能通过store对象的dispatch方法调用
TypeScript
TypeScript介绍
- TypeScript(简称:TS)是微软推出的开源语言
- TypeScript是JavaScript的超集(JS有的TS都有)
- TypeScript = Type + JavaScript(在JS基础上增加了类型支持)
- TypeScript文件扩展名为ts
- TypeScript可编译成标准的JavaScript,并且在编译时进行类型检查
安装typescript(全局安装)
如果安装失败,以管理员身份运行命令行窗口,可以在安装命令后加上 @5.0.2,以指定版本
npm install -g typescript
查看TS版本
tsc -v
示例
// 通过ts代码,指定函数的参数类型为string
function hello(msg:string) {
console.log(msg)
}
// 传入的参数类型为number
hello(123)
编译:tsc + 文件名
改正后(传参为:’123’)
思考
1. TS为什么要增加类型支持?
- TS属于静态类型编程语言,JS属于动态类型编程语言
- 静态类型在编译期做类型检查,动态类型在执行期间做类型检查
- 对于JS来说,需要等到代码执行的时候才可以发现错误(晚)
- 对于TS来说,在代码编译的时候就可以发现错误(早)
- 配合VSCode开发工具,TS可以提前在编写代码的同时就发现代码中的错误,减少找Bug、改Bug的时间
2. 如何理解TypeScript?
- 是JavaScript的超集,兼容JavaScript
- 扩展了JavaScript的语法,文件扩展名为ts
- 可以编译成标准的JavaScript,并且可以在编译时进行类型检查
- 全局安装npm install -g typescript
- 视图tsc命令将ts文件编译成js文件
- 使用node命令运行js文件
TypeScript常用类型
类型 | 例 | 备注 |
字符串类型 | string | |
数字类型 | number | |
布尔类型 | boolean | |
数组类型 | number[], string[], boolean[]依此类推 | |
任意类型 | any | 相当于又回到了没有类型的时代 |
复杂类型 | type与interface | |
函数类型 | () => void | 对函数的参数和返回值进行说明 |
字面量类型 | “a”|”b”|”c” | 限制变量或参数的取值 |
class类 | class Animal |
类型标注的位置
- 标注变量
- 标注参数
- 标注返回值
项目示例
1. 创建项目时勾选上TypeScript、Router、Vuex
2. 字符串类型、布尔类型、数字类型
// 字符串类型
let username: string = 'itcast'
// 数字类型
let age: number = 20
// 布尔类型
let isTrue: boolean = true
console.log(username)
console.log(age)
console.log(isTrue)
3. 字面量类型
// 字面量类型
function printText(s: string, alignment: 'left'|'right'|'center') {
console.log(s, alignment)
}
printText('hello', 'left')
printText('hello', 'right')
4. 复杂类型——interface
小技巧:可以通过在属性名后面加上?,表示当前属性为可选
// 定义接口
interface Cat {
name: string,
age: number
}
// 定义变量为Cat类型
const c1: Cat = {name: '小白', age: 1}
// const c2: Cat = {name: '小白'} // 错误:缺少age属性
// const c3: Cat = {name: '小白', age: 1, sex: '公'} // 错误:多了sex属性
console.log(c1)
5. class类
注意:使用class关键字来定义类,类中可以包含属性、构造方法、普通方法
// 定义一个类,使用class关键字
class User {
name: string; // 属性
constructor(name: string) {
// 构造方法
this.name = name
}
// 方法
study() {
console.log(this.name + '正在学习')
}
}
// 使用User类型
const user = new User('张三')
// 输出类中的属性
console.log(user.name)
// 调用类中的方法
user.study()
6. Class类实现interface
interface Animal {
name: string
eat(): void
}
// 定义一个类Bird,实现上面的Animal接口
class Bird implements Animal {
name: string
constructor(name: string) {
this.name = name
}
eat(): void {
console.log(this.name + ' eat')
}
}
// 创建类型为Bird的对象
const b1 = new Bird('杜鹃')
console.log(b1.name)
b1.eat()
7. class类——类的继承
// 定义一个类Bird,实现上面的Animal接口
class Bird implements Animal {
name: string
constructor(name: string) {
this.name = name
}
eat(): void {
console.log(this.name + ' eat')
}
}
// 定义Parrot类,并且继承Bird类
class Parrot extends Bird {
say():void {
console.log(this.name + ' say hello')
}
}
const myParrot = new Parrot('Polly')
myParrot.say()
myParrot.eat()
小结
1.TypeScript的常用类型有哪些?
- string、number、boolean
- 字面量、void
- interface、class
2. TypeScript文件能直接运行吗?
- 需要将TS文件编译为JS文件才能运行
- 编译后的JS文件中类型会擦除
三、苍穹外卖前端项目环境搭建、员工管理
技术选型
- node.js
- vue
- ElementUI
- axios
- vuex
- vue-router
- typescript
熟悉前端代码结构
1. 代码导入:直接导入课程资料中提供的前端工程,在此基础上开发即可
在苍穹外卖前端课程->资料->day02->资料->苍穹外卖前端初始工程
2. 重点文件/目录
3. 通过登录功能梳理前端代码
①先运行后端服务
②下载前端中的依赖(不需要指定安装哪些包,会自动扫描):npm install
③把nodejs的版本降级到12版本,如果出现安全性问题,代开cmd执行下面的命令
可以参考这篇文章:node.js安装配置详细介绍以及nodejs版本降级_nodejs低版本-CSDN博客
我是把node.js降级到了12.22.12
npm config set strict-ssl false
npm install
④修改后端服务的地址(如果前面课程中修改了后端服务的端口号)
⑤npm run serve,前端的端口号为8888
⑥通过登录功能梳理前端代码
- 获得登录页面路由地址
- 从main.ts中找到路由文件
- 从路由文件中找到登录视图组件
- 从登录视图组件中找到登录方法
- 跟踪登录方法的执行过程
员工分页查询
需求分析和接口设计
业务规则
根据页码展示员工信息
每页展示10条数据
分页查询可以根据需要,输入员工姓名进行查询
接口设计
代码开发
①从路由文件router.ts中找到员工管理页面(组件)
②初始页面
③制作页面头部效果
查询
+ 添加员工
注意
- 输入框和按钮都是使用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
查询
+ 添加员工
{{ scope.row.status === 0 ? '禁用' : '启用' }}
修改
{{
scope.row.status === 1 ? '禁用' : '启用'
}}
.disabled-text {
color: #bac0cd !important;
}
功能测试
启用、禁用员工账号
需求分析和接口设计
业务规则
可以对状态为“启用”的员工账号进行“禁用”操作
可以对状态为“禁用”的员工账号进行“启用”操作
状态为“禁用”的员工账号不能登录系统
接口设计
代码开发
①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
查询
+ 添加员工
{{ scope.row.status === 0 ? '禁用' : '启用' }}
修改
{{scope.row.status === 1 ? '禁用' : '启用'}}
.disabled-text {
color: #bac0cd !important;
}
功能测试
添加员工
需求分析和接口设计
产品原型
接口设计
代码开发
添加员工操作步骤
- 点击“添加员工”按钮,跳转到新增页面
- 在新增员工页面录入员工相关信息
- 点击“保存”按钮完成新增操作
①为“添加员工”按钮绑定单击事件: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
男
女
保存
保存并继续添加员工
this.$router.push('/employee')">返回
.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;
}
}
}
功能测试
修改员工
需求分析和接口设计
产品原型
编辑员工功能涉及到两个接口:
- 根据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
查询
批量删除
+新建套餐
.el-table-column--selection .cell {
padding-left: 10px;
}
.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;
}
}
}
}
注意
- 输入框、按钮、下拉框都是使用ElementUI提供的组件
- 对于前端的组件只需要参考ElementUI提供的文档,进行修改即可
③导入查询套餐分类的JS方法,动态填充套餐分类下拉框,src/views/setmeal/index.vue
完整代码(做了一些小调整)
查询
批量删除
+新建套餐
.el-table-column--selection .cell {
padding-left: 10px;
}
.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;
}
}
}
}
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
{{ scope.row.status === 0 ? '停售' : '启售' }}
修改
{{ scope.row.status == '1' ? '停售' : '启售' }}
删除
功能测试
启售停售套餐
需求分析和接口设计
产品原型
业务规则
- 可以对状态为“启售”的套餐进行“停售:操作
- 可以对状态为”停售“的套餐进行”启售“操作
接口设计
代码开发
①为启售、停售按钮绑定单击事件,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
+ 添加菜品
+ 添加菜品
{{ (Number(scope.row.price).toFixed(2) * 100) / 100 }}
删除
图片大小不超过2M
仅能上传 PNG JPEG JPG类型图片
建议上传200*200或300*300尺寸的图片
$router.back()">
取消
保存
保存并继续添加
.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;
}
// .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;
}
}
}
}
.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;
}
}
}
}
}
src/views/setmeal/components/AddDish.vue
{{ item.name }}
0"
v-model="checkedList"
@change="checkedListHandle">
{{
item.dishName
}}
{{ item.status == 0 ? '停售' : '在售' }}
{{ (Number(item.price) ).toFixed(2)*100/100 }}
已选菜品({{ checkedListAll.length }})
{{ item.dishName || item.name }}
¥ {{ (Number(item.price) ).toFixed(2)*100/100 }}
.addDish {
.el-checkbox__label {
width: 100%;
}
.empty-box {
margin-top: 50px;
margin-bottom: 0px;
}
}
.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;
}
}
}
}
}
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 }
})
}
③点击”保存“按钮完成新增操作
功能测试
完结!!!