增加了详情页缓存的功能

master
toesbieya 6 years ago
parent 9b0428468b
commit f754005959
  1. 1
      vue/src/components/Charts/PieChart.vue
  2. 99
      vue/src/layout/components/KeepAlive.js
  3. 7
      vue/src/layout/components/Main.vue
  4. 11
      vue/src/layout/components/Sidebar/index.vue
  5. 35
      vue/src/layout/components/TagsView/index.vue
  6. 12
      vue/src/layout/components/TagsView/mixin/decideRouterTransition.js
  7. 10
      vue/src/layout/components/TagsView/mixin/shortcuts.js
  8. 6
      vue/src/main.js
  9. 2
      vue/src/mixins/bizDocumentTableMixin.js
  10. 4
      vue/src/mixins/chart/logic.js
  11. 2
      vue/src/router/constant/modules/app.js
  12. 12
      vue/src/router/constant/modules/example.js
  13. 13
      vue/src/router/index.js
  14. 4
      vue/src/router/util.js
  15. 48
      vue/src/store/modules/tagsView.js
  16. 3
      vue/src/views/app/redirect.vue
  17. 17
      vue/src/views/example/detailPage.vue
  18. 15
      vue/src/views/system/role/index.vue
  19. 49
      vue/vue.config.js

@ -17,6 +17,7 @@
data: {
deep: true,
handler(val) {
if (!window.echarts) return
this.init(val)
}
}

@ -0,0 +1,99 @@
import {isEmpty} from "@/utils"
const KEY = '_routerViewKey'
function getFirstComponentChild(children) {
if (Array.isArray(children)) {
for (const c of children) {
if (!isEmpty(c) && (!isEmpty(c.componentOptions) || c.isComment && c.asyncFactory)) {
return c
}
}
}
}
function getComponentName(opts) {
return opts && (opts.Ctor.options.name || opts.tag)
}
function pruneCacheEntry(cache, key, current) {
const cached = cache[key]
if (cached && (!current || cached.tag !== current.tag)) {
cached.componentInstance && cached.componentInstance.$destroy()
}
delete cache[key]
}
function getCacheKey(route, componentOptions) {
if (KEY in componentOptions) return componentOptions[KEY]
const {path, meta} = route
const key = meta && meta.isDetailPage ? path : getComponentName(componentOptions)
componentOptions[KEY] = key
return key
}
export default {
name: 'keep-router-view-alive',
abstract: true,
props: {include: Array},
watch: {
include: {
deep: true,
handler(val) {
const {cache, $route, _vnode} = this
for (const key of Object.keys(cache)) {
const cachedNode = cache[key]
const name = getCacheKey($route, cachedNode.componentOptions)
if (name && !val.includes(name)) {
pruneCacheEntry(cache, key, _vnode)
}
}
}
}
},
created() {
this.cache = Object.create(null)
},
beforeDestroy() {
for (const key of Object.keys(this.cache)) {
pruneCacheEntry(this.cache, key)
}
},
render() {
const slot = this.$slots.default
const vnode = getFirstComponentChild(slot)
let componentOptions = vnode && vnode.componentOptions
if (componentOptions) {
//忽略<transition/>组件
const child = componentOptions.tag === 'transition' ? componentOptions.children[0] : vnode
componentOptions = child && child.componentOptions
if (componentOptions) {
const key = getCacheKey(this.$route, componentOptions)
const {include, cache} = this
if (include && !include.includes(key)) {
return vnode
}
if (cache[key]) {
child.componentInstance = cache[key].componentInstance
}
else cache[key] = child
child.data.keepAlive = true
}
}
return vnode || (slot && slot[0])
}
}

@ -2,11 +2,11 @@
<main class="app-main">
<el-scrollbar ref="scrollbar" v-show="!showIframe" class="scroll-container">
<div class="page-view">
<keep-router-view-alive :include="cachedViews">
<transition :name="transitionName" mode="out-in">
<keep-alive :include="cachedViews">
<router-view/>
</keep-alive>
</transition>
</keep-router-view-alive>
</div>
</el-scrollbar>
@ -29,11 +29,14 @@
<script>
import Vue from 'vue'
import {mapState} from 'vuex'
import KeepRouterViewAlive from "./KeepAlive"
import Footer from "./Footer"
export default {
name: 'AppMain',
components: {KeepRouterViewAlive},
computed: {
...mapState('app', {
scrollTop: state => state.scrollTop,

@ -32,6 +32,15 @@
sidebarAutoHidden: state => state.sidebarAutoHidden
}),
activeMenu() {
const route = this.$route
const {meta, path} = route
if (path.startsWith('/redirect')) {
return path.replace('/redirect', '')
}
return meta && meta.activeMenu ? meta.activeMenu : path
},
//pc
collapse() {
return this.sidebarCollapse && this.device === 'pc'
@ -116,7 +125,7 @@
background-color={variables['menu-background']}
collapse={this.collapse}
collapse-transition={false}
default-active={this.$route.path}
default-active={this.activeMenu}
unique-opened={this.sidebarUniqueOpen}
mode="vertical"
on-select={this.select}

@ -9,17 +9,17 @@
class="tags-view-item"
:to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }"
tag="div"
@contextmenu.prevent.native="openMenu(tag,$event)"
@dblclick.prevent.native="closeSelectedTag(tag)"
@contextmenu.prevent.native="e=>openMenu(tag,e)"
@dblclick.prevent.native="()=>closeSelectedTag(tag)"
>
{{ tag.title }}
<i v-if="!isAffix(tag)" class="el-icon-close" @click.prevent.stop="closeSelectedTag(tag)"/>
<i v-if="!isAffix(tag)" class="el-icon-close" @click.prevent.stop="()=>closeSelectedTag(tag)"/>
</router-link>
</scroll-pane>
<context-menu v-model="contextmenu.show" :left="contextmenu.left" :top="contextmenu.top">
<context-menu-item @click="refreshSelectedTag(selectedTag)">刷新</context-menu-item>
<context-menu-item v-show="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)">
<context-menu-item v-show="!isAffix(selectedTag)" @click="()=>closeSelectedTag(selectedTag)">
关闭
</context-menu-item>
<context-menu-item @click="closeOthersTags">关闭其他</context-menu-item>
@ -32,11 +32,11 @@
import ScrollPane from './ScrollPane'
import ContextMenu from "@/components/ContextMenu"
import ContextMenuItem from "@/components/ContextMenu/ContextMenuItem"
import shortcutsMixin from './mixin/shortcutsMixin'
import decideRouterTransitionMixin from './mixin/decideRouterTransition'
import shortcuts from './mixin/shortcuts'
import decideRouterTransition from './mixin/decideRouterTransition'
export default {
mixins: [shortcutsMixin, decideRouterTransitionMixin],
mixins: [shortcuts, decideRouterTransition],
components: {ContextMenu, ContextMenuItem, ScrollPane},
@ -66,8 +66,7 @@
watch: {
$route(to, from) {
this.decideRouteTransition && this.decideRouteTransition(to, from)
this.addTags(to)
this.moveToCurrentTag()
this.addTags(to).then(this.moveToCurrentTag)
},
'contextmenu.show'(v) {
@ -84,7 +83,7 @@
},
filterAffixTags(routes) {
let tags = []
const tags = []
routes.forEach(route => {
if (route.name && route.meta && route.meta.affix) {
tags.push({
@ -110,20 +109,16 @@
//nametab
addTags(to = this.$route) {
if (!to.name) return
return this.$store.dispatch('tagsView/addView', this.$route)
return to.name ? this.$store.dispatch('tagsView/addView', to) : Promise.resolve()
},
//tab
moveToCurrentTag() {
this.$nextTick(() => {
let tag = this.$refs.tag.find(i => i.to.path === this.$route.path)
const tag = this.$refs.tag.find(i => i.to.path === this.$route.path)
if (!tag) return
this.$refs.scrollPane.moveToTarget(tag)
//
if (tag.to.fullPath !== this.$route.fullPath) {
this.$store.commit('tagsView/updateVisitedViews', this.$route)
}
})
},
@ -138,7 +133,7 @@
closeSelectedTag(view) {
if (this.isAffix(view)) return
this.$store.dispatch('tagsView/delView', view)
.then(() => this.isActive(view) && this.gotoViewLast())
.then(() => this.isActive(view) && this.gotoLastView())
},
closeOthersTags() {
this.$store.dispatch('tagsView/delOthersViews', this.selectedTag)
@ -150,10 +145,10 @@
},
closeAllTags() {
this.$store.dispatch('tagsView/delAllViews')
.then(() => this.gotoViewLast())
.then(() => this.gotoLastView())
},
gotoViewLast() {
gotoLastView() {
if (this.visitedViews.length === 0) return this.$router.push('/')
const latest = this.visitedViews[this.visitedViews.length - 1]
if (this.$route.path !== latest.path) this.$router.push(latest.path)

@ -1,16 +1,16 @@
import {leftSideRouteTransition, rightSideRouteTransition} from '@/config'
import {leftSideRouteTransition as left, rightSideRouteTransition as right} from '@/config'
export default {
methods: {
//根据访问的tab页的左右顺序来确定路由动画
decideRouteTransition(to, from) {
let fromIndex = this.visitedViews.findIndex(i => i.path === from.path)
if (fromIndex < 0) return this.$store.commit('tagsView/transitionName', rightSideRouteTransition)
let transitionName = right
let toIndex = this.visitedViews.findIndex(i => i.path === to.path)
if (toIndex < 0) return this.$store.commit('tagsView/transitionName', rightSideRouteTransition)
const fromIndex = this.visitedViews.findIndex(i => i.path === from.path)
const toIndex = this.visitedViews.findIndex(i => i.path === to.path)
if (fromIndex < toIndex) transitionName = left
this.$store.commit('tagsView/transitionName', toIndex > fromIndex ? rightSideRouteTransition : leftSideRouteTransition)
this.$store.commit('tagsView/transitionName', transitionName)
},
}
}

@ -1,5 +1,11 @@
/**
* 多页签快捷键混入
* ctrl + 下一个页签
* ctrl + 上一个页签
*/
export default {
methods: {
//上一个页签
gotoViewFront() {
if (this.visitedViews.length <= 1) return
let index = this.visitedViews.findIndex(view => view.path === this.$route.path)
@ -7,6 +13,8 @@ export default {
if (index === 0) index = this.visitedViews.length
return this.$router.push({path: this.visitedViews[index - 1].path})
},
//下一个页签
gotoViewBehind() {
if (this.visitedViews.length <= 1) return
let index = this.visitedViews.findIndex(view => view.path === this.$route.path)
@ -31,9 +39,11 @@ export default {
}
}
},
mounted() {
window.addEventListener('keydown', this.shortcutsListen)
},
beforeDestroy() {
window.removeEventListener('keydown', this.shortcutsListen)
}

@ -42,3 +42,9 @@ new Vue({
//页面刷新时socket重连
store.dispatch('socket/init', store.state.user).catch()
window.addEventListener('unhandledrejection', event => {
if (event.reason.stack.startsWith('Error: Redirected when going from')) {
event.preventDefault()
}
})

@ -136,7 +136,7 @@ export default {
}
},
activated() {
let {type, id} = this.$route.params
const {type, id} = this.$route.params
if (!['see', 'edit'].includes(type) || isEmpty(id)) return
this.row = null
this.$nextTick(() => {

@ -11,12 +11,14 @@ export default {
default: '350px'
}
},
data() {
return {
loading: false,
chart: null
}
},
methods: {
$_getChartInstance() {
return waitUntilSuccess(
@ -28,9 +30,11 @@ export default {
return this.$el
}
},
mounted() {
this.$_getChartInstance().then(() => this.init())
},
beforeDestroy() {
if (!this.chart) return
this.chart.dispose()

@ -11,7 +11,7 @@ const router = [
component: Layout,
children: [
{
path: '/redirect/:path(.*)',
path: ':path(.*)',
component: Redirect
}
]

@ -91,6 +91,18 @@ const router = {
name: 'developingTest',
component: () => lazyLoadView(import('@/views/example/developingTest')),
meta: {title: '开发测试'}
},
{
path: 'detailPage1',
name: 'detailPage1',
component: () => lazyLoadView(import('@/views/example/detailPage')),
meta: {title: '详情页缓存1', noCache: false, isDetailPage: true}
},
{
path: 'detailPage2',
name: 'detailPage2',
component: () => lazyLoadView(import('@/views/example/detailPage')),
meta: {title: '详情页缓存2', noCache: false, isDetailPage: true}
}
]
}

@ -1,12 +1,13 @@
/*
* 路由配置
*
* 需要鉴权的路由meta && !meta.noAuth
* 需要鉴权的路由!meta.noAuth
* 左侧菜单显示name && !hidden && meta.title
* 左侧菜单排序能显示 && sort升序排列
* 左侧菜单不折叠只有一个children的路由alwaysShow
* 面包屑显示meta.title
* 搜索选项显示name && meta.title
* tab栏显示name && meta.title
* tab栏固定显示meta.affix
* 页面不缓存!name || meta.noCache
* 打开iframemeta.iframe不会重复打开相同src的iframe
@ -44,7 +45,15 @@ router.beforeEach(async (to, from, next) => {
//使用redirect进行跳转时不显示进度条
!to.path.startsWith('/redirect') && NProgress.start()
document.title = getPageTitle(to.meta.title)
//若是详情页之间的跳转,借助redirect避免组件复用
const isToDetailPage = to.meta && to.meta.isDetailPage,
isFromDetailPage = from.meta && from.meta.isDetailPage
if (isToDetailPage !== undefined && isToDetailPage === isFromDetailPage) {
//这里vue-router会报redirect错误
return next({path: `/redirect${to.path}`, query: to.query})
}
document.title = getPageTitle(to)
//白名单内不需要进行权限控制
if (whiteList.some(reg => reg.test(to.path))) return next()

@ -4,7 +4,9 @@ import {title} from "@/config"
import PageSkeleton from "@/components/Skeleton/PageSkeleton"
//拼接页面标题
export function getPageTitle(pageTitle) {
export function getPageTitle(route) {
let pageTitle = route.meta && route.meta.title
if (typeof pageTitle === 'function') pageTitle = pageTitle(route)
return pageTitle ? `${pageTitle} - ${title}` : title
}

@ -1,24 +1,36 @@
import {rightSideRouteTransition} from '@/config'
import {createMutations} from "@/utils"
const state = {
//显示的页签,{...route,title:route.meta.title}对象数组
visitedViews: [],
//缓存的页签,用于<keep-router-view-alive/>:include
cachedViews: [],
transitionName: rightSideRouteTransition
//路由过渡动画名称
transitionName: ''
}
const mutations = {
...createMutations(state),
addVisitedView(state, view) {
const {title = '暂无标题'} = view.meta || {}
if (state.visitedViews.some(v => v.path === view.path)) return
state.visitedViews.push({...view, title: view.meta.title || 'no-name'})
state.visitedViews.push({...view, title})
},
addCachedView(state, view) {
if (state.cachedViews.includes(view.name)) return
if (!view.meta.noCache && !view.meta.iframe) {
state.cachedViews.push(view.name)
}
const {noCache, iframe} = view.meta || {}
if (noCache || iframe) return
const key = getCachedViewKey(view)
if (state.cachedViews.includes(key)) return
state.cachedViews.push(key)
},
delVisitedView(state, view) {
@ -26,17 +38,9 @@ const mutations = {
index > -1 && state.visitedViews.splice(index, 1)
},
delCachedView(state, view) {
const index = state.cachedViews.indexOf(view.name)
const key = getCachedViewKey(view)
const index = state.cachedViews.indexOf(key)
index > -1 && state.cachedViews.splice(index, 1)
},
updateVisitedViews(state, view) {
for (let v of state.visitedViews) {
if (v.path === view.path) {
v = Object.assign(v, view)
break
}
}
}
}
@ -53,16 +57,22 @@ const actions = {
delOthersViews({state, commit}, view) {
const visitedViews = state.visitedViews.filter(v => v.meta.affix || v.path === view.path)
const name = state.cachedViews.find(name => name === view.name)
const name = state.cachedViews.find(name => name === getCachedViewKey(view))
commit('visitedViews', visitedViews)
commit('cachedViews', name ? [name] : [])
},
delAllViews({state, commit}) {
commit('visitedViews', state.visitedViews.filter(tag => tag.meta.affix))
commit('visitedViews', state.visitedViews.filter(tag => tag.meta && tag.meta.affix))
commit('cachedViews', [])
}
}
function getCachedViewKey(view) {
const isDetailPage = view.meta && view.meta.isDetailPage
return isDetailPage ? view.path : view.name
}
export default {
namespaced: true,
state,

@ -2,8 +2,9 @@
export default {
created() {
const {params, query} = this.$route
//使name+params
if ('params' in query) {
let {name, params} = JSON.parse(query.params)
const {name, params} = JSON.parse(query.params)
this.$router.replace({name, params})
}
else this.$router.replace({path: '/' + params.path, query})

@ -0,0 +1,17 @@
<template>
<div>
<div class="tip-row">详情页缓存示例</div>
<el-input v-model="data"/>
</div>
</template>
<script>
export default {
name: "detailPage",
data() {
return {
data: ''
}
}
}
</script>

@ -36,9 +36,6 @@
:data="tableData"
@row-click="row=$event"
>
<el-table-column align="center" type="expand">
<role-resource slot-scope="{row}" :ids="row.resource_id" :map="resourceMap"/>
</el-table-column>
<el-table-column align="center" label="#" type="index" width="80"/>
<el-table-column align="center" label="角色名" prop="name" show-overflow-tooltip/>
<el-table-column align="center" label="创建人" prop="cname" show-overflow-tooltip/>
@ -71,7 +68,6 @@
import SearchForm from "@/components/SearchForm"
import SearchFormItem from "@/components/SearchForm/SearchFormItem"
import EditDialog from './components/EditDialog'
import RoleResource from "./components/RoleResource"
import {delRole, searchRoles} from "@/api/system/role"
import {isEmpty} from '@/utils'
import {elConfirm, elError, elSuccess} from "@/utils/message"
@ -85,7 +81,7 @@
mixins: [tableMixin],
components: {SearchForm, SearchFormItem, EditDialog, RoleResource},
components: {SearchForm, SearchFormItem, EditDialog},
data() {
return {
@ -112,15 +108,6 @@
canDel() {
return auth(baseUrl + '/del')
},
resourceMap() {
const resource = this.$store.state.resource
if (resource.data.length <= 0) return {}
return resource.data.reduce((map, resource) => {
map[resource.id] = resource
return map
}, {})
}
},

@ -47,7 +47,7 @@ module.exports = {
resolve: {
alias: {
'@': resolve('src'),
'@ele': resolve('element-ui-personal'),
'@ele': resolve('element-ui-personal')
}
},
plugins: [
@ -80,53 +80,6 @@ module.exports = {
.loader('svg-sprite-loader')
.options({symbolId: 'icon-[name]'})
.end()
// set preserveWhitespace
config.module
.rule('vue')
.use('vue-loader')
.loader('vue-loader')
.tap(options => {
options.compilerOptions.preserveWhitespace = true
return options
})
.end()
config
.when(process.env.NODE_ENV !== 'development',
config => {
config
.plugin('ScriptExtHtmlWebpackPlugin')
.after('html')
.use('script-ext-html-webpack-plugin', [{inline: /runtime\..*\.js$/}])
.end()
config
.optimization.splitChunks({
chunks: 'all',
cacheGroups: {
libs: {
name: 'chunk-libs',
test: /[\\/]node_modules[\\/]/,
priority: 10,
chunks: 'initial' // only package third parties that are initially dependent
},
elementUI: {
name: 'chunk-elementUI', // split elementUI into a single package
priority: 20, // the weight needs to be larger than libs and app or it will be packaged into libs or app
test: /[\\/]node_modules[\\/]_?element-ui(.*)/ // in order to adapt to cnpm
},
commons: {
name: 'chunk-commons',
test: resolve('src/components'), // can customize your rules
minChunks: 3, // minimum common number
priority: 5,
reuseExistingChunk: true
}
}
})
config.optimization.runtimeChunk('single')
}
)
},
css: {
loaderOptions: {

Loading…
Cancel
Save