diff --git a/README.md b/README.md index 0962275..6a3350c 100644 --- a/README.md +++ b/README.md @@ -1,87 +1,56 @@ -# vue-blog-template +# vueBlog-template -[![image](https://img.shields.io/badge/vue-2.6.8-brightgreen.svg)](https://github.com/vuejs/vue) -[![image](https://img.shields.io/badge/vue--router-3.0.2-brightgreen.svg)](https://github.com/vuejs/vue-router) -[![image](https://img.shields.io/badge/vuex-3.1.0-brightgreen.svg)](https://github.com/vuejs/vuex) -[![image](https://img.shields.io/badge/vue--cli-3.x-brightgreen.svg)](https://cli.vuejs.org/zh/) -[![image](https://img.shields.io/badge/element--ui-2.7.0-9cf.svg)](https://github.com/ElemeFE/element) -[![GitHub release](https://img.shields.io/github/release/uncleLian/vue-blog-template.svg)](https://github.com/uncleLian/vue-blog/releases) +![image](https://img.shields.io/badge/vue-2.6.8-green.svg) +![image](https://img.shields.io/badge/vue--router-3.0.2-green.svg) +![image](https://img.shields.io/badge/vuex-3.1.0-green.svg) +![image](https://img.shields.io/badge/element--ui-2.10.0-blue.svg) -##### 注:master分支基于 vue-cli-3.x,vue-cli-2.x请移步到v1.0分支 > 这是一个极简的管理后台模板,它只包含了搭建管理后台的一些必要功能 +##### 注:master分支基于 vue-cli-3.x,vue-cli-2.x请移步到[v1.0分支](https://github.com/uncleLian/vueBlog-template/tree/v1.0) + - [在线演示](http://template.liansixin.win) -- [使用文档](https://unclelian.github.io/vue-blog-docs/) +- [使用文档](http://liansixin.win/vue-blog-book) - + ## 功能 -功能持续迭代中,欢迎 [pr](https://github.com/uncleLian/vue-blog/pulls) 和 [issue](https://github.com/uncleLian/vue-blog/issues) - -``` -- 登录/注销 -- 权限验证 - - 页面级权限 - - 按钮级权限 -- 多环境 - - dev - - prod - - stage -- 动态侧边栏 -- 动态面包屑 -- 错误处理 - - 401 - - 404 - - 错误日志 -- 其他处理 - - axios封装 - - cache封装 - - 页面平滑过渡 - - css预处理器全局变量 - - 包体积优化 -- svg icon / iconfont -- mock -- 进度条 -``` - -## 开发 +- [x] 登录/注销 +- [x] 权限验证(页面级) +- [x] 动态侧边栏 +- [x] 动态面包屑 +- [x] 导航标签 +- [x] 401、404、全局错误捕捉 +- [x] 多环境(dev、sit、prod) +- [x] svg icon / iconfont +- [x] 进度条 +- [x] element-ui +- [x] axios封装(统一处理请求、拦截、报错等) +- [x] cache封装 + + +## 开发和发布 ```bash # 克隆项目 -git clone https://github.com/uncleLian/vue-blog-template.git +git clone https://github.com/uncleLian/vueBlog-template.git # 安装依赖 npm install -# 启动服务:localhost:8003 +# 启动服务 npm run dev -``` - -## 发布 -```bash -# 构建测试环境 -npm run build:stage -# 构建生产环境 +# 发布 npm run build -# 查看构建报告 +# 测试环境 +npm run build:stage + +# 报告 npm run build:report ``` -## 版本日志 -[发行说明](https://github.com/uncleLian/vue-blog-template/releases)中记录了每个版本的详细更改。 - -## Browsers support -| [IE / Edge](http://godban.github.io/browsers-support-badges/)
IE / Edge | [Firefox](http://godban.github.io/browsers-support-badges/)
Firefox | [Chrome](http://godban.github.io/browsers-support-badges/)
Chrome | [Safari](http://godban.github.io/browsers-support-badges/)
Safari | [Opera](http://godban.github.io/browsers-support-badges/)
Opera | -| --------- | --------- | --------- | --------- | --------- | -| IE10, IE11, Edge| last 2 versions| last 2 versions| last 2 versions| last 2 versions - -## 捐赠 -如果觉得这个项目帮助到了你,你可以请作者喝杯饮料表示支持 :green_heart: - -![image](http://poci6sbqi.bkt.clouddn.com/donate.jpg) - ## 交流 欢迎热爱学习、忠于分享的朋友一起来交流 - Vue交流群:338241465 diff --git a/package.json b/package.json index 22575ee..3632cba 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vue-blog-template", - "version": "2.0.0", + "version": "2.1.0", "private": true, "scripts": { "dev": "vue-cli-service serve", @@ -11,7 +11,7 @@ }, "dependencies": { "axios": "^0.18.0", - "element-ui": "^2.7.2", + "element-ui": "^2.10.0", "js-cookie": "^2.2.0", "mockjs": "^1.0.1-beta3", "normalize.css": "^8.0.1", diff --git a/src/App.vue b/src/App.vue index 92e2b25..2681a29 100755 --- a/src/App.vue +++ b/src/App.vue @@ -1,6 +1,6 @@ diff --git a/src/api/login.js b/src/api/login.js index 883b951..4638767 100644 --- a/src/api/login.js +++ b/src/api/login.js @@ -7,6 +7,6 @@ export function getLogin(form) { } // 用户信息 export function getUser(token) { - let res = request('/api/user', 'GET', token) + let res = request('/api/user', 'POST', token) return res } diff --git a/src/components/UserSelect/index.vue b/src/components/UserSelect/index.vue index 73ed935..2a1713f 100644 --- a/src/components/UserSelect/index.vue +++ b/src/components/UserSelect/index.vue @@ -14,9 +14,9 @@ import { mapState } from 'vuex' export default { computed: { - ...mapState([ - 'user' - ]), + ...mapState('login', { + user: state => state.user + }), version() { return 'v' + require('../../../package.json').version } @@ -24,8 +24,9 @@ export default { methods: { onSelected(val) { if (val === 'exit') { - this.$store.commit('SET_LOGOUT') - this.$router.push('/login') + this.$store.dispatch('login/logout').then(() => { + this.$router.push('/login') + }) } } } diff --git a/src/components/index.js b/src/components/index.js index 93847e4..19c878c 100755 --- a/src/components/index.js +++ b/src/components/index.js @@ -1,8 +1,8 @@ import Vue from 'vue' -import view from '@/layout/view' +import PageView from '@/layout/PageView' const components = { - 'app-view': view + 'app-pageView': PageView } // 注册全局组件 diff --git a/src/filters/index.js b/src/filters/index.js index 22a768c..7a4c8db 100644 --- a/src/filters/index.js +++ b/src/filters/index.js @@ -1,9 +1,6 @@ import Vue from 'vue' const filters = { - devide_10k: function (num) { - return num >= 10000 ? (num / 10000).toFixed(1) + '万' : num - }, // 时间格式化 formatTime: function (time, formatType) { if (arguments.length === 0) { diff --git a/src/layout/NavBar/index.vue b/src/layout/NavBar/index.vue new file mode 100644 index 0000000..e0aeef3 --- /dev/null +++ b/src/layout/NavBar/index.vue @@ -0,0 +1,63 @@ + + + diff --git a/src/layout/view.vue b/src/layout/PageView/index.vue similarity index 100% rename from src/layout/view.vue rename to src/layout/PageView/index.vue diff --git a/src/layout/Sidebar/index.vue b/src/layout/Sidebar/index.vue index d7be2dc..8689d60 100755 --- a/src/layout/Sidebar/index.vue +++ b/src/layout/Sidebar/index.vue @@ -11,7 +11,6 @@ + diff --git a/src/layout/TagsView/scrollPane.vue b/src/layout/TagsView/scrollPane.vue new file mode 100644 index 0000000..96e88c8 --- /dev/null +++ b/src/layout/TagsView/scrollPane.vue @@ -0,0 +1,56 @@ + + diff --git a/src/layout/header.vue b/src/layout/header.vue deleted file mode 100755 index 0a9c2ac..0000000 --- a/src/layout/header.vue +++ /dev/null @@ -1,72 +0,0 @@ - - - diff --git a/src/pages/index/children/dashboard/index.vue b/src/pages/index/children/dashboard/index.vue new file mode 100644 index 0000000..c8a07b1 --- /dev/null +++ b/src/pages/index/children/dashboard/index.vue @@ -0,0 +1,22 @@ + + + diff --git a/src/pages/index/children/home/home.vue b/src/pages/index/children/user/index.vue similarity index 75% rename from src/pages/index/children/home/home.vue rename to src/pages/index/children/user/index.vue index 95a675d..59a0266 100644 --- a/src/pages/index/children/home/home.vue +++ b/src/pages/index/children/user/index.vue @@ -1,22 +1,25 @@ diff --git a/src/pages/index/index.vue b/src/pages/index/index.vue index 1b7e30c..6f414c0 100755 --- a/src/pages/index/index.vue +++ b/src/pages/index/index.vue @@ -1,24 +1,29 @@ @@ -35,7 +40,15 @@ export default { height: 100%; transition: width 0.28s; } - #main { + #app-header { + position: relative; + width: 100%; + padding: 0; + background-color: #fff; + box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12), 0 0 3px 0 rgba(0, 0, 0, 0.04); + z-index: 1000; + } + #app-main { position: relative; width: 100%; background: #fff; diff --git a/src/pages/login/login.vue b/src/pages/login/login.vue index bbd90e6..e50f23b 100755 --- a/src/pages/login/login.vue +++ b/src/pages/login/login.vue @@ -49,9 +49,9 @@ export default { login() { let successMsg = '登录成功' let errorMsg = '账号或密码错误' - this.$store.dispatch('GET_LOGIN_DATA', this.form).then((res) => { + this.$store.dispatch('login/getLoginToken', this.form).then((res) => { this.$message.success(successMsg) - this.$route.query.redirect ? this.$router.push(this.$route.query.redirect) : this.$router.push('/') + this.$route.query.redirect ? this.$router.push(this.$route.query.redirect) : this.$router.push('/index') }).catch((err) => { console.log(err) this.$message.error(errorMsg) diff --git a/src/pages/other/redirect.vue b/src/pages/other/redirect.vue new file mode 100644 index 0000000..a5998da --- /dev/null +++ b/src/pages/other/redirect.vue @@ -0,0 +1,12 @@ + diff --git a/src/router/index.js b/src/router/index.js index 35b41bd..2af2d62 100755 --- a/src/router/index.js +++ b/src/router/index.js @@ -2,100 +2,78 @@ import Vue from 'vue' import Router from 'vue-router' // 视图组件 -// const view = () => import('@/layout/view') +// const PageView = () => import('@/layout/PageView') Vue.use(Router) -/* sideRoutes config +/* Routes Config * @meta * icon: '' 菜单图标(支持svg-icon、el-icon) * title: '' 菜单标题 * login: false 是否需要登录 * role: 'admin' || ['admin'] 是否需要权限 -* keep: false 是否需要缓存 +* keep: false 是否需要缓存(需要name才能生效) * hidden: false 是否显示在菜单 * open: false 是否展开菜单(有子菜单前提下) * redirectIndex: 0 重定向到第index位子菜单(有子菜单前提下) +* affix: false 是否常驻在tagView组件上(外链无效) */ -// 要在侧边栏渲染的路由 -export const sideRoutes = [ +// 异步路由 +export const asyncRoutes = [ { - name: 'home', - path: 'home', - component: () => import('@/pages/index/children/home/home'), + name: 'dashboard', + path: 'dashboard', + component: () => import('@/pages/index/children/dashboard'), meta: { icon: 'dashboard', - title: '主页' + title: '主页', + affix: true + } + }, + { + name: 'user', + path: 'user', + component: () => import('@/pages/index/children/user'), + meta: { + icon: 'user', + title: '用户管理' } } ] -const routes = setRedirect([ +// 本地路由 +export const localRoutes = [ { path: '', - redirect: '/index' + redirect: '/login' }, { - name: 'index', - path: '/index', - component: () => import('@/pages/index/index'), - meta: { - title: '首页', - login: true - }, - children: sideRoutes - }, - { - name: 'login', path: '/login', component: () => import('@/pages/login/login') }, { - name: 'page401', path: '/page401', component: () => import('@/pages/other/page401') }, { - name: 'page404', path: '/page404', component: () => import('@/pages/other/page404') - }, - { - path: '*', - redirect: '/page404' } -]) +] -export default new Router({ +const createRouter = () => new Router({ // mode: 'history', - routes, - scrollBehavior(to, from, savedPosition) { - if (savedPosition) { - return savedPosition - } else { - return { x: 0, y: 0 } - } - } + routes: localRoutes, + scrollBehavior: () => ({ y: 0 }) }) -// 自动设置路由的重定向(有子路由前提下) -function setRedirect(routes, redirect = '') { - routes.forEach(route => { - if (route.children && route.children.length > 0) { - if (!route.redirect) { - let defaultRedirectRoute = route.children.filter(item => !item.meta || !item.meta.hidden)[0] - let redirectIndex = route.meta && route.meta.redirectIndex - if (redirectIndex) { - defaultRedirectRoute = route.children[redirectIndex] - } - let redirectName = defaultRedirectRoute.name - route.redirect = `${redirect}/${route.name}/${redirectName}` - } - let index = route.redirect && route.redirect.lastIndexOf('/') - let fatherDir = route.redirect && route.redirect.substring(0, index) - route.children = setRedirect(route.children, fatherDir) - } - }) - return routes +const router = createRouter() + +// Detail see: https://github.com/vuejs/vue-router/issues/1234#issuecomment-357941465 +export function resetRouter() { + const newRouter = createRouter() + router.matcher = newRouter.matcher // reset router } + +export default router diff --git a/src/store/index.js b/src/store/index.js index 6b0462d..98d4f73 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -1,13 +1,11 @@ import Vue from 'vue' import Vuex from 'vuex' import cache from '@/utils/cache' -import { getLogin, getUser } from '@/api/login' Vue.use(Vuex) const state = { logs: [], - user: '', sidebarStatus: cache.getCookie('sidebarStatus') !== 'false' } const getters = { @@ -16,13 +14,6 @@ const mutations = { SET_LOGS(state, error) { state.logs.unshift(error) }, - SET_USER(state, val) { - state.user = val - }, - SET_LOGOUT(state) { - state.user = '' - cache.removeToken() - }, SET_SIDEBAR_STATUS(state) { let status = !state.sidebarStatus state.sidebarStatus = status @@ -30,42 +21,21 @@ const mutations = { } } const actions = { - // 获取登录数据 - async GET_LOGIN_DATA({ commit }, params) { - return new Promise((resolve, reject) => { - getLogin(params).then(res => { - // console.log('login', res) - if (res && res.token) { - cache.setToken(res.token) - resolve(res) - } else { - reject(new Error('nothing login data')) - } - }).catch(err => { - reject(err) - }) - }) - }, - // 获取用户数据 - async GET_USER_DATA({ commit }, token) { - return new Promise((resolve, reject) => { - getUser(token).then(res => { - // console.log('user', res) - if (res && res.code === 200 && res.data) { - commit('SET_USER', res.data) - resolve(res.data) - } else { - reject(new Error('nothing user data')) - } - }).catch(err => { - reject(err) - }) - }) - } } + +// 自动引入和注册modules下的文件 +const modulesFiles = require.context('./modules', false, /\.js$/) +const modules = modulesFiles.keys().reduce((modules, modulePath) => { + const moduleName = modulePath.replace(/^\.\/(.*)\.\w+$/, '$1') + const value = modulesFiles(modulePath) + modules[moduleName] = value.default + return modules +}, {}) + export default new Vuex.Store({ state, getters, mutations, - actions + actions, + modules }) diff --git a/src/store/modules/login.js b/src/store/modules/login.js new file mode 100644 index 0000000..b4688a2 --- /dev/null +++ b/src/store/modules/login.js @@ -0,0 +1,58 @@ +import { getLogin, getUser } from '@/api/login' +import { resetRouter } from '@/router' +import cache from '@/utils/cache' + +export default { + namespaced: true, + state: { + user: '' + }, + mutations: { + SET_USER(state, val) { + state.user = val + } + }, + actions: { + // 获取登录数据 + async getLoginToken({ commit }, params) { + return new Promise((resolve, reject) => { + getLogin(params).then(res => { + // console.log('login', res) + if (res && res.token) { + cache.setToken(res.token) + resolve(res) + } else { + reject(new Error('nothing login data')) + } + }).catch(err => { + reject(err) + }) + }) + }, + // 获取用户数据 + async getUserData({ commit }) { + return new Promise((resolve, reject) => { + let token = cache.getToken() + getUser(token).then(res => { + // console.log('user', res) + if (res.data) { + commit('SET_USER', res.data) + resolve(res.data) + } else { + reject(new Error('nothing user data')) + } + }).catch(err => { + reject(err) + }) + }) + }, + logout({ commit }) { + return new Promise((resolve, reject) => { + cache.removeToken() + resetRouter() + commit('SET_USER', '') + resolve() + }) + } + } +} diff --git a/src/store/modules/routes.js b/src/store/modules/routes.js new file mode 100644 index 0000000..cc5830f --- /dev/null +++ b/src/store/modules/routes.js @@ -0,0 +1,73 @@ +import { localRoutes, asyncRoutes } from '@/router' + +export default { + namespaced: true, + state: { + allRoutes: [], // 全部路由 + sideRoutes: [] // 侧边栏路由 + }, + mutations: { + SET_ALL_ROUTES: (state, routes) => { + state.allRoutes = [...routes] + }, + SET_SIDE_ROUTES: (state, routes) => { + state.sideRoutes = [...routes] + } + }, + actions: { + generateRoutes({ commit }) { + return new Promise(resolve => { + asyncParentRoutes.children = [...asyncRoutes, extraPanentRoutes] + const sideRoutes = setRedirect([asyncParentRoutes]) + const addRoutes = [...sideRoutes, extraGlobalRoutes] // 实际动态添加的路由 + const allRoutes = [...localRoutes, ...addRoutes] // 所有路由 + commit('SET_SIDE_ROUTES', sideRoutes) + commit('SET_ALL_ROUTES', allRoutes) + resolve(addRoutes) + }) + } + } +} + +const asyncParentRoutes = { + name: 'index', + path: '/index', + component: () => import('@/pages/index/index'), + meta: { + login: true, + title: '首页' + }, + children: [] +} +const extraPanentRoutes = { + path: 'redirect/:path*', + component: () => import('@/pages/other/redirect'), + meta: { + hidden: true + } +} +const extraGlobalRoutes = { + path: '*', + redirect: '/page404' +} + +// 自动设置路由的重定向(有子路由前提下) +function setRedirect(routes, redirect = '') { + routes.forEach(route => { + if (route.children && route.children.length > 0) { + if (!route.redirect) { + let defaultRedirectRoute = route.children.filter(item => !item.meta || !item.meta.hidden)[0] + let redirectIndex = route.meta && route.meta.redirectIndex + if (redirectIndex) { + defaultRedirectRoute = route.children[redirectIndex] + } + let redirectName = defaultRedirectRoute.name + route.redirect = `${redirect}/${route.name}/${redirectName}` + } + let index = route.redirect && route.redirect.lastIndexOf('/') + let fatherDir = route.redirect && route.redirect.substring(0, index) + route.children = setRedirect(route.children, fatherDir) + } + }) + return routes +} diff --git a/src/store/modules/tagsView.js b/src/store/modules/tagsView.js new file mode 100644 index 0000000..af926ea --- /dev/null +++ b/src/store/modules/tagsView.js @@ -0,0 +1,72 @@ +import path from 'path' +import { isExternal } from '@/utils/validate' + +export default { + namespaced: true, + state: { + tagsView: [] + }, + mutations: { + SET_TAGS_VIEW(state, views) { + state.tagsView = [...views] + }, + ADD_TAGS_VIEW(state, view) { + if (view.name && view.meta && !view.meta.hidden) { + if (state.tagsView.some(v => v.path === view.path)) return + state.tagsView.push({ ...view }) + } + }, + CLOSE_TAGS_VIEW(state, view) { + let index = state.tagsView.findIndex(v => v.path === view.path) + state.tagsView.splice(index, 1) + }, + CLOSE_OTHER_TAGS_VIEW(state, view) { + state.tagsView = state.tagsView.filter(v => v.meta.affix || v.path === view.path) + }, + CLEAR_TAGS_VIEW(state) { + state.tagsView = state.tagsView.filter(v => v.meta.affix) + } + }, + actions: { + initTagsView({ commit, rootState }) { + let affixTags = filterAffixTags(rootState.routes.sideRoutes[0].children) + commit('SET_TAGS_VIEW', affixTags) + function filterAffixTags(routes, basePath = '/index') { + let tags = [] + routes.forEach(route => { + // 过滤外链 + if (!isExternal(route.path)) { + let tagPath = path.resolve(basePath, route.path) + if (route.meta && route.meta.affix) { + tags.push({ + fullPath: tagPath, + path: tagPath, + name: route.name, + meta: { ...route.meta } + }) + } + if (route.children) { + const tempTags = filterAffixTags(route.children, tagPath) + if (tempTags.length >= 1) { + tags = [...tags, ...tempTags] + } + } + } + }) + return tags + } + }, + closeTagsView({ state, commit }, view) { + return new Promise(resolve => { + commit('CLOSE_TAGS_VIEW', view) + resolve([...state.tagsView]) + }) + }, + clearTagsView({ state, commit }, view) { + return new Promise(resolve => { + commit('CLEAR_TAGS_VIEW', view) + resolve([...state.tagsView]) + }) + } + } +} diff --git a/src/utils/permission.js b/src/utils/permission.js index 4273d9a..bec204d 100755 --- a/src/utils/permission.js +++ b/src/utils/permission.js @@ -5,39 +5,39 @@ import cache from '@/utils/cache' // 登录验证,权限验证 router.beforeEach((to, from, next) => { // 是否需要登录 - if (to.matched.some(record => record.meta.login)) { - if (cache.getToken()) { - if (to.path === '/login') { - next('/') + if (cache.getToken()) { + if (to.path === '/login') { + next('/index') + } else { + // 是否已有用户信息 + let userInfo = store.state.login.user + if (userInfo) { + assessPermission(userInfo.roles, to.meta.roles, next) } else { - // 是否已有用户信息 - if (store.state.user) { - assessPermission(store.state.user.role, to.meta.role, next) - } else { - store.dispatch('GET_USER_DATA').then(res => { - assessPermission(res.role, to.meta.role, next) - }).catch(err => { - console.log(err) - // 可根据错误信息,做相应需求,这里默认token值失效 - window.alert('登录已失效,请重新登录') - store.commit('SET_LOGOUT') - next({ path: '/login', query: { redirect: to.fullPath } }) + store.dispatch('login/getUserData').then(res => { + store.dispatch('routes/generateRoutes').then(addRoutes => { + router.addRoutes(addRoutes) + next({ ...to, replace: true }) }) - } + }).catch(err => { + console.log(err) + // 可根据错误信息,做相应需求,这里默认token值失效 + window.alert('登录已失效,请重新登录') + store.commit('login/SET_LOGOUT') + next({ path: '/login', query: { redirect: to.fullPath } }) + }) } - } else { - next({ path: '/login', query: { redirect: to.fullPath } }) } } else { - if (to.path === '/login' && cache.getToken()) { - next('/') - } else { + if (to.path === '/login') { next() + } else { + next({ path: '/login', query: { redirect: to.fullPath } }) } } }) -// 验证权限 +// 验证权限(页面级) function assessPermission(userRole, pageRole, next) { let pass = false // 页面无需权限 || 用户是管理员 diff --git a/src/utils/request.js b/src/utils/request.js index 77c46fd..f552ccf 100755 --- a/src/utils/request.js +++ b/src/utils/request.js @@ -52,8 +52,14 @@ export const request = async (url = '', type = 'GET', data = {}, isForm = false) if (isForm) { let form = new FormData() Object.keys(data).forEach(key => { - console.log('key', key) - form.append(key, data[key]) + let value = data[key] + if (Array.isArray(value)) { + value.forEach(item => { + form.append(key, item) + }) + } else { + form.append(key, data[key]) + } }) data = form } diff --git a/src/utils/validate.js b/src/utils/validate.js new file mode 100644 index 0000000..4393187 --- /dev/null +++ b/src/utils/validate.js @@ -0,0 +1,7 @@ +/** + * @param {String} path + * @returns {Boolean} + */ +export function isExternal(path) { + return /^(https?:|mailto:|tel:)/.test(path) +}