使用 pinia 替换 vuex

This commit is contained in:
hooray 2021-12-30 03:09:32 +08:00
parent 65eff15674
commit 5d7461c5cc
36 changed files with 699 additions and 640 deletions

View File

@ -46,8 +46,6 @@ module.exports = {
withDefaults: 'readonly',
// vue-router
useRoute: 'readonly',
useRouter: 'readonly',
// vuex
useStore: 'readonly'
useRouter: 'readonly'
}
}

View File

@ -31,12 +31,12 @@
"nprogress": "^0.2.0",
"path-browserify": "^1.0.1",
"path-to-regexp": "^6.2.0",
"pinia": "^2.0.9",
"qs": "^6.10.2",
"screenfull": "^6.0.0",
"tinymce": "^5.10.2",
"vue": "^3.2.24",
"vue-router": "^4.0.12",
"vuex": "^4.0.2"
"vue-router": "^4.0.12"
},
"devDependencies": {
"@vitejs/plugin-vue": "^1.10.2",

View File

@ -4,18 +4,8 @@
</div>
</template>
<script>
export default {
{{#if isGlobal}}
name: '{{ properCase name }}',
{{/if}}
props: {},
data() {
return {}
},
mounted() {},
methods: {}
}
<script setup{{#if isGlobal}} name="{{ properCase name }}"{{/if}}>
// const { proxy } = getCurrentInstance()
</script>
<style lang="scss" scoped>

View File

@ -11,7 +11,6 @@
// const { proxy } = getCurrentInstance()
// const router = useRouter()
// const route = useRoute()
// const store = useStore()
</script>
<style lang="scss" scoped>

View File

@ -1,15 +1,16 @@
const state = () => ({})
import { defineStore } from 'pinia'
import { piniaStore } from '@/store'
const getters = {}
export const use{{ properCase name }}Store = defineStore(
// 唯一ID
'{{ camelCase name }}',
{
state: () => ({}),
getters: {},
actions: {}
}
)
const actions = {}
const mutations = {}
export default {
namespaced: true,
state,
actions,
getters,
mutations
export function use{{ properCase name }}OutsideStore() {
return use{{ properCase name }}Store(piniaStore)
}

View File

@ -14,11 +14,11 @@ module.exports = {
}
}
],
actions: data => {
actions: () => {
const actions = [
{
type: 'add',
path: `src/store/modules/${data.name}.js`,
path: 'src/store/modules/{{camelCase name}}.js',
templateFile: 'plop-templates/store/index.hbs'
}
]

View File

@ -22,6 +22,7 @@ specifiers:
nprogress: ^0.2.0
path-browserify: ^1.0.1
path-to-regexp: ^6.2.0
pinia: ^2.0.9
plop: ^3.0.4
postcss-html: ^1.3.0
postcss-scss: ^4.0.2
@ -51,7 +52,6 @@ specifiers:
vue: ^3.2.24
vue-eslint-parser: ^8.0.1
vue-router: ^4.0.12
vuex: ^4.0.2
dependencies:
'@element-plus/icons-vue': 0.2.4_vue@3.2.24
@ -68,12 +68,12 @@ dependencies:
nprogress: 0.2.0
path-browserify: 1.0.1
path-to-regexp: 6.2.0
pinia: 2.0.9_vue@3.2.24
qs: 6.10.2
screenfull: 6.0.0
tinymce: 5.10.2
vue: 3.2.24
vue-router: 4.0.12_vue@3.2.24
vuex: 4.0.2_vue@3.2.24
devDependencies:
'@vitejs/plugin-vue': 1.10.2_vite@2.7.1
@ -413,6 +413,10 @@ packages:
resolution: {integrity: sha1-+OiAWdqkJFFZkkJqDH6lzeB+mb8=, tarball: '@vue/devtools-api/download/@vue/devtools-api-6.0.0-beta.19.tgz'}
dev: false
/@vue/devtools-api/6.0.0-beta.21.1:
resolution: {integrity: sha512-FqC4s3pm35qGVeXRGOjTsRzlkJjrBLriDS9YXbflHLsfA9FrcKzIyWnLXoNm+/7930E8rRakXuAc2QkC50swAw==, tarball: '@vue/devtools-api/download/@vue/devtools-api-6.0.0-beta.21.1.tgz'}
dev: false
/@vue/reactivity/3.2.24:
resolution: {integrity: sha512-5eVsO9wfQ5erCMSRBjpqLkkI+LglJS7E0oLZJs2gsChpvOjH2Uwt3Hk1nVv0ywStnWg71Ykn3SyQwtnl7PknOQ==, tarball: '@vue/reactivity/download/@vue/reactivity-3.2.24.tgz'}
dependencies:
@ -3808,6 +3812,23 @@ packages:
engines: {node: '>=8.6'}
dev: true
/pinia/2.0.9_vue@3.2.24:
resolution: {integrity: sha512-iuYdxLJKQ07YPyOHYH05wNG9eKWqkP/4y4GE8+RqEYtz5fwHgPA5kr6zQbg/DoEJGnR2XCm1w1vdt6ppzL9ATg==, tarball: pinia/download/pinia-2.0.9.tgz}
peerDependencies:
'@vue/composition-api': ^1.4.0
typescript: '>=4.4.4'
vue: ^2.6.14 || ^3.2.0
peerDependenciesMeta:
'@vue/composition-api':
optional: true
typescript:
optional: true
dependencies:
'@vue/devtools-api': 6.0.0-beta.21.1
vue: 3.2.24
vue-demi: 0.12.1_vue@3.2.24
dev: false
/pixelsmith/2.5.0:
resolution: {integrity: sha1-ez5SLgjqh3vXC63xWb3D7Ffh6EE=, tarball: pixelsmith/download/pixelsmith-2.5.0.tgz}
engines: {node: '>= 8.0.0'}
@ -5477,15 +5498,6 @@ packages:
'@vue/shared': 3.2.24
dev: false
/vuex/4.0.2_vue@3.2.24:
resolution: {integrity: sha1-+Jbb1b8qDpY/AMZ+m2EN50nMrMk=, tarball: vuex/download/vuex-4.0.2.tgz}
peerDependencies:
vue: ^3.0.2
dependencies:
'@vue/devtools-api': 6.0.0-beta.19
vue: 3.2.24
dev: false
/wcwidth/1.0.1:
resolution: {integrity: sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=, tarball: wcwidth/download/wcwidth-1.0.1.tgz}
dependencies:

View File

@ -8,13 +8,14 @@
</template>
<script setup>
const store = useStore()
import { useSettingsStore } from '@/store/modules/settings'
const settingsStore = useSettingsStore()
//
const mainSidebarActualWidth = computed(() => {
let actualWidth = getComputedStyle(document.documentElement).getPropertyValue('--g-main-sidebar-width')
actualWidth = parseInt(actualWidth)
if (['head', 'single'].includes(store.state.settings.menuMode)) {
if (['head', 'single'].includes(settingsStore.menuMode)) {
actualWidth = 0
}
return `${actualWidth}px`
@ -24,39 +25,39 @@ const mainSidebarActualWidth = computed(() => {
const subSidebarActualWidth = computed(() => {
let actualWidth = getComputedStyle(document.documentElement).getPropertyValue('--g-sub-sidebar-width')
actualWidth = parseInt(actualWidth)
if (store.state.settings.sidebarCollapse) {
if (settingsStore.sidebarCollapse) {
actualWidth = 64
}
return `${actualWidth}px`
})
watch(() => store.state.settings.mode, () => {
if (store.state.settings.mode === 'pc') {
store.commit('settings/updateThemeSetting', {
'sidebarCollapse': store.state.settings.sidebarCollapseLastStatus
watch(() => settingsStore.mode, () => {
if (settingsStore.mode === 'pc') {
settingsStore.updateThemeSetting({
'sidebarCollapse': settingsStore.sidebarCollapseLastStatus
})
} else if (store.state.settings.mode === 'mobile') {
store.commit('settings/updateThemeSetting', {
} else if (settingsStore.mode === 'mobile') {
settingsStore.updateThemeSetting({
'sidebarCollapse': true
})
}
document.body.setAttribute('data-mode', store.state.settings.mode)
document.body.setAttribute('data-mode', settingsStore.mode)
}, {
immediate: true
})
watch(() => store.state.settings.menuMode, () => {
document.body.setAttribute('data-menu-mode', store.state.settings.menuMode)
watch(() => settingsStore.menuMode, () => {
document.body.setAttribute('data-menu-mode', settingsStore.menuMode)
}, {
immediate: true
})
watch([
() => store.state.settings.enableDynamicTitle,
() => store.state.settings.title
() => settingsStore.enableDynamicTitle,
() => settingsStore.title
], () => {
if (store.state.settings.enableDynamicTitle && store.state.settings.title) {
let title = store.state.settings.title
if (settingsStore.enableDynamicTitle && settingsStore.title) {
let title = settingsStore.title
document.title = `${title} - ${import.meta.env.VITE_APP_TITLE}`
} else {
document.title = import.meta.env.VITE_APP_TITLE
@ -67,7 +68,7 @@ watch([
onMounted(() => {
window.onresize = () => {
store.commit('settings/setMode', document.documentElement.clientWidth)
settingsStore.setMode(document.documentElement.clientWidth)
}
window.onresize()
})

View File

@ -1,11 +1,12 @@
import axios from 'axios'
// import qs from 'qs'
import router from '@/router/index'
import store from '@/store/index'
import { ElMessage } from 'element-plus'
import { useUserOutsideStore } from '@/store/modules/user'
const toLogin = () => {
store.dispatch('user/logout').then(() => {
const userOutsideStore = useUserOutsideStore()
userOutsideStore.logout().then(() => {
router.push({
name: 'login',
query: {
@ -23,12 +24,13 @@ const api = axios.create({
api.interceptors.request.use(
request => {
const userOutsideStore = useUserOutsideStore()
/**
* 全局拦截请求发送前提交的参数
* 以下代码为示例在请求头里带上 token 信息
*/
if (store.getters['user/isLogin']) {
request.headers['Token'] = store.state.user.token
if (userOutsideStore.isLogin) {
request.headers['Token'] = userOutsideStore.token
}
// 是否将 POST 请求参数进行字符串化处理
if (request.method === 'post') {

View File

@ -1,12 +1,17 @@
<template>
<footer class="copyright">
Copyright © {{ $store.state.settings.copyrightDates }}
<a v-if="$store.state.settings.copyrightWebsite" :href="$store.state.settings.copyrightWebsite" target="_blank" rel="noopener">{{ $store.state.settings.copyrightCompany }},</a>
<span v-else>{{ $store.state.settings.copyrightCompany }},</span>
Copyright © {{ settingsStore.copyrightDates }}
<a v-if="settingsStore.copyrightWebsite" :href="settingsStore.copyrightWebsite" target="_blank" rel="noopener">{{ settingsStore.copyrightCompany }},</a>
<span v-else>{{ settingsStore.copyrightCompany }},</span>
All Rights Reserved
</footer>
</template>
<script setup>
import { useSettingsStore } from '@/store/modules/settings'
const settingsStore = useSettingsStore()
</script>
<style lang="scss" scoped>
footer {
margin: 40px 0 20px;

View File

@ -1,13 +1,13 @@
<template>
<transition name="header">
<header v-if="$store.state.settings.mode === 'pc' && $store.state.settings.menuMode === 'head'">
<header v-if="settingsStore.mode === 'pc' && settingsStore.menuMode === 'head'">
<div class="header-container">
<div class="main">
<Logo />
<!-- 顶部模式 -->
<div class="nav">
<template v-for="(item, index) in $store.getters['menu/routes']">
<div v-if="item.children && item.children.length !== 0" :key="index" class="item" :class="{'active': index == $store.state.menu.headerActived}" @click="switchMenu(index)">
<template v-for="(item, index) in menuStore.transformRoutes">
<div v-if="item.children && item.children.length !== 0" :key="index" class="item" :class="{'active': index == menuStore.headerActived}" @click="switchMenu(index)">
<svg-icon v-if="item.meta.icon" :name="item.meta.icon" />
<span v-if="item.meta.title">{{ item.meta.title }}</span>
</div>
@ -24,6 +24,11 @@
import Logo from '../Logo/index.vue'
import UserMenu from '../UserMenu/index.vue'
import { useSettingsStore } from '@/store/modules/settings'
const settingsStore = useSettingsStore()
import { useMenuStore } from '@/store/modules/menu'
const menuStore = useMenuStore()
const switchMenu = inject('switchMenu')
</script>

View File

@ -1,5 +1,5 @@
<template>
<router-link :to="to" class="title" :class="{'is-link': $store.state.settings.enableDashboard}" :title="title">
<router-link :to="to" class="title" :class="{'is-link': settingsStore.enableDashboard}" :title="title">
<img v-if="showLogo" :src="logo" class="logo">
<span v-if="showTitle">{{ title }}</span>
</router-link>
@ -8,7 +8,8 @@
<script setup>
import imgLogo from '@/assets/images/logo.png'
const store = useStore()
import { useSettingsStore } from '@/store/modules/settings'
const settingsStore = useSettingsStore()
defineProps({
showLogo: {
@ -26,7 +27,7 @@ const logo = ref(imgLogo)
const to = computed(() => {
let rtn = {}
if (store.state.settings.enableDashboard) {
if (settingsStore.enableDashboard) {
rtn.name = 'dashboard'
}
return rtn

View File

@ -1,14 +1,14 @@
<template>
<transition name="main-sidebar">
<div v-if="$store.state.settings.menuMode === 'side' || ($store.state.settings.mode === 'mobile' && $store.state.settings.menuMode !== 'single')" class="main-sidebar-container">
<div v-if="settingsStore.menuMode === 'side' || (settingsStore.mode === 'mobile' && settingsStore.menuMode !== 'single')" class="main-sidebar-container">
<Logo :show-title="false" class="sidebar-logo" />
<!-- 侧边栏模式含主导航 -->
<div class="nav">
<template v-for="(item, index) in $store.getters['menu/routes']">
<template v-for="(item, index) in menuStore.transformRoutes">
<div
v-if="item.children && item.children.length !== 0" :key="index" :class="{
'item': true,
'active': index === $store.state.menu.headerActived
'active': index === menuStore.headerActived
}" :title="item.meta.title" @click="switchMenu(index)"
>
<svg-icon v-if="item.meta.icon" :name="item.meta.icon" />
@ -23,6 +23,11 @@
<script setup>
import Logo from '../Logo/index.vue'
import { useSettingsStore } from '@/store/modules/settings'
const settingsStore = useSettingsStore()
import { useMenuStore } from '@/store/modules/menu'
const menuStore = useMenuStore()
const switchMenu = inject('switchMenu')
</script>

View File

@ -3,7 +3,7 @@
<div class="container">
<div class="search-box" @click.stop>
<el-input ref="input" v-model="searchInput" prefix-icon="el-icon-search" placeholder="搜索页面支持标题、URL模糊查询" clearable @keydown.esc="$eventBus.emit('global-search-toggle')" @keydown.up.prevent="keyUp" @keydown.down.prevent="keyDown" @keydown.enter.prevent="keyEnter" />
<div v-if="$store.state.settings.mode === 'pc'" class="tips">
<div v-if="settingsStore.mode === 'pc'" class="tips">
<div class="tip">
<span>Alt</span>+<span>S</span>
唤醒搜索面板
@ -23,7 +23,7 @@
</div>
</div>
</div>
<div ref="search" class="result" :class="{'mobile': $store.state.settings.mode === 'mobile'}">
<div ref="search" class="result" :class="{'mobile': settingsStore.mode === 'mobile'}">
<router-link v-for="(item, index) in resultList" :key="item.path" v-slot="{ href, navigate }" custom :to="isShow ? item.path : ''">
<a :ref="`search-item-${index}`" :href="isExternalLink(item.path) ? item.path : href" class="item" :class="{'actived': index === actived}" :target="isExternalLink(item.path) ? '_blank' : '_self'" @click="navigate" @mouseover="actived = index">
<div class="icon">
@ -53,7 +53,11 @@
import { deepClone, isExternalLink } from '@/util'
const { proxy } = getCurrentInstance()
const store = useStore()
import { useSettingsStore } from '@/store/modules/settings'
const settingsStore = useSettingsStore()
import { useMenuStore } from '@/store/modules/menu'
const menuStore = useMenuStore()
const isShow = ref(false)
const searchInput = ref('')
@ -110,12 +114,12 @@ onMounted(() => {
isShow.value = !isShow.value
})
proxy.$hotkeys('alt+s', e => {
if (store.state.settings.enableNavSearch) {
if (settingsStore.enableNavSearch) {
e.preventDefault()
isShow.value = true
}
})
store.state.menu.routes.map(item => {
menuStore.routes.map(item => {
getSourceList(item.children)
})
})

View File

@ -1,20 +1,20 @@
<template>
<div v-if="['side', 'head', 'single'].includes($store.state.settings.menuMode) || $store.state.settings.mode === 'mobile'" class="sub-sidebar-container" :class="{'is-collapse': $store.state.settings.mode === 'pc' && $store.state.settings.sidebarCollapse}" @scroll="onSidebarScroll">
<div v-if="['side', 'head', 'single'].includes(settingsStore.menuMode) || settingsStore.mode === 'mobile'" class="sub-sidebar-container" :class="{'is-collapse': settingsStore.mode === 'pc' && settingsStore.sidebarCollapse}" @scroll="onSidebarScroll">
<Logo
:show-logo="$store.state.settings.menuMode === 'single'" :class="{
:show-logo="settingsStore.menuMode === 'single'" :class="{
'sidebar-logo': true,
'sidebar-logo-bg': $store.state.settings.menuMode === 'single',
'sidebar-logo-bg': settingsStore.menuMode === 'single',
'shadow': sidebarScrollTop
}"
/>
<!-- 侧边栏模式无主导航 -->
<el-menu
:unique-opened="$store.state.settings.sidebarUniqueOpened" :default-openeds="$store.state.menu.defaultOpenedPaths" :default-active="$route.meta.activeMenu || $route.path" :collapse="$store.state.settings.mode === 'pc' && $store.state.settings.sidebarCollapse" :collapse-transition="false" :class="{
'is-collapse-without-logo': $store.state.settings.menuMode !== 'single' && $store.state.settings.sidebarCollapse
:unique-opened="settingsStore.sidebarUniqueOpened" :default-openeds="menuStore.defaultOpenedPaths" :default-active="$route.meta.activeMenu || $route.path" :collapse="settingsStore.mode === 'pc' && settingsStore.sidebarCollapse" :collapse-transition="false" :class="{
'is-collapse-without-logo': settingsStore.menuMode !== 'single' && settingsStore.sidebarCollapse
}"
>
<transition-group name="sub-sidebar">
<template v-for="route in $store.getters['menu/sidebarRoutes']">
<template v-for="route in menuStore.sidebarRoutes">
<SidebarItem v-if="route.meta.sidebar !== false" :key="route.path" :item="route" :base-path="route.path" />
</template>
</transition-group>
@ -26,6 +26,11 @@
import Logo from '../Logo/index.vue'
import SidebarItem from '../SidebarItem/index.vue'
import { useSettingsStore } from '@/store/modules/settings'
const settingsStore = useSettingsStore()
import { useMenuStore } from '@/store/modules/menu'
const menuStore = useMenuStore()
const sidebarScrollTop = ref(0)
function onSidebarScroll(e) {

View File

@ -2,8 +2,8 @@
<div>
<el-drawer v-model="isShow" title="主题配置" direction="rtl" :size="300">
<el-alert title="主题配置可实时预览效果,更多设置请在 src/settings.js 中进行设置,建议在生产环境隐藏主题配置功能" type="error" :closable="false" />
<el-divider v-if="$store.state.settings.mode === 'pc'">导航栏模式</el-divider>
<div v-if="$store.state.settings.mode === 'pc'" class="menu-mode">
<el-divider v-if="settingsStore.mode === 'pc'">导航栏模式</el-divider>
<div v-if="settingsStore.mode === 'pc'" class="menu-mode">
<el-tooltip content="侧边栏模式(含主导航)" placement="top" :show-after="500" :append-to-body="false">
<div class="mode mode-side" :class="{'active': menuMode === 'side'}" @click="menuMode = 'side'">
<svg-icon name="el-icon-check" />
@ -21,7 +21,7 @@
</el-tooltip>
</div>
<el-divider>侧边栏</el-divider>
<div v-if="$store.state.settings.mode === 'pc'" class="setting-item">
<div v-if="settingsStore.mode === 'pc'" class="setting-item">
<div class="label">折叠按钮</div>
<el-switch v-model="enableSidebarCollapse" />
</div>
@ -36,7 +36,7 @@
<svg-icon name="el-icon-question-filled" />
</el-tooltip>
</div>
<el-switch v-model="switchSidebarAndPageJump" :disabled="['single'].includes($store.state.settings.menuMode)" />
<el-switch v-model="switchSidebarAndPageJump" :disabled="['single'].includes(settingsStore.menuMode)" />
</div>
<div class="setting-item">
<div class="label">
@ -57,7 +57,7 @@
</div>
<el-switch v-model="topbarFixed" />
</div>
<div v-if="$store.state.settings.mode === 'pc'" class="setting-item">
<div v-if="settingsStore.mode === 'pc'" class="setting-item">
<div class="label">面包屑导航</div>
<el-switch v-model="enableBreadcrumb" />
</div>
@ -71,7 +71,7 @@
</div>
<el-switch v-model="enableNavSearch" />
</div>
<div v-if="$store.state.settings.mode === 'pc'" class="setting-item">
<div v-if="settingsStore.mode === 'pc'" class="setting-item">
<div class="label">
全屏
<el-tooltip content="该功能使用场景极少,用户习惯于通过窗口“最大化”功能来扩大显示区域,以显示更多内容,并且使用 F11 键也可以进入全屏效果" placement="top" :append-to-body="false">
@ -141,32 +141,36 @@
<script setup>
const { proxy } = getCurrentInstance()
const store = useStore()
const route = useRoute()
import { useSettingsStore } from '@/store/modules/settings'
const settingsStore = useSettingsStore()
import { useMenuStore } from '@/store/modules/menu'
const menuStore = useMenuStore()
const reload = inject('reload')
const isShow = ref(false)
const menuMode = computed({
get: function() {
return store.state.settings.menuMode
return settingsStore.menuMode
},
set: function(newValue) {
store.commit('menu/switchHeaderActived', 0)
store.commit('settings/updateThemeSetting', {
menuStore.switchHeaderActived(0)
settingsStore.updateThemeSetting({
'menuMode': newValue
})
store.state.settings.menuMode !== 'single' && store.commit('menu/setHeaderActived', route.fullPath)
settingsStore.menuMode !== 'single' && menuStore.setHeaderActived(route.fullPath)
}
})
const elementSize = computed({
get: function() {
return store.state.settings.elementSize
return settingsStore.elementSize
},
set: function(newValue) {
proxy.$ELEMENT.size = newValue
store.commit('settings/updateThemeSetting', {
settingsStore.updateThemeSetting({
'elementSize': newValue
})
reload()
@ -174,130 +178,130 @@ const elementSize = computed({
})
const enableSidebarCollapse = computed({
get: function() {
return store.state.settings.enableSidebarCollapse
return settingsStore.enableSidebarCollapse
},
set: function(newValue) {
store.commit('settings/updateThemeSetting', {
settingsStore.updateThemeSetting({
'enableSidebarCollapse': newValue
})
}
})
const sidebarCollapse = computed({
get: function() {
return store.state.settings.sidebarCollapse
return settingsStore.sidebarCollapse
},
set: function(newValue) {
store.commit('settings/updateThemeSetting', {
settingsStore.updateThemeSetting({
'sidebarCollapse': newValue
})
}
})
const switchSidebarAndPageJump = computed({
get: function() {
return store.state.settings.switchSidebarAndPageJump
return settingsStore.switchSidebarAndPageJump
},
set: function(newValue) {
store.commit('settings/updateThemeSetting', {
settingsStore.updateThemeSetting({
'switchSidebarAndPageJump': newValue
})
}
})
const sidebarUniqueOpened = computed({
get: function() {
return store.state.settings.sidebarUniqueOpened
return settingsStore.sidebarUniqueOpened
},
set: function(newValue) {
store.commit('settings/updateThemeSetting', {
settingsStore.updateThemeSetting({
'sidebarUniqueOpened': newValue
})
}
})
const topbarFixed = computed({
get: function() {
return store.state.settings.topbarFixed
return settingsStore.topbarFixed
},
set: function(newValue) {
store.commit('settings/updateThemeSetting', {
settingsStore.updateThemeSetting({
'topbarFixed': newValue
})
}
})
const enableBreadcrumb = computed({
get: function() {
return store.state.settings.enableBreadcrumb
return settingsStore.enableBreadcrumb
},
set: function(newValue) {
store.commit('settings/updateThemeSetting', {
settingsStore.updateThemeSetting({
'enableBreadcrumb': newValue
})
}
})
const showCopyright = computed({
get: function() {
return store.state.settings.showCopyright
return settingsStore.showCopyright
},
set: function(newValue) {
store.commit('settings/updateThemeSetting', {
settingsStore.updateThemeSetting({
'showCopyright': newValue
})
}
})
const enableNavSearch = computed({
get: function() {
return store.state.settings.enableNavSearch
return settingsStore.enableNavSearch
},
set: function(newValue) {
store.commit('settings/updateThemeSetting', {
settingsStore.updateThemeSetting({
'enableNavSearch': newValue
})
}
})
const enableFullscreen = computed({
get: function() {
return store.state.settings.enableFullscreen
return settingsStore.enableFullscreen
},
set: function(newValue) {
store.commit('settings/updateThemeSetting', {
settingsStore.updateThemeSetting({
'enableFullscreen': newValue
})
}
})
const enablePageReload = computed({
get: function() {
return store.state.settings.enablePageReload
return settingsStore.enablePageReload
},
set: function(newValue) {
store.commit('settings/updateThemeSetting', {
settingsStore.updateThemeSetting({
'enablePageReload': newValue
})
}
})
const enableProgress = computed({
get: function() {
return store.state.settings.enableProgress
return settingsStore.enableProgress
},
set: function(newValue) {
store.commit('settings/updateThemeSetting', {
settingsStore.updateThemeSetting({
'enableProgress': newValue
})
}
})
const enableDynamicTitle = computed({
get: function() {
return store.state.settings.enableDynamicTitle
return settingsStore.enableDynamicTitle
},
set: function(newValue) {
store.commit('settings/updateThemeSetting', {
settingsStore.updateThemeSetting({
'enableDynamicTitle': newValue
})
}
})
const enableDashboard = computed({
get: function() {
return store.state.settings.enableDashboard
return settingsStore.enableDashboard
},
set: function(newValue) {
store.commit('settings/updateThemeSetting', {
settingsStore.updateThemeSetting({
'enableDashboard': newValue
})
}

View File

@ -1,15 +1,15 @@
<template>
<div
class="topbar-container" :class="{
'fixed': $store.state.settings.topbarFixed,
'fixed': settingsStore.topbarFixed,
'shadow': scrollTop
}" data-fixed-calc-width
>
<div class="left-box">
<div v-if="$store.state.settings.mode === 'mobile' || (['side', 'head', 'single'].includes($store.state.settings.menuMode) && $store.state.settings.enableSidebarCollapse)" class="sidebar-collapse" :class="{'is-collapse': $store.state.settings.sidebarCollapse}" @click="$store.commit('settings/toggleSidebarCollapse')">
<div v-if="enableSidebarCollapse" class="sidebar-collapse" :class="{'is-collapse': settingsStore.sidebarCollapse}" @click="settingsStore.toggleSidebarCollapse()">
<svg-icon name="toolbar-collapse" />
</div>
<el-breadcrumb v-if="$store.state.settings.enableBreadcrumb && $store.state.settings.mode === 'pc'" separator-class="el-icon-arrow-right">
<el-breadcrumb v-if="settingsStore.enableBreadcrumb && settingsStore.mode === 'pc'" separator-class="el-icon-arrow-right">
<transition-group name="breadcrumb">
<template v-for="(item, index) in breadcrumbList">
<el-breadcrumb-item v-if="index < breadcrumbList.length - 1" :key="item.path" :to="pathCompile(item.path)">
@ -31,15 +31,24 @@ import { compile } from 'path-to-regexp'
import { deepClone } from '@/util'
import UserMenu from '../UserMenu/index.vue'
const store = useStore()
const route = useRoute()
import { useSettingsStore } from '@/store/modules/settings'
const settingsStore = useSettingsStore()
const enableSidebarCollapse = computed(() => {
return settingsStore.mode === 'mobile' || (
['side', 'head', 'single'].includes(settingsStore.menuMode) &&
settingsStore.enableSidebarCollapse
)
})
const breadcrumbList = computed(() => {
let breadcrumbList = []
if (store.state.settings.enableDashboard) {
if (settingsStore.enableDashboard) {
breadcrumbList.push({
path: '/dashboard',
title: store.state.settings.dashboardTitle
title: settingsStore.dashboardTitle
})
}
if (route.meta.breadcrumbNeste) {

View File

@ -1,20 +1,20 @@
<template>
<div class="user">
<div class="tools">
<span v-if="$store.state.settings.mode == 'pc'" class="item item-pro" @click="pro">
<span v-if="settingsStore.mode == 'pc'" class="item item-pro" @click="pro">
<svg-icon name="pro" />
<span class="title">查看专业版</span>
</span>
<span v-if="$store.state.settings.enableNavSearch" class="item" @click="$eventBus.emit('global-search-toggle')">
<span v-if="settingsStore.enableNavSearch" class="item" @click="$eventBus.emit('global-search-toggle')">
<svg-icon name="search" />
</span>
<span v-if="$store.state.settings.mode === 'pc' && isFullscreenEnable && $store.state.settings.enableFullscreen" class="item" @click="fullscreen">
<span v-if="settingsStore.mode === 'pc' && isFullscreenEnable && settingsStore.enableFullscreen" class="item" @click="fullscreen">
<svg-icon :name="isFullscreen ? 'fullscreen-exit' : 'fullscreen'" />
</span>
<span v-if="$store.state.settings.enablePageReload" class="item" @click="reload()">
<span v-if="settingsStore.enablePageReload" class="item" @click="reload()">
<svg-icon name="toolbar-reload" />
</span>
<span v-if="$store.state.settings.enableThemeSetting" class="item" @click="$eventBus.emit('global-theme-toggle')">
<span v-if="settingsStore.enableThemeSetting" class="item" @click="$eventBus.emit('global-theme-toggle')">
<svg-icon name="toolbar-theme" />
</span>
</div>
@ -23,12 +23,12 @@
<el-avatar size="medium">
<el-icon><el-icon-user-filled /></el-icon>
</el-avatar>
{{ $store.state.user.account }}
{{ userStore.account }}
<el-icon><el-icon-caret-bottom /></el-icon>
</div>
<template #dropdown>
<el-dropdown-menu class="user-dropdown">
<el-dropdown-item v-if="$store.state.settings.enableDashboard" command="dashboard">控制台</el-dropdown-item>
<el-dropdown-item v-if="settingsStore.enableDashboard" command="dashboard">控制台</el-dropdown-item>
<el-dropdown-item command="setting">个人设置</el-dropdown-item>
<el-dropdown-item divided command="logout">退出登录</el-dropdown-item>
</el-dropdown-menu>
@ -41,9 +41,13 @@
import screenfull from 'screenfull'
const reload = inject('reload')
const store = useStore()
const router = useRouter()
import { useSettingsStore } from '@/store/modules/settings'
const settingsStore = useSettingsStore()
import { useUserStore } from '@/store/modules/user'
const userStore = useUserStore()
const isFullscreenEnable = computed(() => screenfull.isEnabled)
const isFullscreen = ref(false)
@ -77,7 +81,7 @@ function userCommand(command) {
})
break
case 'logout':
store.dispatch('user/logout').then(() => {
userStore.logout().then(() => {
router.push({
name: 'login'
})

View File

@ -3,17 +3,17 @@
<div id="app-main">
<Header />
<div class="wrapper">
<div class="sidebar-container" :class="{'show': $store.state.settings.mode === 'mobile' && !$store.state.settings.sidebarCollapse}">
<div class="sidebar-container" :class="{'show': settingsStore.mode === 'mobile' && !settingsStore.sidebarCollapse}">
<MainSidebar />
<SubSidebar />
</div>
<div class="sidebar-mask" :class="{'show': $store.state.settings.mode === 'mobile' && !$store.state.settings.sidebarCollapse}" @click="$store.commit('settings/toggleSidebarCollapse')" />
<div class="sidebar-mask" :class="{'show': settingsStore.mode === 'mobile' && !settingsStore.sidebarCollapse}" @click="settingsStore.toggleSidebarCollapse()" />
<div class="main-container" :style="{'padding-bottom': $route.meta.paddingBottom}">
<Topbar v-if="!($store.state.settings.menuMode === 'head' && !$store.state.settings.enableSidebarCollapse && !$store.state.settings.enableBreadcrumb)" />
<Topbar v-if="!(settingsStore.menuMode === 'head' && !settingsStore.enableSidebarCollapse && !settingsStore.enableBreadcrumb)" />
<div class="main">
<router-view v-slot="{ Component, route }">
<transition name="main" mode="out-in" appear>
<keep-alive :include="$store.state.keepAlive.list">
<keep-alive :include="keepAliveStore.list">
<component :is="Component" :key="route.fullPath" />
</keep-alive>
</transition>
@ -41,15 +41,21 @@ import BuyIt from './components/BuyIt/index.vue'
import { isExternalLink } from '@/util'
const { proxy } = getCurrentInstance()
const store = useStore()
const routeInfo = useRoute(), router = useRouter()
import { useSettingsStore } from '@/store/modules/settings'
const settingsStore = useSettingsStore()
import { useKeepAliveStore } from '@/store/modules/keepAlive'
const keepAliveStore = useKeepAliveStore()
import { useMenuStore } from '@/store/modules/menu'
const menuStore = useMenuStore()
const showCopyright = computed(() => {
return typeof routeInfo.meta.copyright !== 'undefined' ? routeInfo.meta.copyright : store.state.settings.showCopyright
return typeof routeInfo.meta.copyright !== 'undefined' ? routeInfo.meta.copyright : settingsStore.showCopyright
})
watch(() => store.state.settings.sidebarCollapse, val => {
if (store.state.settings.mode === 'mobile') {
watch(() => settingsStore.sidebarCollapse, val => {
if (settingsStore.mode === 'mobile') {
if (!val) {
document.querySelector('body').classList.add('hidden')
} else {
@ -59,8 +65,8 @@ watch(() => store.state.settings.sidebarCollapse, val => {
})
watch(() => routeInfo.path, () => {
if (store.state.settings.mode === 'mobile') {
store.commit('settings/updateThemeSetting', {
if (settingsStore.mode === 'mobile') {
settingsStore.updateThemeSetting({
sidebarCollapse: true
})
}
@ -68,7 +74,7 @@ watch(() => routeInfo.path, () => {
onMounted(() => {
proxy.$hotkeys('f5', e => {
if (store.state.settings.enablePageReload) {
if (settingsStore.enablePageReload) {
e.preventDefault()
reload()
}
@ -83,12 +89,12 @@ function reload() {
provide('switchMenu', switchMenu)
function switchMenu(index) {
store.commit('menu/switchHeaderActived', index)
if (store.state.settings.switchSidebarAndPageJump) {
if (isExternalLink(store.getters['menu/sidebarRoutesFirstDeepestPath'])) {
window.open(store.getters['menu/sidebarRoutesFirstDeepestPath'], '_blank')
menuStore.switchHeaderActived(index)
if (settingsStore.switchSidebarAndPageJump) {
if (isExternalLink(menuStore.sidebarRoutesFirstDeepestPath)) {
window.open(menuStore.sidebarRoutesFirstDeepestPath, '_blank')
} else {
router.push(store.getters['menu/sidebarRoutesFirstDeepestPath'])
router.push(menuStore.sidebarRoutesFirstDeepestPath)
}
}
}

View File

@ -2,8 +2,9 @@ import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
import store from './store'
app.use(store)
import { piniaStore } from './store'
import { useSettingsOutsideStore } from './store/modules/settings'
app.use(piniaStore)
import router from './router'
app.use(router)
@ -18,7 +19,7 @@ for (var key in ElementIcons) {
import zhCn from 'element-plus/es/locale/lang/zh-cn'
app.use(ElementPlus, {
locale: zhCn,
size: store.state.settings.elementSize
size: useSettingsOutsideStore().elementSize
})
import globalProperties from '@/util/global.properties'

View File

@ -1,7 +1,10 @@
import { createRouter, createWebHashHistory } from 'vue-router'
import store from '@/store'
import NProgress from 'nprogress'
import 'nprogress/nprogress.css' // progress bar style
import { useSettingsOutsideStore } from '@/store/modules/settings'
import { useKeepAliveOutsideStore } from '@/store/modules/keepAlive'
import { useUserOutsideStore } from '@/store/modules/user'
import { useMenuOutsideStore } from '@/store/modules/menu'
// 固定路由
const constantRoutes = [
@ -23,7 +26,10 @@ const constantRoutes = [
name: 'dashboard',
component: () => import('@/views/index.vue'),
meta: {
title: () => store.state.settings.dashboardTitle
title: () => {
const settingsOutsideStore = useSettingsOutsideStore()
return settingsOutsideStore.dashboardTitle
}
}
},
{
@ -66,7 +72,7 @@ import ComponentExtendExample from './modules/component.extend.example'
import PermissionExample from './modules/permission.example'
import MockExample from './modules/mock.example'
import ExternalLinkExample from './modules/external.link.example'
import VideosExample from './modules/videos.example'
// import VideosExample from './modules/videos.example'
import EcologyExample from './modules/ecology.example'
import CooperationExample from './modules/cooperation.example'
@ -88,15 +94,15 @@ const asyncRoutes = [
ExternalLinkExample
]
},
{
meta: {
title: '教程',
icon: 'sidebar-videos'
},
children: [
...VideosExample
]
},
// {
// meta: {
// title: '教程',
// icon: 'sidebar-videos'
// },
// children: [
// ...VideosExample
// ]
// },
{
meta: {
title: '生态',
@ -131,13 +137,16 @@ const router = createRouter({
})
router.beforeEach(async(to, from, next) => {
store.state.settings.enableProgress && NProgress.start()
const settingsOutsideStore = useSettingsOutsideStore()
const userOutsideStore = useUserOutsideStore()
const menuOutsideStore = useMenuOutsideStore()
settingsOutsideStore.enableProgress && NProgress.start()
// 是否已登录
if (store.getters['user/isLogin']) {
if (userOutsideStore.isLogin) {
// 是否已根据权限动态生成并挂载路由
if (store.state.menu.isGenerate) {
if (menuOutsideStore.isGenerate) {
// 导航栏如果不是 single 模式,则需要根据 path 定位主导航的选中状态
store.state.settings.menuMode !== 'single' && store.commit('menu/setHeaderActived', to.path)
settingsOutsideStore.menuMode !== 'single' && menuOutsideStore.setHeaderActived(to.path)
if (to.name) {
if (to.matched.length !== 0) {
// 如果已登录状态下,进入登录页会强制跳转到控制台页面
@ -146,11 +155,11 @@ router.beforeEach(async(to, from, next) => {
name: 'dashboard',
replace: true
})
} else if (!store.state.settings.enableDashboard && to.name == 'dashboard') {
} else if (!settingsOutsideStore.enableDashboard && to.name == 'dashboard') {
// 如果未开启控制台页面,则默认进入侧边栏导航第一个模块
if (store.getters['menu/sidebarRoutes'].length > 0) {
if (menuOutsideStore.sidebarRoutes.length > 0) {
next({
path: store.getters['menu/sidebarRoutesFirstDeepestPath'],
path: menuOutsideStore.sidebarRoutesFirstDeepestPath,
replace: true
})
} else {
@ -170,10 +179,10 @@ router.beforeEach(async(to, from, next) => {
}
} else {
let accessRoutes = []
if (!store.state.settings.enableBackendReturnRoute) {
accessRoutes = await store.dispatch('menu/generateRoutesAtFront', asyncRoutes)
if (!settingsOutsideStore.enableBackendReturnRoute) {
accessRoutes = await menuOutsideStore.generateRoutesAtFront(asyncRoutes)
} else {
accessRoutes = await store.dispatch('menu/generateRoutesAtBack')
accessRoutes = await menuOutsideStore.generateRoutesAtBack()
}
accessRoutes.push(lastRoute)
let removeRoutes = []
@ -183,7 +192,7 @@ router.beforeEach(async(to, from, next) => {
}
})
// 记录的 accessRoutes 路由数据,在登出时会使用到,不使用 router.removeRoute 是考虑配置的路由可能不一定有设置 name ,则通过调用 router.addRoute() 返回的回调进行删除
store.commit('menu/setCurrentRemoveRoutes', removeRoutes)
menuOutsideStore.setCurrentRemoveRoutes(removeRoutes)
next({ ...to, replace: true })
}
} else {
@ -201,14 +210,16 @@ router.beforeEach(async(to, from, next) => {
})
router.afterEach((to, from) => {
store.state.settings.enableProgress && NProgress.done()
const settingsOutsideStore = useSettingsOutsideStore()
const keepAliveOutsideStore = useKeepAliveOutsideStore()
settingsOutsideStore.enableProgress && NProgress.done()
// 设置页面 title
to.meta.title && store.commit('settings/setTitle', typeof to.meta.title === 'function' ? to.meta.title() : to.meta.title)
to.meta.title && settingsOutsideStore.setTitle(typeof to.meta.title === 'function' ? to.meta.title() : to.meta.title)
// 判断当前页面是否开启缓存,如果开启,则将当前页面的 name 信息存入 keep-alive 全局状态
if (to.meta.cache) {
let componentName = to.matched[to.matched.length - 1].components.default.name
if (componentName) {
store.commit('keepAlive/add', componentName)
keepAliveOutsideStore.add(componentName)
} else {
console.warn('该页面组件未设置组件名,会导致缓存失效,请检查')
}
@ -220,18 +231,18 @@ router.afterEach((to, from) => {
switch (typeof from.meta.cache) {
case 'string':
if (from.meta.cache != to.name) {
store.commit('keepAlive/remove', componentName)
keepAliveOutsideStore.remove(componentName)
}
break
case 'object':
if (!from.meta.cache.includes(to.name)) {
store.commit('keepAlive/remove', componentName)
keepAliveOutsideStore.remove(componentName)
}
break
}
// 如果进入的是 reload 页面,则也将离开页面的缓存清空
if (to.name == 'reload') {
store.commit('keepAlive/remove', componentName)
keepAliveOutsideStore.remove(componentName)
}
}
})

View File

@ -1,13 +1,2 @@
import { createStore, createLogger } from 'vuex'
const modules = {}
const modulesContext = import.meta.globEager('./modules/*.js')
for (const path in modulesContext) {
modules[path.slice(10, -3)] = modulesContext[path].default
}
export default createStore({
modules: modules,
strict: !import.meta.env.PROD,
plugins: !import.meta.env.PROD ? [createLogger()] : []
})
import { createPinia } from 'pinia'
export const piniaStore = createPinia()

View File

@ -1,41 +1,41 @@
const state = () => ({
list: []
})
import { defineStore } from 'pinia'
import { piniaStore } from '@/store'
const getters = {}
const actions = {}
const mutations = {
add(state, name) {
if (typeof name === 'string') {
!state.list.includes(name) && state.list.push(name)
} else {
name.map(v => {
v && !state.list.includes(v) && state.list.push(v)
})
export const useKeepAliveStore = defineStore(
// 唯一ID
'keepAlive',
{
state: () => ({
list: []
}),
actions: {
add(name) {
if (typeof name === 'string') {
!this.list.includes(name) && this.list.push(name)
} else {
name.map(v => {
v && !this.list.includes(v) && this.list.push(v)
})
}
},
remove(name) {
if (typeof name === 'string') {
this.list = this.list.filter(v => {
return v !== name
})
} else {
this.list = this.list.filter(v => {
return !name.includes(v)
})
}
},
clean() {
this.list = []
}
}
},
remove(state, name) {
if (typeof name === 'string') {
state.list = state.list.filter(v => {
return v != name
})
} else {
state.list = state.list.filter(v => {
return !name.includes(v)
})
}
},
clean(state) {
state.list = []
}
}
)
export default {
namespaced: true,
state,
actions,
getters,
mutations
export function useKeepAliveOutsideStore() {
return useKeepAliveStore(piniaStore)
}

View File

@ -1,7 +1,12 @@
import { defineStore } from 'pinia'
import { piniaStore } from '@/store'
import path from 'path-browserify'
import { deepClone, resolveRoutePath } from '@/util'
import api from '@/api'
import { useSettingsStore } from './settings'
import { useUserStore } from './user'
function hasPermission(permissions, route) {
let isAuth = false
if (route.meta && route.meta.auth) {
@ -137,160 +142,170 @@ function getDeepestPath(routes, rootPath = '') {
return retnPath
}
const state = () => ({
isGenerate: false,
routes: [],
defaultOpenedPaths: [],
headerActived: 0,
currentRemoveRoutes: []
})
const getters = {
// 由于 getter 的结果不会被缓存,导致导航栏切换时有明显的延迟,该问题会在 Vue 3.2 版本中修复,详看 https://github.com/vuejs/vuex/pull/1883
routes: (state, getters, rootState) => {
let routes
if (rootState.settings.menuMode === 'single') {
routes = [{ children: [] }]
state.routes.map(item => {
routes[0].children.push(...item.children)
})
} else {
routes = state.routes
}
return routes
},
sidebarRoutes: (state, getters) => {
return getters.routes.length > 0 ? getters.routes[state.headerActived].children : []
},
sidebarRoutesFirstDeepestPath: (state, getters) => {
return getters.routes.length > 0 ? getDeepestPath(getters.sidebarRoutes[0]) : '/'
}
}
const actions = {
// 根据权限动态生成路由(前端生成)
generateRoutesAtFront({ rootState, dispatch, commit }, asyncRoutes) {
// eslint-disable-next-line no-async-promise-executor
return new Promise(async resolve => {
let accessedRoutes
// 如果权限功能开启,则需要对路由数据进行筛选过滤
if (rootState.settings.enablePermission) {
const permissions = await dispatch('user/getPermissions', null, { root: true })
accessedRoutes = filterAsyncRoutes(asyncRoutes, permissions)
} else {
accessedRoutes = deepClone(asyncRoutes)
}
commit('setRoutes', accessedRoutes)
let routes = []
accessedRoutes.map(item => {
routes.push(...item.children)
})
// 将三级及以上路由数据拍平成二级
routes.map(item => {
if (item.children) {
item.children = flatAsyncRoutes(item.children, [{
path: item.path,
title: item.meta.title
}], item.path)
}
})
commit('setDefaultOpenedPaths', routes)
resolve(routes)
})
},
// 生成路由(后端获取)
generateRoutesAtBack({ rootState, dispatch, commit }) {
return new Promise(resolve => {
api.get('route/list', {
baseURL: '/mock/'
}).then(async res => {
let asyncRoutes = formatBackRoutes(res.data)
let accessedRoutes
// 如果权限功能开启,则需要对路由数据进行筛选过滤
if (rootState.settings.enablePermission) {
const permissions = await dispatch('user/getPermissions', null, { root: true })
accessedRoutes = filterAsyncRoutes(asyncRoutes, permissions)
export const useMenuStore = defineStore(
// 唯一ID
'menu',
{
state: () => ({
isGenerate: false,
routes: [],
defaultOpenedPaths: [],
headerActived: 0,
currentRemoveRoutes: []
}),
getters: {
transformRoutes: state => {
let routes
const settingsStore = useSettingsStore()
if (settingsStore.menuMode === 'single') {
routes = [{ children: [] }]
state.routes.map(item => {
routes[0].children.push(...item.children)
})
} else {
accessedRoutes = deepClone(asyncRoutes)
routes = state.routes
}
commit('setRoutes', accessedRoutes)
let routes = []
accessedRoutes.map(item => {
routes.push(...item.children)
return routes
},
sidebarRoutes() {
return this.transformRoutes.length > 0 ? this.transformRoutes[this.headerActived].children : []
},
sidebarRoutesFirstDeepestPath() {
return this.transformRoutes.length > 0 ? getDeepestPath(this.sidebarRoutes[0]) : '/'
}
},
actions: {
// 根据权限动态生成路由(前端生成)
generateRoutesAtFront(asyncRoutes) {
// eslint-disable-next-line no-async-promise-executor
return new Promise(async resolve => {
const settingsStore = useSettingsStore()
const userStore = useUserStore()
let accessedRoutes
// 如果权限功能开启,则需要对路由数据进行筛选过滤
if (settingsStore.enablePermission) {
const permissions = await userStore.getPermissions()
accessedRoutes = filterAsyncRoutes(asyncRoutes, permissions)
} else {
accessedRoutes = deepClone(asyncRoutes)
}
// 设置 routes 数据
this.isGenerate = true
let newRoutes = deepClone(accessedRoutes)
this.routes = newRoutes.filter(item => {
return item.children.length != 0
})
// 将三级及以上路由数据拍平成二级
let routes = []
accessedRoutes.map(item => {
routes.push(...item.children)
})
routes.map(item => {
if (item.children) {
item.children = flatAsyncRoutes(item.children, [{
path: item.path,
title: item.meta.title
}], item.path)
}
})
// 设置 defaultOpenedPaths 数据
let defaultOpenedPaths = []
routes.map(item => {
item.meta.defaultOpened && defaultOpenedPaths.push(item.path)
item.children && item.children.map(child => {
child.meta.defaultOpened && defaultOpenedPaths.push(path.resolve(item.path, child.path))
})
})
this.defaultOpenedPaths = defaultOpenedPaths
resolve(routes)
})
// 将三级及以上路由数据拍平成二级
routes.map(item => {
if (item.children) {
item.children = flatAsyncRoutes(item.children, [{
path: item.path,
title: item.meta.title
}], item.path)
},
// 生成路由(后端获取)
generateRoutesAtBack() {
return new Promise(resolve => {
api.get('route/list', {
baseURL: '/mock/'
}).then(async res => {
const settingsStore = useSettingsStore()
const userStore = useUserStore()
let asyncRoutes = formatBackRoutes(res.data)
let accessedRoutes
// 如果权限功能开启,则需要对路由数据进行筛选过滤
if (settingsStore.enablePermission) {
const permissions = await userStore.getPermissions()
accessedRoutes = filterAsyncRoutes(asyncRoutes, permissions)
} else {
accessedRoutes = deepClone(asyncRoutes)
}
// 设置 routes 数据
this.isGenerate = true
let newRoutes = deepClone(accessedRoutes)
this.routes = newRoutes.filter(item => {
return item.children.length != 0
})
// 将三级及以上路由数据拍平成二级
let routes = []
accessedRoutes.map(item => {
routes.push(...item.children)
})
routes.map(item => {
if (item.children) {
item.children = flatAsyncRoutes(item.children, [{
path: item.path,
title: item.meta.title
}], item.path)
}
})
// 设置 defaultOpenedPaths 数据
let defaultOpenedPaths = []
routes.map(item => {
item.meta.defaultOpened && defaultOpenedPaths.push(item.path)
item.children && item.children.map(child => {
child.meta.defaultOpened && defaultOpenedPaths.push(path.resolve(item.path, child.path))
})
})
this.defaultOpenedPaths = defaultOpenedPaths
resolve(routes)
})
})
},
invalidRoutes() {
this.isGenerate = false
this.routes = []
this.defaultOpenedPaths = []
this.headerActived = 0
},
// 根据路由判断属于哪个头部导航
setHeaderActived(path) {
this.routes.map((item, index) => {
if (
item.children.some(r => {
return path.indexOf(r.path + '/') === 0 || path == r.path
})
) {
this.headerActived = index
}
})
commit('setDefaultOpenedPaths', routes)
resolve(routes)
})
})
}
}
const mutations = {
invalidRoutes(state) {
state.isGenerate = false
state.routes = []
state.defaultOpenedPaths = []
state.headerActived = 0
},
setRoutes(state, routes) {
state.isGenerate = true
let newRoutes = deepClone(routes)
state.routes = newRoutes.filter(item => {
return item.children.length != 0
})
},
setDefaultOpenedPaths(state, routes) {
let defaultOpenedPaths = []
routes.map(item => {
item.meta.defaultOpened && defaultOpenedPaths.push(item.path)
item.children && item.children.map(child => {
child.meta.defaultOpened && defaultOpenedPaths.push(path.resolve(item.path, child.path))
})
})
state.defaultOpenedPaths = defaultOpenedPaths
},
// 根据路由判断属于哪个头部导航
setHeaderActived(state, path) {
state.routes.map((item, index) => {
if (
item.children.some(r => {
return path.indexOf(r.path + '/') === 0 || path == r.path
},
// 切换头部导航
switchHeaderActived(index) {
this.headerActived = index
},
// 记录 accessRoutes 路由,用于登出时删除路由
setCurrentRemoveRoutes(routes) {
this.currentRemoveRoutes = routes
},
// 清空动态路由
removeRoutes() {
this.currentRemoveRoutes.forEach(removeRoute => {
removeRoute()
})
) {
state.headerActived = index
this.currentRemoveRoutes = []
}
})
},
// 切换头部导航
switchHeaderActived(state, index) {
state.headerActived = index
},
// 记录 accessRoutes 路由,用于登出时删除路由
setCurrentRemoveRoutes(state, routes) {
state.currentRemoveRoutes = routes
},
// 清空动态路由
removeRoutes(state) {
state.currentRemoveRoutes.forEach(removeRoute => {
removeRoute()
})
state.currentRemoveRoutes = []
}
}
}
)
export default {
namespaced: true,
state,
actions,
getters,
mutations
export function useMenuOutsideStore() {
return useMenuStore(piniaStore)
}

View File

@ -1,62 +1,58 @@
/**
* 存放全局公用状态
*/
import { defineStore } from 'pinia'
import { piniaStore } from '@/store'
import settings from '@/settings'
const state = () => ({
...settings,
// 侧边栏是否收起(用于记录 pc 模式下最后的状态)
sidebarCollapseLastStatus: settings.sidebarCollapse,
// 显示模式支持mobile、pc
mode: 'pc',
// 页面标题
title: ''
})
const getters = {}
const actions = {}
const mutations = {
// 设置访问模式
setMode(state, width) {
if (state.enableMobileAdaptation) {
// 先判断 UA 是否为移动端设备(手机&平板)
if (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)) {
state.mode = 'mobile'
} else {
// 如果为桌面设备,再根据页面宽度判断是否需要切换为移动端展示
if (width < 992) {
state.mode = 'mobile'
export const useSettingsStore = defineStore(
// 唯一ID
'settings',
{
state: () => ({
...settings,
// 侧边栏是否收起(用于记录 pc 模式下最后的状态)
sidebarCollapseLastStatus: settings.sidebarCollapse,
// 显示模式支持mobile、pc
mode: 'pc',
// 页面标题
title: ''
}),
actions: {
// 设置访问模式
setMode(width) {
if (this.enableMobileAdaptation) {
// 先判断 UA 是否为移动端设备(手机&平板)
if (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)) {
this.mode = 'mobile'
} else {
// 如果为桌面设备,再根据页面宽度判断是否需要切换为移动端展示
if (width < 992) {
this.mode = 'mobile'
} else {
this.mode = 'pc'
}
}
} else {
state.mode = 'pc'
this.mode = 'pc'
}
},
// 设置网页标题
setTitle(title) {
this.title = title
},
// 切换侧边栏导航展开/收起
toggleSidebarCollapse() {
this.sidebarCollapse = !this.sidebarCollapse
if (this.mode == 'pc') {
this.sidebarCollapseLastStatus = !this.sidebarCollapseLastStatus
}
},
// 更新主题配置
updateThemeSetting(data) {
Object.assign(this, data)
}
} else {
state.mode = 'pc'
}
},
// 设置网页标题
setTitle(state, title) {
state.title = title
},
// 切换侧边栏导航展开/收起
toggleSidebarCollapse(state) {
state.sidebarCollapse = !state.sidebarCollapse
if (state.mode == 'pc') {
state.sidebarCollapseLastStatus = !state.sidebarCollapseLastStatus
}
},
// 更新主题配置
updateThemeSetting(state, data) {
Object.assign(state, data)
}
}
)
export default {
namespaced: true,
state,
actions,
getters,
mutations
export function useSettingsOutsideStore() {
return useSettingsStore(piniaStore)
}

View File

@ -1,102 +1,95 @@
import { defineStore } from 'pinia'
import { piniaStore } from '@/store'
import api from '@/api'
const state = () => ({
account: localStorage.account || '',
token: localStorage.token || '',
failure_time: localStorage.failure_time || '',
permissions: []
})
import { useMenuStore } from './menu'
const getters = {
isLogin: state => {
let retn = false
if (state.token) {
if (new Date().getTime() < state.failure_time * 1000) {
retn = true
export const useUserStore = defineStore(
// 唯一ID
'user',
{
state: () => ({
account: localStorage.account || '',
token: localStorage.token || '',
failure_time: localStorage.failure_time || '',
permissions: []
}),
getters: {
isLogin: state => {
let retn = false
if (state.token) {
if (new Date().getTime() < state.failure_time * 1000) {
retn = true
}
}
return retn
}
},
actions: {
login(data) {
return new Promise((resolve, reject) => {
// 通过 mock 进行登录
api.post('member/login', data, {
baseURL: '/mock/'
}).then(res => {
localStorage.setItem('account', res.data.account)
localStorage.setItem('token', res.data.token)
localStorage.setItem('failure_time', res.data.failure_time)
this.account = res.data.account
this.token = res.data.token
this.failure_time = res.data.failure_time
resolve()
}).catch(error => {
reject(error)
})
})
},
logout() {
return new Promise(resolve => {
const menuStore = useMenuStore()
localStorage.removeItem('account')
localStorage.removeItem('token')
localStorage.removeItem('failure_time')
this.account = ''
this.token = ''
this.failure_time = ''
menuStore.invalidRoutes()
menuStore.removeRoutes()
resolve()
})
},
// 获取我的权限
getPermissions() {
return new Promise(resolve => {
// 通过 mock 获取权限
api.get('member/permission', {
baseURL: '/mock/',
params: {
account: this.account
}
}).then(res => {
this.permissions = res.data.permissions
resolve(res.data.permissions)
})
})
},
editPassword(data) {
return new Promise(resolve => {
api.post('member/edit/password', {
account: this.account,
password: data.password,
newpassword: data.newpassword
}, {
baseURL: '/mock/'
}).then(() => {
resolve()
})
})
}
}
return retn
}
}
)
const actions = {
login({ commit }, data) {
return new Promise((resolve, reject) => {
// 通过 mock 进行登录
api.post('member/login', data, {
baseURL: '/mock/'
}).then(res => {
commit('setUserData', res.data)
resolve()
}).catch(error => {
reject(error)
})
})
},
logout({ commit }) {
return new Promise(resolve => {
commit('removeUserData')
commit('menu/invalidRoutes', null, { root: true })
commit('menu/removeRoutes', null, { root: true })
resolve()
})
},
// 获取我的权限
getPermissions({ state, commit }) {
return new Promise(resolve => {
// 通过 mock 获取权限
api.get('member/permission', {
baseURL: '/mock/',
params: {
account: state.account
}
}).then(res => {
commit('setPermissions', res.data.permissions)
resolve(res.data.permissions)
})
})
},
editPassword({ state }, data) {
return new Promise(resolve => {
api.post('member/edit/password', {
account: state.account,
password: data.password,
newpassword: data.newpassword
}, {
baseURL: '/mock/'
}).then(() => {
resolve()
})
})
}
}
const mutations = {
setUserData(state, data) {
localStorage.setItem('account', data.account)
localStorage.setItem('token', data.token)
localStorage.setItem('failure_time', data.failure_time)
state.account = data.account
state.token = data.token
state.failure_time = data.failure_time
},
removeUserData(state) {
localStorage.removeItem('account')
localStorage.removeItem('token')
localStorage.removeItem('failure_time')
state.account = ''
state.token = ''
state.failure_time = ''
},
setPermissions(state, permissions) {
state.permissions = permissions
}
}
export default {
namespaced: true,
state,
actions,
getters,
mutations
export function useUserOutsideStore() {
return useUserStore(piniaStore)
}

View File

@ -1,5 +1,6 @@
import path from 'path-browserify'
import store from '@/store'
import { useSettingsOutsideStore } from '@/store/modules/settings'
import { useUserOutsideStore } from '@/store/modules/user'
export function deepClone(target) {
// 定义一个变量
@ -35,8 +36,10 @@ export function deepClone(target) {
}
function hasPermission(permission) {
if (store.state.settings.enablePermission) {
return store.state.user.permissions.some(v => {
const settingsOutsideStore = useSettingsOutsideStore()
const userOutsideStore = useUserOutsideStore()
if (settingsOutsideStore.enablePermission) {
return userOutsideStore.permissions.some(v => {
return v === permission
})
} else {

View File

@ -4,37 +4,36 @@
<div class="content">
<h1>404</h1>
<div class="desc">抱歉你访问的页面不存在</div>
<el-button type="primary" @click="goBack">{{ countdown }}秒后返回首页</el-button>
<el-button type="primary" @click="goBack">{{ data.countdown }} 秒后返回首页</el-button>
</div>
</div>
</template>
<script>
export default {
beforeRouteLeave(to, from, next) {
clearInterval(this.inter)
next()
},
data() {
return {
inter: null,
countdown: 5
<script setup>
import { onBeforeRouteLeave } from 'vue-router'
const router = useRouter()
const data = ref({
inter: null,
countdown: 5
})
onBeforeRouteLeave(() => {
clearInterval(data.value.inter)
})
onMounted(() => {
data.value.inter = setInterval(() => {
data.value.countdown--
if (data.value.countdown == 0) {
clearInterval(data.value.inter)
goBack()
}
},
mounted() {
this.inter = setInterval(() => {
this.countdown--
if (this.countdown == 0) {
clearInterval(this.inter)
this.goBack()
}
}, 1000)
},
methods: {
goBack() {
this.$router.push('/')
}
}
}, 1000)
})
function goBack() {
router.push('/')
}
</script>

View File

@ -3,7 +3,7 @@
<page-main>
<div>层级1</div>
<router-view v-slot="{ Component }">
<keep-alive :include="$store.state.keepAlive.list">
<keep-alive :include="keepAliveStore.list">
<component :is="Component" />
</keep-alive>
</router-view>
@ -11,8 +11,7 @@
</div>
</template>
<script>
export default {
name: 'TabExampleNested1'
}
<script setup name="TabExampleNested1">
import { useKeepAliveStore } from '@/store/modules/keepAlive'
const keepAliveStore = useKeepAliveStore()
</script>

View File

@ -3,7 +3,7 @@
<page-main>
<div>层级1-1</div>
<router-view v-slot="{ Component }">
<keep-alive :include="$store.state.keepAlive.list">
<keep-alive :include="keepAliveStore.list">
<component :is="Component" />
</keep-alive>
</router-view>
@ -11,8 +11,7 @@
</div>
</template>
<script>
export default {
name: 'TabExampleNested2'
}
<script setup name="TabExampleNested2">
import { useKeepAliveStore } from '@/store/modules/keepAlive'
const keepAliveStore = useKeepAliveStore()
</script>

View File

@ -17,12 +17,15 @@
</template>
<script>
import { useKeepAliveStore } from '@/store/modules/keepAlive'
const keepAliveStore = useKeepAliveStore()
export default {
name: 'KeepAliveExamplePage',
beforeRouteEnter(to, from, next) {
// name keep-alive
next(vm => {
vm.$store.commit('keepAlive/add', vm.$options.name)
keepAliveStore.add(vm.$options.name)
})
},
beforeRouteLeave(to, from, next) {
@ -31,10 +34,10 @@ export default {
// name keepAliveExampleDetail keepAliveExampleNestedDetail name keep-alive
if (!['keepAliveExampleDetail', 'keepAliveExampleNestedDetail'].includes(to.name)) {
// name name
this.$store.commit('keepAlive/remove', 'KeepAliveExamplePage')
keepAliveStore.remove('KeepAliveExamplePage')
}
} else {
this.$store.commit('keepAlive/remove', 'KeepAliveExamplePage')
keepAliveStore.remove('KeepAliveExamplePage')
}
next()
},

View File

@ -80,15 +80,19 @@
</el-row>
</el-form>
</div>
<Copyright v-if="$store.state.settings.showCopyright" />
<Copyright v-if="settingsStore.showCopyright" />
</div>
</template>
<script setup name="Login">
const { proxy } = getCurrentInstance()
const store = useStore()
const route = useRoute(), router = useRouter()
import { useSettingsStore } from '@/store/modules/settings'
const settingsStore = useSettingsStore()
import { useUserStore } from '@/store/modules/user'
const userStore = useUserStore()
const title = import.meta.env.VITE_APP_TITLE
// login reset
@ -146,7 +150,7 @@ function handleLogin() {
proxy.$refs.loginFormRef.validate(valid => {
if (valid) {
loading.value = true
store.dispatch('user/login', loginForm.value).then(() => {
userStore.login(loginForm.value).then(() => {
loading.value = false
if (loginForm.value.remember) {
localStorage.setItem('login_account', loginForm.value.account)

View File

@ -9,21 +9,20 @@
</div>
</template>
<script>
export default {
methods: {
getPermission() {
this.$store.dispatch('user/getPermissions').then(res => {
this.$notify({
title: '当前用户权限',
dangerouslyUseHTMLString: true,
message: res.map(p => `<p>${p}</p>`).join('')
})
})
},
open(url) {
window.open(url, 'top')
}
}
<script setup>
import { useUserStore } from '@/store/modules/user'
const userStore = useUserStore()
function getPermission() {
userStore.getPermissions().then(res => {
this.$notify({
title: '当前用户权限',
dangerouslyUseHTMLString: true,
message: res.map(p => `<p>${p}</p>`).join('')
})
})
}
function open(url) {
window.open(url, 'top')
}
</script>

View File

@ -2,15 +2,15 @@
<div>
<page-header title="权限验证" />
<page-main>
<div v-if="!$store.state.settings.enablePermission">请到 seeting.js 里打开权限功能再进入该页面查看演示</div>
<div v-if="!settingsStore.enablePermission">请到 seeting.js 里打开权限功能再进入该页面查看演示</div>
<div v-else>
<h3>切换帐号</h3>
<el-radio-group v-model="account" @change="accountChange">
<el-radio-group v-model="userStore.account" @change="accountChange">
<el-radio-button label="admin" />
<el-radio-button label="test" />
</el-radio-group>
<h3>帐号权限</h3>
<div>{{ $store.state.user.permissions }}</div>
<div>{{ userStore.permissions }}</div>
<h3>鉴权组件请对照代码查看</h3>
<div>
<auth value="permission.browse" style="margin-bottom: 10px;">
@ -69,43 +69,41 @@
</div>
</template>
<script>
export default {
data() {
return {
account: this.$store.state.user.account
}
},
methods: {
accountChange(val) {
this.$loading({
lock: true,
text: '帐号切换中',
background: 'rgba(0, 0, 0, 0.7)'
})
this.$store.dispatch('user/login', {
account: val,
password: ''
}).then(() => {
setTimeout(() => {
location.reload()
}, 1000)
})
},
permissionCheck(permissions) {
if (this.$auth(permissions)) {
this.$message.success('校验通过')
} else {
this.$message.error('校验不通过')
}
},
permissionCheck2(permissions) {
if (this.$authAll(permissions)) {
this.$message.success('校验通过')
} else {
this.$message.error('校验不通过')
}
}
<script setup>
const { proxy } = getCurrentInstance()
import { useSettingsStore } from '@/store/modules/settings'
const settingsStore = useSettingsStore()
import { useUserStore } from '@/store/modules/user'
const userStore = useUserStore()
function accountChange(val) {
proxy.$loading({
lock: true,
text: '帐号切换中',
background: 'rgba(0, 0, 0, 0.7)'
})
userStore.login({
account: val,
password: ''
}).then(() => {
setTimeout(() => {
location.reload()
}, 1000)
})
}
function permissionCheck(permissions) {
if (proxy.$auth(permissions)) {
proxy.$message.success('校验通过')
} else {
proxy.$message.error('校验不通过')
}
}
function permissionCheck2(permissions) {
if (proxy.$authAll(permissions)) {
proxy.$message.success('校验通过')
} else {
proxy.$message.error('校验不通过')
}
}
</script>

View File

@ -25,10 +25,12 @@
</template>
<script setup name="PersonalEditPassword">
const store = useStore()
const route = useRoute(), router = useRouter()
const { proxy } = getCurrentInstance()
import { useUserStore } from '@/store/modules/user'
const userStore = useUserStore()
const validatePassword = (rule, value, callback) => {
if (value !== form.value.newpassword) {
callback(new Error('请确认新密码'))
@ -60,12 +62,12 @@ const rules = ref({
function onSubmit() {
proxy.$refs['formRef'].validate(valid => {
if (valid) {
store.dispatch('user/editPassword', form.value).then(() => {
userStore.editPassword(form.value).then(() => {
proxy.$message({
type: 'success',
message: '模拟修改成功,请重新登录'
})
store.dispatch('user/logout').then(() => {
userStore.logout().then(() => {
router.push({
name: 'login',
query: {

View File

@ -4,10 +4,7 @@ export default function createAutoImport() {
return autoImport({
imports: [
'vue',
'vue-router',
{
'vuex': ['useStore']
}
'vue-router'
],
dts: false
})