npm对应yarn命令
解决跨域问题
vue-cil代理服务器
- 修改配置文件.env.development
//.env.development 中的VUE_APP_BASE_API改成 /api
# just a flag
ENV = 'development'
# base api
VUE_APP_BASE_API = '/api'
- vue-config配置module.exports
devServer: {
proxy: {
// 这里的api 表示如果我们的请求地址有/api的时候,就出触发代理机制
'/api': {
target: 'http://ihrm.itheima.net/', // 我们要代理请求的地址
changeOrigin: true //是否跨域
}
}
},
登录模块设计流程
- 设计表单、登录按钮等布局
- 表单绑定data数据对象
- 表单校验
- 处理登录按钮事件,点击登录先进行兜底校验(可封装在utils中的validate.js中),校验通过再执行下面的:
- 根据接口文档写好请求:写封装在api文件夹的js中
- 随后发送请求登录(判断是否有数据需要存入vuex,有则使用vuex请求)
- 将token存入并持久化,持久化可使用cookies
- 跳转至登录后页面
退出登录流程
-
store设置好actions:
- 清除用户信息,
- 清除token,
- 跳转登录页,
- 提示
-
设置
退出登录按钮事件,执行上面的actions
登录未遂设置
退出登录时
-
退出登录时跳转并携带当前路由
// 跳转并携带退出前路由 // ?后面的redirect名字为自己定义的,=后面的参数会存到route.query中 this.$router.replace(`/login?redirect=${encodeURIComponent(this.$route.fullPath)}`) -
在登录跳转判断route.query.replace有路径则跳转至这里,否则跳转至首页
// 跳转 :判断有携带地址则跳转携带地址,否则跳转首页 this.$router.push(this.$route.query.redirect || '/')
token过期退出登录时
-
token过期退出登录时携带当前路由
// 跳转并携带当前页面路径 router.replace(`/login?redirect=${encodeURIComponent(router.currentRoute.fullPath)}`) -
在登录跳转判断route.query.replace有路径则跳转至这里,否则跳转至首页
// 跳转 :判断有携带地址则跳转携带地址,否则跳转首页 this.$router.push(this.$route.query.redirect || '/')
让axios少一层data
// 响应拦截器
instance.interceptors.response.use(function(response) {
// 对响应数据做点什么
const res = response.data
return res
}, function(error) {
// 对响应错误做点什么
return Promise.reject(error)
})
让路由跳转有进度条
使用nprogress:
安装
npm install --save nprogress
直接引入js、css或者通过cdn引入。
<script src='nprogress.js'></script>
<link rel='stylesheet' href='nprogress.css'/>
使用
直接调用 start()或者done()来控制进度条。
NProgress.start();
NProgress.done();
可以通过调用 .set(n)来设置进度,n是0-1的数字。
NProgress.set(0.0); // Sorta same as .start()
NProgress.set(0.4);
NProgress.set(1.0); // Sorta same as .done()
页面token判断
跳转路由时判断token是否存在:
- 将请求获得得token存入state中,并存入Cookies中,将state中的值设置为cookies存入的函数,否则刷新页面则会失效
- 判断如果有token:访问的是login页面则直接跳转到主页,不是登录也则放行
- 如果没有token:判断是白名单的直接放行,否则跳转至注册页
// 路由白名单
const whiteList = ['/login']
// 路由前置守卫
router.beforeEach(async(to, from, next) => {
// 进度条开始
NProgress.start()
// 获取vuex中getters的token
const token = store.getters.token
// 判断是否有token
if (token) {
// 有token访问的是login
if (to.path === '/login') {
// 跳转至主页
next('/')
NProgress.done()
} else {
// 放行
next()
NProgress.done()
}
} else {
// 如果没有token
// 判断访问的如果是白名单的地址
if (whiteList.some(item => item === to.path )) {
// 放行
next()
NProgress.done()
} else {
// 跳转至登录页
next('/login')
NProgress.done()
}
}
})
// 路由后置守卫
router.afterEach(() => {
// 进度条结束
NProgress.done()
})
跳转路由时判断token是否存在:
-
在响应拦截器中错误中判断错误code是否为token过期
-
如果过期:
- 删除token
- 删除用户信息
- 跳转并携带当前页面路径
// 响应拦截器 service.interceptors.response.use( (response) => { return response.data }, (error) => { // 如果过期:删除token 删除用户信息 跳转并携带当前页面路径 if (error.response && error.response.data && error.response.data.code === 10002) { // 提醒 Message.error('登录过期,请重新登录') // 删除token store.commit('user/REMOVE_TOKEN') // 删除用户信息 store.commit('user/RESET_STATE') // 跳转并携带当前页面路径 router.replace(`/login?redirect=${encodeURIComponent(router.currentRoute.fullPath)}`) } else { Message.error({ message: error || '出现错误,请稍后再试' }) } console.dir(error) return Promise.reject(error) } )
路由前置统一设置请求头token
-
将vuex中state的token放在getters中
-
请求拦截器设置:
// 请求拦截器 service.interceptors.request.use( (config) => { const token = store.getters.token if (token) { config.headers.Authorization = `Bearer ${token}` } return config }, (error) => { return Promise.reject(error) } )
导航栏logo控制
- 是否显示设置在src下的settings.js中
- 存入vuex中的state中
- 在logo组件的v-if等于vuex中的state
获取用户信息
- 封装获取用户请求
- 在前置路由守卫调用请求,存入vuex中
全局组件统一封装
如果希望在所有页面都要用, 那么就要全局注册组件,如果有很多个组件, 都要全局注册, 那么又会在main.js 中写很多内容
- Vue.use 可以接收一个对象, Vue.use(obj)
- 对象中需要提供一个 install 函数
- install 函数可以拿到参数 Vue, 且将来会在 Vue.use 时, 自动调用该 install 函数
index.js统一引入组件
import PageTools from './PageTools'
export default {
install(Vue) {
Vue.component('PageTools', PageTools)
}
}
入口文件注册插件(main.js)
import Components from './components'
Vue.use(Components)
封装过滤器
-
src目录下创建
filters/index.js文件// 导入dayjs import dayjs from 'dayjs' // 格式化组件过滤器 export function formatData(value, str = 'YYYY年MM月DD日') { return dayjs(value).format(str) } -
入口文件
main.js全局注册过滤器// 引入过滤器 import * as filters from '@/filters' Object.keys(filters).forEach(key => { // 注册过滤器 Vue.filter(key, filters[key]) })
Excel导入
-
封装excel导入组件(uploadExcel)
需要的依赖:
xlsx<template> <div> <input ref="excel-upload-input" class="excel-upload-input" type="file" accept=".xlsx, .xls" @change="handleClick" > <div class="box"> <div class="left"> <el-button :loading="loading" style="margin-left: 16px" size="mini" type="primary" @click="handleUpload" > 点击上传 </el-button> <div class="text">(推荐下载模板文件,填写后上传)</div> <div class="text">点击查看文件上传要求</div> </div> <div class="rigth" @drop="handleDrop" @dragover="handleDragover" @dragenter="handleDragover" @click="handleUpload" > <div class="el-icon-upload" /> <div>将文件拖入上传</div> </div> </div> </div> </template> <script> import * as XLSX from 'xlsx' export default { props: { beforeUpload: Function, // eslint-disable-line onSuccess: Function // eslint-disable-line }, data() { return { loading: false, excelData: { header: null, results: null } } }, methods: { generateData({ header, results }) { this.excelData.header = header this.excelData.results = results this.onSuccess && this.onSuccess(this.excelData) }, handleDrop(e) { e.stopPropagation() e.preventDefault() if (this.loading) return const files = e.dataTransfer.files if (files.length !== 1) { this.$message.error('Only support uploading one file!') return } const rawFile = files[0] // only use files[0] if (!this.isExcel(rawFile)) { this.$message.error( 'Only supports upload .xlsx, .xls, .csv suffix files' ) return false } this.upload(rawFile) e.stopPropagation() e.preventDefault() }, handleDragover(e) { e.stopPropagation() e.preventDefault() e.dataTransfer.dropEffect = 'copy' }, handleUpload() { this.$refs['excel-upload-input'].click() }, handleClick(e) { const files = e.target.files const rawFile = files[0] // only use files[0] if (!rawFile) return this.upload(rawFile) }, upload(rawFile) { this.$refs['excel-upload-input'].value = null // fix can't select the same excel if (!this.beforeUpload) { this.readerData(rawFile) return } const before = this.beforeUpload(rawFile) if (before) { this.readerData(rawFile) } }, readerData(rawFile) { this.loading = true return new Promise((resolve, reject) => { const reader = new FileReader() reader.onload = (e) => { const data = e.target.result const workbook = XLSX.read(data, { type: 'array' }) const firstSheetName = workbook.SheetNames[0] const worksheet = workbook.Sheets[firstSheetName] const header = this.getHeaderRow(worksheet) const results = XLSX.utils.sheet_to_json(worksheet) this.generateData({ header, results }) this.loading = false resolve() } reader.readAsArrayBuffer(rawFile) }) }, getHeaderRow(sheet) { const headers = [] const range = XLSX.utils.decode_range(sheet['!ref']) let C const R = range.s.r /* start in the first row */ for (C = range.s.c; C <= range.e.c; ++C) { /* walk every column in the range */ const cell = sheet[XLSX.utils.encode_cell({ c: C, r: R })] /* find the cell in the first row */ let hdr = 'UNKNOWN ' + C // <-- replace with your desired default if (cell && cell.t) hdr = XLSX.utils.format_cell(cell) headers.push(hdr) } return headers }, isExcel(file) { return /.(xlsx|xls|csv)$/.test(file.name) } } } </script> <style scoped> .box { text-align: center; } .excel-upload-input { display: none; z-index: -9999; } .rigth { border: 2px dashed #bbb; width: 400px; height: 180px; font-size: 24px; border-radius: 5px; text-align: center; color: #bbb; display: inline-block; border-left: 0; } .left { vertical-align: top; padding: 50px; border: 2px dashed #bbb; width: 400px; height: 180px; font-size: 24px; border-radius: 5px; text-align: center; color: #bbb; display: inline-block; } .el-icon-upload { font-size: 100px; } .left .text { font-size: 12px; color: #000; margin-top: 5px; } </style> -
上述组件接受两个函数
beforeUpload 导入前的回调 有一个实参: 文件对象
- beforeUpload需要返回true才可继续导入成功的回调
onSuccess 导入成功后的回调 有一个实参:一个对象,对象有两个属性
results,header**- results 代表 表体数据
- header 代表 表头数据
<template> <div class="app-container"> <el-card> <div class="title">员工导入</div> <el-alert type="warning" :closable="false" show-icon title="每次导入仅可添加1000名员工,姓名、手机、入职时间、聘用形式为必填项" /> <div class="import"> <!-- 导入组件 --> <upload-excel-component :on-success="handleSuccess" :before-upload="beforeUpload" /> </div> </el-card> </div> </template> <script> import UploadExcelComponent from '@/components/UploadExcel/index.vue' export default { name: 'UploadExcel', components: { UploadExcelComponent }, data() { return {} }, methods: { // 导入成功回调 handleSuccess({ results, header }) { console.log(results) console.log(header) }, // 导入前的回调,可用于判断文件大小、类型等 beforeUpload(file) { console.log(file) } } } </script> <style lang="scss" scoped> .title { text-align: center; font-size: 20px; font-weight: 900; margin-bottom: 15px; } .import { margin-top: 100px; } .app-container { background-color: #f0f2f4; } </style> -
最后header 获取的是表头的数组
['入职日期', '手机号', '姓名', '转正日期', '工号'] -
results 获取的表体 每一行一个对象的数组
[{…}, {…}] -
Excel日期格式化
formatDate(numb, format) { const time = new Date((numb - 25567) * 24 * 3600000 - 5 * 60 * 1000 - 43 * 1000 - 24 * 3600000 - 8 * 3600000) const year = time.getFullYear() + '' const month = time.getMonth() + 1 + '' const date = time.getDate() + '' if (format && format.length === 1) { return year + format + (month < 10 ? '0' + month : month) + format + (date < 10 ? '0' + date : date) } return year + (month < 10 ? '0' + month : month) + (date < 10 ? '0' + date : date) }
\