zoukankan      html  css  js  c++  java
  • vue后台管理系统项目

    项目介绍

    1.项目根目录文件

    2.源码子目录结构

    3.api目录

    4.assets目录

    5.components目录

    6.mixins目录

    7.permission目录

    8.router目录

    9.store目录

    10.styles目录

    11.utils目录

    项目文件介绍

    1.安装element-ui组件实现按需加载

    // 1.1.npm i element-ui -S
    
    // 1.2.在babel.config.js中配置
    
    module.exports = {
    
        plugins: [
    
            [
                  'component',
                  {
                    libraryName: 'element-ui',
                    styleLibraryName: 'theme-chalk'
                  }
            ],
        ]
         
    }
    
    // utils下新建文件Element.js按需加载组件
    
    import Vue from 'vue'
    
    // 按需引入
    
    import {
    
      Button,
    
    } from 'element-ui'
    
    // 注册组件,之后就可以在所有组件中使用
    
    Vue.use(Button)
    
    /*
        注意:列如消息类组件,如:Message,Notification,MessageBox等
        
        不能像上面一样注册使用,否则还没有使用该组件该组件就会被初始化
    */
    
    
    // 解决:绑定到vue原型上
    
    Vue.prototype.$message = Message
    Vue.prototype.$notify = Notification
    Vue.prototype.$confirm = MessageBox
    
    // 再组件使用时直接this调用即可
    
    this.$message.success('注册成功')
    
    // 如果想使用MessageBox中的确认弹框
    
    this.$confirm.confirm()

    2.封装二次请求使用起来更方便,在utils中新建request.js中写入

    // 安装第三方请求工具axios
    
    npm install axios
    
    // request.js
    
    import axios from 'axios'
    
    // 处理javascript大数字的问题
    
    // 安装 npm i json-bigint
    
    import jsonBig from 'json-bigint'
    
    
    import store from '@/store/index'
    
    
    // element-ui的加载组件和消息提示组件
    import { message, Loading } from 'element-ui'
    
    // 是我们项目的配置文件,项目的接口和其他的配置放在utils目录中的config.js中
    
    import { config, loaddingConfig } from '@/utils/config'
    
    // 创建一个新的axios对象供我们使用
    const request = axios.create({
    
      baseURL: `${config.baseURL}${config.prot}`,
      timeout: config.timeout
    })
    
    
    // 请求拦截器
    
    // 注意在单独的js中使用Loading和在组件中使用有一点不同
    
    /*
    
        1.在组件中使用
    
        const loading = this.$loading({
              lock: true,
              text: 'Loading',
              spinner: 'el-icon-loading',
              background: 'rgba(0, 0, 0, 0.7)'
         });
    
    
        关闭加载
    
        loading.close()
    
    
        2.在单独的js文件中使用
    
        Loading.service({
    
             lock: true,
             text: 'Loading',
             spinner: 'el-icon-loading',
             background: 'rgba(0, 0, 0, 0.7)'
        })
    
        关闭加载状态
        
        Loading.service().close()
    
    */ 
    
    request.interceptors.request.use(config => {
      
      // 开始加载
      Loading.service(loaddingConfig)
    
      if (store.getters.getuserInfo.token) {
    
        // 每次登陆的时候将token插入到请求头中
        config.headers.authorization = store.getters.getuserInfo.token
      }
    
      return config
    
    }, error => {
    
      // 取消失败终止
      Loading.service().close()
      return Promise.reject(error)
    })
    
    
    
    // 处理大数字的问题
    request.defaults.transformResponse = [function (data) {
      try {
        return jsonBig.parse(data)
      } catch (err) {
        return data
      }
    }]
    
    
    
    // 响应拦截器
    request.interceptors.response.use(response => {
    
      // 请求完毕取消加载
      Loading.service().close()
      return response
    }, error => {
    
      Loading.service().close()
    
        // 处理请求错误的函数
      errorMsg(error.response)
    
      return Promise.reject(error)
    })
    
    
    // 异常处理
    function errorMsg (error) {
    
      if (error) {
    
        return
      }
    
      switch (error.status) {
        case 400:
          message.error('亲,您查看的资料出现了错误')
          break
        case 401:
          message.error('请检查token,可能已经过期,需要重新登陆')
          tokenOverdue(error)
          break
        case 403:
          message.error('抱歉您的权限还需要升级')
          break
        case 404:
          message.error('资源被狗狗调走了')
          break
        case 408:
          message.error('请求超时,请重试')
          break
        case 500:
          message.error('可爱的服务器好像奔溃了')
          break
        case 502:
          message.error('请仔细检查您的网络')
          break
        case 504:
          message.error('您的网络很慢,已经超时,请重试')
          break
        case 503:
          message.error('当前服务不支持')
          break
        default:
          message.error('与服务器的连接断开了')
          break
      }
    }
    
    
    // token的过期处理
    
    function tokenOverdue (error) {
      // 如果有token的话重新存储,刷新
    
      if (error.data.token) {
        // 将刷新的token重新存储到本地即可
        const userInfo = {
          ...store.getters.getuserInfo,
          token: error.data.token
        }
        
        // 将新的token重新存储到本地,
        store.commit('SET_USERINFO', userInfo)
        
        // 将错误的请求再重新发送刷新token
        return request(error.config)
      }
    }
    
    
    // 导出请求接口封装函数
    export default (method, url, data = null) => {
      method = method.toUpperCase()
    
      if (method === 'POST') {
        return request.post(url, data)
      } else if (method === 'GET') {
        return request.get(url, { params: data })
      } else if (method === 'DELETE') {
        return request.delete(url, { params: data })
      } else if (method === 'PUT') {
        return request.put(url, data)
      }
    }

    3.创建api根目录,里面当接口文件

    // user.js文件
    
    import request from '@/utils/request'

    // 用户登陆 export const userLogin = params => request('GET', '/admin/user/login', params) // 用户注册 export const userRegister = params => request('GET', '/admin/user/register', params) // 获取用户的权限数据 export const getUserAuth = params => request('GET', '/admin/user/user_auth', params) // 获取用户头像 export const getUserImg = params => request('GET', '/admin/user/user_img', params) // 获取用户登陆时的头像 export const getLoginImg = params => request('GET', '/admin/user/login_img', params) // 修改用户资料 export const editUserInfo = data => request('POST', '/admin/user/edit_user', data)

    4.require.context(path, Boolean, file)动态加载组件

      1.为什么使用下一步components目录中的组件要引入使用,那么就存在一个问题,如果我们一个页面要引入很多组件那么就会有很多的import。

         此时可以通过使用require.context(path, Boolean, file) 之后就可以通过组件的name属性来调用组件

    // 在components目录下新建 cptsRegister.js
    
    // cptsRegister.js
    
    import Vue from 'vue'
    
    function capitalizeFirstLetter (str) {
      return str.charAt(0).toUpperCase() + str.slice(1)
    }
    
    // 用来匹配.vue的前缀函数
    function validateFileName (str) {
      return /^S+.vue$/.test(str) && str.replace(/^S+/(w+).vue$/, (res, $1) => capitalizeFirstLetter($1))
    }
    
    const requireComponent = require.context('./', true, /.vue$/)
    
    // 遍历匹配到的文件夹及文件名,并且遍历得到每一个
    requireComponent.keys().forEach(filePath => {
    
      // 得到每一个.vue文件中的属性方法和组件的name值
      const componentConfig = requireComponent(filePath)
      // 得到文件名的前半部分index
      const fileName = validateFileName(filePath)
      // 判断如果是以index开头命名的就返回组件的name值来注册组件,否则就使用文件名来注册
    
      const componentName = fileName.toLowerCase() === 'index' ? 
    capitalizeFirstLetter(componentConfig.default.name) : fileName
      Vue.component(componentName, componentConfig.default || componentConfig)
    })
    
    
    // components目录下的cheking目录的vue
    
    <template>
    
        <div class="container">
            我是一个组件
        </div>
    
    </template>
    
    <script>
    
    export default {
    
        name: 'Checking',
        
    }
    
    </script>
    
    
    // 其他组件中使用,直接使用,无需引入和注册,但是必须和组件的name名字保持一致,是不是很棒
    
    <Checking />

    5.处理一下过滤器把

    // 在utils目录下创建filters.js文件,我们一次性注册掉所有的过滤器
    
    // filter.js
    
    // 事件格式化第三方插件
    import moment from 'moment'
    
    const filters = {
    
      relativeTime (value) {
        return moment(value).startOf('second').fromNow()
      },
    
      formatTime (value, format = 'YYYY-MM-DD HH:mm:ss') {
        return moment(value).format(format)
      },
    
      statusFilter (status) {
        const statusText = ['审核通过', '草稿', '待审核', '审核失败']
    
        if (status === undefined) return []
    
        if (status.length) {
    
          status.forEach(item => {
            
            item.statusContent = statusText[item.status]
          })
        }
        return status
      }
    }
    
    export default filters
    
    
    // 之后在main.js中一次性注册
    
    
    import filterRegister from './utils/filters'
    
    for (const key in filterRegister) {
    
      Vue.filter(key, filterRegister[key])
    }

    6.图片懒加载( vue-lazyload )

    // 安装 vue-lazyload 插件
    
    npm i vue-lazyload
    
    // 在main.js中引入使用
    
    import VueLazyload from 'vue-lazyload'
    
    // 引入图片懒加载的配置文件
    import { lazyLoad } from './utils/config'
    
    
    // 注册使用
    Vue.use(VueLazyload, lazyLoad)
    
    // 在组件中img标签上的src属性替换成v-lazy="reqquire('../../assets/images')",
    注意图片的地址必须是网络地址,如果是本地地址需要跳过require()导入使用
    
    
    // config中的配置
    
    // 图片懒加载的配置
    export const lazyLoad = {
    
      preLoad: 1.3,
    
      // 加载中显示的图片
      loading: 'https://ss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=2843662159,2317606805&fm=16&gp=0.jpg',
    
      // 加载失败显示的图片
      error: 'https://ss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=104394466,2625116464&fm=11&gp=0.jpg',
    
      // 尝试加载一次
      attempt: 1
    }

    7.路由切换进度条(nprogress

    // 安装进度条插件
    
    npm i nprogress
    
    // 在全局路由导航中使用,在目录permission下的index.js中使用
    
    import router from '@/router'
    
    // 导入进度条
    import nprogress from 'nprogress'
    
    router.beforeEach((to, from, next) => {
    
       // 开启进度条
      nprogress.start()
    })
    
    
    router.afterEach((to, from, next) => {
      // 关闭进度条
      nprogress.done()
    })
    
    
    // main.js中引入样式文件
    
    // 引入滚动条的样式文件
    import 'nprogress/nprogress.css'
    
    
    // 引入全局路由导航拦截
    import './permission/index'
    
    
    // 自定义滚动条的颜色
    
    #nprogress .bar {
      background: #c3f909 !important;
    }

    8.看一下路由的设置

    import Vue from 'vue'
    import VueRouter from 'vue-router'
    
    // 公共路由
    export const publicRoutesMap = [
      {
        path: '/login',
        name: 'login',
        component: () => import(/* webpackChunkName: "user" */ '@/views/Login')
      },
      {
        path: '/register',
        name: 'register',
        component: () => import(/* webpackChunkName: "user" */ '@/views/Register')
      },
    ]
    
    
    // 权限路由
    
    // 需要的权限路由,动态挂载的路由
    export const asyncRoutesMap = [
      {
        path: '*',
        redirect: '/404',
        hidden: true,
        meta: {
          roles: ['user']
        }
      },
      {
        path: '/404',
        name: 'four',
        component: () => import('../views/Error/foruAndFour.vue'),
        meta: {
          roles: ['user']
        }
      }
    ]

    9.在store目录下的modules目录新建permission.js文件

    // 把公共路由和权限的路由导出来
    import router, { publicRoutesMap, asyncRoutesMap } from '@/router'
    
    // 定义一个函数用来筛选后端返回来的权限数据,如果筛选成功的话返回true,否则false
    function hasPerMission (roles, route) {
      if (route && route.meta.roles) {
        return roles.some(role => route.meta.roles.indexOf(role) >= 0)
      } else {
        return true
      }
    }
    
    const permission = {
    
      state: {
        routers: publicRoutesMap,
        addRouters: []
      },
      mutations: {
        SET_ROUTERS (state, routers) {
          state.addRouters = routers
          state.routers = publicRoutesMap.concat(routers)
        }
      },
      actions: {
        generateRoutes ({ state, commit }, data) {
          // 返回一个promise回调
    
          return new Promise((resolve, reject) => {
            // 遍历权限数组
            const accessedRoutes = asyncRoutesMap.filter(v => {
              // 如果包含admin,说明就是管理员直接进入即可
              if (data.indexOf('admin') >= 0) return true
    
              // 之后就是调用hasPerMission函数对象权限动态路由和后台返回的用户权限进行严格匹配
              if (hasPerMission(data, v)) {
                // 判断是否有权限路由是否有子路由,有子路由继续遍历
                if (v.children && v.children.length > 0) {
                  v.children = v.children.filter(child => {
                    // 对权限子路由和后台返回的用户权限数据,在进行匹配,匹配成功返回
                    if (hasPerMission(data, child)) {
                      return child
                    }
                    // 失败返回false
                    return false
                  })
    
                  // 并且要把权限的父路由返回来,不光要把权限子路由返回,
                  // 无论权限子路有还是没有,都应该把权限父路由返回来
                  return v
                } else {
                  // 否则说明没有子路由,直接把父路由返回
                  return v
                }
              }
    
              // 如果权限验证没有匹配项,直接返回false
              return false
            })
    
            // 将返回的动态路由存储到公共路由中
            commit('SET_ROUTERS', accessedRoutes)
            resolve()
          })
        }
      },
      getters: {
        // 只要权限路由数组发生变化就重新计算
        addRouters (state) {
          return state.routers
        }
      }
    }
    
    export default permission

    10.在全局路由导航中进行拦截当天添加

    import router from '@/router'
    import store from '@/store'
    
    // 导入进度条
    import nprogress from 'nprogress'
    
    const whiteList = ['/login', '/register'] // 不重定向白名单
    
    router.beforeEach((to, from, next) => {
      // 开启进度条
      nprogress.start()
    
      // 检查token
      if (store.getters.getuserInfo.token) {
        // 有token的话去登录页,直接拦截到首页,因为我们需要用户点击按钮退出,不希望通过浏览器的back按钮
        if (to.path === '/login') {
          next()
        } else {
          // 如果没有用户的权限数据,去拉取一下用户的权限数据
          if (store.getters.roles.length === 0) {
    
            store.dispatch('getUserAuth', store.getters.getuserInfo.token).then(res => {
              
              // 调用该方法对用户的权限进行一次筛选
              store.dispatch('generateRoutes', res).then(() => {
    
                router.addRoutes(store.getters.addRouters)
    
                if (from.path !== '/login') {
                  next({ ...to, replace: true })
                } else {
                  next()
                }
              })
            }).catch(error => {
              // 验证失败重新登陆
              next({ path: '/login' })
            })
          } else {
            next()
          }
        }
      } else {
    
        // 需要重定向的白名单
        if (whiteList.indexOf(to.path) !== -1) {
          next()
        } else {
          next('/login')
        }
      }
    })
    
    router.afterEach((to, from, next) => {
      // 关闭进度条
      nprogress.done()
    })

    11.看一下mixins目录中的文件

    // 写法和vue一模一样,在需要使用的vue组件中混入使用即可
    
    // mixins目录下的index.js
    
    export const publicLogic = {
    
        data () {
    
            return {
                
                msg: "我是mixins的数据"
            }
        },
    
        methods: {},
    
        created () {},
    
        watch : {
    
        },
    
        mounted () {}
    }
    
    
    // 在其他组件中混入
    
    <template>
    
    </template>
    
    <script>
    
    import { publicLogic } from '@/mixins/index'
    
    export default {
        
        // 通过mixins书信混入
        mixins: [publicLogic],
    
        data () {
    
            return {
    
            }
        },
    
        created () {
            
            console.log(this.msg)    // 即可拿到混入的数据
        }
    }
    
    </script>

    12.pc端的滚动条插件(vue-happy-scroll)

    // 安装使用
    
    npm i vue-happy-scroll
    
    import { HappyScroll } from 'vue-happy-scroll'
    
    // 在组件中注册
    components: {
    
        HappyScroll
    }
    
    
    <happy-scroll :min-length-v="0.2" color="rgba(3,253,244,1)" size="10" hide-horizontal>
    
        // ...需要滚动的y元素
    
    </happy-scroll>
    
    
    // 先取消浏览器默认的滚动
    
    body,html {
      overflow-y: hidden;
      overflow-x: hidden;
    }

    13.store目录具体结构

    // store下的index.js
    
    import Vue from 'vue'
    import Vuex from 'vuex'
    
    // 把用户权限验证模块引入
    import permission from './modules/permission'
    
    // 用户存储
    import user from './modules/user'
    
    // 公共计算属性
    import getters from './getters'
    Vue.use(Vuex)
    
    export default new Vuex.Store({
      modules: {
        permission,
        user
      },
      getters
    })
    
    
    // store目录下的getters.js
    
    const getters = {
      getuserInfo: state => state.user.userInfo,
      roles: state => state.user.roles,
      navBars: state => state.user.navBars,
      cache: state => state.user.cache
    }
    
    export default getters
    
    
    // store目录下的modules目录下的js文件,都是一个一个的模块
    
    // user.js 用来存储放置用户的逻辑操作
    
    
    
    // 引入api接口
    import { userLogin, getUserAuth } from '@/api/user'
    
    const user = {
    
      state: {
    
        // 需要往本地存储的用户信息包含token
        userInfo: JSON.parse(window.sessionStorage.getItem('userInfo')) || {},
        // 用户的权限信息
        roles: [],
        // 用户的导航栏数据
        navBars: JSON.parse(window.sessionStorage.getItem('nav_bar')) || {
          navBarArr: [],
          index: 0
        },
        // 缓存组件
        cache: []
      },
    
      mutations: {
    
        SET_USERINFO (state, userInfo) {
          state.userInfo = userInfo
          window.sessionStorage.setItem('userInfo', JSON.stringify(state.userInfo))
        },
    
        // 存储用户的navBar导航栏数据
        SET_USERNAVBAR ({ navBars }, navBar) {
    
          if (navBars.navBarArr.length === 0) {
      
            navBars.navBarArr.push(navBar)
          } else {
      
            const navItem = navBars.navBarArr.find((item, index) => {
              if (item.path === navBar.path) {
                navBars.index = index
                return true
              }
            })
      
            if (!navItem) {
              navBars.navBarArr.push(navBar)
              navBars.index = navBars.navBarArr.length - 1
            }
          }
      
          window.sessionStorage.setItem('nav_bar', JSON.stringify(navBars))
        },
    
        // 动态添加缓存组件
        ADD_CACHE ({ cache }, name) {
          if (cache.includes(name)) return
    
          cache.push(name)
        },
    
        // 动态删除缓存组件
        REMOVE_CACHE ({ cache }, name) {
          const index = cache.indexOf(name)
    
          if (index === -1) return
    
          cache.splice(index, 1)
        }
      },
    
      actions: {
    
        // 获取用户信息
        userLogin ({ commit }, userInfo) {
          return new Promise((resolve, reject) => {
            userLogin(userInfo).then(res => {
             
              if (res.data.code === 200) {
                
                commit('SET_USERINFO', res.data.userInfo)
              }
              resolve(res.data)
            }).catch(error => {
    
              reject(error)
            })
          })
        },
    
         // 获取用户权限
         getUserAuth ({ state }, token) {
          return new Promise((resolve, reject) => {
            getUserAuth({ token }).then(res => {
              state.roles.push(...res.data)
              resolve(res.data)
            }).catch(error => {
              reject(error)
            })
          })
         }
      }
    }
    
    export default user
    
    
    // permission.js 用来存储放置用户的权限方法

    14.vue.config.js配置文件

    const CompressionPlugin = require('compression-webpack-plugin')
    
    const path = require('path')
    
    const BundleAnalyzer = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
    
    
    module.exports = {
    
      // 是否触发eslint检查
      lintOnSave: false,
    
      // 是否使用包含运行时编译器的 Vue 构建版本, 但是这会让你的应用额外增加 10kb 左右
      runtimeCompiler: false,
    
      publicPath: '/',
    
      // 打包文件的出口
      outputDir: 'dist',
    
      // 放置生成的css和js和img和fonts的目录
      assetsDir: 'static',
    
      // 数组存放html的路径
      indexPath: 'index.html',
    
      productionSourceMap: false,
    
      /*
      
        默认情况下,生成的静态资源在它们的文件名中包含了 hash 以便更好的控制缓存,
    
        前提是保证index.html是vue cli自动生成的,如果无法保证的话就设置为false
      */
      filenameHashing: true,
    
      chainWebpack: config => {
        // ============压缩图片 start============
        config.module.rule('images')
            .test(/.(png|jpe?g|gif|svg)(?.*)?$/)
            .use('url-loader')
            .loader('url-loader')
            .options({
              limit: 10240,
              outputPath: 'static/images'
            })
            .end()
        // ============压缩图片 end============
      },
    
      configureWebpack: config => {
    
        return {
    
          plugins: [
    
            // 压缩js和css和html
            new CompressionPlugin({
              test: /.js$|.html$|.css/,
              threshold: 10240,
              deleteOriginalAssets: false
            }),
    
            // 图形化展示打包后的详情
            new BundleAnalyzer()
          ],
    
          performance: {
            // 关闭webpack的性能提示
            hints:'warning',
            //入口起点的最大体积
                maxEntrypointSize: 50000000,
                //生成文件的最大体积
            maxAssetSize: 30000000,
            //只给出 js 文件的性能提示
            assetFilter: function(assetFilename) {
                    return assetFilename.endsWith('.js');
                }
          },
    
          // 指定不打包的第三方包,那么这些包需要在html页面通过cdn引入
          externals: {
            'vue': 'Vue',
            'vuex': 'Vuex',
            'vue-router': 'VueRouter',
            'axios': 'axios',
            "moment": "moment",
            'echarts': 'echarts'
          }
        }
      },
    
    }
    
    
    // index.html
    
    <script
       src="https://cdn.bootcss.com/vue/2.6.11/vue.runtime.min.js"
       crossorigin="anonymous"
    >
    </script>
    <script
       src="https://cdn.bootcss.com/vue-router/3.1.3/vue-router.min.js"
       crossorigin="anonymous"
    >
    </script>
    <script
       src="https://cdn.bootcss.com/vuex/3.1.2/vuex.min.js"
       crossorigin="anonymous"
    >
    </script>
    <script
       src="https://cdn.bootcss.com/axios/0.19.2/axios.min.js"
       crossorigin="anonymous"
    >
    </script>
    <script
       src="https://cdn.bootcss.com/moment.js/2.24.0/moment.min.js"
       crossorigin="anonymous"
    >
    </script>
    <script
       src="https://cdn.bootcss.com/echarts/3.7.1/echarts.min.js"
       crossorigin="anonymous"
    >
    </script>

    15.babel.config.js

    module.exports = {
      presets: [
        '@vue/cli-plugin-babel/preset'
      ],
      plugins: [
        [
          'component',
          {
            libraryName: 'element-ui',
            styleLibraryName: 'theme-chalk'
          }
        ],
    
        /*
        
          把es6转换成es5的时候,babel会需要一些辅助函数,如果多个源码文件都依赖这些辅助函数
    
          那么这些辅助函数会出现很多次,造成代码的沉余,为了不让这些辅助函数的代码重复注销
    
          是 babel-plugin-transform-runtime插件可以做到让他们只出现一次,将这些辅助函数帮到
    
          一个单独的模块 babel-runtime 中,这样做能减小项目文件的大小。
        
        */
        "@babel/plugin-transform-runtime"
      ]
    }
  • 相关阅读:
    关于【无法将*.dll复制到指定目录】错误的解决
    给一个容器控件内(如:div,form)文本框批量添加验证事件
    js操作dom(2)
    关于.net中修饰符的一点总结
    js操作dom(3)
    关于【源文件与模块生成时的文件不同】问题的解决
    pb中用OLE Control 报错解决方法 (转载)
    Web前端技术(摘抄)
    Uva1420 Priest John's Busiest Day
    UVa1445 Cubist Artwork
  • 原文地址:https://www.cnblogs.com/zxuedong/p/12781723.html
Copyright © 2011-2022 走看看