refactor: 优化pinia setup store组合式函数写法

Former-commit-id: 27347ede51d0952d3422c3a6c3a86652f91e5639
This commit is contained in:
haoxr
2022-12-18 15:27:53 +08:00
parent fe49485563
commit 2a36afae16
27 changed files with 944 additions and 973 deletions

View File

@@ -1,36 +1,15 @@
<template>
<el-dropdown class="lang-select" trigger="click" @command="handleSetLanguage">
<div class="lang-select__icon">
<svg-icon class-name="international-icon" icon-class="language" />
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item :disabled="language === 'zh-cn'" command="zh-cn">
中文
</el-dropdown-item>
<el-dropdown-item :disabled="language === 'en'" command="en">
English
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue';
import useStore from '@/store';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { ElMessage } from 'element-plus'; import { ElMessage } from 'element-plus';
import SvgIcon from '@/components/SvgIcon/index.vue'; import SvgIcon from '@/components/SvgIcon/index.vue';
import { useAppStore } from '@/store/modules/app';
const { app } = useStore(); const appStore = useAppStore();
const language = computed(() => app.language);
const { locale } = useI18n(); const { locale } = useI18n();
function handleSetLanguage(lang: string) { function handleLanguageChange(lang: string) {
locale.value = lang; locale.value = lang;
app.setLanguage(lang); appStore.changeLanguage(lang);
if (lang == 'en') { if (lang == 'en') {
ElMessage.success('Switch Language Successful!'); ElMessage.success('Switch Language Successful!');
} else { } else {
@@ -39,6 +18,31 @@ function handleSetLanguage(lang: string) {
} }
</script> </script>
<template>
<el-dropdown
class="lang-select"
trigger="click"
@command="handleLanguageChange"
>
<div class="lang-select__icon">
<svg-icon class-name="international-icon" icon-class="language" />
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
:disabled="appStore.language === 'zh-cn'"
command="zh-cn"
>
中文
</el-dropdown-item>
<el-dropdown-item :disabled="appStore.language === 'en'" command="en">
English
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
<style lang="scss" scoped> <style lang="scss" scoped>
.lang-select__icon { .lang-select__icon {
line-height: 50px; line-height: 50px;

View File

@@ -1,36 +1,16 @@
<template>
<div ref="rightPanel" :class="{ show: show }">
<div class="right-panel-background" />
<div class="right-panel">
<div
class="right-panel__button"
:style="{ top: buttonTop + 'px', 'background-color': theme }"
@click="show = !show"
>
<Close class="icon" v-show="show" />
<Setting class="icon" v-show="!show" />
</div>
<div class="right-panel__items">
<slot />
</div>
</div>
</div>
</template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'; import { onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { addClass, removeClass } from '@/utils/index'; import { addClass, removeClass } from '@/utils/index';
import useStore from '@/store'; import { useSettingsStore } from '@/store/modules/settings';
// 图标依赖 // 图标依赖
import { Close, Setting } from '@element-plus/icons-vue'; import { Close, Setting } from '@element-plus/icons-vue';
import { ElColorPicker } from 'element-plus'; import { ElColorPicker } from 'element-plus';
const { setting } = useStore(); const settingsStore = useSettingsStore();
const theme = computed(() => setting.theme);
const show = ref(false); const show = ref(false);
defineProps({ defineProps({
@@ -87,6 +67,28 @@ onBeforeUnmount(() => {
}); });
</script> </script>
<template>
<div ref="rightPanel" :class="{ show: show }">
<div class="right-panel-background" />
<div class="right-panel">
<div
class="right-panel__button"
:style="{
top: buttonTop + 'px',
'background-color': settingsStore.theme
}"
@click="show = !show"
>
<Close class="icon" v-show="show" />
<Setting class="icon" v-show="!show" />
</div>
<div class="right-panel__items">
<slot />
</div>
</div>
</div>
</template>
<style> <style>
.showRightPanel { .showRightPanel {
overflow: hidden; overflow: hidden;

View File

@@ -1,14 +1,35 @@
<script setup lang="ts">
import { ref } from 'vue';
import { ElMessage } from 'element-plus';
import { useAppStore } from '@/store/modules/app';
import SvgIcon from '@/components/SvgIcon/index.vue';
const appStore = useAppStore();
const sizeOptions = ref([
{ label: '默认', value: 'default' },
{ label: '大型', value: 'large' },
{ label: '小型', value: 'small' }
]);
function handleSizeChange(size: string) {
appStore.changeSize(size);
ElMessage.success('切换布局大小成功');
}
</script>
<template> <template>
<el-dropdown class="size-select" trigger="click" @command="handleSetSize"> <el-dropdown trigger="click" @command="handleSizeChange">
<div class="size-select__icon"> <div style="line-height: 50px">
<svg-icon class-name="size-icon" icon-class="size" /> <svg-icon icon-class="size" />
</div> </div>
<template #dropdown> <template #dropdown>
<el-dropdown-menu> <el-dropdown-menu>
<el-dropdown-item <el-dropdown-item
v-for="item of sizeOptions" v-for="item of sizeOptions"
:key="item.value" :key="item.value"
:disabled="(size || 'default') == item.value" :disabled="appStore.size == item.value"
:command="item.value" :command="item.value"
> >
{{ item.label }} {{ item.label }}
@@ -17,31 +38,3 @@
</template> </template>
</el-dropdown> </el-dropdown>
</template> </template>
<script setup lang="ts">
import { ref, computed } from 'vue';
import { ElMessage } from 'element-plus';
import useStore from '@/store';
import SvgIcon from '@/components/SvgIcon/index.vue';
const { app } = useStore();
const size = computed(() => app.size);
const sizeOptions = ref([
{ label: '默认', value: 'default' },
{ label: '大型', value: 'large' },
{ label: '小型', value: 'small' }
]);
function handleSetSize(size: string) {
app.setSize(size);
ElMessage.success('切换布局大小成功');
}
</script>
<style lang="scss" scoped>
.size-select__icon {
line-height: 50px;
}
</style>

View File

@@ -1,6 +1,6 @@
<template> <template>
<el-color-picker <el-color-picker
v-model="theme" v-model="settingsStore.theme"
:predefine="[ :predefine="[
'#409EFF', '#409EFF',
'#1890ff', '#1890ff',
@@ -17,42 +17,14 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, watch } from 'vue'; import { useSettingsStore } from '@/store/modules/settings';
import useStore from '@/store'; const settingsStore = useSettingsStore();
import { localStorage } from '@/utils/storage';
// 参考连接:https://juejin.cn/post/7024025899813044232#heading-1
import { mix } from '@/utils';
// 白色混合色
const mixWhite = '#ffffff';
// 黑色混合色
const mixBlack = '#000000';
const node = document.documentElement;
const { setting } = useStore();
const theme = computed(() => setting.theme);
watch(theme, (color: string) => {
node.style.setProperty('--el-color-primary', color);
localStorage.set('theme', color);
for (let i = 1; i < 10; i += 1) {
node.style.setProperty(
`--el-color-primary-light-${i}`,
mix(color, mixWhite, i * 0.1)
);
}
node.style.setProperty('--el-color-primary-dark', mix(color, mixBlack, 0.1));
localStorage.set('style', node.style.cssText);
});
</script> </script>
<style> <style>
.theme-message, .theme-message,
.theme-picker-dropdown { .theme-picker-dropdown {
z-index: 99999 !important; z-index: 9999 !important;
} }
.theme-picker .el-color-picker__trigger { .theme-picker .el-color-picker__trigger {

View File

@@ -1,4 +1,4 @@
import useStore from '@/store'; import { useUserStoreHook } from '@/store/modules/user';
import { Directive, DirectiveBinding } from 'vue'; import { Directive, DirectiveBinding } from 'vue';
/** /**
@@ -7,8 +7,7 @@ import { Directive, DirectiveBinding } from 'vue';
export const hasPerm: Directive = { export const hasPerm: Directive = {
mounted(el: HTMLElement, binding: DirectiveBinding) { mounted(el: HTMLElement, binding: DirectiveBinding) {
// 「超级管理员」拥有所有的按钮权限 // 「超级管理员」拥有所有的按钮权限
const { user } = useStore(); const { roles, perms } = useUserStoreHook();
const roles = user.roles;
if (roles.includes('ROOT')) { if (roles.includes('ROOT')) {
return true; return true;
} }
@@ -17,7 +16,7 @@ export const hasPerm: Directive = {
if (value) { if (value) {
const requiredPerms = value; // DOM绑定需要的按钮权限标识 const requiredPerms = value; // DOM绑定需要的按钮权限标识
const hasPerm = user.perms?.some(perm => { const hasPerm = perms?.some(perm => {
return requiredPerms.includes(perm); return requiredPerms.includes(perm);
}); });
@@ -41,8 +40,8 @@ export const hasRole: Directive = {
if (value) { if (value) {
const requiredRoles = value; // DOM绑定需要的角色编码 const requiredRoles = value; // DOM绑定需要的角色编码
const { user } = useStore(); const { roles } = useUserStoreHook();
const hasRole = user.roles.some(perm => { const hasRole = roles.some(perm => {
return requiredRoles.includes(perm); return requiredRoles.includes(perm);
}); });

View File

@@ -1,6 +1,6 @@
// 自定义国际化配置 // 自定义国际化配置
import { createI18n } from 'vue-i18n'; import { createI18n } from 'vue-i18n';
import { localStorage } from '@/utils/storage'; import { localStorage } from '@/utils/localStorage';
// 本地语言包 // 本地语言包
import enLocale from './en'; import enLocale from './en';

View File

@@ -1,8 +1,14 @@
<script setup lang="ts">
import { useTagsViewStore } from '@/store/modules/tagsView';
const tagsViewStore = useTagsViewStore();
</script>
<template> <template>
<section class="app-main"> <section class="app-main">
<router-view v-slot="{ Component, route }"> <router-view v-slot="{ Component, route }">
<transition name="router-fade" mode="out-in"> <transition name="router-fade" mode="out-in">
<keep-alive :include="cachedViews"> <keep-alive :include="tagsViewStore.cachedViews">
<component :is="Component" :key="route.fullPath" /> <component :is="Component" :key="route.fullPath" />
</keep-alive> </keep-alive>
</transition> </transition>
@@ -10,15 +16,6 @@
</section> </section>
</template> </template>
<script setup lang="ts">
import { computed } from 'vue';
import useStore from '@/store';
const { tagsView } = useStore();
const cachedViews = computed(() => tagsView.cachedViews);
</script>
<style lang="scss" scoped> <style lang="scss" scoped>
.app-main { .app-main {
/* 50= navbar 50 */ /* 50= navbar 50 */

View File

@@ -1,8 +1,55 @@
<script setup lang="ts">
import { computed } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { ElMessageBox } from 'element-plus';
import Breadcrumb from '@/components/Breadcrumb/index.vue';
import Hamburger from '@/components/Hamburger/index.vue';
import Screenfull from '@/components/Screenfull/index.vue';
import SizeSelect from '@/components/SizeSelect/index.vue';
import LangSelect from '@/components/LangSelect/index.vue';
import { CaretBottom } from '@element-plus/icons-vue';
import { useAppStore, DeviceType } from '@/store/modules/app';
import { useTagsViewStore } from '@/store/modules/tagsView';
import { useUserStore } from '@/store/modules/user';
const appStore = useAppStore();
const tagsViewStore = useTagsViewStore();
const userStore = useUserStore();
const route = useRoute();
const router = useRouter();
const device = computed(() => appStore.device);
function toggleSideBar() {
appStore.toggleSidebar(true);
}
function logout() {
ElMessageBox.confirm('确定注销并退出系统吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
userStore
.logout()
.then(() => {
tagsViewStore.delAllViews();
})
.then(() => {
router.push(`/login?redirect=${route.fullPath}`);
});
});
}
</script>
<template> <template>
<div class="navbar"> <div class="navbar">
<hamburger <hamburger
id="hamburger-container" id="hamburger-container"
:is-active="sidebar.opened" :is-active="appStore.sidebar.opened"
class="hamburger-container" class="hamburger-container"
@toggleClick="toggleSideBar" @toggleClick="toggleSideBar"
/> />
@@ -10,9 +57,7 @@
<breadcrumb id="breadcrumb-container" class="breadcrumb-container" /> <breadcrumb id="breadcrumb-container" class="breadcrumb-container" />
<div class="right-menu"> <div class="right-menu">
<template v-if="device !== 'mobile'"> <template v-if="device !== DeviceType.mobile">
<!-- <search id="header-search" class="right-menu-item" />
<error-log class="errLog-container right-menu-item hover-effect" />-->
<screenfull id="screenfull" class="right-menu-item hover-effect" /> <screenfull id="screenfull" class="right-menu-item hover-effect" />
<el-tooltip content="布局大小" effect="dark" placement="bottom"> <el-tooltip content="布局大小" effect="dark" placement="bottom">
<size-select id="size-select" class="right-menu-item hover-effect" /> <size-select id="size-select" class="right-menu-item hover-effect" />
@@ -25,7 +70,7 @@
trigger="click" trigger="click"
> >
<div class="avatar-wrapper"> <div class="avatar-wrapper">
<img :src="avatar + '?imageView2/1/w/80/h/80'" class="user-avatar" /> <img :src="userStore.avatar + '?imageView2/1/w/80/h/80'" />
<CaretBottom style="width: 0.6em; height: 0.6em; margin-left: 5px" /> <CaretBottom style="width: 0.6em; height: 0.6em; margin-left: 5px" />
</div> </div>
@@ -52,53 +97,6 @@
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts">
import { computed } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { ElMessageBox } from 'element-plus';
import useStore from '@/store';
// 组件依赖
import Breadcrumb from '@/components/Breadcrumb/index.vue';
import Hamburger from '@/components/Hamburger/index.vue';
import Screenfull from '@/components/Screenfull/index.vue';
import SizeSelect from '@/components/SizeSelect/index.vue';
import LangSelect from '@/components/LangSelect/index.vue';
// 图标依赖
import { CaretBottom } from '@element-plus/icons-vue';
const { app, user, tagsView } = useStore();
const route = useRoute();
const router = useRouter();
const sidebar = computed(() => app.sidebar);
const device = computed(() => app.device);
const avatar = computed(() => user.avatar);
function toggleSideBar() {
app.toggleSidebar();
}
function logout() {
ElMessageBox.confirm('确定注销并退出系统吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
user
.logout()
.then(() => {
tagsView.delAllViews();
})
.then(() => {
router.push(`/login?redirect=${route.fullPath}`);
});
});
}
</script>
<style lang="scss" scoped> <style lang="scss" scoped>
ul { ul {
@@ -164,7 +162,7 @@ ul {
margin-top: 5px; margin-top: 5px;
position: relative; position: relative;
.user-avatar { img {
cursor: pointer; cursor: pointer;
width: 40px; width: 40px;
height: 40px; height: 40px;

View File

@@ -1,3 +1,15 @@
<script setup lang="ts">
import { useSettingsStore } from '@/store/modules/settings';
import ThemePicker from '@/components/ThemePicker/index.vue';
const settingsStore = useSettingsStore();
function themeChange(val: string) {
settingsStore.changeSetting({ key: 'theme', value: val });
}
</script>
<template> <template>
<div class="drawer-container"> <div class="drawer-container">
<h3 class="drawer-title">系统布局配置</h3> <h3 class="drawer-title">系统布局配置</h3>
@@ -10,17 +22,17 @@
<div class="drawer-item"> <div class="drawer-item">
<span>开启 Tags-View</span> <span>开启 Tags-View</span>
<el-switch v-model="tagsView" class="drawer-switch" /> <el-switch v-model="settingsStore.tagsView" class="drawer-switch" />
</div> </div>
<div class="drawer-item"> <div class="drawer-item">
<span>固定 Header</span> <span>固定 Header</span>
<el-switch v-model="fixedHeader" class="drawer-switch" /> <el-switch v-model="settingsStore.fixedHeader" class="drawer-switch" />
</div> </div>
<div class="drawer-item"> <div class="drawer-item">
<span>侧边栏 Logo</span> <span>侧边栏 Logo</span>
<el-switch v-model="sidebarLogo" class="drawer-switch" /> <el-switch v-model="settingsStore.sidebarLogo" class="drawer-switch" />
</div> </div>
<el-divider>导航栏模式</el-divider> <el-divider>导航栏模式</el-divider>
@@ -48,49 +60,6 @@
</div> </div>
</template> </template>
<script setup lang="ts">
import { reactive, toRefs, watch } from 'vue';
import ThemePicker from '@/components/ThemePicker/index.vue';
import useStore from '@/store';
const { setting } = useStore();
const state = reactive({
fixedHeader: setting.fixedHeader,
tagsView: setting.tagsView,
sidebarLogo: setting.sidebarLogo
});
const { fixedHeader, tagsView, sidebarLogo } = toRefs(state);
function themeChange(val: any) {
setting.changeSetting({ key: 'theme', value: val });
}
watch(
() => state.fixedHeader,
value => {
setting.changeSetting({ key: 'fixedHeader', value: value });
}
);
watch(
() => state.tagsView,
value => {
setting.changeSetting({ key: 'tagsView', value: value });
}
);
watch(
() => state.sidebarLogo,
value => {
setting.changeSetting({ key: 'sidebarLogo', value: value });
}
);
</script>
<style lang="scss" scoped> <style lang="scss" scoped>
.drawer-container { .drawer-container {
padding: 24px; padding: 24px;

View File

@@ -1,3 +1,32 @@
<script lang="ts" setup>
import { computed } from 'vue';
import { isExternal } from '@/utils/validate';
import { useRouter } from 'vue-router';
import { DeviceType, useAppStore } from '@/store/modules/app';
const appStore = useAppStore();
const sidebar = computed(() => appStore.sidebar);
const device = computed(() => appStore.device);
const props = defineProps({
to: {
type: String,
required: true
}
});
const router = useRouter();
function push() {
if (device.value === DeviceType.mobile && sidebar.value.opened == true) {
appStore.closeSideBar(false);
}
router.push(props.to).catch(err => {
console.error(err);
});
}
</script>
<template> <template>
<a v-if="isExternal(to)" :href="to" target="_blank" rel="noopener"> <a v-if="isExternal(to)" :href="to" target="_blank" rel="noopener">
<slot /> <slot />
@@ -6,40 +35,3 @@
<slot /> <slot />
</div> </div>
</template> </template>
<script lang="ts">
import { computed, defineComponent } from 'vue';
import { isExternal } from '@/utils/validate';
import { useRouter } from 'vue-router';
import useStore from '@/store';
const { app } = useStore();
const sidebar = computed(() => app.sidebar);
const device = computed(() => app.device);
export default defineComponent({
props: {
to: {
type: String,
required: true
}
},
setup(props) {
const router = useRouter();
const push = () => {
if (device.value === 'mobile' && sidebar.value.opened == true) {
app.closeSideBar(false);
}
router.push(props.to).catch(err => {
console.log(err);
});
};
return {
push,
isExternal
};
}
});
</script>

View File

@@ -1,3 +1,18 @@
<script lang="ts" setup>
import { ref } from 'vue';
defineProps({
collapse: {
type: Boolean,
required: true
}
});
const logo = ref<string>(
new URL(`../../../assets/logo.png`, import.meta.url).href
);
</script>
<template> <template>
<div class="sidebar-logo-container" :class="{ collapse: collapse }"> <div class="sidebar-logo-container" :class="{ collapse: collapse }">
<transition name="sidebarLogoFade"> <transition name="sidebarLogoFade">
@@ -18,24 +33,6 @@
</div> </div>
</template> </template>
<script setup lang="ts">
import { reactive, toRefs } from 'vue';
const props = defineProps({
collapse: {
type: Boolean,
required: true
}
});
const state = reactive({
isCollapse: props.collapse,
logo: new URL(`../../../assets/logo.png`, import.meta.url).href
});
const { logo } = toRefs(state);
</script>
<style lang="scss" scoped> <style lang="scss" scoped>
.sidebarLogoFade-enter-active { .sidebarLogoFade-enter-active {
transition: opacity 1.5s; transition: opacity 1.5s;

View File

@@ -1,51 +1,3 @@
<template>
<div v-if="!item.meta || !item.meta.hidden">
<template
v-if="
hasOneShowingChild(item.children, item) &&
(!onlyOneChild.children || onlyOneChild.noShowingChildren) &&
(!item.meta || !item.meta.alwaysShow)
"
>
<app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path)">
<el-menu-item
:index="resolvePath(onlyOneChild.path)"
:class="{ 'submenu-title-noDropdown': !isNest }"
>
<svg-icon
v-if="onlyOneChild.meta && onlyOneChild.meta.icon"
:icon-class="onlyOneChild.meta.icon"
/>
<template #title>
{{ generateTitle(onlyOneChild.meta.title) }}
</template>
</el-menu-item>
</app-link>
</template>
<el-sub-menu v-else :index="resolvePath(item.path)" popper-append-to-body>
<!-- popper-append-to-body -->
<template #title>
<svg-icon
v-if="item.meta && item.meta.icon"
:icon-class="item.meta.icon"
></svg-icon>
<span v-if="item.meta && item.meta.title">{{
generateTitle(item.meta.title)
}}</span>
</template>
<sidebar-item
v-for="child in item.children"
:key="child.path"
:item="child"
:is-nest="true"
:base-path="resolvePath(child.path)"
class="nest-menu"
/>
</el-sub-menu>
</div>
</template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'; import { ref } from 'vue';
import path from 'path-browserify'; import path from 'path-browserify';
@@ -110,5 +62,52 @@ function resolvePath(routePath: string) {
return path.resolve(props.basePath, routePath); return path.resolve(props.basePath, routePath);
} }
</script> </script>
<template>
<div v-if="!item.meta || !item.meta.hidden">
<template
v-if="
hasOneShowingChild(item.children, item) &&
(!onlyOneChild.children || onlyOneChild.noShowingChildren) &&
(!item.meta || !item.meta.alwaysShow)
"
>
<app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path)">
<el-menu-item
:index="resolvePath(onlyOneChild.path)"
:class="{ 'submenu-title-noDropdown': !isNest }"
>
<svg-icon
v-if="onlyOneChild.meta && onlyOneChild.meta.icon"
:icon-class="onlyOneChild.meta.icon"
/>
<template #title>
{{ generateTitle(onlyOneChild.meta.title) }}
</template>
</el-menu-item>
</app-link>
</template>
<el-sub-menu v-else :index="resolvePath(item.path)" popper-append-to-body>
<!-- popper-append-to-body -->
<template #title>
<svg-icon
v-if="item.meta && item.meta.icon"
:icon-class="item.meta.icon"
></svg-icon>
<span v-if="item.meta && item.meta.title">{{
generateTitle(item.meta.title)
}}</span>
</template>
<sidebar-item
v-for="child in item.children"
:key="child.path"
:item="child"
:is-nest="true"
:base-path="resolvePath(child.path)"
class="nest-menu"
/>
</el-sub-menu>
</div>
</template>
<style lang="scss" scoped></style> <style lang="scss" scoped></style>

View File

@@ -1,6 +1,37 @@
<script setup lang="ts">
import { computed } from 'vue';
import { useRoute } from 'vue-router';
import SidebarItem from './SidebarItem.vue';
import Logo from './Logo.vue';
import variables from '@/styles/variables.module.scss';
import { useSettingsStore } from '@/store/modules/settings';
import { usePermissionStore } from '@/store/modules/permission';
import { useAppStore } from '@/store/modules/app';
import { storeToRefs } from 'pinia';
const settingsStore = useSettingsStore();
const permissionStore = usePermissionStore();
const appStore = useAppStore();
const { sidebarLogo } = storeToRefs(settingsStore);
const route = useRoute();
const isCollapse = computed(() => !appStore.sidebar.opened);
const activeMenu = computed<string>(() => {
const { meta, path } = route;
if (meta?.activeMenu) {
return meta.activeMenu as string;
}
return path;
});
</script>
<template> <template>
<div :class="{ 'has-logo': showLogo }"> <div :class="{ 'has-logo': sidebarLogo }">
<logo v-if="showLogo" :collapse="isCollapse" /> <logo v-if="sidebarLogo" :collapse="isCollapse" />
<el-scrollbar> <el-scrollbar>
<el-menu <el-menu
:default-active="activeMenu" :default-active="activeMenu"
@@ -13,7 +44,7 @@
mode="vertical" mode="vertical"
> >
<sidebar-item <sidebar-item
v-for="route in routes" v-for="route in permissionStore.routes"
:item="route" :item="route"
:key="route.path" :key="route.path"
:base-path="route.path" :base-path="route.path"
@@ -23,29 +54,3 @@
</el-scrollbar> </el-scrollbar>
</div> </div>
</template> </template>
<script setup lang="ts">
import { computed } from 'vue';
import { useRoute } from 'vue-router';
import SidebarItem from './SidebarItem.vue';
import Logo from './Logo.vue';
import variables from '@/styles/variables.module.scss';
import useStore from '@/store';
const { permission, setting, app } = useStore();
const route = useRoute();
const routes = computed(() => permission.routes);
const showLogo = computed(() => setting.sidebarLogo);
const isCollapse = computed(() => !app.sidebar.opened);
const activeMenu = computed(() => {
const { meta, path } = route;
// if set path, the sidebar will highlight the path you set
if (meta.activeMenu) {
return meta.activeMenu as string;
}
return path;
});
</script>

View File

@@ -1,14 +1,3 @@
<template>
<el-scrollbar
ref="scrollContainer"
:vertical="false"
class="scroll-container"
@wheel.prevent="handleScroll"
>
<slot />
</el-scrollbar>
</template>
<script setup lang="ts"> <script setup lang="ts">
import { import {
ref, ref,
@@ -17,8 +6,7 @@ import {
onBeforeUnmount, onBeforeUnmount,
getCurrentInstance getCurrentInstance
} from 'vue'; } from 'vue';
import useStore from '@/store'; import { useTagsViewStore, TagView } from '@/store/modules/tagsView';
import { TagView } from '@/store/modules/types';
const tagAndTagSpacing = ref(4); const tagAndTagSpacing = ref(4);
const { proxy } = getCurrentInstance() as any; const { proxy } = getCurrentInstance() as any;
@@ -28,9 +16,7 @@ const emitScroll = () => {
emits('scroll'); emits('scroll');
}; };
const { tagsView } = useStore(); const tagsViewStore = useTagsViewStore();
const visitedViews = computed(() => tagsView.visitedViews);
const scrollWrapper = computed( const scrollWrapper = computed(
() => proxy?.$refs.scrollContainer.$refs.wrapRef () => proxy?.$refs.scrollContainer.$refs.wrapRef
@@ -58,9 +44,9 @@ function moveToTarget(currentTag: TagView) {
let lastTag = null; let lastTag = null;
// find first tag and last tag // find first tag and last tag
if (visitedViews.value.length > 0) { if (tagsViewStore.visitedViews.length > 0) {
firstTag = visitedViews.value[0]; firstTag = tagsViewStore.visitedViews[0];
lastTag = visitedViews.value[visitedViews.value.length - 1]; lastTag = tagsViewStore.visitedViews[tagsViewStore.visitedViews.length - 1];
} }
if (firstTag === currentTag) { if (firstTag === currentTag) {
@@ -69,7 +55,7 @@ function moveToTarget(currentTag: TagView) {
$scrollWrapper.scrollLeft = $scrollWrapper.scrollWidth - $containerWidth; $scrollWrapper.scrollLeft = $scrollWrapper.scrollWidth - $containerWidth;
} else { } else {
const tagListDom = document.getElementsByClassName('tags-view__item'); const tagListDom = document.getElementsByClassName('tags-view__item');
const currentIndex = visitedViews.value.findIndex( const currentIndex = tagsViewStore.visitedViews.findIndex(
item => item === currentTag item => item === currentTag
); );
let prevTag = null; let prevTag = null;
@@ -78,13 +64,13 @@ function moveToTarget(currentTag: TagView) {
if (k !== 'length' && Object.hasOwnProperty.call(tagListDom, k)) { if (k !== 'length' && Object.hasOwnProperty.call(tagListDom, k)) {
if ( if (
(tagListDom[k] as any).dataset.path === (tagListDom[k] as any).dataset.path ===
visitedViews.value[currentIndex - 1].path tagsViewStore.visitedViews[currentIndex - 1].path
) { ) {
prevTag = tagListDom[k]; prevTag = tagListDom[k];
} }
if ( if (
(tagListDom[k] as any).dataset.path === (tagListDom[k] as any).dataset.path ===
visitedViews.value[currentIndex + 1].path tagsViewStore.visitedViews[currentIndex + 1].path
) { ) {
nextTag = tagListDom[k]; nextTag = tagListDom[k];
} }
@@ -113,6 +99,17 @@ defineExpose({
}); });
</script> </script>
<template>
<el-scrollbar
ref="scrollContainer"
:vertical="false"
class="scroll-container"
@wheel.prevent="handleScroll"
>
<slot />
</el-scrollbar>
</template>
<style lang="scss" scoped> <style lang="scss" scoped>
.scroll-container { .scroll-container {
.el-scrollbar__bar { .el-scrollbar__bar {

View File

@@ -1,66 +1,5 @@
<template>
<div class="tags-view__container">
<scroll-pane
ref="scrollPaneRef"
class="tags-view__wrapper"
@scroll="handleScroll"
>
<router-link
v-for="tag in visitedViews"
:key="tag.path"
:data-path="tag.path"
:class="isActive(tag) ? 'active' : ''"
:to="{ path: tag.path, query: tag.query }"
class="tags-view__item"
@click.middle="!isAffix(tag) ? closeSelectedTag(tag) : ''"
@contextmenu.prevent="openMenu(tag, $event)"
>
{{ generateTitle(tag.meta.title) }}
<span
v-if="!isAffix(tag)"
class="icon-close"
@click.prevent.stop="closeSelectedTag(tag)"
>
<svg-icon icon-class="close" />
</span>
</router-link>
</scroll-pane>
<ul
v-show="visible"
:style="{ left: left + 'px', top: top + 'px' }"
class="tags-view__menu"
>
<li @click="refreshSelectedTag(selectedTag)">
<svg-icon icon-class="refresh" />
刷新
</li>
<li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)">
<svg-icon icon-class="close" />
关闭
</li>
<li @click="closeOtherTags">
<svg-icon icon-class="close_other" />
关闭其它
</li>
<li v-if="!isFirstView()" @click="closeLeftTags">
<svg-icon icon-class="close_left" />
关闭左侧
</li>
<li v-if="!isLastView()" @click="closeRightTags">
<svg-icon icon-class="close_right" />
关闭右侧
</li>
<li @click="closeAllTags(selectedTag)">
<svg-icon icon-class="close_all" />
关闭所有
</li>
</ul>
</div>
</template>
<script setup lang="ts"> <script setup lang="ts">
import { import {
computed,
getCurrentInstance, getCurrentInstance,
nextTick, nextTick,
ref, ref,
@@ -76,24 +15,23 @@ import { useRoute, useRouter } from 'vue-router';
import ScrollPane from './ScrollPane.vue'; import ScrollPane from './ScrollPane.vue';
import SvgIcon from '@/components/SvgIcon/index.vue'; import SvgIcon from '@/components/SvgIcon/index.vue';
import { generateTitle } from '@/utils/i18n'; import { generateTitle } from '@/utils/i18n';
import useStore from '@/store';
import { TagView } from '@/store/modules/types';
const { tagsView, permission } = useStore(); import { usePermissionStore } from '@/store/modules/permission';
import { useTagsViewStore, TagView } from '@/store/modules/tagsView';
const permissionStore = usePermissionStore();
const tagsViewStore = useTagsViewStore();
const { proxy } = getCurrentInstance() as ComponentInternalInstance; const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const router = useRouter(); const router = useRouter();
const route = useRoute(); const route = useRoute();
const visitedViews = computed<any>(() => tagsView.visitedViews);
const routes = computed<any>(() => permission.routes);
const affixTags = ref([]);
const visible = ref(false); const visible = ref(false);
const selectedTag = ref({}); const selectedTag = ref({});
const scrollPaneRef = ref(); const scrollPaneRef = ref();
const left = ref(0); const left = ref(0);
const top = ref(0); const top = ref(0);
const affixTags = ref<TagView[]>([]);
watch( watch(
route, route,
@@ -140,30 +78,30 @@ function filterAffixTags(routes: any[], basePath = '/') {
} }
function initTags() { function initTags() {
const res = filterAffixTags(routes.value) as []; const tags = filterAffixTags(permissionStore.routes);
affixTags.value = res; affixTags.value = tags;
for (const tag of res) { for (const tag of tags) {
// Must have tag name // Must have tag name
if ((tag as TagView).name) { if ((tag as TagView).name) {
tagsView.addVisitedView(tag); tagsViewStore.addVisitedView(tag);
} }
} }
} }
function addTags() { function addTags() {
if (route.name) { if (route.name) {
tagsView.addView(route); tagsViewStore.addView(route);
} }
} }
function moveToCurrentTag() { function moveToCurrentTag() {
nextTick(() => { nextTick(() => {
for (const r of visitedViews.value) { for (const r of tagsViewStore.visitedViews) {
if (r.path === route.path) { if (r.path === route.path) {
scrollPaneRef.value.moveToTarget(r); scrollPaneRef.value.moveToTarget(r);
// when query is different then update // when query is different then update
if (r.fullPath !== route.fullPath) { if (r.fullPath !== route.fullPath) {
tagsView.updateVisitedView(route); tagsViewStore.updateVisitedView(route);
} }
} }
} }
@@ -182,7 +120,7 @@ function isFirstView() {
try { try {
return ( return (
(selectedTag.value as TagView).fullPath === (selectedTag.value as TagView).fullPath ===
visitedViews.value[1].fullPath || tagsViewStore.visitedViews[1].fullPath ||
(selectedTag.value as TagView).fullPath === '/index' (selectedTag.value as TagView).fullPath === '/index'
); );
} catch (err) { } catch (err) {
@@ -194,7 +132,7 @@ function isLastView() {
try { try {
return ( return (
(selectedTag.value as TagView).fullPath === (selectedTag.value as TagView).fullPath ===
visitedViews.value[visitedViews.value.length - 1].fullPath tagsViewStore.visitedViews[tagsViewStore.visitedViews.length - 1].fullPath
); );
} catch (err) { } catch (err) {
return false; return false;
@@ -202,7 +140,7 @@ function isLastView() {
} }
function refreshSelectedTag(view: TagView) { function refreshSelectedTag(view: TagView) {
tagsView.delCachedView(view); tagsViewStore.delCachedView(view);
const { fullPath } = view; const { fullPath } = view;
nextTick(() => { nextTick(() => {
router.replace({ path: '/redirect' + fullPath }).catch(err => { router.replace({ path: '/redirect' + fullPath }).catch(err => {
@@ -228,7 +166,7 @@ function toLastView(visitedViews: TagView[], view?: any) {
} }
function closeSelectedTag(view: TagView) { function closeSelectedTag(view: TagView) {
tagsView.delView(view).then((res: any) => { tagsViewStore.delView(view).then((res: any) => {
if (isActive(view)) { if (isActive(view)) {
toLastView(res.visitedViews, view); toLastView(res.visitedViews, view);
} }
@@ -236,7 +174,7 @@ function closeSelectedTag(view: TagView) {
} }
function closeLeftTags() { function closeLeftTags() {
tagsView.delLeftViews(selectedTag.value).then((res: any) => { tagsViewStore.delLeftViews(selectedTag.value).then((res: any) => {
if ( if (
!res.visitedViews.find((item: any) => item.fullPath === route.fullPath) !res.visitedViews.find((item: any) => item.fullPath === route.fullPath)
) { ) {
@@ -245,7 +183,7 @@ function closeLeftTags() {
}); });
} }
function closeRightTags() { function closeRightTags() {
tagsView.delRightViews(selectedTag.value).then((res: any) => { tagsViewStore.delRightViews(selectedTag.value).then((res: any) => {
if ( if (
!res.visitedViews.find((item: any) => item.fullPath === route.fullPath) !res.visitedViews.find((item: any) => item.fullPath === route.fullPath)
) { ) {
@@ -256,13 +194,13 @@ function closeRightTags() {
function closeOtherTags() { function closeOtherTags() {
router.push(selectedTag.value); router.push(selectedTag.value);
tagsView.delOtherViews(selectedTag.value).then(() => { tagsViewStore.delOtherViews(selectedTag.value).then(() => {
moveToCurrentTag(); moveToCurrentTag();
}); });
} }
function closeAllTags(view: TagView) { function closeAllTags(view: TagView) {
tagsView.delAllViews().then((res: any) => { tagsViewStore.delAllViews().then((res: any) => {
toLastView(res.visitedViews, view); toLastView(res.visitedViews, view);
}); });
} }
@@ -298,6 +236,66 @@ onMounted(() => {
}); });
</script> </script>
<template>
<div class="tags-view__container">
<scroll-pane
ref="scrollPaneRef"
class="tags-view__wrapper"
@scroll="handleScroll"
>
<router-link
v-for="tag in tagsViewStore.visitedViews"
:key="tag.path"
:data-path="tag.path"
:class="isActive(tag) ? 'active' : ''"
:to="{ path: tag.path, query: tag.query }"
class="tags-view__item"
@click.middle="!isAffix(tag) ? closeSelectedTag(tag) : ''"
@contextmenu.prevent="openMenu(tag, $event)"
>
{{ generateTitle(tag.meta?.title) }}
<span
v-if="!isAffix(tag)"
class="icon-close"
@click.prevent.stop="closeSelectedTag(tag)"
>
<svg-icon icon-class="close" />
</span>
</router-link>
</scroll-pane>
<ul
v-show="visible"
:style="{ left: left + 'px', top: top + 'px' }"
class="tags-view__menu"
>
<li @click="refreshSelectedTag(selectedTag)">
<svg-icon icon-class="refresh" />
刷新
</li>
<li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)">
<svg-icon icon-class="close" />
关闭
</li>
<li @click="closeOtherTags">
<svg-icon icon-class="close_other" />
关闭其它
</li>
<li v-if="!isFirstView()" @click="closeLeftTags">
<svg-icon icon-class="close_left" />
关闭左侧
</li>
<li v-if="!isLastView()" @click="closeRightTags">
<svg-icon icon-class="close_right" />
关闭右侧
</li>
<li @click="closeAllTags(selectedTag)">
<svg-icon icon-class="close_all" />
关闭所有
</li>
</ul>
</div>
</template>
<style lang="scss" scoped> <style lang="scss" scoped>
.tags-view__container { .tags-view__container {
height: 34px; height: 34px;

View File

@@ -1,15 +1,72 @@
<script setup lang="ts">
import { computed, watchEffect } from 'vue';
import { useWindowSize } from '@vueuse/core';
import { AppMain, Navbar, Settings, TagsView } from './components/index';
import Sidebar from './components/Sidebar/index.vue';
import RightPanel from '@/components/RightPanel/index.vue';
import { DeviceType, useAppStore } from '@/store/modules/app';
import { useSettingsStore } from '@/store/modules/settings';
const { width } = useWindowSize();
/**
* 响应式布局容器固定宽度
*
* 大屏(>=1200px
* 中屏(>=992px
* 小屏(>=768px
*/
const WIDTH = 992;
const appStore = useAppStore();
const settingsStore = useSettingsStore();
const fixedHeader = computed(() => settingsStore.fixedHeader);
const showTagsView = computed(() => settingsStore.tagsView);
const showSettings = computed(() => settingsStore.showSettings);
const classObj = computed(() => ({
hideSidebar: !appStore.sidebar.opened,
openSidebar: appStore.sidebar.opened,
withoutAnimation: appStore.sidebar.withoutAnimation,
mobile: appStore.device === DeviceType.mobile
}));
watchEffect(() => {
if (width.value < WIDTH) {
appStore.toggleDevice(DeviceType.mobile);
appStore.closeSideBar(true);
} else {
appStore.toggleDevice(DeviceType.desktop);
if (width.value >= 1200) {
//大屏
appStore.openSideBar(true);
} else {
appStore.closeSideBar(true);
}
}
});
function handleOutsideClick() {
appStore.closeSideBar(false);
}
</script>
<template> <template>
<div :class="classObj" class="app-wrapper"> <div :class="classObj" class="app-wrapper">
<!-- 手机设备 && 侧边栏 显示遮罩层 -->
<div <div
v-if="device === 'mobile' && sidebar.opened" v-if="classObj.mobile && classObj.openSidebar"
class="drawer-bg" class="drawer-bg"
@click="handleClickOutside" @click="handleOutsideClick"
/> ></div>
<Sidebar class="sidebar-container" /> <Sidebar class="sidebar-container" />
<div :class="{ hasTagsView: needTagsView }" class="main-container"> <div :class="{ hasTagsView: showTagsView }" class="main-container">
<div :class="{ 'fixed-header': fixedHeader }"> <div :class="{ 'fixed-header': fixedHeader }">
<navbar /> <navbar />
<tags-view v-if="needTagsView" /> <tags-view v-if="showTagsView" />
</div> </div>
<!--主页面--> <!--主页面-->
@@ -23,47 +80,6 @@
</div> </div>
</template> </template>
<script setup lang="ts">
import { computed, watchEffect } from 'vue';
import { useWindowSize } from '@vueuse/core';
import { AppMain, Navbar, Settings, TagsView } from './components/index';
import Sidebar from './components/Sidebar/index.vue';
import RightPanel from '@/components/RightPanel/index.vue';
import useStore from '@/store';
const { width } = useWindowSize();
const WIDTH = 992;
const { app, setting } = useStore();
const sidebar = computed(() => app.sidebar);
const device = computed(() => app.device);
const needTagsView = computed(() => setting.tagsView);
const fixedHeader = computed(() => setting.fixedHeader);
const showSettings = computed(() => setting.showSettings);
const classObj = computed(() => ({
hideSidebar: !sidebar.value.opened,
openSidebar: sidebar.value.opened,
withoutAnimation: sidebar.value.withoutAnimation,
mobile: device.value === 'mobile'
}));
watchEffect(() => {
if (width.value < WIDTH) {
app.toggleDevice('mobile');
app.closeSideBar(true);
} else {
app.toggleDevice('desktop');
}
});
function handleClickOutside() {
app.closeSideBar(false);
}
</script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import '@/styles/mixin.scss'; @import '@/styles/mixin.scss';
@import '@/styles/variables.module.scss'; @import '@/styles/variables.module.scss';
@@ -79,7 +95,6 @@ function handleClickOutside() {
top: 0; top: 0;
} }
} }
.drawer-bg { .drawer-bg {
background: #000; background: #000;
opacity: 0.3; opacity: 0.3;
@@ -98,11 +113,9 @@ function handleClickOutside() {
width: calc(100% - #{$sideBarWidth}); width: calc(100% - #{$sideBarWidth});
transition: width 0.28s; transition: width 0.28s;
} }
.hideSidebar .fixed-header { .hideSidebar .fixed-header {
width: calc(100% - 54px); width: calc(100% - 54px);
} }
.mobile .fixed-header { .mobile .fixed-header {
width: 100%; width: 100%;
} }

View File

@@ -1,11 +1,10 @@
import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router'; import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router';
import useStore from '@/store'; import { usePermissionStoreHook } from '@/store/modules/permission';
export const Layout = () => import('@/layout/index.vue'); export const Layout = () => import('@/layout/index.vue');
// 参数说明: https://panjiachen.github.io/vue-element-admin-site/guide/essentials/router-and-nav.html
// 静态路由 // 静态路由
export const constantRoutes: Array<RouteRecordRaw> = [ export const constantRoutes: RouteRecordRaw[] = [
{ {
path: '/redirect', path: '/redirect',
component: Layout, component: Layout,
@@ -43,7 +42,7 @@ export const constantRoutes: Array<RouteRecordRaw> = [
path: '401', path: '401',
component: () => import('@/views/error-page/401.vue'), component: () => import('@/views/error-page/401.vue'),
meta: { hidden: true } meta: { hidden: true }
}, }
] ]
} }
@@ -110,8 +109,8 @@ const router = createRouter({
// 重置路由 // 重置路由
export function resetRouter() { export function resetRouter() {
const { permission } = useStore(); const permissionStore = usePermissionStoreHook();
permission.routes.forEach(route => { permissionStore.routes.forEach(route => {
const name = route.name; const name = route.name;
if (name && router.hasRoute(name)) { if (name && router.hasRoute(name)) {
router.removeRoute(name); router.removeRoute(name);

View File

@@ -1,15 +1,11 @@
import useUserStore from './modules/user'; import type { App } from 'vue';
import useAppStore from './modules/app'; import { createPinia } from 'pinia';
import usePermissionStore from './modules/permission';
import useSettingStore from './modules/settings';
import useTagsViewStore from './modules/tagsView';
const useStore = () => ({ const store = createPinia();
user: useUserStore(),
app: useAppStore(),
permission: usePermissionStore(),
setting: useSettingStore(),
tagsView: useTagsViewStore()
});
export default useStore; // 全局挂载store
export function setupStore(app: App<Element>) {
app.use(store);
}
export { store };

View File

@@ -1,48 +1,96 @@
import { AppState } from './types'; import {
import { localStorage } from '@/utils/storage'; getSidebarStatus,
setSidebarStatus,
getSize,
setSize,
setLanguage
} from '@/utils/localStorage';
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { getLanguage } from '@/lang/index'; import { getLanguage } from '@/lang/index';
import { computed, reactive, ref } from 'vue';
const useAppStore = defineStore({ // Element Plus 语言包
id: 'app', import zhCn from 'element-plus/es/locale/lang/zh-cn';
state: (): AppState => ({ import en from 'element-plus/es/locale/lang/en';
device: 'desktop',
sidebar: { export enum DeviceType {
opened: localStorage.get('sidebarStatus') mobile,
? !!+localStorage.get('sidebarStatus') desktop
: true,
withoutAnimation: false,
},
language: getLanguage(),
size: localStorage.get('size') || 'default',
}),
actions: {
toggleSidebar() {
this.sidebar.opened = !this.sidebar.opened;
this.sidebar.withoutAnimation = false;
if (this.sidebar.opened) {
localStorage.set('sidebarStatus', 1);
} else {
localStorage.set('sidebarStatus', 0);
} }
},
closeSideBar(withoutAnimation: any) { export enum SizeType {
localStorage.set('sidebarStatus', 0); default,
this.sidebar.opened = false; large,
this.sidebar.withoutAnimation = withoutAnimation; small
}, }
toggleDevice(device: string) {
this.device = device; // setup
}, export const useAppStore = defineStore('app', () => {
setSize(size: string) { // state
this.size = size; const device = ref<DeviceType>(DeviceType.desktop);
localStorage.set('size', size); const size = ref(getSize() || 'default');
}, const language = ref(getLanguage());
setLanguage(language: string) { const sidebar = reactive({
this.language = language; opened: getSidebarStatus() !== 'closed',
localStorage.set('language', language); withoutAnimation: false
},
},
}); });
export default useAppStore; const locale = computed(() => {
if (language?.value == 'en') {
return en;
} else {
return zhCn;
}
});
// actions
function toggleSidebar(withoutAnimation: boolean) {
sidebar.opened = !sidebar.opened;
sidebar.withoutAnimation = withoutAnimation;
if (sidebar.opened) {
setSidebarStatus('opened');
} else {
setSidebarStatus('closed');
}
}
function closeSideBar(withoutAnimation: boolean) {
sidebar.opened = false;
sidebar.withoutAnimation = withoutAnimation;
setSidebarStatus('closed');
}
function openSideBar(withoutAnimation: boolean) {
sidebar.opened = true;
sidebar.withoutAnimation = withoutAnimation;
setSidebarStatus('opened');
}
function toggleDevice(val: DeviceType) {
device.value = val;
}
function changeSize(val: string) {
size.value = val;
setSize(val);
}
function changeLanguage(val: string) {
language.value = val;
setLanguage(val);
}
return {
device,
sidebar,
language,
locale,
size,
toggleDevice,
changeSize,
changeLanguage,
toggleSidebar,
closeSideBar,
openSideBar
};
});

View File

@@ -1,8 +1,9 @@
import { PermissionState } from './types';
import { RouteRecordRaw } from 'vue-router'; import { RouteRecordRaw } from 'vue-router';
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { constantRoutes } from '@/router'; import { constantRoutes } from '@/router';
import { store } from '@/store';
import { listRoutes } from '@/api/menu'; import { listRoutes } from '@/api/menu';
import { ref } from 'vue';
const modules = import.meta.glob('../../views/**/**.vue'); const modules = import.meta.glob('../../views/**/**.vue');
export const Layout = () => import('@/layout/index.vue'); export const Layout = () => import('@/layout/index.vue');
@@ -21,10 +22,7 @@ const hasPermission = (roles: string[], route: RouteRecordRaw) => {
return false; return false;
}; };
export const filterAsyncRoutes = ( const filterAsyncRoutes = (routes: RouteRecordRaw[], roles: string[]) => {
routes: RouteRecordRaw[],
roles: string[]
) => {
const res: RouteRecordRaw[] = []; const res: RouteRecordRaw[] = [];
routes.forEach(route => { routes.forEach(route => {
const tmp = { ...route } as any; const tmp = { ...route } as any;
@@ -49,24 +47,25 @@ export const filterAsyncRoutes = (
return res; return res;
}; };
const usePermissionStore = defineStore({ // setup
id: 'permission', export const usePermissionStore = defineStore('permission', () => {
state: (): PermissionState => ({ // state
routes: [], const routes = ref<RouteRecordRaw[]>([]);
addRoutes: [] const addRoutes = ref<RouteRecordRaw[]>([]);
}),
actions: { // auctions
setRoutes(routes: RouteRecordRaw[]) { function setRoutes(newRoutes: RouteRecordRaw[]) {
this.addRoutes = routes; addRoutes.value = newRoutes;
this.routes = constantRoutes.concat(routes); routes.value = constantRoutes.concat(newRoutes);
}, }
generateRoutes(roles: string[]) {
return new Promise((resolve, reject) => { function generateRoutes(roles: string[]) {
return new Promise<RouteRecordRaw[]>((resolve, reject) => {
listRoutes() listRoutes()
.then(response => { .then(response => {
const asyncRoutes = response.data; const asyncRoutes = response.data;
const accessedRoutes = filterAsyncRoutes(asyncRoutes, roles); const accessedRoutes = filterAsyncRoutes(asyncRoutes, roles);
this.setRoutes(accessedRoutes); setRoutes(accessedRoutes);
resolve(accessedRoutes); resolve(accessedRoutes);
}) })
.catch(error => { .catch(error => {
@@ -74,7 +73,10 @@ const usePermissionStore = defineStore({
}); });
}); });
} }
} return { routes, setRoutes, generateRoutes };
}); });
export default usePermissionStore; // 非setup
export function usePermissionStoreHook() {
return usePermissionStore(store);
}

View File

@@ -1,50 +1,55 @@
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { SettingState } from './types';
import defaultSettings from '../../settings'; import defaultSettings from '../../settings';
import { localStorage } from '@/utils/storage'; import { localStorage } from '@/utils/localStorage';
import { ref } from 'vue';
const { showSettings, tagsView, fixedHeader, sidebarLogo } = defaultSettings;
const el = document.documentElement; const el = document.documentElement;
export const useSettingStore = defineStore({ export const useSettingsStore = defineStore('setting', () => {
id: 'setting', // state
state: (): SettingState => ({ const theme = ref(
theme:
localStorage.get('theme') || localStorage.get('theme') ||
getComputedStyle(el).getPropertyValue(`--el-color-primary`), getComputedStyle(el).getPropertyValue(`--el-color-primary`)
showSettings: showSettings, );
tagsView:
localStorage.get('tagsView') != null const showSettings = ref<boolean>(defaultSettings.showSettings);
? localStorage.get('tagsView') const tagsView = ref<boolean>(
: tagsView, localStorage.get('tagsView') || defaultSettings.tagsView
fixedHeader: fixedHeader, );
sidebarLogo: sidebarLogo, const fixedHeader = ref<boolean>(defaultSettings.fixedHeader);
}), const sidebarLogo = ref<boolean>(defaultSettings.sidebarLogo);
actions: {
async changeSetting(payload: { key: string; value: any }) { // auction
const { key, value } = payload; function changeSetting(param: { key: string; value: any }) {
const { key, value } = param;
switch (key) { switch (key) {
case 'theme': case 'theme':
this.theme = value; theme.value = value;
break; break;
case 'showSettings': case 'showSettings':
this.showSettings = value; showSettings.value = value;
break; break;
case 'fixedHeader': case 'fixedHeader':
this.fixedHeader = value; fixedHeader.value = value;
break;
case 'tagsView':
this.tagsView = value;
localStorage.set('tagsView', value); localStorage.set('tagsView', value);
break; break;
case 'sidebarLogo': case 'tagsView':
this.sidebarLogo = value; tagsView.value = value;
break;
case 'sidevarLogo':
sidebarLogo.value = value;
break; break;
default: default:
break; break;
} }
}, }
},
});
export default useSettingStore; return {
theme,
showSettings,
tagsView,
fixedHeader,
sidebarLogo,
changeSetting
};
});

View File

@@ -1,181 +1,214 @@
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { TagsViewState } from './types'; import { ref } from 'vue';
import { RouteLocationNormalized } from 'vue-router';
const useTagsViewStore = defineStore({ export interface TagView extends Partial<RouteLocationNormalized> {
id: 'tagsView', title?: string;
state: (): TagsViewState => ({ }
visitedViews: [],
cachedViews: [], // keepAlive 缓存页面 // setup
}), export const useTagsViewStore = defineStore('tagsView', () => {
actions: { // state
addVisitedView(view: any) { const visitedViews = ref<TagView[]>([]);
if (this.visitedViews.some((v) => v.path === view.path)) return; const cachedViews = ref<string[]>([]);
// auctions
function addVisitedView(view: TagView) {
if (visitedViews.value.some(v => v.path === view.path)) return;
if (view.meta && view.meta.affix) { if (view.meta && view.meta.affix) {
this.visitedViews.unshift( visitedViews.value.unshift(
Object.assign({}, view, { Object.assign({}, view, {
title: view.meta?.title || 'no-name', title: view.meta?.title || 'no-name'
}) })
); );
} else { } else {
this.visitedViews.push( visitedViews.value.push(
Object.assign({}, view, { Object.assign({}, view, {
title: view.meta?.title || 'no-name', title: view.meta?.title || 'no-name'
}) })
); );
} }
},
addCachedView(view: any) {
if (this.cachedViews.includes(view.name)) return;
if (view.meta.keepAlive) {
this.cachedViews.push(view.name);
} }
},
delVisitedView(view: any) { function addCachedView(view: TagView) {
return new Promise((resolve) => { const viewName = view.name as string;
for (const [i, v] of this.visitedViews.entries()) { if (cachedViews.value.includes(viewName)) return;
if (view.meta?.keepAlive) {
cachedViews.value.push(viewName);
}
}
function delVisitedView(view: TagView) {
return new Promise(resolve => {
for (const [i, v] of visitedViews.value.entries()) {
if (v.path === view.path) { if (v.path === view.path) {
this.visitedViews.splice(i, 1); visitedViews.value.splice(i, 1);
break; break;
} }
} }
resolve([...this.visitedViews]); resolve([...visitedViews.value]);
}); });
}, }
delCachedView(view: any) {
return new Promise((resolve) => { function delCachedView(view: TagView) {
const index = this.cachedViews.indexOf(view.name); const viewName = view.name as string;
index > -1 && this.cachedViews.splice(index, 1); return new Promise(resolve => {
resolve([...this.cachedViews]); const index = cachedViews.value.indexOf(viewName);
index > -1 && cachedViews.value.splice(index, 1);
resolve([...cachedViews.value]);
}); });
}, }
delOtherVisitedViews(view: any) {
return new Promise((resolve) => { function delOtherVisitedViews(view: TagView) {
this.visitedViews = this.visitedViews.filter((v) => { return new Promise(resolve => {
visitedViews.value = visitedViews.value.filter(v => {
return v.meta?.affix || v.path === view.path; return v.meta?.affix || v.path === view.path;
}); });
resolve([...this.visitedViews]); resolve([...visitedViews.value]);
}); });
}, }
delOtherCachedViews(view: any) {
return new Promise((resolve) => { function delOtherCachedViews(view: TagView) {
const index = this.cachedViews.indexOf(view.name); const viewName = view.name as string;
return new Promise(resolve => {
const index = cachedViews.value.indexOf(viewName);
if (index > -1) { if (index > -1) {
this.cachedViews = this.cachedViews.slice(index, index + 1); cachedViews.value = cachedViews.value.slice(index, index + 1);
} else { } else {
// if index = -1, there is no cached tags // if index = -1, there is no cached tags
this.cachedViews = []; cachedViews.value = [];
} }
resolve([...this.cachedViews]); resolve([...cachedViews.value]);
}); });
}, }
updateVisitedView(view: any) { function updateVisitedView(view: TagView) {
for (let v of this.visitedViews) { for (let v of visitedViews.value) {
if (v.path === view.path) { if (v.path === view.path) {
v = Object.assign(v, view); v = Object.assign(v, view);
break; break;
} }
} }
}, }
addView(view: any) {
this.addVisitedView(view); function addView(view: TagView) {
this.addCachedView(view); addVisitedView(view);
}, addCachedView(view);
delView(view: any) { }
return new Promise((resolve) => {
this.delVisitedView(view); function delView(view: TagView) {
this.delCachedView(view); return new Promise(resolve => {
delVisitedView(view);
delCachedView(view);
resolve({ resolve({
visitedViews: [...this.visitedViews], visitedViews: [...visitedViews.value],
cachedViews: [...this.cachedViews], cachedViews: [...cachedViews.value]
}); });
}); });
}, }
delOtherViews(view: any) {
return new Promise((resolve) => { function delOtherViews(view: TagView) {
this.delOtherVisitedViews(view); return new Promise(resolve => {
this.delOtherCachedViews(view); delOtherVisitedViews(view);
delOtherCachedViews(view);
resolve({ resolve({
visitedViews: [...this.visitedViews], visitedViews: [...visitedViews.value],
cachedViews: [...this.cachedViews], cachedViews: [...cachedViews.value]
}); });
}); });
}, }
delLeftViews(view: any) {
return new Promise((resolve) => { function delLeftViews(view: TagView) {
const currIndex = this.visitedViews.findIndex( return new Promise(resolve => {
(v) => v.path === view.path const currIndex = visitedViews.value.findIndex(v => v.path === view.path);
);
if (currIndex === -1) { if (currIndex === -1) {
return; return;
} }
this.visitedViews = this.visitedViews.filter((item, index) => { visitedViews.value = visitedViews.value.filter((item, index) => {
// affix:true 固定tag例如“首页” // affix:true 固定tag例如“首页”
if (index >= currIndex || (item.meta && item.meta.affix)) { if (index >= currIndex || (item.meta && item.meta.affix)) {
return true; return true;
} }
const cacheIndex = this.cachedViews.indexOf(item.name as string); const cacheIndex = cachedViews.value.indexOf(item.name as string);
if (cacheIndex > -1) { if (cacheIndex > -1) {
this.cachedViews.splice(cacheIndex, 1); cachedViews.value.splice(cacheIndex, 1);
} }
return false; return false;
}); });
resolve({ resolve({
visitedViews: [...this.visitedViews], visitedViews: [...visitedViews.value]
}); });
}); });
}, }
delRightViews(view: any) { function delRightViews(view: TagView) {
return new Promise((resolve) => { return new Promise(resolve => {
const currIndex = this.visitedViews.findIndex( const currIndex = visitedViews.value.findIndex(v => v.path === view.path);
(v) => v.path === view.path
);
if (currIndex === -1) { if (currIndex === -1) {
return; return;
} }
this.visitedViews = this.visitedViews.filter((item, index) => { visitedViews.value = visitedViews.value.filter((item, index) => {
// affix:true 固定tag例如“首页” // affix:true 固定tag例如“首页”
if (index <= currIndex || (item.meta && item.meta.affix)) { if (index <= currIndex || (item.meta && item.meta.affix)) {
return true; return true;
} }
const cacheIndex = this.cachedViews.indexOf(item.name as string); const cacheIndex = cachedViews.value.indexOf(item.name as string);
if (cacheIndex > -1) { if (cacheIndex > -1) {
this.cachedViews.splice(cacheIndex, 1); cachedViews.value.splice(cacheIndex, 1);
} }
return false; return false;
}); });
resolve({ resolve({
visitedViews: [...this.visitedViews], visitedViews: [...visitedViews.value]
}); });
}); });
}, }
delAllViews() {
return new Promise((resolve) => {
const affixTags = this.visitedViews.filter((tag) => tag.meta?.affix);
this.visitedViews = affixTags;
this.cachedViews = [];
resolve({
visitedViews: [...this.visitedViews],
cachedViews: [...this.cachedViews],
});
});
},
delAllVisitedViews() {
return new Promise((resolve) => {
const affixTags = this.visitedViews.filter((tag) => tag.meta?.affix);
this.visitedViews = affixTags;
resolve([...this.visitedViews]);
});
},
delAllCachedViews() {
return new Promise((resolve) => {
this.cachedViews = [];
resolve([...this.cachedViews]);
});
},
},
});
export default useTagsViewStore; function delAllViews() {
return new Promise(resolve => {
const affixTags = visitedViews.value.filter(tag => tag.meta?.affix);
visitedViews.value = affixTags;
cachedViews.value = [];
resolve({
visitedViews: [...visitedViews.value],
cachedViews: [...cachedViews.value]
});
});
}
function delAllVisitedViews() {
return new Promise(resolve => {
const affixTags = visitedViews.value.filter(tag => tag.meta?.affix);
visitedViews.value = affixTags;
resolve([...visitedViews.value]);
});
}
function delAllCachedViews() {
return new Promise(resolve => {
cachedViews.value = [];
resolve([...cachedViews.value]);
});
}
return {
visitedViews,
cachedViews,
addVisitedView,
addCachedView,
delVisitedView,
delCachedView,
delOtherVisitedViews,
delOtherCachedViews,
updateVisitedView,
addView,
delView,
delOtherViews,
delLeftViews,
delRightViews,
delAllViews,
delAllVisitedViews,
delAllCachedViews
};
});

View File

@@ -1,103 +1,101 @@
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { UserState } from './types';
import { localStorage } from '@/utils/storage'; import { getToken, setToken, removeToken } from '@/utils/auth';
import { loginApi, logoutApi } from '@/api/auth'; import { loginApi, logoutApi } from '@/api/auth';
import { getUserInfo } from '@/api/user'; import { getUserInfo } from '@/api/user';
import { resetRouter } from '@/router'; import { resetRouter } from '@/router';
import { LoginForm } from '@/api/auth/types'; import { store } from '@/store';
import { LoginData } from '@/api/auth/types';
import { ref } from 'vue';
import { UserInfo } from '@/api/user/types';
const useUserStore = defineStore({ export const useUserStore = defineStore('user', () => {
id: 'user', // state
state: (): UserState => ({ const token = ref<string>(getToken() || '');
token: localStorage.get('token') || '', const nickname = ref<string>('');
nickname: '', const avatar = ref<string>('');
avatar: '', const roles = ref<Array<string>>([]); // 用户角色编码集合 → 判断路由权限
roles: [], const perms = ref<Array<string>>([]); // 用户权限编码集合 → 判断按钮权限
perms: []
}), // auctions
actions: {
async RESET_STATE() { // 登录
this.$reset(); function login(loginData: LoginData) {
}, return new Promise<void>((resolve, reject) => {
/** loginApi(loginData)
* 登录
*/
login(data: LoginForm) {
const { username, password } = data;
return new Promise((resolve, reject) => {
loginApi({
grant_type: 'password',
username: username.trim(),
password: password
})
.then(response => { .then(response => {
console.log('response.data', response.data); const { accessToken } = response.data;
const accessToken = response.data; token.value = accessToken;
localStorage.set('token', accessToken); setToken(accessToken);
this.token = accessToken; resolve();
resolve(accessToken);
}) })
.catch(error => { .catch(error => {
reject(error); reject(error);
}); });
}); });
}, }
/**
* 获取用户信息(昵称、头像、角色集合、权限集合 // 获取信息(用户昵称、头像、角色集合、权限集合)
*/ function getInfo() {
getUserInfo() { return new Promise<UserInfo>((resolve, reject) => {
return new Promise((resolve, reject) => {
getUserInfo() getUserInfo()
.then(({ data }) => { .then(({ data }) => {
if (!data) { if (!data) {
return reject('Verification failed, please Login again.'); return reject('Verification failed, please Login again.');
} }
const { nickname, avatar, roles, perms } = data; if (!data.roles || data.roles.length <= 0) {
if (!roles || roles.length <= 0) {
reject('getUserInfo: roles must be a non-null array!'); reject('getUserInfo: roles must be a non-null array!');
} }
this.nickname = nickname; nickname.value = data.nickname;
this.avatar = avatar; avatar.value = data.avatar;
this.roles = roles; roles.value = data.roles;
this.perms = perms; perms.value = data.perms;
resolve(data); resolve(data);
}) })
.catch(error => { .catch(error => {
reject(error); reject(error);
}); });
}); });
}, }
/** // 注销
* 注销 function logout() {
*/ return new Promise<void>((resolve, reject) => {
logout() {
return new Promise((resolve, reject) => {
logoutApi() logoutApi()
.then(() => { .then(() => {
localStorage.remove('token');
this.RESET_STATE();
resetRouter(); resetRouter();
resolve(null); resetToken();
resolve();
}) })
.catch(error => { .catch(error => {
reject(error); reject(error);
}); });
}); });
}, }
/** // 重置
* 清除 Token function resetToken() {
*/ removeToken();
resetToken() { token.value = '';
return new Promise(resolve => { nickname.value = '';
localStorage.remove('token'); avatar.value = '';
this.RESET_STATE(); roles.value = [];
resolve(null); perms.value = [];
});
}
} }
return {
token,
nickname,
avatar,
roles,
perms,
login,
getInfo,
logout,
resetToken
};
}); });
export default useUserStore; // 非setup
export function useUserStoreHook() {
return useUserStore(store);
}

View File

@@ -1,45 +0,0 @@
/**
* window.localStorage 浏览器永久缓存
*/
export const localStorage = {
// 设置永久缓存
set(key: string, val: any) {
window.localStorage.setItem(key, JSON.stringify(val));
},
// 获取永久缓存
get(key: string) {
const json: any = window.localStorage.getItem(key);
return JSON.parse(json);
},
// 移除永久缓存
remove(key: string) {
window.localStorage.removeItem(key);
},
// 移除全部永久缓存
clear() {
window.localStorage.clear();
}
};
/**
* window.sessionStorage 浏览器临时缓存
*/
export const sessionStorage = {
// 设置临时缓存
set(key: string, val: any) {
window.sessionStorage.setItem(key, JSON.stringify(val));
},
// 获取临时缓存
get(key: string) {
const json: any = window.sessionStorage.getItem(key);
return JSON.parse(json);
},
// 移除临时缓存
remove(key: string) {
window.sessionStorage.removeItem(key);
},
// 移除全部临时缓存
clear() {
window.sessionStorage.clear();
}
};

View File

@@ -2,7 +2,7 @@
<div class="login-container"> <div class="login-container">
<el-form <el-form
ref="loginFormRef" ref="loginFormRef"
:model="loginForm" :model="loginData"
:rules="loginRules" :rules="loginRules"
class="login-form" class="login-form"
auto-complete="on" auto-complete="on"
@@ -19,7 +19,7 @@
</span> </span>
<el-input <el-input
ref="username" ref="username"
v-model="loginForm.username" v-model="loginData.username"
:placeholder="$t('login.username')" :placeholder="$t('login.username')"
name="username" name="username"
type="text" type="text"
@@ -40,7 +40,7 @@
<el-input <el-input
ref="passwordRef" ref="passwordRef"
:key="passwordType" :key="passwordType"
v-model="loginForm.password" v-model="loginData.password"
:type="passwordType" :type="passwordType"
placeholder="Password" placeholder="Password"
name="password" name="password"
@@ -95,13 +95,13 @@ import LangSelect from '@/components/LangSelect/index.vue';
import SvgIcon from '@/components/SvgIcon/index.vue'; import SvgIcon from '@/components/SvgIcon/index.vue';
// 状态管理依赖 // 状态管理依赖
import useStore from '@/store'; import { useUserStore } from '@/store/modules/user';
// API依赖 // API依赖
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { LoginForm } from '@/api/auth/types'; import { LoginData } from '@/api/auth/types';
const { user } = useStore(); const userStore = useUserStore();
const route = useRoute(); const route = useRoute();
const loginFormRef = ref(ElForm); const loginFormRef = ref(ElForm);
@@ -109,10 +109,10 @@ const passwordRef = ref(ElInput);
const state = reactive({ const state = reactive({
redirect: '', redirect: '',
loginForm: { loginData: {
username: 'admin', username: 'admin',
password: '123456' password: '123456'
} as LoginForm, } as LoginData,
loginRules: { loginRules: {
username: [{ required: true, trigger: 'blur' }], username: [{ required: true, trigger: 'blur' }],
password: [{ required: true, trigger: 'blur', validator: validatePassword }] password: [{ required: true, trigger: 'blur', validator: validatePassword }]
@@ -136,7 +136,7 @@ function validatePassword(rule: any, value: any, callback: any) {
} }
const { const {
loginForm, loginData,
loginRules, loginRules,
loading, loading,
passwordType, passwordType,
@@ -162,14 +162,14 @@ function showPwd() {
} }
/** /**
* 登录处理 * 登录
*/ */
function handleLogin() { function handleLogin() {
loginFormRef.value.validate((valid: boolean) => { loginFormRef.value.validate((valid: boolean) => {
if (valid) { if (valid) {
state.loading = true; state.loading = true;
user userStore
.login(state.loginForm) .login(state.loginData)
.then(() => { .then(() => {
router.push({ path: state.redirect || '/', query: state.otherQuery }); router.push({ path: state.redirect || '/', query: state.otherQuery });
state.loading = false; state.loading = false;

View File

@@ -168,10 +168,10 @@ function handleDelete(row: any) {
ElMessage.success('删除成功'); ElMessage.success('删除成功');
}) })
.catch(() => { .catch(() => {
console.log(`删除失败`); ElMessage.error('删除失败');
}); });
}) })
.catch(() => ElMessage.info('已取消删除')); .catch(() => ElMessage.warning('已取消删除'));
} }
/** /**

View File

@@ -344,7 +344,7 @@ async function getDeptOptions() {
* 获取性别下拉项 * 获取性别下拉项
*/ */
function getGenderOptions() { function getGenderOptions() {
proxy.$listDictItemsByTypeCode('gender').then((response: any) => { proxy.$getDictionaries('gender').then((response: any) => {
state.genderOptions = response?.data; state.genderOptions = response?.data;
}); });
} }