浏览代码

增加、配置标签页,日志管理

xiao547607 5 年之前
父节点
当前提交
215528ef78
共有 52 个文件被更改,包括 1227 次插入108 次删除
  1. 17 9
      src/App.vue
  2. 11 0
      src/api/sys/log.js
  3. 24 10
      src/routers/index.js
  4. 102 59
      src/routers/modules/base.js
  5. 6 4
      src/routers/modules/business.js
  6. 28 11
      src/routers/modules/sys.js
  7. 8 1
      src/store/index.js
  8. 71 0
      src/store/modules/permission.js
  9. 179 0
      src/store/modules/tagsView.js
  10. 48 0
      src/styles/transition.scss
  11. 1 1
      src/views/Home.vue
  12. 2 1
      src/views/base/alarmConfig-list.vue
  13. 1 0
      src/views/base/alarmInfo-alert-list.vue
  14. 1 0
      src/views/base/alarmInfo-fence-list.vue
  15. 1 0
      src/views/base/companyInfo-list.vue
  16. 1 0
      src/views/base/deviceInfo-list.vue
  17. 1 0
      src/views/base/devicePerson-list.vue
  18. 1 0
      src/views/base/electricClientInfo-list.vue
  19. 1 0
      src/views/base/electricMeterInfo-list.vue
  20. 1 0
      src/views/base/employeeInfo-list.vue
  21. 1 0
      src/views/base/holidayInfo-list.vue
  22. 1 0
      src/views/base/informationInfo-complaint-list.vue
  23. 1 0
      src/views/base/informationInfo-warranty-list.vue
  24. 1 1
      src/views/base/messageNotice-detail.vue
  25. 1 0
      src/views/base/messageNotice-list.vue
  26. 1 0
      src/views/base/messageReport-list.vue
  27. 1 0
      src/views/base/ownerInfo-list.vue
  28. 1 0
      src/views/base/parkingApply-list.vue
  29. 1 0
      src/views/base/parkingInfo-list.vue
  30. 1 0
      src/views/base/personDeviceLog-list.vue
  31. 1 0
      src/views/base/personInfo-list.vue
  32. 1 0
      src/views/base/rechange-list.vue
  33. 1 0
      src/views/base/rechargeRecord-list.vue
  34. 1 0
      src/views/base/rechargeRecordProperty-list.vue
  35. 2 1
      src/views/base/rechargeRecordWater-list.vue
  36. 1 0
      src/views/base/roomInfo-list.vue
  37. 1 0
      src/views/base/temperatureRecord-list.vue
  38. 1 0
      src/views/base/terminalInfo-list.vue
  39. 1 0
      src/views/base/warningPusher-list.vue
  40. 1 1
      src/views/business/fillAttendance-list.vue
  41. 1 0
      src/views/business/workAttendance-list.vue
  42. 27 0
      src/views/layout/AppMain.vue
  43. 85 0
      src/views/layout/TagsView/ScrollPane.vue
  44. 293 0
      src/views/layout/TagsView/index.vue
  45. 31 9
      src/views/layout/index.vue
  46. 12 0
      src/views/redirect/index.vue
  47. 1 0
      src/views/sys/dataDictionary-list.vue
  48. 247 0
      src/views/sys/log-list.vue
  49. 1 0
      src/views/sys/menu-list.vue
  50. 1 0
      src/views/sys/permission-list.vue
  51. 1 0
      src/views/sys/role-list.vue
  52. 1 0
      src/views/sys/user-list.vue

+ 17 - 9
src/App.vue

@@ -15,16 +15,24 @@ export default {
 };
 </script>
 <style rel="stylesheet/scss" lang="scss">
+body{
+  margin: 0px;
+}
+
 #app {
-  position: absolute;
-  right: 0;
-  left: 0;
-  bottom: 0;
-  top: 0;
+  margin: 0px;
+  padding: 0px;
   background: #efefef;
-  overflow: auto;
-  z-index: 900;
-  display: flex;
-  flex-direction: row;
+  position:absolute;
+  top:0px;
+  left:0px;
+  right:0px;
+  bottom:0px;
+  display:flex;
+  flex-direction: column;
+}
+
+.sticky-bg{
+  background-color:#fff;
 }
 </style>

+ 11 - 0
src/api/sys/log.js

@@ -0,0 +1,11 @@
+import request from '@/utils/request'
+import constant from '@/constant'
+
+function pageList(formData) {
+    return request.post(constant.serverUrl + "/sys/log/pageList", formData);
+}
+
+
+export default {
+    pageList
+}

+ 24 - 10
src/routers/index.js

@@ -14,36 +14,51 @@ Vue.use(Router)
 
 NProgress.configure({ showSpinner: true }) // NProgress Configuration
 
-var routes = [
+export const constantRoutes = [
   {
     path: '/layout',
-    component:Layout,
+    component: Layout,
     children: [
       {
         path: '/home',
-        name: 'home',
-        component: Home
+        name: 'Home',
+        component: Home,
+        meta: {
+          title: '控制台',
+          affix: true
+        }
       },
       ...sysRouters,
       ...baseRouters,
-      ...businessRouters
+      ...businessRouters,
     ]
   },
   {
-    path:'/',
+    path: '/',
     redirect: '/home'
   },
   {
     path: '/login',
     component: () => import('@/views/Login')
-  }
+  },
+  {
+    path: '/redirect',
+    component: Layout,
+    hidden: true,
+    children: [
+      {
+        path: '/redirect/:path(.*)',
+        component: () => import('@/views/redirect/index')
+      }
+    ]
+  },
 ];
 
 // Array.prototype.push.apply(routes, caseRouters);
 // Array.prototype.push.apply(routes, sysRouters);
 
 var router = new Router({
-  routes
+  routes: constantRoutes
 })
 
 router.beforeEach((to, from, next) => {
@@ -67,7 +82,7 @@ router.beforeEach((to, from, next) => {
     if (to.path === '/login') {
       next();
     }
-    else{
+    else {
       next(`/login?redirect=${to.path}`);
     }
 
@@ -79,5 +94,4 @@ router.afterEach(() => {
   NProgress.done();
 });
 
-
 export default router;

+ 102 - 59
src/routers/modules/base.js

@@ -1,312 +1,355 @@
 var routers = [
         {
+                //终端管理
                 path: '/base/terminalInfo/list',
-                name: 'base-terminalInfo-list',
+                name: 'BaseTerminalInfoList',
                 // route level code-splitting
                 // this generates a separate chunk (about.[hash].js) for this route
                 // which is lazy-loaded when the route is visited.
                 component: () => import('@/views/base/terminalInfo-list.vue'),
                 meta: {
-                        roles: ["admin"]
+                        roles: ["admin"],
+                        title: '终端管理'
                 }
         },
         {
+                //户号管理
                 path: '/base/electricClientInfo/list',
-                name: 'base-electricClientInfo-list',
+                name: 'BaseElectricClientInfoList',
                 // route level code-splitting
                 // this generates a separate chunk (about.[hash].js) for this route
                 // which is lazy-loaded when the route is visited.
                 component: () => import('@/views/base/electricClientInfo-list.vue'),
                 meta: {
-                        roles: ["admin"]
+                        roles: ["admin"],
+                        title: '户号管理'
                 }
         },
         {
+                //电表管理
                 path: '/base/electricMeterInfo/list',
-                name: 'base-electricMeterInfo-list',
+                name: 'BaseElectricMeterInfoList',
                 // route level code-splitting
                 // this generates a separate chunk (about.[hash].js) for this route
                 // which is lazy-loaded when the route is visited.
                 component: () => import('@/views/base/electricMeterInfo-list.vue'),
                 meta: {
-                        roles: ["admin"]
+                        roles: ["admin"],
+                        title: '电表管理'
                 }
         },
         {
+                //充值流水
                 path: '/base/rechargeRecord/list',
-                name: 'base-rechargeRecord-list',
+                name: 'BaseRechargeRecordList',
                 // route level code-splitting
                 // this generates a separate chunk (about.[hash].js) for this route
                 // which is lazy-loaded when the route is visited.
                 component: () => import('@/views/base/rechargeRecord-list.vue'),
                 meta: {
-                        roles: ["admin"]
+                        roles: ["admin"],
+                        title: '充值流水'
                 }
         },
         {
+                //物业充值流水
                 path: '/base/rechargeRecordWater/list',
-                name: 'base-rechargeRecordWater-list',
+                name: 'BaseRechargeRecordWaterList',
                 // route level code-splitting
                 // this generates a separate chunk (about.[hash].js) for this route
                 // which is lazy-loaded when the route is visited.
                 component: () => import('@/views/base/rechargeRecordWater-list.vue'),
                 meta: {
-                        roles: ["admin"]
+                        roles: ["admin"],
+                        title: '物业充值流水'
                 }
         },
         {
+                //物业费充值记录
                 path: '/base/rechargeRecordProperty/list',
-                name: 'base-rechargeRecordProperty-list',
+                name: 'BaseRechargeRecordPropertyList',
                 // route level code-splitting
                 // this generates a separate chunk (about.[hash].js) for this route
                 // which is lazy-loaded when the route is visited.
                 component: () => import('@/views/base/rechargeRecordProperty-list.vue'),
                 meta: {
-                        roles: ["admin"]
+                        roles: ["admin"],
+                        title: '物业费充值记录'
                 }
         },
         {
+                //房间管理
                 path: '/base/roomInfo/list',
-                name: 'base-roomInfo-list',
+                name: 'BaseRoomInfoList',
                 // route level code-splitting
                 // this generates a separate chunk (about.[hash].js) for this route
                 // which is lazy-loaded when the route is visited.
                 component: () => import('@/views/base/roomInfo-list.vue'),
                 meta: {
-                        roles: ["admin"]
+                        roles: ["admin"],
+                        title: '房间管理'
                 }
         },
         {
+                //线下充值
                 path: '/base/rechange/list',
-                name: 'base-rechange-list',
+                name: 'BaseRechangeList',
                 // route level code-splitting
                 // this generates a separate chunk (about.[hash].js) for this route
                 // which is lazy-loaded when the route is visited.
                 component: () => import('@/views/base/rechange-list.vue'),
                 meta: {
-                        roles: ["admin"]
+                        roles: ["admin"],
+                        title: '线下充值'
                 }
         },
         {
+                //业主信息管理
                 path: '/base/ownerInfo/list',
-                name: 'base-ownerInfo-list',
+                name: 'BaseOwnerInfoList',
                 // route level code-splitting
                 // this generates a separate chunk (about.[hash].js) for this route
                 // which is lazy-loaded when the route is visited.
                 component: () => import('@/views/base/ownerInfo-list.vue'),
                 meta: {
-                        roles: ["admin"]
+                        roles: ["admin"],
+                        title: '业主信息管理'
                 }
         },
         {
+                //车位管理
                 path: '/base/parkingInfo/list',
-                name: 'base-parkingInfo-list',
+                name: 'BaseParkingInfoList',
                 // route level code-splitting
                 // this generates a separate chunk (about.[hash].js) for this route
                 // which is lazy-loaded when the route is visited.
                 component: () => import('@/views/base/parkingInfo-list.vue'),
                 meta: {
-                        roles: ["admin"]
+                        roles: ["admin"],
+                        title: '车位管理'
                 }
         },
         {
+                //车位申请
                 path: '/base/parkingApply/list',
-                name: 'base-parkingApply-list',
+                name: 'BaseParkingApplyList',
                 // route level code-splitting
                 // this generates a separate chunk (about.[hash].js) for this route
                 // which is lazy-loaded when the route is visited.
                 component: () => import('@/views/base/parkingApply-list.vue'),
                 meta: {
-                        roles: ["admin"]
+                        roles: ["admin"],
+                        title: '车位申请'
                 }
         },
         {
+                //围墙报警
                 path: '/base/alarmInfoFence/list',
-                name: 'base-alarmInfoFence-list',
+                name: 'BaseAlarmInfoFenceList',
                 // route level code-splitting
                 // this generates a separate chunk (about.[hash].js) for this route
                 // which is lazy-loaded when the route is visited.
                 component: () => import('@/views/base/alarmInfo-fence-list.vue'),
                 meta: {
-                        roles: ["admin"]
+                        roles: ["admin"],
+                        title: '围墙报警'
                 }
         },
         {
+                //警戒报警
                 path: '/base/alarmInfoAlert/list',
-                name: 'base-alarmInfoAlert-list',
+                name: 'BaseAlarmInfoAlertList',
                 // route level code-splitting
                 // this generates a separate chunk (about.[hash].js) for this route
                 // which is lazy-loaded when the route is visited.
                 component: () => import('@/views/base/alarmInfo-alert-list.vue'),
                 meta: {
-                        roles: ["admin"]
+                        roles: ["admin"],
+                        title: '警戒报警'
                 }
         },
         {
+                //投诉建议管理
                 path: '/base/informationInfoComplaint/list',
-                name: 'base-informationInfoComplaint-list',
+                name: 'BaseInformationInfoComplaintList',
                 // route level code-splitting
                 // this generates a separate chunk (about.[hash].js) for this route
                 // which is lazy-loaded when the route is visited.
                 component: () => import('@/views/base/informationInfo-complaint-list.vue'),
                 meta: {
-                        roles: ["admin"]
+                        roles: ["admin"],
+                        title: '投诉建议管理'
                 }
         },
         {
+                //报事报修管理
                 path: '/base/informationInfoWarranty/list',
-                name: 'base-informationInfoWarranty-list',
+                name: 'BaseInformationInfoWarrantyList',
                 // route level code-splitting
                 // this generates a separate chunk (about.[hash].js) for this route
                 // which is lazy-loaded when the route is visited.
                 component: () => import('@/views/base/informationInfo-warranty-list.vue'),
                 meta: {
-                        roles: ["admin"]
+                        roles: ["admin"],
+                        title: '报事报修管理'
                 }
         },
         {
+                //企业员工管理
                 path: '/base/employeeInfo/list',
-                name: 'base-employeeInfo-list',
+                name: 'BaseEmployeeInfoList',
                 // route level code-splitting
                 // this generates a separate chunk (about.[hash].js) for this route
                 // which is lazy-loaded when the route is visited.
                 component: () => import('@/views/base/employeeInfo-list.vue'),
                 meta: {
-                        roles: ["admin"]
+                        roles: ["admin"],
+                        title: '企业员工管理'
                 }
         },
         {
                 //单位管理
                 path: '/base/companyInfo/list',
-                name: 'base-companyInfo-list',
+                name: 'BaseCompanyInfoList',
                 // route level code-splitting
                 // this generates a separate chunk (about.[hash].js) for this route
                 // which is lazy-loaded when the route is visited.
                 component: () => import('@/views/base/companyInfo-list.vue'),
                 meta: {
-                        roles: ["admin"]
+                        roles: ["admin"],
+                        title: '单位管理'
                 }
         },
         {
                 //人员管理
                 path: '/base/personInfo/list',
-                name: 'base-personInfo-list',
+                name: 'BasePersonInfoList',
                 // route level code-splitting
                 // this generates a separate chunk (about.[hash].js) for this route
                 // which is lazy-loaded when the route is visited.
                 component: () => import('@/views/base/personInfo-list.vue'),
                 meta: {
-                        roles: ["admin"]
+                        roles: ["admin"],
+                        title: '人员管理'
                 }
         },
         {
                 //设备管理
                 path: '/base/deviceInfo/list',
-                name: 'base-deviceInfo-list',
+                name: 'BaseDeviceInfoList',
                 // route level code-splitting
                 // this generates a separate chunk (about.[hash].js) for this route
                 // which is lazy-loaded when the route is visited.
                 component: () => import('@/views/base/deviceInfo-list.vue'),
                 meta: {
-                        roles: ["admin"]
+                        roles: ["admin"],
+                        title: '设备管理'
                 }
         },
         {
-                //台账信息
+                //测温记录
                 path: '/base/personDeviceLog/list',
-                name: 'base-personDeviceLog-list',
+                name: 'BasePersonDeviceLogList',
                 // route level code-splitting
                 // this generates a separate chunk (about.[hash].js) for this route
                 // which is lazy-loaded when the route is visited.
                 component: () => import('@/views/base/personDeviceLog-list.vue'),
                 meta: {
-                        roles: ["admin"]
+                        roles: ["admin"],
+                        title: '测温记录'
                 }
         },
         {
                 //检测预警上报人员管理
                 path: '/base/warningPusher/list',
-                name: 'base-warningPusher-list',
+                name: 'BaseWarningPusherList',
                 // route level code-splitting
                 // this generates a separate chunk (about.[hash].js) for this route
                 // which is lazy-loaded when the route is visited.
                 component: () => import('@/views/base/warningPusher-list.vue'),
                 meta: {
-                        roles: ["admin"]
+                        roles: ["admin"],
+                        title: '预警上报人员管理'
                 }
         },
         {
-                //防疫通知
+                //通知公告
                 path: '/base/messageNotice/list',
-                name: 'base-messageNotice-list',
+                name: 'BaseMessageNoticeList',
                 // route level code-splitting
                 // this generates a separate chunk (about.[hash].js) for this route
                 // which is lazy-loaded when the route is visited.
                 component: () => import('@/views/base/messageNotice-list.vue'),
                 meta: {
-                        roles: ["admin"]
+                        roles: ["admin"],
+                        title: '通知公告'
                 }
         },
         {
-                //异常上报
+                //异常上报
                 path: '/base/messageReport/list',
-                name: 'base-messageReport-list',
+                name: 'BaseMessageReportList',
                 // route level code-splitting
                 // this generates a separate chunk (about.[hash].js) for this route
                 // which is lazy-loaded when the route is visited.
                 component: () => import('@/views/base/messageReport-list.vue'),
                 meta: {
-                        roles: ["admin"]
+                        roles: ["admin"],
+                        title: '异常上报'
                 }
         },
         {
                 //打卡时间
                 path: '/base/alarmConfig/list',
-                name: 'base-alarmConfig-list',
+                name: 'BaseAlarmConfigList',
                 // route level code-splitting
                 // this generates a separate chunk (about.[hash].js) for this route
                 // which is lazy-loaded when the route is visited.
                 component: () => import('@/views/base/alarmConfig-list.vue'),
                 meta: {
-                        roles: ["admin"]
+                        roles: ["admin"],
+                        title: '考勤时间设置'
                 }
         },
         {
                 //测温统计
                 path: '/base/temperatureRecord/list',
-                name: 'base-temperatureRecord-list',
+                name: 'BaseTemperatureRecordList',
                 // route level code-splitting
                 // this generates a separate chunk (about.[hash].js) for this route
                 // which is lazy-loaded when the route is visited.
                 component: () => import('@/views/base/temperatureRecord-list.vue'),
                 meta: {
-                        roles: ["admin"]
+                        roles: ["admin"],
+                        title: '测温统计'
                 }
         },
         {
-                //单位人员管理
+                //设备人员管理
                 path: '/base/devicePerson/list',
-                name: 'base-devicePerson-list',
+                name: 'BaseDevicePersonList',
                 // route level code-splitting
                 // this generates a separate chunk (about.[hash].js) for this route
                 // which is lazy-loaded when the route is visited.
                 component: () => import('@/views/base/devicePerson-list.vue'),
                 meta: {
-                        roles: ["admin"]
+                        roles: ["admin"],
+                        title: '设备人员管理'
                 }
         },
         {
                 //节假日设置
                 path: '/base/holidayInfo/list',
-                name: 'base-holidayInfo-list',
+                name: 'BaseHolidayInfoList',
                 // route level code-splitting
                 // this generates a separate chunk (about.[hash].js) for this route
                 // which is lazy-loaded when the route is visited.
                 component: () => import('@/views/base/holidayInfo-list.vue'),
                 meta: {
-                        roles: ["admin"]
+                        roles: ["admin"],
+                        title: '节假日设置'
                 }
-        }
+        },
 ]
 
 export default routers;

+ 6 - 4
src/routers/modules/business.js

@@ -2,25 +2,27 @@ var routers = [
         {
                 //考勤记录
                 path: '/business/workAttendance/list',
-                name: 'business-workAttendance-list',
+                name: 'BusinessWorkAttendanceList',
                 // route level code-splitting
                 // this generates a separate chunk (about.[hash].js) for this route
                 // which is lazy-loaded when the route is visited.
                 component: () => import('@/views/business/workAttendance-list.vue'),
                 meta: {
-                        roles: ["admin"]
+                        roles: ["admin"],
+                        title: '考勤记录'
                 }
         },
         {
                 //补卡申请记录
                 path: '/business/fillAttendance/list',
-                name: 'business-fillAttendance-list',
+                name: 'BusinessFillAttendanceList',
                 // route level code-splitting
                 // this generates a separate chunk (about.[hash].js) for this route
                 // which is lazy-loaded when the route is visited.
                 component: () => import('@/views/business/fillAttendance-list.vue'),
                 meta: {
-                        roles: ["admin"]
+                        roles: ["admin"],
+                        title: '补卡申请记录'
                 }
         }
 ]

+ 28 - 11
src/routers/modules/sys.js

@@ -1,59 +1,76 @@
 var routers = [
         {
                 path: '/sys/user/list',
-                name: 'sys-user-list',
+                name: 'SysUserList',
                 // route level code-splitting
                 // this generates a separate chunk (about.[hash].js) for this route
                 // which is lazy-loaded when the route is visited.
                 component: () => import('@/views/sys/user-list.vue'),
                 meta: {
-                        roles: ["admin"]
+                        roles: ["admin"],
+                        title: '用户管理'
                 }
         },
         {
                 path: '/sys/role/list',
-                name: 'sys-role-list',
+                name: 'SysRoleList',
                 // route level code-splitting
                 // this generates a separate chunk (about.[hash].js) for this route
                 // which is lazy-loaded when the route is visited.
                 component: () => import('@/views/sys/role-list.vue'),
                 meta: {
-                        roles: ["admin"]
+                        roles: ["admin"],
+                        title: '角色管理'
                 }
         },
         {
                 path: '/sys/menu/list',
-                name: 'sys-menu-list',
+                name: 'SysMenuList',
                 // route level code-splitting
                 // this generates a separate chunk (about.[hash].js) for this route
                 // which is lazy-loaded when the route is visited.
                 component: () => import('@/views/sys/menu-list.vue'),
                 meta: {
-                        roles: ["admin"]
+                        roles: ["admin"],
+                        title: '菜单管理'
                 }
         },
         {
                 path: '/sys/permission/list',
-                name: 'sys-permission-list',
+                name: 'SysPermissionList',
                 // route level code-splitting
                 // this generates a separate chunk (about.[hash].js) for this route
                 // which is lazy-loaded when the route is visited.
                 component: () => import('@/views/sys/permission-list.vue'),
                 meta: {
-                        roles: ["admin"]
+                        roles: ["admin"],
+                        title: '接口权限管理'
                 }
         },
         {
                 path: '/sys/dataDictionary/list',
-                name: 'sys-dataDictionary-list',
+                name: 'SysDataDictionaryList',
                 // route level code-splitting
                 // this generates a separate chunk (about.[hash].js) for this route
                 // which is lazy-loaded when the route is visited.
                 component: () => import('@/views/sys/dataDictionary-list.vue'),
                 meta: {
-                        roles: ["admin"]
+                        roles: ["admin"],
+                        title: '数据字典管理'
                 }
-        }
+        },
+        {
+                path: '/sys/log/list',
+                name: 'SysLogList',
+                // route level code-splitting
+                // this generates a separate chunk (about.[hash].js) for this route
+                // which is lazy-loaded when the route is visited.
+                component: () => import('@/views/sys/log-list.vue'),
+                meta: {
+                        roles: ["admin"],
+                        title: '日志管理'
+                }
+        },
 ]
 
 export default routers;

+ 8 - 1
src/store/index.js

@@ -1,6 +1,8 @@
 import Vue from 'vue'
 import Vuex from 'vuex'
 import user from './modules/user'
+import tagsView from './modules/tagsView'
+import permission from './modules/permission'
 
 Vue.use(Vuex)
 
@@ -8,7 +10,7 @@ const debug = process.env.NODE_ENV !== 'production'
 
 export default new Vuex.Store({
   modules: {
-    user
+    user,tagsView,permission
   },
   strict: debug,
   state:{
@@ -19,5 +21,10 @@ export default new Vuex.Store({
   },
   actions:{
 
+  },
+  getters:{
+    visitedViews: state => state.tagsView.visitedViews,
+    cachedViews: state => state.tagsView.cachedViews,
+    permission_routes: state => state.permission.routes
   }
 })

+ 71 - 0
src/store/modules/permission.js

@@ -0,0 +1,71 @@
+import { constantRoutes } from '@/routers'
+
+const asyncRoutes = []
+
+/**
+ * Use meta.role to determine if the current user has permission
+ * @param roles
+ * @param route
+ */
+function hasPermission(roles, route) {
+  if (route.meta && route.meta.roles) {
+    return roles.some(role => route.meta.roles.includes(role))
+  } else {
+    return true
+  }
+}
+
+/**
+ * Filter asynchronous routing tables by recursion
+ * @param routes asyncRoutes
+ * @param roles
+ */
+export function filterAsyncRoutes(routes, roles) {
+  const res = []
+
+  routes.forEach(route => {
+    const tmp = { ...route }
+    if (hasPermission(roles, tmp)) {
+      if (tmp.children) {
+        tmp.children = filterAsyncRoutes(tmp.children, roles)
+      }
+      res.push(tmp)
+    }
+  })
+
+  return res
+}
+
+const state = {
+  routes: [],
+  addRoutes: []
+}
+
+const mutations = {
+  SET_ROUTES: (state, routes) => {
+    state.addRoutes = routes
+    state.routes = constantRoutes.concat(routes)
+  }
+}
+
+const actions = {
+  generateRoutes({ commit }, roles) {
+    return new Promise(resolve => {
+      let accessedRoutes
+      if (roles.includes('admin')) {
+        accessedRoutes = asyncRoutes || []
+      } else {
+        accessedRoutes = filterAsyncRoutes(asyncRoutes, roles)
+      }
+      commit('SET_ROUTES', accessedRoutes)
+      resolve(accessedRoutes)
+    })
+  }
+}
+
+export default {
+  namespaced: true,
+  state,
+  mutations,
+  actions
+}

+ 179 - 0
src/store/modules/tagsView.js

@@ -0,0 +1,179 @@
+const state = {
+  visitedViews: [],
+  cachedViews: []
+}
+
+const mutations = {
+  ADD_VISITED_VIEW: (state, view) => {
+    console.log("ADD_VISITED_VIEW");
+    console.log(view.path);
+
+    if (state.visitedViews.some(v => v.path === view.path)) return
+
+    var newView =  Object.assign({}, view, {
+      title: view.meta.title || 'no-name'
+    });
+
+    console.log(newView);
+
+    state.visitedViews.push({
+      path : view.path,
+      title: view.meta.title || 'no-name',
+      meta: view.meta,
+      name: view.name,
+      fullPath: view.fullPath
+    })
+    // state.visitedViews.push({
+    //   title:'测试'
+    // });
+    // console.log(state.visitedViews);
+  },
+  ADD_CACHED_VIEW: (state, view) => {
+    console.log("ADD_CACHED_VIEW");
+    if (state.cachedViews.includes(view.name)) return
+    if (!view.meta.noCache) {
+      state.cachedViews.push(view.name)
+    }
+  },
+
+  DEL_VISITED_VIEW: (state, view) => {
+    for (const [i, v] of state.visitedViews.entries()) {
+      if (v.path === view.path) {
+        state.visitedViews.splice(i, 1)
+        break
+      }
+    }
+  },
+  DEL_CACHED_VIEW: (state, view) => {
+    const index = state.cachedViews.indexOf(view.name)
+    index > -1 && state.cachedViews.splice(index, 1)
+  },
+
+  DEL_OTHERS_VISITED_VIEWS: (state, view) => {
+    state.visitedViews = state.visitedViews.filter(v => {
+      return v.meta.affix || v.path === view.path
+    })
+  },
+  DEL_OTHERS_CACHED_VIEWS: (state, view) => {
+    const index = state.cachedViews.indexOf(view.name)
+    if (index > -1) {
+      state.cachedViews = state.cachedViews.slice(index, index + 1)
+    } else {
+      // if index = -1, there is no cached tags
+      state.cachedViews = []
+    }
+  },
+
+  DEL_ALL_VISITED_VIEWS: state => {
+    // keep affix tags
+    const affixTags = state.visitedViews.filter(tag => tag.meta.affix)
+    state.visitedViews = affixTags
+  },
+  DEL_ALL_CACHED_VIEWS: state => {
+    state.cachedViews = []
+  },
+
+  UPDATE_VISITED_VIEW: (state, view) => {
+    for (let v of state.visitedViews) {
+      if (v.path === view.path) {
+        v = Object.assign(v, view)
+        break
+      }
+    }
+  }
+}
+
+const actions = {
+  addView({ dispatch }, view) {
+    console.log("addView");
+
+    dispatch('addVisitedView', view)
+    dispatch('addCachedView', view)
+  },
+  addVisitedView({ commit }, view) {
+    commit('ADD_VISITED_VIEW', view)
+  },
+  addCachedView({ commit }, view) {
+    commit('ADD_CACHED_VIEW', view)
+  },
+
+  delView({ dispatch, state }, view) {
+    return new Promise(resolve => {
+      dispatch('delVisitedView', view)
+      dispatch('delCachedView', view)
+      resolve({
+        visitedViews: [...state.visitedViews],
+        cachedViews: [...state.cachedViews]
+      })
+    })
+  },
+  delVisitedView({ commit, state }, view) {
+    return new Promise(resolve => {
+      commit('DEL_VISITED_VIEW', view)
+      resolve([...state.visitedViews])
+    })
+  },
+  delCachedView({ commit, state }, view) {
+    return new Promise(resolve => {
+      commit('DEL_CACHED_VIEW', view)
+      resolve([...state.cachedViews])
+    })
+  },
+
+  delOthersViews({ dispatch, state }, view) {
+    return new Promise(resolve => {
+      dispatch('delOthersVisitedViews', view)
+      dispatch('delOthersCachedViews', view)
+      resolve({
+        visitedViews: [...state.visitedViews],
+        cachedViews: [...state.cachedViews]
+      })
+    })
+  },
+  delOthersVisitedViews({ commit, state }, view) {
+    return new Promise(resolve => {
+      commit('DEL_OTHERS_VISITED_VIEWS', view)
+      resolve([...state.visitedViews])
+    })
+  },
+  delOthersCachedViews({ commit, state }, view) {
+    return new Promise(resolve => {
+      commit('DEL_OTHERS_CACHED_VIEWS', view)
+      resolve([...state.cachedViews])
+    })
+  },
+
+  delAllViews({ dispatch, state }, view) {
+    return new Promise(resolve => {
+      dispatch('delAllVisitedViews', view)
+      dispatch('delAllCachedViews', view)
+      resolve({
+        visitedViews: [...state.visitedViews],
+        cachedViews: [...state.cachedViews]
+      })
+    })
+  },
+  delAllVisitedViews({ commit, state }) {
+    return new Promise(resolve => {
+      commit('DEL_ALL_VISITED_VIEWS')
+      resolve([...state.visitedViews])
+    })
+  },
+  delAllCachedViews({ commit, state }) {
+    return new Promise(resolve => {
+      commit('DEL_ALL_CACHED_VIEWS')
+      resolve([...state.cachedViews])
+    })
+  },
+
+  updateVisitedView({ commit }, view) {
+    commit('UPDATE_VISITED_VIEW', view)
+  }
+}
+
+export default {
+  namespaced: true,
+  state,
+  mutations,
+  actions
+}

+ 48 - 0
src/styles/transition.scss

@@ -0,0 +1,48 @@
+// global transition css
+
+/* fade */
+.fade-enter-active,
+.fade-leave-active {
+  transition: opacity 0.28s;
+}
+
+.fade-enter,
+.fade-leave-active {
+  opacity: 0;
+}
+
+/* fade-transform */
+.fade-transform-leave-active,
+.fade-transform-enter-active {
+  transition: all .2s;
+}
+
+.fade-transform-enter {
+  opacity: 0;
+  transform: translateX(-30px);
+}
+
+.fade-transform-leave-to {
+  opacity: 0;
+  transform: translateX(30px);
+}
+
+/* breadcrumb transition */
+.breadcrumb-enter-active,
+.breadcrumb-leave-active {
+  transition: all .5s;
+}
+
+.breadcrumb-enter,
+.breadcrumb-leave-active {
+  opacity: 0;
+  transform: translateX(20px);
+}
+
+.breadcrumb-move {
+  transition: all .5s;
+}
+
+.breadcrumb-leave-active {
+  position: absolute;
+}

+ 1 - 1
src/views/Home.vue

@@ -85,7 +85,7 @@ import "nprogress/nprogress.css"; // progress bar style
 import NProgress from "nprogress"; // progress bar
 // @ is an alias to /src
 export default {
-  name: "home",
+  name: "Home",
   data() {
     return {
       companyNum: "0",

+ 2 - 1
src/views/base/alarmConfig-list.vue

@@ -61,7 +61,7 @@
       >删除选中项</el-button>
     </el-row>
     <el-table
-    ref="formTable"
+      ref="formTable"
       :data="tableData"
       v-loading="loading"
       :height="tableHeight"
@@ -126,6 +126,7 @@ import NProgress from "nprogress"; // progress bar
 import "nprogress/nprogress.css"; // progress bar style
 
 export default {
+  name: "BaseAlarmConfigList",
   data() {
     var self = this;
 

+ 1 - 0
src/views/base/alarmInfo-alert-list.vue

@@ -81,6 +81,7 @@ import NProgress from "nprogress"; // progress bar
 import "nprogress/nprogress.css"; // progress bar style
 
 export default {
+  name: "BaseAlarmInfoAlertList",
   data() {
     var self = this;
 

+ 1 - 0
src/views/base/alarmInfo-fence-list.vue

@@ -81,6 +81,7 @@ import NProgress from "nprogress"; // progress bar
 import "nprogress/nprogress.css"; // progress bar style
 
 export default {
+  name: "BaseAlarmInfoFenceList",
   data() {
     var self = this;
 

+ 1 - 0
src/views/base/companyInfo-list.vue

@@ -192,6 +192,7 @@ import NProgress from "nprogress"; // progress bar
 import "nprogress/nprogress.css"; // progress bar style
 
 export default {
+  name: "BaseCompanyInfoList",
   data() {
     var self = this;
     return {

+ 1 - 0
src/views/base/deviceInfo-list.vue

@@ -277,6 +277,7 @@ import NProgress from "nprogress"; // progress bar
 import "nprogress/nprogress.css"; // progress bar style
 
 export default {
+  name: "BaseDeviceInfoList",
   data() {
     var self = this;
 

+ 1 - 0
src/views/base/devicePerson-list.vue

@@ -309,6 +309,7 @@ import NProgress from "nprogress"; // progress bar
 import "nprogress/nprogress.css"; // progress bar style
 
 export default {
+  name: "BaseDevicePersonList",
   data() {
     var self = this;
 

+ 1 - 0
src/views/base/electricClientInfo-list.vue

@@ -136,6 +136,7 @@ import NProgress from "nprogress"; // progress bar
 import "nprogress/nprogress.css"; // progress bar style
 
 export default {
+  name: "BaseElectricClientInfoList",
   data() {
     var self = this;
 

+ 1 - 0
src/views/base/electricMeterInfo-list.vue

@@ -111,6 +111,7 @@ import NProgress from "nprogress"; // progress bar
 import "nprogress/nprogress.css"; // progress bar style
 
 export default {
+  name: "BaseElectricMeterInfoList",
   data() {
     var self = this;
 

+ 1 - 0
src/views/base/employeeInfo-list.vue

@@ -147,6 +147,7 @@ import NProgress from "nprogress"; // progress bar
 import "nprogress/nprogress.css"; // progress bar style
 
 export default {
+  name: "BaseEmployeeInfoList",
   data() {
     var self = this;
 

+ 1 - 0
src/views/base/holidayInfo-list.vue

@@ -77,6 +77,7 @@ import NProgress from "nprogress"; // progress bar
 import "nprogress/nprogress.css"; // progress bar style
 
 export default {
+  name: "BaseHolidayInfoList",
   data() {
     var self = this;
 

+ 1 - 0
src/views/base/informationInfo-complaint-list.vue

@@ -104,6 +104,7 @@ import NProgress from "nprogress"; // progress bar
 import "nprogress/nprogress.css"; // progress bar style
 
 export default {
+  name: "BaseInformationInfoComplaintList",
   data() {
     var self = this;
 

+ 1 - 0
src/views/base/informationInfo-warranty-list.vue

@@ -111,6 +111,7 @@ import NProgress from "nprogress"; // progress bar
 import "nprogress/nprogress.css"; // progress bar style
 
 export default {
+  name: "BaseInformationInfoWarrantyList",
   data() {
     var self = this;
 

+ 1 - 1
src/views/base/messageNotice-detail.vue

@@ -59,7 +59,7 @@ import Constant from "@/constant";
 import messageNoticeApi from "@/api/base/messageNotice";
 import companyInfoApi from "@/api/base/companyInfo";
 import { getToken } from '@/utils/auth'
-import MceEditor from "@/components/tinymce";
+import MceEditor from "@/components/Tinymce";
 
 export default {
   props: ["businessKey", "title"],

+ 1 - 0
src/views/base/messageNotice-list.vue

@@ -92,6 +92,7 @@ import NProgress from "nprogress"; // progress bar
 import "nprogress/nprogress.css"; // progress bar style
 
 export default {
+  name: "BaseMessageNoticeList",
   data() {
     var self = this;
 

+ 1 - 0
src/views/base/messageReport-list.vue

@@ -71,6 +71,7 @@ import NProgress from "nprogress"; // progress bar
 import "nprogress/nprogress.css"; // progress bar style
 
 export default {
+  name: "BaseMessageReportList",
   data() {
     var self = this;
 

+ 1 - 0
src/views/base/ownerInfo-list.vue

@@ -138,6 +138,7 @@ import NProgress from "nprogress"; // progress bar
 import "nprogress/nprogress.css"; // progress bar style
 
 export default {
+  name: "BaseOwnerInfoList",
   data() {
     var self = this;
 

+ 1 - 0
src/views/base/parkingApply-list.vue

@@ -110,6 +110,7 @@ import NProgress from "nprogress"; // progress bar
 import "nprogress/nprogress.css"; // progress bar style
 
 export default {
+  name: "BaseParkingApplyList",
   data() {
     var self = this;
 

+ 1 - 0
src/views/base/parkingInfo-list.vue

@@ -101,6 +101,7 @@ import NProgress from "nprogress"; // progress bar
 import "nprogress/nprogress.css"; // progress bar style
 
 export default {
+  name: "BaseParkingInfoList",
   data() {
     var self = this;
 

+ 1 - 0
src/views/base/personDeviceLog-list.vue

@@ -185,6 +185,7 @@ import NProgress from "nprogress"; // progress bar
 import "nprogress/nprogress.css"; // progress bar style
 
 export default {
+  name: "BasePersonDeviceLogList",
   data() {
     var self = this;
 

+ 1 - 0
src/views/base/personInfo-list.vue

@@ -475,6 +475,7 @@ import NProgress from "nprogress"; // progress bar
 import "nprogress/nprogress.css"; // progress bar style
 
 export default {
+  name: "BasePersonInfoList",
   data() {
     var self = this;
 

+ 1 - 0
src/views/base/rechange-list.vue

@@ -177,6 +177,7 @@ import NProgress from "nprogress"; // progress bar
 import "nprogress/nprogress.css"; // progress bar style
 
 export default {
+  name: "BaseRechangeList",
   data() {
     var self = this;
 

+ 1 - 0
src/views/base/rechargeRecord-list.vue

@@ -227,6 +227,7 @@ import NProgress from "nprogress"; // progress bar
 import "nprogress/nprogress.css"; // progress bar style
 
 export default {
+  name: "BaseRechargeRecordList",
   data() {
     var self = this;
 

+ 1 - 0
src/views/base/rechargeRecordProperty-list.vue

@@ -211,6 +211,7 @@ import NProgress from "nprogress"; // progress bar
 import "nprogress/nprogress.css"; // progress bar style
 
 export default {
+  name: "BaseRechargeRecordPropertyList",
   data() {
     var self = this;
 

+ 2 - 1
src/views/base/rechargeRecordWater-list.vue

@@ -6,7 +6,7 @@
         <a href="#">系统管理</a>
       </el-breadcrumb-item>
       <el-breadcrumb-item>
-        <a href="/rechargeRecord">充值流水</a>
+        <a href="/rechargeRecord">物业充值流水</a>
       </el-breadcrumb-item>
     </el-breadcrumb>
     <el-divider></el-divider>
@@ -227,6 +227,7 @@ import NProgress from "nprogress"; // progress bar
 import "nprogress/nprogress.css"; // progress bar style
 
 export default {
+  name: "BaseRechargeRecordWaterList",
   data() {
     var self = this;
 

+ 1 - 0
src/views/base/roomInfo-list.vue

@@ -81,6 +81,7 @@ import NProgress from "nprogress"; // progress bar
 import "nprogress/nprogress.css"; // progress bar style
 
 export default {
+  name: "BaseRoomInfoList",
   data() {
     var self = this;
 

+ 1 - 0
src/views/base/temperatureRecord-list.vue

@@ -128,6 +128,7 @@ import NProgress from "nprogress"; // progress bar
 import "nprogress/nprogress.css"; // progress bar style
 
 export default {
+  name: "BaseTemperatureRecordList",
   data() {
     var self = this;
 

+ 1 - 0
src/views/base/terminalInfo-list.vue

@@ -101,6 +101,7 @@ import NProgress from "nprogress"; // progress bar
 import "nprogress/nprogress.css"; // progress bar style
 
 export default {
+  name: "BaseTerminalInfoList",
   data() {
     var self = this;
 

+ 1 - 0
src/views/base/warningPusher-list.vue

@@ -153,6 +153,7 @@ import NProgress from "nprogress"; // progress bar
 import "nprogress/nprogress.css"; // progress bar style
 
 export default {
+  name: "BaseWarningPusherList",
   data() {
     var self = this;
 

+ 1 - 1
src/views/business/fillAttendance-list.vue

@@ -104,6 +104,7 @@ import NProgress from "nprogress"; // progress bar
 import "nprogress/nprogress.css"; // progress bar style
 
 export default {
+  name: "BusinessWorkAttendanceList",
   data() {
     var self = this;
 
@@ -170,7 +171,6 @@ export default {
 
         if (jsonData.result) {
           this.treeData = jsonData.data;
-          alert(this.treeData);
         } else {
           this.$message.error(jsonData.message + "");
         }

+ 1 - 0
src/views/business/workAttendance-list.vue

@@ -159,6 +159,7 @@ import NProgress from "nprogress"; // progress bar
 import "nprogress/nprogress.css"; // progress bar style
 
 export default {
+  name: "BusinessWorkAttendanceList",
   data() {
     var self = this;
 

+ 27 - 0
src/views/layout/AppMain.vue

@@ -0,0 +1,27 @@
+<template>
+  <section class="app-main">
+    <transition name="fade-transform" mode="out-in">
+      <keep-alive :include="cachedViews">
+        <router-view :key="key" />
+      </keep-alive>
+    </transition>
+  </section>
+</template>
+
+<script>
+export default {
+  name: 'AppMain',
+  computed: {
+    cachedViews() {
+      return this.$store.state.tagsView.cachedViews
+    },
+    key() {
+      return this.$route.path
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+@import '@/styles/transition.scss';
+</style>

+ 85 - 0
src/views/layout/TagsView/ScrollPane.vue

@@ -0,0 +1,85 @@
+<template>
+  <el-scrollbar ref="scrollContainer" :vertical="false" class="scroll-container" @wheel.native.prevent="handleScroll">
+    <slot />
+  </el-scrollbar>
+</template>
+
+<script>
+const tagAndTagSpacing = 4 // tagAndTagSpacing
+
+export default {
+  name: 'ScrollPane',
+  data() {
+    return {
+      left: 0
+    }
+  },
+  computed: {
+    scrollWrapper() {
+      return this.$refs.scrollContainer.$refs.wrap
+    }
+  },
+  methods: {
+    handleScroll(e) {
+      const eventDelta = e.wheelDelta || -e.deltaY * 40
+      const $scrollWrapper = this.scrollWrapper
+      $scrollWrapper.scrollLeft = $scrollWrapper.scrollLeft + eventDelta / 4
+    },
+    moveToTarget(currentTag) {
+      const $container = this.$refs.scrollContainer.$el
+      const $containerWidth = $container.offsetWidth
+      const $scrollWrapper = this.scrollWrapper
+      const tagList = this.$parent.$refs.tag
+
+      let firstTag = null
+      let lastTag = null
+
+      // find first tag and last tag
+      if (tagList.length > 0) {
+        firstTag = tagList[0]
+        lastTag = tagList[tagList.length - 1]
+      }
+
+      if (firstTag === currentTag) {
+        $scrollWrapper.scrollLeft = 0
+      } else if (lastTag === currentTag) {
+        $scrollWrapper.scrollLeft = $scrollWrapper.scrollWidth - $containerWidth
+      } else {
+        // find preTag and nextTag
+        const currentIndex = tagList.findIndex(item => item === currentTag)
+        const prevTag = tagList[currentIndex - 1]
+        const nextTag = tagList[currentIndex + 1]
+
+        // the tag's offsetLeft after of nextTag
+        const afterNextTagOffsetLeft = nextTag.$el.offsetLeft + nextTag.$el.offsetWidth + tagAndTagSpacing
+
+        // the tag's offsetLeft before of prevTag
+        const beforePrevTagOffsetLeft = prevTag.$el.offsetLeft - tagAndTagSpacing
+
+        if (afterNextTagOffsetLeft > $scrollWrapper.scrollLeft + $containerWidth) {
+          $scrollWrapper.scrollLeft = afterNextTagOffsetLeft - $containerWidth
+        } else if (beforePrevTagOffsetLeft < $scrollWrapper.scrollLeft) {
+          $scrollWrapper.scrollLeft = beforePrevTagOffsetLeft
+        }
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.scroll-container {
+  white-space: nowrap;
+  position: relative;
+  overflow: hidden;
+  width: 100%;
+  /deep/ {
+    .el-scrollbar__bar {
+      bottom: 0px;
+    }
+    .el-scrollbar__wrap {
+      height: 49px;
+    }
+  }
+}
+</style>

+ 293 - 0
src/views/layout/TagsView/index.vue

@@ -0,0 +1,293 @@
+<template>
+  <div id="tags-view-container" class="tags-view-container">
+    <scroll-pane ref="scrollPane" class="tags-view-wrapper">
+      <router-link
+        v-for="tag in visitedViews"
+        ref="tag"
+        :key="tag.path"
+        :class="isActive(tag)?'active':''"
+        :to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }"
+        tag="span"
+        class="tags-view-item"
+        @click.middle.native="!isAffix(tag)?closeSelectedTag(tag):''"
+        @contextmenu.prevent.native="openMenu(tag,$event)"
+      >
+        {{ tag.title }}
+        <span v-if="!isAffix(tag)" class="el-icon-close" @click.prevent.stop="closeSelectedTag(tag)" />
+      </router-link>
+    </scroll-pane>
+    <ul v-show="visible" :style="{left:left+'px',top:top+'px'}" class="contextmenu">
+      <li @click="refreshSelectedTag(selectedTag)">刷新</li>
+      <li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)">关闭</li>
+      <li @click="closeOthersTags">关闭其它</li>
+      <li @click="closeAllTags(selectedTag)">关闭所有</li>
+    </ul>
+  </div>
+</template>
+
+<script>
+import ScrollPane from './ScrollPane'
+import path from 'path'
+
+export default {
+  components: { ScrollPane },
+  data() {
+    return {
+      visible: false,
+      top: 0,
+      left: 0,
+      selectedTag: {},
+      affixTags: []
+    }
+  },
+  computed: {
+    visitedViews() {
+      return this.$store.state.tagsView.visitedViews
+    },
+    routes() {
+      return this.$store.state.permission.routes
+    }
+  },
+  watch: {
+    $route() {
+      this.addTags()
+      this.moveToCurrentTag()
+    },
+    visible(value) {
+      if (value) {
+        document.body.addEventListener('click', this.closeMenu)
+      } else {
+        document.body.removeEventListener('click', this.closeMenu)
+      }
+    }
+  },
+  mounted() {
+    this.initTags()
+    this.addTags()
+  },
+  methods: {
+    isActive(route) {
+      return route.path === this.$route.path
+    },
+    isAffix(tag) {
+      return tag.meta && tag.meta.affix
+    },
+    filterAffixTags(routes, basePath = '/') {
+      let tags = []
+      routes.forEach(route => {
+        if (route.meta && route.meta.affix) {
+          const tagPath = path.resolve(basePath, route.path)
+          tags.push({
+            fullPath: tagPath,
+            path: tagPath,
+            name: route.name,
+            meta: { ...route.meta }
+          })
+        }
+        if (route.children) {
+          const tempTags = this.filterAffixTags(route.children, route.path)
+          if (tempTags.length >= 1) {
+            tags = [...tags, ...tempTags]
+          }
+        }
+      })
+      return tags
+    },
+    initTags() {
+      const affixTags = this.affixTags = this.filterAffixTags(this.routes)
+      for (const tag of affixTags) {
+        // Must have tag name
+        if (tag.name) {
+          this.$store.dispatch('tagsView/addVisitedView', tag)
+        }
+      }
+    },
+    addTags() {
+      console.log("addTags");
+
+      const { name } = this.$route
+      if (name) {
+        this.$store.dispatch('tagsView/addView', this.$route)
+      }
+      return false
+    },
+    moveToCurrentTag() {
+      const tags = this.$refs.tag
+      this.$nextTick(() => {
+        for (const tag of tags) {
+          if (tag.to.path === this.$route.path) {
+            this.$refs.scrollPane.moveToTarget(tag)
+            // when query is different then update
+            if (tag.to.fullPath !== this.$route.fullPath) {
+              this.$store.dispatch('tagsView/updateVisitedView', this.$route)
+            }
+            break
+          }
+        }
+      })
+    },
+    refreshSelectedTag(view) {
+      this.$store.dispatch('tagsView/delCachedView', view).then(() => {
+        const { fullPath } = view
+        this.$nextTick(() => {
+          this.$router.replace({
+            path: '/redirect' + fullPath
+          })
+        })
+      })
+    },
+    closeSelectedTag(view) {
+      this.$store.dispatch('tagsView/delView', view).then(({ visitedViews }) => {
+        if (this.isActive(view)) {
+          this.toLastView(visitedViews, view)
+        }
+      })
+    },
+    closeOthersTags() {
+      this.$router.push(this.selectedTag)
+      this.$store.dispatch('tagsView/delOthersViews', this.selectedTag).then(() => {
+        this.moveToCurrentTag()
+      })
+    },
+    closeAllTags(view) {
+      this.$store.dispatch('tagsView/delAllViews').then(({ visitedViews }) => {
+        if (this.affixTags.some(tag => tag.path === view.path)) {
+          return
+        }
+        this.toLastView(visitedViews, view)
+      })
+    },
+    toLastView(visitedViews, view) {
+      const latestView = visitedViews.slice(-1)[0]
+      if (latestView) {
+        this.$router.push(latestView.fullPath)
+      } else {
+        // now the default is to redirect to the home page if there is no tags-view,
+        // you can adjust it according to your needs.
+        if (view.name === 'Dashboard') {
+          // to reload home page
+          this.$router.replace({ path: '/redirect' + view.fullPath })
+        } else {
+          this.$router.push('/')
+        }
+      }
+    },
+    openMenu(tag, e) {
+      const menuMinWidth = 105
+      const offsetLeft = this.$el.getBoundingClientRect().left // container margin left
+      const offsetWidth = this.$el.offsetWidth // container width
+      const maxLeft = offsetWidth - menuMinWidth // left boundary
+      const left = e.clientX - offsetLeft + 15 // 15: margin right
+
+      if (left > maxLeft) {
+        this.left = maxLeft
+      } else {
+        this.left = left
+      }
+
+      this.top = e.clientY
+      this.visible = true
+      this.selectedTag = tag
+    },
+    closeMenu() {
+      this.visible = false
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.tags-view-container {
+  height: 32px;
+  width: 100%;
+  background: #fff;
+  // border-bottom: 1px solid #d8dce5;
+  // box-shadow: 0 1px 3px 0 rgba(0, 0, 0, .12), 0 0 3px 0 rgba(0, 0, 0, .04);
+  text-align: left;
+  
+  .tags-view-wrapper {
+    .tags-view-item {
+      display: inline-block;
+      position: relative;
+      cursor: pointer;
+      height: 26px;
+      line-height: 26px;
+      border: 1px solid #d8dce5;
+      color: #495060;
+      background: #fff;
+      padding: 0 8px;
+      font-size: 12px;
+      margin-left: 5px;
+      margin-top: 4px;
+      &:first-of-type {
+        margin-left: 15px;
+      }
+      &:last-of-type {
+        margin-right: 15px;
+      }
+      &.active {
+        background-color: #42b983;
+        color: #fff;
+        border-color: #42b983;
+        &::before {
+          content: '';
+          background: #fff;
+          display: inline-block;
+          width: 8px;
+          height: 8px;
+          border-radius: 50%;
+          position: relative;
+          margin-right: 2px;
+        }
+      }
+    }
+  }
+  .contextmenu {
+    margin: 0;
+    background: #fff;
+    z-index: 3000;
+    position: absolute;
+    list-style-type: none;
+    padding: 5px 0;
+    border-radius: 4px;
+    font-size: 12px;
+    font-weight: 400;
+    color: #333;
+    box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, .3);
+    li {
+      margin: 0;
+      padding: 7px 16px;
+      cursor: pointer;
+      &:hover {
+        background: #eee;
+      }
+    }
+  }
+}
+</style>
+
+<style lang="scss">
+//reset element css of el-icon-close
+.tags-view-wrapper {
+  .tags-view-item {
+    .el-icon-close {
+      width: 16px;
+      height: 16px;
+      vertical-align: 2px;
+      border-radius: 50%;
+      text-align: center;
+      transition: all .3s cubic-bezier(.645, .045, .355, 1);
+      transform-origin: 100% 50%;
+      &:before {
+        transform: scale(.6);
+        display: inline-block;
+        vertical-align: -3px;
+      }
+      &:hover {
+        background-color: #b4bccc;
+        color: #fff;
+      }
+    }
+  }
+}
+</style>

+ 31 - 9
src/views/layout/index.vue

@@ -10,9 +10,12 @@
         <a href="#" @click="openChangePwDialog()" style="margin-right:10px;">修改密码</a>
         <a href="#" @click="logout()">退出</a>
       </div>
+      <div style="position:absolute;bottom:0px;left:205px;right:185px;">
+        <tags-view />
+      </div>
     </el-header>
     <el-container>
-      <el-aside :width="sidebarWidth" v-loading="loading" class="sidebar">
+      <el-aside :width="sidebarWidth" :height="sidebarHeight" v-loading="loading" class="sidebar"> 
         <div class="sidebar-collpase-btn" @click="collapse=!collapse" style="cursor:pointer">
           <i :class="sidbarHandlerClass"></i>
         </div>
@@ -22,13 +25,15 @@
           @close="handleClose"
           @select="handleSelect"
           :collapse="collapse"
+          :unique-opened="true"
           :collapse-transition="false"
         >
           <menu-tree-item :routes="menuList"></menu-tree-item>
         </el-menu>
       </el-aside>
       <el-main>
-        <router-view />
+        <!-- <router-view /> -->
+        <app-main />
       </el-main>
       <el-dialog
         title="修改密码"
@@ -74,10 +79,17 @@
 </template>
 <script>
 import MenuTreeItem from "@/components/MenuTreeItem";
+import AppMain from './AppMain';
+import TagsView from './TagsView';
 import menuApi from "@/api/sys/menu";
 import userApi from "@/api/sys/user";
 
 export default {
+  components: {
+    "menu-tree-item": MenuTreeItem,
+    "app-main": AppMain,
+    "tags-view": TagsView
+  },
   data() {
     let samePassword = (rule, value, callback) => {
       var self = this;
@@ -89,6 +101,7 @@ export default {
         return callback();
       }
     };
+
     return {
       ruleValidate: {
         oldPassword: [{ required: true, message: "不能为空", trigger: "blur" }],
@@ -114,7 +127,8 @@ export default {
         oldPassword: "",
         newPassword: "",
         newPasswordTwo: ""
-      }
+      },
+      sidebarHeight: window.innerHeight
     };
   },
   computed: {
@@ -188,9 +202,6 @@ export default {
       this.$refs["form"].resetFields();
     }
   },
-  components: {
-    "menu-tree-item": MenuTreeItem
-  },
   mounted() {
     this.loading = true;
 
@@ -203,6 +214,8 @@ export default {
     menuApi
       .getMenuTree()
       .then(response => {
+        console.log(response);
+
         var jsonData = response.data;
 
         this.menuList = jsonData.data;
@@ -227,10 +240,8 @@ export default {
   background-color: #fff;
   color: #333;
   text-align: left;
-  height: 70px !important;
-  line-height: 70px;
   // border-bottom: 2px solid rgb(36,61,162);
-  border-bottom: 2px solid #64a63c;
+  border-bottom: 2px solid #00aaff;
 }
 
 .el-header h3 {
@@ -298,4 +309,15 @@ export default {
   color: black;
   font-weight: bold;
 }
+
+.menu-wrapper{
+  position: absolute;
+  top:0px;
+  bottom:0px;
+  left:0px;
+  right:0px;
+  overflow: auto;
+  display: flex;
+  flex-direction: column;
+}
 </style>

+ 12 - 0
src/views/redirect/index.vue

@@ -0,0 +1,12 @@
+<script>
+export default {
+  created() {
+    const { params, query } = this.$route
+    const { path } = params
+    this.$router.replace({ path: '/' + path, query })
+  },
+  render: function(h) {
+    return h() // avoid warning message
+  }
+}
+</script>

+ 1 - 0
src/views/sys/dataDictionary-list.vue

@@ -140,6 +140,7 @@ import NProgress from "nprogress"; // progress bar
 import "nprogress/nprogress.css"; // progress bar style
 
 export default {
+  name: 'SysDataDictionaryList',
   data() {
     var self = this;
 

+ 247 - 0
src/views/sys/log-list.vue

@@ -0,0 +1,247 @@
+<template>
+  <div>
+    <el-breadcrumb separator=">">
+      <el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
+      <el-breadcrumb-item>
+        <a href="#">系统管理</a>
+      </el-breadcrumb-item>
+      <el-breadcrumb-item>
+        <a href="/sys/log">日志管理</a>
+      </el-breadcrumb-item>
+    </el-breadcrumb>
+    <el-divider></el-divider>
+    <!--
+      要resetFields起作用,必须配置:model和prop
+    -->
+    <el-form ref="queryForm" :model="queryModel" inline class="demo-form-inline">      
+      <el-form-item label="起始时间" prop="startTime">
+        <el-date-picker
+          v-model="queryModel.startTime"
+          type="datetime" size="mini" format="yyyy-MM-dd HH:mm" value-format="yyyy-MM-dd HH:mm"
+          placeholder="选择日期时间"></el-date-picker>
+      </el-form-item>
+      <el-form-item label="结束时间" prop="endTime">
+        <el-date-picker
+          v-model="queryModel.endTime"
+          type="datetime" size="mini" format="yyyy-MM-dd HH:mm" value-format="yyyy-MM-dd HH:mm"
+          placeholder="选择日期时间"></el-date-picker>
+      </el-form-item>
+      <el-form-item label="用户编号" prop="userId">
+        <el-input type="text" size="mini" v-model="queryModel.userId"></el-input>
+      </el-form-item>
+      <el-form-item label="入口" prop="pointcut">
+        <el-input type="text" size="mini" v-model="queryModel.pointcut"></el-input>
+      </el-form-item>
+      <el-form-item label="备注" prop="remark">
+        <el-input type="text" size="mini" v-model="queryModel.remark"></el-input>
+      </el-form-item>
+      <el-form-item>
+        <el-button
+          type="primary"
+          size="mini"
+          icon="ios-search"
+          @click="changePage(1)"
+          :loading="loading"
+        >查询</el-button>&nbsp;
+        <el-button
+          type="info"
+          size="mini"
+          style="margin-left: 8px"
+          @click="handleReset('queryForm')"
+        >重置</el-button>&nbsp;
+      </el-form-item>
+    </el-form>
+    <el-divider></el-divider>
+    <el-table
+      :data="tableData"
+      style="min-height:400px;"
+      v-loading="loading"
+      stripe
+      border
+    >
+      <el-table-column type="expand">
+        <template slot-scope="{row}">
+          <el-form label-position="left" inline>
+            <el-form-item label="参数:">
+              <span>{{ row.data }}</span>
+            </el-form-item>
+            <el-form-item label="远程IP:">
+              <span>{{ row.remoteIp }}</span>
+            </el-form-item>
+          </el-form>
+        </template>
+      </el-table-column>
+      <el-table-column
+        prop="pointcut"
+        label="入口"
+        width="250"
+      ></el-table-column>
+      <el-table-column
+        prop="userId"
+        label="用户编号"
+        width="180"
+      ></el-table-column>
+      <el-table-column 
+        prop="remark"
+        label="备注">
+        <template slot-scope="{row}">
+            <span>{{ row.remark }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column 
+        prop="elapse"
+        label="耗时(毫秒)">
+        <template slot-scope="{row}">
+            <span>{{ row.elapse }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column
+        prop="createTime"
+        sort-by="create_time"
+        label="创建时间"
+        width="180"
+      ></el-table-column>
+    </el-table>
+    <el-pagination
+      :current-page.sync="pageIndex"
+      :total="totalElements"
+      :page-sizes="pageSizeList"
+      @current-change="changePage"
+      @size-change="pageSizeChange"
+      layout="total, sizes, prev, pager, next, jumper"
+    ></el-pagination>
+  </div>
+</template>
+<script>
+import Constant from "@/constant";
+import logApi from "@/api/sys/log";
+import NProgress from "nprogress"; // progress bar
+import "nprogress/nprogress.css"; // progress bar style
+
+export default {
+  name:'SysLogList',
+  data() {
+    var self = this;
+    var now = new Date();
+
+    var dateStr = [];
+
+    dateStr.push(now.getFullYear());
+    dateStr.push("-");
+
+    var month = now.getMonth()+1;
+
+    if(month<10){
+      dateStr.push("0");
+    }
+
+    dateStr.push(month);
+    dateStr.push("-");
+
+    var day = now.getDate();
+
+    if(day<10){
+      dateStr.push("0");
+    }
+
+    dateStr.push(day);
+
+    return {
+      queryModel: {
+        userId: "",
+        pointcut: "",
+        data: "",
+        remark: "",
+        startTime: dateStr.join("") + " 00:00",
+        endTime: dateStr.join("") + " 23:59"
+      },
+      loading: false,
+      tableData: [],
+      pageIndex: 1,
+      pageSize: 10,
+      totalPages: 0,
+      totalElements: 0,
+      field: "",
+      direction: "",
+      pageSizeList: [10, 20, 30],
+      multipleSelection: []
+    };
+  },
+  methods: {
+    changePage(pageIndex) {
+      var self = this;
+
+      console.log(pageIndex);
+
+      self.pageIndex = pageIndex;
+      var formData = new FormData();
+
+      formData.append("pageIndex", self.pageIndex);
+      formData.append("pageSize", self.pageSize);
+
+      formData.append("userId", self.queryModel.userId);
+      formData.append("pointcut", self.queryModel.pointcut);
+      formData.append("remark", self.queryModel.remark);
+      formData.append("startTime", self.queryModel.startTime);
+      formData.append("endTime", self.queryModel.endTime);
+
+
+      self.loading = true;
+
+      logApi.pageList(formData).then(function(response) {
+        self.loading = false;
+
+        var jsonData = response.data;
+
+        if(jsonData.result){
+          var pageInfo = jsonData.data;
+
+          self.tableData = pageInfo.data;
+          self.totalPages = pageInfo.totalPages;
+          self.totalElements = pageInfo.recordsTotal;
+        }
+        else {
+          self.$message({
+              message: jsonData.message + "",
+              type: "warning"
+            });
+        }
+      }).catch((error)=>{
+        self.loading = false;
+      });
+    },
+    pageSizeChange(pageSize) {
+      this.pageSize = pageSize;
+
+      this.changePage(1);
+    },
+    handleReset(name) {
+      this.$refs[name].resetFields();
+    }
+  },
+  mounted: function() {
+    var self = this;
+    this.changePage(1);
+  }
+};
+</script>
+<style lang="scss" scoped>
+  .el-breadcrumb {
+    margin: 10px;
+    line-height: 20px;
+  }
+
+  .el-divider {
+    margin: 5px 0;
+  }
+
+  .demo-form-inline {
+    margin-left: 10px;
+    text-align: left;
+  }
+
+  .button-group {
+    padding: 10px;
+    text-align: left;
+  }
+</style>

+ 1 - 0
src/views/sys/menu-list.vue

@@ -149,6 +149,7 @@ import NProgress from "nprogress"; // progress bar
 import "nprogress/nprogress.css"; // progress bar style
 
 export default {
+  name: 'SysMenuList',
   data() {
     var self = this;
 

+ 1 - 0
src/views/sys/permission-list.vue

@@ -117,6 +117,7 @@ import NProgress from "nprogress"; // progress bar
 import "nprogress/nprogress.css"; // progress bar style
 
 export default {
+  name: 'SysPermissionList',
   data() {
     var self = this;
 

+ 1 - 0
src/views/sys/role-list.vue

@@ -131,6 +131,7 @@ import NProgress from "nprogress"; // progress bar
 import "nprogress/nprogress.css"; // progress bar style
 
 export default {
+  name: 'SysRoleList',
   data() {
     var self = this;
 

+ 1 - 0
src/views/sys/user-list.vue

@@ -161,6 +161,7 @@ import NProgress from "nprogress"; // progress bar
 import "nprogress/nprogress.css"; // progress bar style
 
 export default {
+  name: 'SysUserList',
   data() {
     var self = this;