多页签增加持久化功能

多页签的快捷键切换功能可自行启用停用
设置面板中的多页签项独立化
config增加了本地存储配置
其他代码优化
master
toesbieya 6 years ago
parent 7a27e8a0b9
commit f6a628cf66
  1. 11
      vue/src/config/index.js
  2. 11
      vue/src/layout/component/Aside/index.vue
  3. 71
      vue/src/layout/component/Header/component/Navbar/component/SettingDrawer.vue
  4. 28
      vue/src/layout/component/Header/component/TagsView/index.vue
  5. 6
      vue/src/layout/mixin/decideRouterTransition.js
  6. 56
      vue/src/layout/mixin/tagsViewPersistent.js
  7. 29
      vue/src/layout/mixin/tagsViewShortcut.js
  8. 10
      vue/src/layout/store/tagsView.js
  9. 2
      vue/src/router/guardian/security.js
  10. 23
      vue/src/store/module/resource.js
  11. 6
      vue/src/util/auth.js
  12. 117
      vue/src/util/storage.js

@ -48,7 +48,7 @@ module.exports = {
//文件配置 //文件配置
file: { file: {
//上传地址 //上传地址
uploadUrl:'https://upload.qiniup.com', uploadUrl: 'https://upload.qiniup.com',
//上传后的存储地址前缀 //上传后的存储地址前缀
storePrefix: 'https://static.toesbieya.cn/', storePrefix: 'https://static.toesbieya.cn/',
@ -57,6 +57,15 @@ module.exports = {
previewPrefix: isDev ? 'http://localhost:8012' : 'https://preview.toesbieya.cn', previewPrefix: isDev ? 'http://localhost:8012' : 'https://preview.toesbieya.cn',
}, },
//本地存储配置
storage: {
//键名前缀
keyPrefix: 'GCC-',
//读写时是否进行压缩的默认值
zip: !isDev
},
//省市地区json数据请求地址 //省市地区json数据请求地址
regionDataUrl: `${contextPath}static/json/region-pca.json` regionDataUrl: `${contextPath}static/json/region-pca.json`
} }

@ -115,11 +115,14 @@ export default {
}, },
// //
shouldBindMouseMoveEvent(v) { shouldBindMouseMoveEvent: {
// immediate: true,
window.removeEventListener('mousemove', this.moveEvent) handler(v) {
//
window.removeEventListener('mousemove', this.moveEvent)
v && window.addEventListener('mousemove', this.moveEvent) v && window.addEventListener('mousemove', this.moveEvent)
}
} }
}, },

@ -26,10 +26,6 @@
<span>显示页头</span> <span>显示页头</span>
<el-switch :value="pageGetters.showPageHeader" @input="changePageShowHeader"/> <el-switch :value="pageGetters.showPageHeader" @input="changePageShowHeader"/>
</div> </div>
<div class="drawer-item">
<span>使用多页签</span>
<el-switch :value="tagsViewGetters.enabled" @input="changeTagsViewEnabled"/>
</div>
<div class="drawer-item"> <div class="drawer-item">
<span>显示返回顶部按钮</span> <span>显示返回顶部按钮</span>
<el-switch :value="pageGetters.showBackToTop" @input="changePageBackToTop"/> <el-switch :value="pageGetters.showBackToTop" @input="changePageBackToTop"/>
@ -56,6 +52,20 @@
<span>自动隐藏</span> <span>自动隐藏</span>
<el-switch :value="asideGetters.autoHide" @input="changeAsideAutoHide"/> <el-switch :value="asideGetters.autoHide" @input="changeAsideAutoHide"/>
</div> </div>
<el-divider>多页签设置</el-divider>
<div class="drawer-item">
<span>启用</span>
<el-switch :value="tagsViewGetters.enabled" @input="changeTagsViewEnabled"/>
</div>
<div class="drawer-item">
<span>启用快捷键切换</span>
<el-switch :value="tagsViewGetters.shortcut" @input="changeTagsViewShortcut"/>
</div>
<div class="drawer-item">
<span>持久化</span>
<el-switch :value="tagsViewGetters.persistent" @input="changeTagsViewPersistent"/>
</div>
</div> </div>
</el-drawer> </el-drawer>
</template> </template>
@ -111,7 +121,6 @@ export default {
}, },
page: { page: {
showPageHeader: true, showPageHeader: true,
useTagsView: true,
showBackToTop: true showBackToTop: true
}, },
aside: { aside: {
@ -120,6 +129,11 @@ export default {
collapse: false, collapse: false,
showParentOnCollapse: false, showParentOnCollapse: false,
autoHide: false autoHide: false
},
tagsView: {
enabled: true,
shortcut: true,
persistent: true
} }
} }
} }
@ -128,13 +142,12 @@ export default {
methods: { methods: {
//layoutstore //layoutstore
updateLayoutStore() { updateLayoutStore() {
const {app, page, aside} = this.setting const {app, page, aside, tagsView} = this.setting
appMutations.color(app.color) appMutations.color(app.color)
appMutations.navMode(app.navMode) appMutations.navMode(app.navMode)
pageMutations.showPageHeader(page.showPageHeader) pageMutations.showPageHeader(page.showPageHeader)
tagsViewMutations.enabled(page.useTagsView)
pageMutations.showBackToTop(page.showBackToTop) pageMutations.showBackToTop(page.showBackToTop)
asideMutations.showLogo(aside.showLogo) asideMutations.showLogo(aside.showLogo)
@ -142,15 +155,18 @@ export default {
asideMutations.collapse(aside.collapse) asideMutations.collapse(aside.collapse)
asideMutations.showParentOnCollapse(aside.showParentOnCollapse) asideMutations.showParentOnCollapse(aside.showParentOnCollapse)
asideMutations.autoHide(aside.autoHide) asideMutations.autoHide(aside.autoHide)
tagsViewMutations.enabled(tagsView.enabled)
tagsViewMutations.shortcut(tagsView.shortcut)
tagsViewMutations.persistent(tagsView.persistent)
}, },
//layoutstore //layoutstore
syncLayoutStore() { syncLayoutStore() {
const {app, page, aside} = this.setting const {app, page, aside, tagsView} = this.setting
app.color = appGetters.color app.color = appGetters.color
app.navMode = appGetters.navMode app.navMode = appGetters.navMode
page.showPageHeader = pageGetters.showPageHeader page.showPageHeader = pageGetters.showPageHeader
page.useTagsView = tagsViewGetters.enabled
page.showBackToTop = pageGetters.showBackToTop page.showBackToTop = pageGetters.showBackToTop
aside.showLogo = asideGetters.showLogo aside.showLogo = asideGetters.showLogo
@ -158,6 +174,10 @@ export default {
aside.collapse = asideGetters.collapse aside.collapse = asideGetters.collapse
aside.showParentOnCollapse = asideGetters.showParentOnCollapse aside.showParentOnCollapse = asideGetters.showParentOnCollapse
aside.autoHide = asideGetters.autoHide aside.autoHide = asideGetters.autoHide
tagsView.enabled = tagsViewGetters.enabled
tagsView.shortcut = tagsViewGetters.shortcut
tagsView.persistent = tagsViewGetters.persistent
}, },
// //
saveSetting() { saveSetting() {
@ -168,49 +188,56 @@ export default {
changeThemeColor(v) { changeThemeColor(v) {
isDev && client.changer.changeColor({newColors: forElementUI.getElementUISeries(v)}) isDev && client.changer.changeColor({newColors: forElementUI.getElementUISeries(v)})
appMutations.color(v) appMutations.color(v)
this.saveSetting()
}, },
changeNavMode(v) { changeNavMode(v) {
appMutations.navMode(v) appMutations.navMode(v)
this.saveSetting()
}, },
changePageShowHeader(v) { changePageShowHeader(v) {
pageMutations.showPageHeader(v) pageMutations.showPageHeader(v)
this.saveSetting()
},
changeTagsViewEnabled(v) {
tagsViewMutations.enabled(v)
this.saveSetting()
}, },
changePageBackToTop(v) { changePageBackToTop(v) {
pageMutations.showBackToTop(v) pageMutations.showBackToTop(v)
this.saveSetting()
}, },
changeAsideShowLogo(v) { changeAsideShowLogo(v) {
asideMutations.showLogo(v) asideMutations.showLogo(v)
this.saveSetting()
}, },
changeAsideUniqueOpen(v) { changeAsideUniqueOpen(v) {
asideMutations.uniqueOpen(v) asideMutations.uniqueOpen(v)
this.saveSetting()
}, },
changeAsideCollapse(v) { changeAsideCollapse(v) {
asideMutations.collapse(v) asideMutations.collapse(v)
this.saveSetting()
}, },
changeAsideCollapseParent(v) { changeAsideCollapseParent(v) {
asideMutations.showParentOnCollapse(v) asideMutations.showParentOnCollapse(v)
this.saveSetting()
}, },
changeAsideAutoHide(v) { changeAsideAutoHide(v) {
asideMutations.autoHide(v) asideMutations.autoHide(v)
this.saveSetting() },
changeTagsViewEnabled(v) {
tagsViewMutations.enabled(v)
},
changeTagsViewShortcut(v) {
tagsViewMutations.shortcut(v)
},
changeTagsViewPersistent(v) {
tagsViewMutations.persistent(v)
} }
}, },
created() { created() {
//change
for (const key of Object.keys(this)) {
if (!key.startsWith('change')) continue
const originMethod = this[key]
this[key] = (...arg) => {
originMethod.apply(this, arg)
this.saveSetting()
}
}
mergeObj(this.setting, getLocalPersonalSettings()) mergeObj(this.setting, getLocalPersonalSettings())
// //

@ -1,6 +1,7 @@
<script type="text/jsx"> <script type="text/jsx">
import shortcutMixin from '@/layout/mixin/shortcut' import shortcutMixin from '@/layout/mixin/tagsViewShortcut'
import decideRouterTransitionMixin from '@/layout/mixin/decideRouterTransition' import decideRouterTransitionMixin from '@/layout/mixin/decideRouterTransition'
import persistenceMixin from '@/layout/mixin/tagsViewPersistent'
import {route as routeConfig} from '@/config' import {route as routeConfig} from '@/config'
import {getters as appGetters} from "@/layout/store/app" import {getters as appGetters} from "@/layout/store/app"
import {getters as tagsViewGetters, mutations as tagsViewMutations} from "@/layout/store/tagsView" import {getters as tagsViewGetters, mutations as tagsViewMutations} from "@/layout/store/tagsView"
@ -8,7 +9,7 @@ import ContextMenu from "@/component/menu/ContextMenu"
import ScrollPanel from './ScrollPanel' import ScrollPanel from './ScrollPanel'
export default { export default {
mixins: [shortcutMixin, decideRouterTransitionMixin], mixins: [shortcutMixin, decideRouterTransitionMixin, persistenceMixin],
components: {ContextMenu, ScrollPanel}, components: {ContextMenu, ScrollPanel},
@ -19,9 +20,7 @@ export default {
top: 0, top: 0,
left: 0, left: 0,
}, },
selectedTag: {}, selectedTag: {}
affixTags: [],
scroller: null
} }
}, },
@ -31,15 +30,14 @@ export default {
}, },
watch: { watch: {
$route(to, from) { $route(to) {
this.decideRouteTransition && this.decideRouteTransition(to, from) this.addTag(to)
this.addTags(to)
this.$nextTick(this.moveToCurrentTag) this.$nextTick(this.moveToCurrentTag)
} }
}, },
methods: { methods: {
// //redirect
isActive({path}) { isActive({path}) {
const {path: routePath} = this.$route const {path: routePath} = this.$route
return routePath === path || routePath === `/redirect${path}` return routePath === path || routePath === `/redirect${path}`
@ -70,14 +68,12 @@ export default {
return tags return tags
} }
this.affixTags = getAffixTags(this.menus) for (const tag of getAffixTags(this.menus)) {
for (const tag of this.affixTags) {
tagsViewMutations.addVisitedView(tag) tagsViewMutations.addVisitedView(tag)
} }
}, },
//meta.titletab
//meta.titletab addTag(to = this.$route) {
addTags(to = this.$route) {
to.meta.title && tagsViewMutations.addView(to) to.meta.title && tagsViewMutations.addView(to)
}, },
@ -145,7 +141,7 @@ export default {
renderTags(h) { renderTags(h) {
return this.visitedViews.map(tag => { return this.visitedViews.map(tag => {
const {title, path, query, fullPath} = tag const {path, query, fullPath, meta: {title}} = tag
const active = this.isActive(tag), affix = this.isAffix(tag) const active = this.isActive(tag), affix = this.isAffix(tag)
const closeSelectedTag = (e, tag) => { const closeSelectedTag = (e, tag) => {
e.preventDefault() e.preventDefault()
@ -191,7 +187,7 @@ export default {
mounted() { mounted() {
this.initTags() this.initTags()
this.addTags() this.addTag()
}, },
render(h) { render(h) {

@ -4,6 +4,12 @@ import {mutations as tagsViewMutations} from "@/layout/store/tagsView"
const {animate} = routeConfig const {animate} = routeConfig
export default { export default {
watch: {
$route(to, from) {
this.decideRouteTransition(to, from)
}
},
methods: { methods: {
//根据访问的tab页的左右顺序来确定路由动画 //根据访问的tab页的左右顺序来确定路由动画
decideRouteTransition(to, from) { decideRouteTransition(to, from) {

@ -0,0 +1,56 @@
/**
* 多页签持久化混入
* 初始化时读取本地存储中的数据
* 页签变化时写入本地存储
*/
import {getters as tagsViewGetters, mutations as tagsViewMutations} from "@/layout/store/tagsView"
import {debounce} from "@/util"
import {getTagsView, setTagsView} from "@/util/storage"
export default {
computed: {
shouldPersistent() {
return tagsViewGetters.persistent
}
},
watch: {
shouldPersistent: {
immediate: true,
handler(v) {
//尝试清除之前的watch
if (this.watchVisitedViewsCallback) {
this.watchVisitedViewsCallback()
this.watchVisitedViewsCallback = null
}
//停用持久化时清空本地存储的页签数据
if (!v) return setTagsView()
//启用时先存储一次(仅在mounted后,否则此时页签数据不完整)
this._isMounted && this.persistentTagsView(this.visitedViews)
this.watchVisitedViewsCallback = this.$watch('visitedViews', this.persistentTagsView)
}
}
},
methods: {
persistentTagsView(v) {
//只提取部分属性
setTagsView(v.map(({name, path, fullPath, meta}) => ({name, path, fullPath, meta})))
}
},
beforeCreate() {
this.persistentTagsView = debounce(this.persistentTagsView)
},
mounted() {
if (!this.shouldPersistent) return
const tags = getTagsView()
Array.isArray(tags) && tags.forEach(tagsViewMutations.addVisitedView)
}
}

@ -3,7 +3,28 @@
* ctrl + 下一个页签 * ctrl + 下一个页签
* ctrl + 上一个页签 * ctrl + 上一个页签
*/ */
import {getters as tagsViewGetters} from "@/layout/store/tagsView"
export default { export default {
computed: {
shouldBindKeydownEvent() {
return tagsViewGetters.shortcut
}
},
watch: {
shouldBindKeydownEvent: {
immediate: true,
handler(v) {
//尝试移除之前可能添加的事件
window.removeEventListener('keydown', this.shortcutsListen)
v && window.addEventListener('keydown', this.shortcutsListen)
}
}
},
methods: { methods: {
//上一个页签 //上一个页签
gotoViewFront() { gotoViewFront() {
@ -40,11 +61,7 @@ export default {
} }
}, },
mounted() { beforeDestroy() {
window.addEventListener('keydown', this.shortcutsListen) window.removeEventListener('keydown', this.shortcutsListen)
this.$once('hook:beforeDestroy', () => {
window.removeEventListener('keydown', this.shortcutsListen)
})
} }
} }

@ -11,8 +11,12 @@ import {createGetters, createMutations} from "@/util/observable"
const state = { const state = {
//是否启用 //是否启用
enabled: true, enabled: true,
//是否启用快捷键切换功能
shortcut: true,
//是否将页签持久化到sessionStorage
persistent: true,
//显示的页签,{...route,title:route.meta.title}对象数组 //显示的页签,routeConfig对象数组
visitedViews: [], visitedViews: [],
//缓存的页签,用于<keep-router-view-alive/>:include //缓存的页签,用于<keep-router-view-alive/>:include
@ -36,11 +40,9 @@ export const mutations = bindThis({
}, },
addVisitedView(view) { addVisitedView(view) {
const {title = '暂无标题'} = view.meta || {}
if (store.visitedViews.some(v => v.path === view.path)) return if (store.visitedViews.some(v => v.path === view.path)) return
store.visitedViews.push({...view, title}) store.visitedViews.push(view)
}, },
addCachedView(view) { addCachedView(view) {
const {noCache, iframe, usePathKey, useFullPathKey} = view.meta || {} const {noCache, iframe, usePathKey, useFullPathKey} = view.meta || {}

@ -18,7 +18,7 @@ const beforeEach = async (to, from, next) => {
//初始化路由和菜单权限 //初始化路由和菜单权限
if (!store.state.resource.init) { if (!store.state.resource.init) {
await store.dispatch('resource/init', {...store.state.user, addRoutes: true}) await store.dispatch('resource/init', store.state.user)
return next({...to, replace: true}) return next({...to, replace: true})
} }

@ -6,7 +6,7 @@ import {getDynamicRoutes} from '@/router/define'
import {parseRoutes, metaExtend} from "@/router/util" import {parseRoutes, metaExtend} from "@/router/util"
import {getAll} from "@/api/system/resource" import {getAll} from "@/api/system/resource"
import {isEmpty} from "@/util" import {isEmpty} from "@/util"
import {needAuth} from "@/util/auth" import {needAuth, auth} from "@/util/auth" // 此处存在循环引用?
import {createTree} from "@/util/tree" import {createTree} from "@/util/tree"
import {isExternal} from "@/util/validate" import {isExternal} from "@/util/validate"
@ -42,20 +42,20 @@ const mutations = {
} }
const actions = { const actions = {
init({commit}, {resources, admin, addRoutes = false}) { init({state, commit}, {resources, admin}) {
return getAll return getAll
.request() .request()
.then(({data}) => { .then(({data}) => {
commit('resource', {data: data || [], admin})
//动态添加路由,这里不需要进行权限过滤 //动态添加路由,这里不需要进行权限过滤
const routes = transformOriginRouteData(data) const routes = transformOriginRouteData(data)
metaExtend(routes) metaExtend(routes)
addRoutes && addDynamicRoutes(routes) //可能存在多次调用的情况,所以仅在第一次调用时添加进vue-router
!state.init && addDynamicRoutes(routes)
//获取经过权限过滤后的菜单 //生成经过权限过滤后的菜单
const menus = getAuthorizedMenus({resources, admin}, routes) appMutations.menus(getAuthorizedMenus({resources, admin}, routes))
appMutations.menus(menus)
commit('resource', {data: data || [], admin})
//设置初始化完成的标志 //设置初始化完成的标志
commit('init', true) commit('init', true)
@ -99,9 +99,10 @@ function getAuthorizedMenus({resources, admin}, menus) {
menus = JSON.parse(JSON.stringify(menus)) menus = JSON.parse(JSON.stringify(menus))
clean(menus) clean(menus)
addFullPath(menus) addFullPath(menus)
if (admin === true) return menus // if (admin === true) return menus
if (!resources) return [] // if (!resources) return []
filter(menus, i => !needAuth(i) || i.fullPath in resources) // filter(menus, i => !needAuth(i) || i.fullPath in resources)
filter(menus, i => !needAuth(i) || auth(i.fullPath))
return menus return menus
} }

@ -2,7 +2,11 @@ import store from '@/store'
import {uppercaseFirst} from "@/filter" import {uppercaseFirst} from "@/filter"
import {isEmpty} from "@/util" import {isEmpty} from "@/util"
//判断路由是否需要鉴权,需要则返回true /**
* @desc 判断是否需要鉴权需要则返回true
* @param route vue-router的routeConfig或项目中说明的路由配置项
* @returns {boolean}
*/
export function needAuth(route) { export function needAuth(route) {
if (route.path.startsWith('/redirect')) return false if (route.path.startsWith('/redirect')) return false
const {meta} = route const {meta} = route

@ -1,56 +1,117 @@
import {storage} from "@/config"
import {isEmpty} from "@/util" import {isEmpty} from "@/util"
import {unzip, zip} from "@/util/secret" import {unzip, zip} from "@/util/secret"
const sessionUserKey = 'GCC-SESS-USER' const {keyPrefix, zip: defaultUseZip} = storage
const localPersonalSettingsKey = 'GCC-LOCAL-PERSONAL-SETTINGS'
/**
* @desc 读取本地存储
* @param key 键名自动加上统一前缀
* @param storage window.sessionStorage或window.localStorage
* @param useZip 是否启用了压缩
* @returns {any|undefined}
*/
export function get(key, storage = window.sessionStorage, useZip = defaultUseZip) {
let obj = storage.getItem(`${keyPrefix}${key}`)
if (isEmpty(obj)) return undefined
try {
obj = JSON.parse(useZip ? unzip(obj) : obj)
}
catch (e) {
console.error(`获取本地存储[${key}]失败`, e)
obj = undefined
}
return obj
}
/**
* 写入本地存储
* @param key 键名自动加上统一前缀
* @param obj 需要写入本地存储的js对象
* @param storage window.sessionStorage或window.localStorage
* @param useZip 是否启用了压缩
*/
export function set(key, obj, storage = window.sessionStorage, useZip = defaultUseZip) {
const item = useZip ? zip(JSON.stringify(obj)) : JSON.stringify(obj)
storage.setItem(`${keyPrefix}${key}`, item)
}
/**
* @desc 判断本地存储中是否存在指定键名
* @param key 键名自动加上统一前缀
* @param storage window.sessionStorage或window.localStorage
* @returns {boolean} 存在则返回true
*/
export function exist(key, storage = window.sessionStorage) {
return !isEmpty(storage.getItem(`${keyPrefix}${key}`))
}
/**
* @desc 根据指定键名移除本地存储对应项
* @param key 键名自动加上统一前缀
* @param storage window.sessionStorage或window.localStorage
*/
export function remove(key, storage = window.sessionStorage) {
storage.removeItem(`${keyPrefix}${key}`)
}
const sessionUserKey = 'SESS-USER'
const localPersonalSettingsKey = 'LOCAL-PERSONAL-SETTINGS'
const sessionTagsViewKey = 'SESS-TAGS-VIEW'
export function isUserExist() { export function isUserExist() {
return !isEmpty(sessionStorage.getItem(sessionUserKey)) return exist(sessionUserKey)
} }
export function getUser() { export function getUser() {
let obj = sessionStorage.getItem(sessionUserKey) let user = get(sessionUserKey)
if (isEmpty(obj)) return {} if (isEmpty(user)) {
try { user = {}
obj = JSON.parse(unzip(obj))
}
catch (e) {
console.error('用户数据异常!', e)
obj = {}
removeUser() removeUser()
} }
return obj return user
} }
export function setUser(user) { export function setUser(user) {
if (isEmpty(user)) return removeUser() isEmpty(user) ? removeUser() : set(sessionUserKey, user)
sessionStorage.setItem(sessionUserKey, zip(JSON.stringify(user)))
} }
export function getLocalPersonalSettings() { export function getLocalPersonalSettings() {
let obj = localStorage.getItem(localPersonalSettingsKey) let settings = get(localPersonalSettingsKey, window.localStorage)
if (isEmpty(obj)) return {} if (isEmpty(settings)) {
try { settings = {}
obj = JSON.parse(obj)
}
catch (e) {
console.error('本地个性化设置数据解析失败!', e)
removeLocalPersonalSettings() removeLocalPersonalSettings()
return {}
} }
return obj return settings
} }
export function setLocalPersonalSettings(settings) { export function setLocalPersonalSettings(settings) {
isEmpty(settings) ? isEmpty(settings)
removeLocalPersonalSettings() : ? removeLocalPersonalSettings()
localStorage.setItem(localPersonalSettingsKey, JSON.stringify(settings)) : set(localPersonalSettingsKey, settings, window.localStorage)
}
export function getTagsView() {
let tagsView = get(sessionTagsViewKey)
if (isEmpty(tagsView)) {
tagsView = []
removeTagsView()
}
return tagsView
}
export function setTagsView(tagsView) {
isEmpty(tagsView) ? removeTagsView() : set(sessionTagsViewKey, tagsView)
} }
function removeUser() { function removeUser() {
sessionStorage.removeItem(sessionUserKey) remove(sessionUserKey)
} }
function removeLocalPersonalSettings() { function removeLocalPersonalSettings() {
localStorage.removeItem(localPersonalSettingsKey) remove(localPersonalSettingsKey, window.localStorage)
}
function removeTagsView() {
remove(sessionTagsViewKey)
} }

Loading…
Cancel
Save