时间:2023-05-11 09:16:52 点击次数:13
vue-element-admin 是一个后台前端解决方案,它基于 vue 和 element-ui实现。它使用了最新的前端技术栈,内置了 i18 国际化解决方案,动态路由,权限验证,提炼了典型的业务模型,提供了丰富的功能组件,它可以帮助你快速搭建企业级中后台产品原型。
项目运行完毕,浏览器会自动打开基础模板的登录页,如上图
我们在做项目时 其中最关注的就是src目录, 里面是所有的源代码和资源, 至于其他目录, 都是对项目的环境和工具的配置。
axios的拦截器原理如下
vue.config.js 就是vue项目相关的编译,配置,打包,启动服务相关的配置文件,它的核心在于webpack,但是又不同于webpack,相当于改良版的webpack。
在项目下, 我们发现了 .env.development 和 .env.production 两个文件
development => 开发环境
production => 生产环境
当我们运行npm run dev进行开发调试的时候,此时会加载执行 .env.development 文件内容
当我们运行npm run build:prod进行生产环境打包的时候,会加载执行 .env.production 文件内容网站名称实际在configureWebpack选项中的name选项,通过阅读代码,我们会发现name实际上来源于src目录下的 settings.js 文件。
module.exports = { title: 小优电商后台管理系统, sidebarLogo: true}@keyup.enter属于按键修饰符,如果我们想监听在按回车键的时候触发,可以如下编写
<input v-on:keyup.enter="submit">当下,最流行的就是前后分离项目,也就是前端项目和后端接口并不在一个域名之下,那么前端项目访问后端接口必然存在跨域的行为.
开发环境的跨域,也就是在vue-cli脚手架环境下开发启动服务时,我们访问接口所遇到的跨域问题,vue-cli为我们在本地开启了一个服务,可以通过这个服务帮我们代理请求,解决跨域问题
proxy: { /api/private/v1/: { target: http://127.0.0.1:8888, // 跨域请求的地址 changeOrigin: true // 只有这个值为true的情况下 才表示开启跨域 }}在 utils/auth.js 中,基础模板已经为我们提供了获取 token ,设置 token ,删除 token 的方法,可以直接使用
const TokenKey = xiaoyouexport function getToken() { return localStorage.getItem(TokenKey)}export function setToken(token) { return localStorage.setItem(TokenKey, token)}export function removeToken() { return localStorage.removeItem(TokenKey)}登录action要做的事情,调用登录接口,成功后设置token到vuex,失败则返回失败
const actions = { async login(context, data) { const result = await login(data) // 实际上就是一个promise result就是执行的结果 if (result.data.success) { context.commit(setToken, result.data.data) } }}为什么会有环境变量之分? 如图
可以在 .env.development 和 .env.production 定义变量,变量自动就为当前环境的值
基础模板在以上文件定义了变量VUE_APP_BASE_API,该变量可以作为axios请求的baseURL
在模板中,两个值分别为/dev-api 和 /prod-api左侧导航组件的样式文件 styles/siderbar.scss
需要把页面设置成如图样式
全局注册自定义指令语法 - 获取焦点指令
Vue.directive(focus, { inserted: function (el) { console.log(el.children[0]) el.children[0].focus() }})在登录组件中使用此指令
<el-input ref="mobile" v-model="loginForm.username" v-focus placeholder="手机号" name="mobile" type="text" tabindex="1" />src/utils/request.js
const timeKey = hrsaas-timestamp-key // 设置一个独一无二的key// 获取时间戳export function getTimeStamp() { return Cookies.get(timeKey)}// 设置时间戳export function setTimeStamp() { Cookies.set(timeKey, Date.now())}src/utils/request.js
import axios from axiosimport store from @/storeimport router from @/routerimport { Message } from element-uiimport { getTimeStamp } from @/utils/authconst TimeOut = 3600 // 定义超时时间const service = axios.create({// 当执行 npm run dev => .evn.development => /api => 跨域代理 baseURL: process.env.VUE_APP_BASE_API, // npm run dev => /api npm run build => /prod-api timeout: 5000 // 设置超时时间})// 请求拦截器service.interceptors.request.use(config => { // config 是请求的配置信息 // 注入token if (store.getters.token) { // 只有在有token的情况下 才有必要去检查时间戳是否超时 if (IsCheckTimeOut()) { // 如果它为true表示 过期了 // token没用了 因为超时了 store.dispatch(user/logout) // 登出操作 // 跳转到登录页 router.push(/login) return Promise.reject(new Error(token超时了)) } config.headers[Authorization] = store.getters.token } return config // 必须要返回的}, error => { return Promise.reject(error)})// 响应拦截器service.interceptors.response.use(response => { // axios默认加了一层data const { success, message, data } = response.data // 要根据success的成功与否决定下面的操作 if (success) { return data } else { // 业务已经错误了 还能进then ? 不能 ! 应该进catch Message.error(message) // 提示错误消息 return Promise.reject(new Error(message)) }}, error => { Message.error(error.message) // 提示错误信息 return Promise.reject(error) // 返回执行错误 让当前的执行链跳出成功 直接进入 catch})// 超时逻辑 (当前时间 - 缓存中的时间) 是否大于 时间差function IsCheckTimeOut() { var currentTime = Date.now() // 当前时间戳 var timeStamp = getTimeStamp() // 缓存时间戳 return (currentTime - timeStamp) / 1000 > TimeOut}export default service
因为复杂中台项目的页面众多,不可能把所有的业务都集中在一个文件上进行管理和维护,并且还有最重要的,前端的页面中主要分为两部分,一部分是所有人都可以访问的, 一部分是只有有权限的人才可以访问的,拆分多个模块便于更好的控制
在 router 目录下新建目录 modules,在此目录中新建各个路由模块
路由模块目录结构
什么叫临时合并?
动态路由是需要权限进行访问的,但是权限的动态路由访问是很复杂的,我们可以先将 静态路由和动态路由进行合并,不考虑权限问题,后面再解决这个问题
路由主文件 src/router/index.js// 引入多个模块的规则import Layout from @/layoutimport userRouter from ./modules/userimport roleRouter from ./modules/roleimport rightsRouter from ./modules/rightimport goodsRouter from ./modules/goodsimport categoryRouter from ./modules/categoryimport reportsRouter from ./modules/report// 动态路由export const asyncRoutes = [ userRouter, roleRouter, rightsRouter, goodsRouter, categoryRouter, reportsRouter]const createRouter = () => new Router({ scrollBehavior: () => ({ y: 0 }), // 管理滚动行为 如果出现滚动 切换就让 让页面回到顶部 routes: [...constantRoutes, ...asyncRoutes]})Vue.js 允许你自定义过滤器,可被用于一些常见的文本格式化。过滤器可以用在两个地方:双花括号插值和 v-bind 表达式 (后者从 2.1.0+ 开始支持)。过滤器应该被添加在 JavaScript 表达式的尾部,由“管道”符号指示:
<el-table-column label="入职时间" sortable prop="timeOfEntry"> <template slot-scope="obj">{ {obj.row.timeOfEntry | 过滤器}}</template></el-table-column>安装 moment
npm i moment编写过滤器函数
import moment from momentexport function formatTime(value) { return moment(value * 1000).format(YYYY-MM-DD HH:mm:ss)}在 main.js 中全局注册过滤器
import * as filters from @/filtersObject.keys(filters).forEach(key => { Vue.filter(key, filters[key])})在 views/user 目录下新建一个弹层组件 src/views/user/components/add-user.vue
<template> <el-dialog title="新增用户" :visible.sync="dialogVisible" width="50%"> <el-form ref="form" :rules="rules" :model="userForm" label-width="80px"> <el-form-item label="用户名" prop="username"> <el-input v-model="userForm.username" /> </el-form-item> <el-form-item label="密码" prop="password"> <el-input v-model="userForm.password" /> </el-form-item> <el-form-item label="手机号" prop="mobile"> <el-input v-model="userForm.mobile" /> </el-form-item> <el-form-item label="邮箱" prop="email"> <el-input v-model="userForm.email" /> </el-form-item> <el-form-item label="部门" prop="department_title"> <el-input v-model="userForm.department_title" @focus="getAllDepartment"/> <el-tree v-if="showTree" v-loading="loading" :data="treeData" :props="{ label: department_title }" @node-click="handleNodeClick"/> </el-form-item> </el-form> <span slot="footer" class="dialog-footer"> <el-button @click="btnCancel">取 消</el-button> <el-button type="primary" @click="saveUser">确 定</el-button> </span> </el-dialog></template><script> import { getDepartMent } from @/api/department import { tranListToTreeData } from @/utils import { addUser } from @/api/user export default { data() { return { treeData: [], // 存储部门的树形数据 showTree: false, // 部门文本框获取焦点时,设置为true,展示部门信息 loading: false, // 显示或隐藏进度 dialogVisible: false, userForm: { username: , password: , email: , mobile: , department_id: , department_title: }, rules: { username: [ { required: true, trigger: blur, message: 用户名不能为空 }, { min: 6, max: 10, trigger: blur, message: 长度在6-10位之间 } ], password: [ { required: true, trigger: blur, message: 密码不能为空 }, { min: 6, max: 12, trigger: blur, message: 长度在6-12位之间 } ], mobile: [ { required: true, trigger: blur, message: 手机号不能为空 }, { pattern: /^1[3-9]\d{9}$/, trigger: blur, message: 手机号格式不正确 } ], email: [ { required: true, trigger: blur, message: 邮箱不能为空 }, { pattern: /^([a-zA-Z0-9]+[_|\_|\.]?)*[a-zA-Z0-9]+@([a-zA-Z0-9]+[_|\_|\.]?)*[a-zA-Z0-9]+\.[a-zA-Z]{2,3}$/,trigger: blur, message: 邮箱格式不正确 } ], department_title: [{ required: true, message: 部门不能为空, trigger: change }] } } }, methods: { onSubmit() { } } } </script>父组件中引用,弹出层
import AddUser from ./components/add-user<add-user ref="adduser" />点击 新增用户 按钮,弹出弹出层
<el-button size="small" type="primary" @click="adduser">新增用户</el-button>点击按钮展示弹出层的关键,就是设置组件中 el-dialog 组件中的如下属性的值
:visible.sync="dialogVisible"按钮在父组件,变量 dialogVisible在子组件,如何改变? 可以在子组件中的 props 中新建属性
dialogVisible然后父组件中为其赋值
<add-user ref="adduser" :dialog-visible="addDialogVisible" />最后在父组件的 data 中定义变量
addDialogVisible:false但是上面的解决方案有一个问题:点击对话框右上角的 X ,或者“取消”按钮,或者点击其他区域关闭对话框时,会抛出如下错误
**错误原因:**进行上面的几个操作时,会导致自动修改 props 中的 dialogVisible 变量的值,但这是不允许的
**解决方案:**参考上面的实现,直接在父组件中操作子组件中的 data 变量的值第一步,安装全局插件screenfull
$ npm i screenfull第二步,封装全屏显示的插件src/components/ScreenFull/index.vue
<template> <div> <svg-icon icon-class="fullscreen" style="color:#fff; width: 20px; height: 20px" @click="changeScreen" /> </div></template><script>import ScreenFull from screenfullexport default { methods: { changeScreen() { if (!ScreenFull.isEnabled) { this.$message.warning(此时全屏组件不可用) return } ScreenFull.toggle() } }}</script>第三步,全局注册该组件 src/components/index.js
import ScreenFull from ./ScreenFullVue.component(ScreenFull, ScreenFull) // 注册全屏组件第四步,放置于layout/navbar.vue中
<screen-full class="right-menu-item" />.right-menu-item { vertical-align: middle;}第一步, 封装颜色选择组件 ThemePicker 代码地址:@/components/ThemePicker
<template> <el-color-picker v-model="theme" :predefine="[#409EFF, #1890ff, #304156,#212121,#11a983, #13c2c2, #6959CD, #f5222d, ]" class="theme-picker" popper-class="theme-picker-dropdown" /></template><script>const version = require(element-ui/package.json).version // element-ui version from node_modulesconst ORIGINAL_THEME = #409EFF // default colorexport default { data() { return { chalk: , // content of theme-chalk css theme: } }, computed: { defaultTheme() { return this.$store.state.settings.theme } }, watch: { defaultTheme: { handler: function(val, oldVal) { this.theme = val }, immediate: true }, async theme(val) { const oldVal = this.chalk ? this.theme : ORIGINAL_THEME if (typeof val !== string) return const themeCluster = this.getThemeCluster(val.replace(#, )) const originalCluster = this.getThemeCluster(oldVal.replace(#, )) console.log(themeCluster, originalCluster) const $message = this.$message({ message: Compiling the theme, customClass: theme-message, type: success, duration: 0, iconClass: el-icon-loading }) const getHandler = (variable, id) => { return () => { const originalCluster = this.getThemeCluster(ORIGINAL_THEME.replace(#, )) const newStyle = this.updateStyle(this[variable], originalCluster, themeCluster) let styleTag = document.getElementById(id) if (!styleTag) { styleTag = document.createElement(style) styleTag.setAttribute(id, id) document.head.appendChild(styleTag) } styleTag.innerText = newStyle } } if (!this.chalk) { const url = `https://unpkg.com/element-ui@${version}/lib/theme-chalk/index.css` await this.getCSSString(url, chalk) } const chalkHandler = getHandler(chalk, chalk-style) chalkHandler() const styles = [].slice.call(document.querySelectorAll(style)) .filter(style => { const text = style.innerText return new RegExp(oldVal, i).test(text) && !/Chalk Variables/.test(text) }) styles.forEach(style => { const { innerText } = style if (typeof innerText !== string) return style.innerText = this.updateStyle(innerText, originalCluster, themeCluster) }) this.$emit(change, val) $message.close() } }, methods: { updateStyle(style, oldCluster, newCluster) { let newStyle = style oldCluster.forEach((color, index) => { newStyle = newStyle.replace(new RegExp(color, ig), newCluster[index]) }) return newStyle }, getCSSString(url, variable) { return new Promise(resolve => { const xhr = new XMLHttpRequest() xhr.onreadystatechange = () => { if (xhr.readyState === 4 && xhr.status === 200) { this[variable] = xhr.responseText.replace(/@font-face{[^}]+}/, ) resolve() } } xhr.open(GET, url) xhr.send() }) }, getThemeCluster(theme) { const tintColor = (color, tint) => { let red = parseInt(color.slice(0, 2), 16) let green = parseInt(color.slice(2, 4), 16) let blue = parseInt(color.slice(4, 6), 16) if (tint === 0) { // when primary color is in its rgb space return [red, green, blue].join(,) } else { red += Math.round(tint * (255 - red)) green += Math.round(tint * (255 - green)) blue += Math.round(tint * (255 - blue)) red = red.toString(16) green = green.toString(16) blue = blue.toString(16) return `#${red}${green}${blue}` } } const shadeColor = (color, shade) => { let red = parseInt(color.slice(0, 2), 16) let green = parseInt(color.slice(2, 4), 16) let blue = parseInt(color.slice(4, 6), 16) red = Math.round((1 - shade) * red) green = Math.round((1 - shade) * green) blue = Math.round((1 - shade) * blue) red = red.toString(16) green = green.toString(16) blue = blue.toString(16) return `#${red}${green}${blue}` } const clusters = [theme] for (let i = 0; i <= 9; i++) { clusters.push(tintColor(theme, Number((i / 10).toFixed(2)))) } clusters.push(shadeColor(theme, 0.1)) return clusters } }}</script><style>.theme-message,.theme-picker-dropdown { z-index: 99999 !important;}.theme-picker .el-color-picker__trigger { height: 26px !important; width: 26px !important; padding: 2px;}.theme-picker-dropdown .el-color-dropdown__link-btn { display: none;}.el-color-picker { height: auto !important;}</style>import ThemePicker from ./ThemePickerVue.component(ThemePicker, ThemePicker)第二步, 放置于layout/navbar.vue中
<theme-picker class="right-menu-item" />第一步,我们需要首先国际化的包
$ npm i vue-i18n第二步,需要单独一个多语言的实例化文件 src/lang/index.js
import customZH from ./zh // 引入自定义中文包import customEN from ./en // 引入自定义英文包Vue.use(VueI18n) // 全局注册国际化包export default new VueI18n({ locale: Cookie.get(language) || zh, // 从cookie中获取语言类型 获取不到就是中文 messages: { en: { ...elementEN, // 将饿了么的英文语言包引入 ...customEN }, zh: { ...elementZH, // 将饿了么的中文语言包引入 ...customZH } }})第三步,在main.js中对挂载 i18n的插件,并设置element为当前的语言
Vue.use(ElementUI, { i18n: (key, value) => i18n.t(key, value)})new Vue({ el: #app, router, store, i18n, render: h => h(App)})第四步,在左侧菜单应用
<item :icon="onlyOneChild.meta.icon||(item.meta&&item.meta.icon)" :title="$t(route.+onlyOneChild.name)" />第五步,封装多语言组件 src/components/lang/index.vue
<template> <el-dropdown trigger="click" @command="changeLanguage"> <div> <svg-icon style="color:#fff;font-size:20px" icon-class="language" /> </div> <el-dropdown-menu slot="dropdown"> <el-dropdown-item command="zh" :disabled="zh=== $i18n.locale ">中文</el-dropdown-item> <el-dropdown-item command="en" :disabled="en=== $i18n.locale ">en</el-dropdown-item> </el-dropdown-menu> </el-dropdown></template><script>import Cookie from js-cookieexport default { methods: { changeLanguage(lang) { Cookie.set(language, lang) // 切换多语言 this.$i18n.locale = lang // 设置给本地的i18n插件 this.$message.success(切换多语言成功) } }}</script>第六步,在Navbar组件中引入
<lang class="right-menu-item" />hash模式 : #后面是路由路径,特点是前端访问,#后面的变化不会经过服务器
history模式:正常的/访问模式,特点是后端访问,任意地址的变化都会访问服务器
改成history模式非常简单,只需要将路由的mode类型改成history即可
const createRouter = () => new Router({ mode: history, // require service support scrollBehavior: () => ({ y: 0 }), // 管理滚动行为 如果出现滚动 切换就让 让页面回到顶部 routes: [...constantRoutes] // 改成只有静态路由})先找到 vue.config.js, 添加 externals 让 webpack 不打包 xlsx 和 element
externals: { vue: Vue, element-ui: ELEMENT, xlsx: XLSX }但是请注意,这时的配置实际上是对开发环境和生产环境都生效的,在开发环境时,没有必要使用CDN,此时我们可以使用环境变量来进行区分
let cdn = { css: [], js: [] }// 通过环境变量 来区分是否使用cdnconst isProd = process.env.NODE_ENV === production // 判断是否是生产环境let externals = {}if (isProd) { // 如果是生产环境 就排除打包 否则不排除 externals = { // key(包名) / value(这个值 是 需要在CDN中获取js, 相当于 获取的js中 的该包的全局的对象的名字) vue: Vue, // 后面的名字不能随便起 应该是 js中的全局对象名 element-ui: ELEMENT, // 都是js中全局定义的 xlsx: XLSX // 都是js中全局定义的 } cdn = { css: [ https://unpkg.com/element-ui/lib/theme-chalk/index.css // 提前引入elementUI样式 ], // 放置css文件目录 js: [ https://unpkg.com/vue/dist/vue.js, // vuejs https://unpkg.com/element-ui/lib/index.js, // element https://cdn.jsdelivr.net/npm/xlsx@0.16.6/dist/xlsx.full.min.js, // xlsx 相关 https://cdn.jsdelivr.net/npm/xlsx@0.16.6/dist/jszip.min.js // xlsx 相关 ] // 放置js文件目录 }}之后通过 html-webpack-plugin注入到 index.html之中:
config.plugin(html).tap(args => { args[0].cdn = cdn return args})找到 public/index.html。通过你配置的CDN Config 依次注入 css 和 js。
<head> <!-- 引入样式 --> <% for(var css of htmlWebpackPlugin.options.cdn.css) { %> <link rel="stylesheet" href="<%=css%>"> <% } %></head><!-- 引入JS --><% for(var js of htmlWebpackPlugin.options.cdn.js) { %> <script src="<%=js%>"></script><% } %>最后,进行打包
$ npm run build:prod,https://blog.csdn.net/chuenst/article/details/117065028
举报/反馈