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">
import { computed } from 'vue';
import useStore from '@/store';
import { useI18n } from 'vue-i18n';
import { ElMessage } from 'element-plus';
import SvgIcon from '@/components/SvgIcon/index.vue';
import { useAppStore } from '@/store/modules/app';
const { app } = useStore();
const language = computed(() => app.language);
const appStore = useAppStore();
const { locale } = useI18n();
function handleSetLanguage(lang: string) {
function handleLanguageChange(lang: string) {
locale.value = lang;
app.setLanguage(lang);
appStore.changeLanguage(lang);
if (lang == 'en') {
ElMessage.success('Switch Language Successful!');
} else {
@@ -39,6 +18,31 @@ function handleSetLanguage(lang: string) {
}
</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>
.lang-select__icon {
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">
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { onBeforeUnmount, onMounted, ref, watch } from 'vue';
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 { ElColorPicker } from 'element-plus';
const { setting } = useStore();
const settingsStore = useSettingsStore();
const theme = computed(() => setting.theme);
const show = ref(false);
defineProps({
@@ -87,6 +67,28 @@ onBeforeUnmount(() => {
});
</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>
.showRightPanel {
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>
<el-dropdown class="size-select" trigger="click" @command="handleSetSize">
<div class="size-select__icon">
<svg-icon class-name="size-icon" icon-class="size" />
<el-dropdown trigger="click" @command="handleSizeChange">
<div style="line-height: 50px">
<svg-icon icon-class="size" />
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
v-for="item of sizeOptions"
:key="item.value"
:disabled="(size || 'default') == item.value"
:disabled="appStore.size == item.value"
:command="item.value"
>
{{ item.label }}
@@ -17,31 +38,3 @@
</template>
</el-dropdown>
</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>
<el-color-picker
v-model="theme"
v-model="settingsStore.theme"
:predefine="[
'#409EFF',
'#1890ff',
@@ -17,42 +17,14 @@
</template>
<script setup lang="ts">
import { computed, watch } from 'vue';
import useStore from '@/store';
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);
});
import { useSettingsStore } from '@/store/modules/settings';
const settingsStore = useSettingsStore();
</script>
<style>
.theme-message,
.theme-picker-dropdown {
z-index: 99999 !important;
z-index: 9999 !important;
}
.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';
/**
@@ -7,8 +7,7 @@ import { Directive, DirectiveBinding } from 'vue';
export const hasPerm: Directive = {
mounted(el: HTMLElement, binding: DirectiveBinding) {
// 「超级管理员」拥有所有的按钮权限
const { user } = useStore();
const roles = user.roles;
const { roles, perms } = useUserStoreHook();
if (roles.includes('ROOT')) {
return true;
}
@@ -17,7 +16,7 @@ export const hasPerm: Directive = {
if (value) {
const requiredPerms = value; // DOM绑定需要的按钮权限标识
const hasPerm = user.perms?.some(perm => {
const hasPerm = perms?.some(perm => {
return requiredPerms.includes(perm);
});
@@ -41,8 +40,8 @@ export const hasRole: Directive = {
if (value) {
const requiredRoles = value; // DOM绑定需要的角色编码
const { user } = useStore();
const hasRole = user.roles.some(perm => {
const { roles } = useUserStoreHook();
const hasRole = roles.some(perm => {
return requiredRoles.includes(perm);
});

View File

@@ -1,6 +1,6 @@
// 自定义国际化配置
import { createI18n } from 'vue-i18n';
import { localStorage } from '@/utils/storage';
import { localStorage } from '@/utils/localStorage';
// 本地语言包
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>
<section class="app-main">
<router-view v-slot="{ Component, route }">
<transition name="router-fade" mode="out-in">
<keep-alive :include="cachedViews">
<keep-alive :include="tagsViewStore.cachedViews">
<component :is="Component" :key="route.fullPath" />
</keep-alive>
</transition>
@@ -10,15 +16,6 @@
</section>
</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>
.app-main {
/* 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>
<div class="navbar">
<hamburger
id="hamburger-container"
:is-active="sidebar.opened"
:is-active="appStore.sidebar.opened"
class="hamburger-container"
@toggleClick="toggleSideBar"
/>
@@ -10,9 +57,7 @@
<breadcrumb id="breadcrumb-container" class="breadcrumb-container" />
<div class="right-menu">
<template v-if="device !== 'mobile'">
<!-- <search id="header-search" class="right-menu-item" />
<error-log class="errLog-container right-menu-item hover-effect" />-->
<template v-if="device !== DeviceType.mobile">
<screenfull id="screenfull" class="right-menu-item hover-effect" />
<el-tooltip content="布局大小" effect="dark" placement="bottom">
<size-select id="size-select" class="right-menu-item hover-effect" />
@@ -25,7 +70,7 @@
trigger="click"
>
<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" />
</div>
@@ -52,53 +97,6 @@
</div>
</div>
</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>
ul {
@@ -164,7 +162,7 @@ ul {
margin-top: 5px;
position: relative;
.user-avatar {
img {
cursor: pointer;
width: 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>
<div class="drawer-container">
<h3 class="drawer-title">系统布局配置</h3>
@@ -10,17 +22,17 @@
<div class="drawer-item">
<span>开启 Tags-View</span>
<el-switch v-model="tagsView" class="drawer-switch" />
<el-switch v-model="settingsStore.tagsView" class="drawer-switch" />
</div>
<div class="drawer-item">
<span>固定 Header</span>
<el-switch v-model="fixedHeader" class="drawer-switch" />
<el-switch v-model="settingsStore.fixedHeader" class="drawer-switch" />
</div>
<div class="drawer-item">
<span>侧边栏 Logo</span>
<el-switch v-model="sidebarLogo" class="drawer-switch" />
<el-switch v-model="settingsStore.sidebarLogo" class="drawer-switch" />
</div>
<el-divider>导航栏模式</el-divider>
@@ -48,49 +60,6 @@
</div>
</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>
.drawer-container {
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>
<a v-if="isExternal(to)" :href="to" target="_blank" rel="noopener">
<slot />
@@ -6,40 +35,3 @@
<slot />
</div>
</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>
<div class="sidebar-logo-container" :class="{ collapse: collapse }">
<transition name="sidebarLogoFade">
@@ -18,24 +33,6 @@
</div>
</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>
.sidebarLogoFade-enter-active {
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">
import { ref } from 'vue';
import path from 'path-browserify';
@@ -110,5 +62,52 @@ function resolvePath(routePath: string) {
return path.resolve(props.basePath, routePath);
}
</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>

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>
<div :class="{ 'has-logo': showLogo }">
<logo v-if="showLogo" :collapse="isCollapse" />
<div :class="{ 'has-logo': sidebarLogo }">
<logo v-if="sidebarLogo" :collapse="isCollapse" />
<el-scrollbar>
<el-menu
:default-active="activeMenu"
@@ -13,7 +44,7 @@
mode="vertical"
>
<sidebar-item
v-for="route in routes"
v-for="route in permissionStore.routes"
:item="route"
:key="route.path"
:base-path="route.path"
@@ -23,29 +54,3 @@
</el-scrollbar>
</div>
</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">
import {
ref,
@@ -17,8 +6,7 @@ import {
onBeforeUnmount,
getCurrentInstance
} from 'vue';
import useStore from '@/store';
import { TagView } from '@/store/modules/types';
import { useTagsViewStore, TagView } from '@/store/modules/tagsView';
const tagAndTagSpacing = ref(4);
const { proxy } = getCurrentInstance() as any;
@@ -28,9 +16,7 @@ const emitScroll = () => {
emits('scroll');
};
const { tagsView } = useStore();
const visitedViews = computed(() => tagsView.visitedViews);
const tagsViewStore = useTagsViewStore();
const scrollWrapper = computed(
() => proxy?.$refs.scrollContainer.$refs.wrapRef
@@ -58,9 +44,9 @@ function moveToTarget(currentTag: TagView) {
let lastTag = null;
// find first tag and last tag
if (visitedViews.value.length > 0) {
firstTag = visitedViews.value[0];
lastTag = visitedViews.value[visitedViews.value.length - 1];
if (tagsViewStore.visitedViews.length > 0) {
firstTag = tagsViewStore.visitedViews[0];
lastTag = tagsViewStore.visitedViews[tagsViewStore.visitedViews.length - 1];
}
if (firstTag === currentTag) {
@@ -69,7 +55,7 @@ function moveToTarget(currentTag: TagView) {
$scrollWrapper.scrollLeft = $scrollWrapper.scrollWidth - $containerWidth;
} else {
const tagListDom = document.getElementsByClassName('tags-view__item');
const currentIndex = visitedViews.value.findIndex(
const currentIndex = tagsViewStore.visitedViews.findIndex(
item => item === currentTag
);
let prevTag = null;
@@ -78,13 +64,13 @@ function moveToTarget(currentTag: TagView) {
if (k !== 'length' && Object.hasOwnProperty.call(tagListDom, k)) {
if (
(tagListDom[k] as any).dataset.path ===
visitedViews.value[currentIndex - 1].path
tagsViewStore.visitedViews[currentIndex - 1].path
) {
prevTag = tagListDom[k];
}
if (
(tagListDom[k] as any).dataset.path ===
visitedViews.value[currentIndex + 1].path
tagsViewStore.visitedViews[currentIndex + 1].path
) {
nextTag = tagListDom[k];
}
@@ -113,6 +99,17 @@ defineExpose({
});
</script>
<template>
<el-scrollbar
ref="scrollContainer"
:vertical="false"
class="scroll-container"
@wheel.prevent="handleScroll"
>
<slot />
</el-scrollbar>
</template>
<style lang="scss" scoped>
.scroll-container {
.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">
import {
computed,
getCurrentInstance,
nextTick,
ref,
@@ -76,24 +15,23 @@ import { useRoute, useRouter } from 'vue-router';
import ScrollPane from './ScrollPane.vue';
import SvgIcon from '@/components/SvgIcon/index.vue';
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 router = useRouter();
const route = useRoute();
const visitedViews = computed<any>(() => tagsView.visitedViews);
const routes = computed<any>(() => permission.routes);
const affixTags = ref([]);
const visible = ref(false);
const selectedTag = ref({});
const scrollPaneRef = ref();
const left = ref(0);
const top = ref(0);
const affixTags = ref<TagView[]>([]);
watch(
route,
@@ -140,30 +78,30 @@ function filterAffixTags(routes: any[], basePath = '/') {
}
function initTags() {
const res = filterAffixTags(routes.value) as [];
affixTags.value = res;
for (const tag of res) {
const tags = filterAffixTags(permissionStore.routes);
affixTags.value = tags;
for (const tag of tags) {
// Must have tag name
if ((tag as TagView).name) {
tagsView.addVisitedView(tag);
tagsViewStore.addVisitedView(tag);
}
}
}
function addTags() {
if (route.name) {
tagsView.addView(route);
tagsViewStore.addView(route);
}
}
function moveToCurrentTag() {
nextTick(() => {
for (const r of visitedViews.value) {
for (const r of tagsViewStore.visitedViews) {
if (r.path === route.path) {
scrollPaneRef.value.moveToTarget(r);
// when query is different then update
if (r.fullPath !== route.fullPath) {
tagsView.updateVisitedView(route);
tagsViewStore.updateVisitedView(route);
}
}
}
@@ -182,7 +120,7 @@ function isFirstView() {
try {
return (
(selectedTag.value as TagView).fullPath ===
visitedViews.value[1].fullPath ||
tagsViewStore.visitedViews[1].fullPath ||
(selectedTag.value as TagView).fullPath === '/index'
);
} catch (err) {
@@ -194,7 +132,7 @@ function isLastView() {
try {
return (
(selectedTag.value as TagView).fullPath ===
visitedViews.value[visitedViews.value.length - 1].fullPath
tagsViewStore.visitedViews[tagsViewStore.visitedViews.length - 1].fullPath
);
} catch (err) {
return false;
@@ -202,7 +140,7 @@ function isLastView() {
}
function refreshSelectedTag(view: TagView) {
tagsView.delCachedView(view);
tagsViewStore.delCachedView(view);
const { fullPath } = view;
nextTick(() => {
router.replace({ path: '/redirect' + fullPath }).catch(err => {
@@ -228,7 +166,7 @@ function toLastView(visitedViews: TagView[], view?: any) {
}
function closeSelectedTag(view: TagView) {
tagsView.delView(view).then((res: any) => {
tagsViewStore.delView(view).then((res: any) => {
if (isActive(view)) {
toLastView(res.visitedViews, view);
}
@@ -236,7 +174,7 @@ function closeSelectedTag(view: TagView) {
}
function closeLeftTags() {
tagsView.delLeftViews(selectedTag.value).then((res: any) => {
tagsViewStore.delLeftViews(selectedTag.value).then((res: any) => {
if (
!res.visitedViews.find((item: any) => item.fullPath === route.fullPath)
) {
@@ -245,7 +183,7 @@ function closeLeftTags() {
});
}
function closeRightTags() {
tagsView.delRightViews(selectedTag.value).then((res: any) => {
tagsViewStore.delRightViews(selectedTag.value).then((res: any) => {
if (
!res.visitedViews.find((item: any) => item.fullPath === route.fullPath)
) {
@@ -256,13 +194,13 @@ function closeRightTags() {
function closeOtherTags() {
router.push(selectedTag.value);
tagsView.delOtherViews(selectedTag.value).then(() => {
tagsViewStore.delOtherViews(selectedTag.value).then(() => {
moveToCurrentTag();
});
}
function closeAllTags(view: TagView) {
tagsView.delAllViews().then((res: any) => {
tagsViewStore.delAllViews().then((res: any) => {
toLastView(res.visitedViews, view);
});
}
@@ -298,6 +236,66 @@ onMounted(() => {
});
</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>
.tags-view__container {
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>
<div :class="classObj" class="app-wrapper">
<!-- 手机设备 && 侧边栏 显示遮罩层 -->
<div
v-if="device === 'mobile' && sidebar.opened"
v-if="classObj.mobile && classObj.openSidebar"
class="drawer-bg"
@click="handleClickOutside"
/>
@click="handleOutsideClick"
></div>
<Sidebar class="sidebar-container" />
<div :class="{ hasTagsView: needTagsView }" class="main-container">
<div :class="{ hasTagsView: showTagsView }" class="main-container">
<div :class="{ 'fixed-header': fixedHeader }">
<navbar />
<tags-view v-if="needTagsView" />
<tags-view v-if="showTagsView" />
</div>
<!--主页面-->
@@ -23,47 +80,6 @@
</div>
</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>
@import '@/styles/mixin.scss';
@import '@/styles/variables.module.scss';
@@ -79,7 +95,6 @@ function handleClickOutside() {
top: 0;
}
}
.drawer-bg {
background: #000;
opacity: 0.3;
@@ -98,11 +113,9 @@ function handleClickOutside() {
width: calc(100% - #{$sideBarWidth});
transition: width 0.28s;
}
.hideSidebar .fixed-header {
width: calc(100% - 54px);
}
.mobile .fixed-header {
width: 100%;
}

View File

@@ -1,11 +1,10 @@
import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router';
import useStore from '@/store';
import { usePermissionStoreHook } from '@/store/modules/permission';
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',
component: Layout,
@@ -43,7 +42,7 @@ export const constantRoutes: Array<RouteRecordRaw> = [
path: '401',
component: () => import('@/views/error-page/401.vue'),
meta: { hidden: true }
},
}
]
}
@@ -110,8 +109,8 @@ const router = createRouter({
// 重置路由
export function resetRouter() {
const { permission } = useStore();
permission.routes.forEach(route => {
const permissionStore = usePermissionStoreHook();
permissionStore.routes.forEach(route => {
const name = route.name;
if (name && router.hasRoute(name)) {
router.removeRoute(name);

View File

@@ -1,15 +1,11 @@
import useUserStore from './modules/user';
import useAppStore from './modules/app';
import usePermissionStore from './modules/permission';
import useSettingStore from './modules/settings';
import useTagsViewStore from './modules/tagsView';
import type { App } from 'vue';
import { createPinia } from 'pinia';
const useStore = () => ({
user: useUserStore(),
app: useAppStore(),
permission: usePermissionStore(),
setting: useSettingStore(),
tagsView: useTagsViewStore()
});
const store = createPinia();
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 { localStorage } from '@/utils/storage';
import {
getSidebarStatus,
setSidebarStatus,
getSize,
setSize,
setLanguage
} from '@/utils/localStorage';
import { defineStore } from 'pinia';
import { getLanguage } from '@/lang/index';
import { computed, reactive, ref } from 'vue';
const useAppStore = defineStore({
id: 'app',
state: (): AppState => ({
device: 'desktop',
sidebar: {
opened: localStorage.get('sidebarStatus')
? !!+localStorage.get('sidebarStatus')
: 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);
// Element Plus 语言包
import zhCn from 'element-plus/es/locale/lang/zh-cn';
import en from 'element-plus/es/locale/lang/en';
export enum DeviceType {
mobile,
desktop
}
},
closeSideBar(withoutAnimation: any) {
localStorage.set('sidebarStatus', 0);
this.sidebar.opened = false;
this.sidebar.withoutAnimation = withoutAnimation;
},
toggleDevice(device: string) {
this.device = device;
},
setSize(size: string) {
this.size = size;
localStorage.set('size', size);
},
setLanguage(language: string) {
this.language = language;
localStorage.set('language', language);
},
},
export enum SizeType {
default,
large,
small
}
// setup
export const useAppStore = defineStore('app', () => {
// state
const device = ref<DeviceType>(DeviceType.desktop);
const size = ref(getSize() || 'default');
const language = ref(getLanguage());
const sidebar = reactive({
opened: getSidebarStatus() !== 'closed',
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 { defineStore } from 'pinia';
import { constantRoutes } from '@/router';
import { store } from '@/store';
import { listRoutes } from '@/api/menu';
import { ref } from 'vue';
const modules = import.meta.glob('../../views/**/**.vue');
export const Layout = () => import('@/layout/index.vue');
@@ -21,10 +22,7 @@ const hasPermission = (roles: string[], route: RouteRecordRaw) => {
return false;
};
export const filterAsyncRoutes = (
routes: RouteRecordRaw[],
roles: string[]
) => {
const filterAsyncRoutes = (routes: RouteRecordRaw[], roles: string[]) => {
const res: RouteRecordRaw[] = [];
routes.forEach(route => {
const tmp = { ...route } as any;
@@ -49,24 +47,25 @@ export const filterAsyncRoutes = (
return res;
};
const usePermissionStore = defineStore({
id: 'permission',
state: (): PermissionState => ({
routes: [],
addRoutes: []
}),
actions: {
setRoutes(routes: RouteRecordRaw[]) {
this.addRoutes = routes;
this.routes = constantRoutes.concat(routes);
},
generateRoutes(roles: string[]) {
return new Promise((resolve, reject) => {
// setup
export const usePermissionStore = defineStore('permission', () => {
// state
const routes = ref<RouteRecordRaw[]>([]);
const addRoutes = ref<RouteRecordRaw[]>([]);
// auctions
function setRoutes(newRoutes: RouteRecordRaw[]) {
addRoutes.value = newRoutes;
routes.value = constantRoutes.concat(newRoutes);
}
function generateRoutes(roles: string[]) {
return new Promise<RouteRecordRaw[]>((resolve, reject) => {
listRoutes()
.then(response => {
const asyncRoutes = response.data;
const accessedRoutes = filterAsyncRoutes(asyncRoutes, roles);
this.setRoutes(accessedRoutes);
setRoutes(accessedRoutes);
resolve(accessedRoutes);
})
.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 { SettingState } from './types';
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;
export const useSettingStore = defineStore({
id: 'setting',
state: (): SettingState => ({
theme:
export const useSettingsStore = defineStore('setting', () => {
// state
const theme = ref(
localStorage.get('theme') ||
getComputedStyle(el).getPropertyValue(`--el-color-primary`),
showSettings: showSettings,
tagsView:
localStorage.get('tagsView') != null
? localStorage.get('tagsView')
: tagsView,
fixedHeader: fixedHeader,
sidebarLogo: sidebarLogo,
}),
actions: {
async changeSetting(payload: { key: string; value: any }) {
const { key, value } = payload;
getComputedStyle(el).getPropertyValue(`--el-color-primary`)
);
const showSettings = ref<boolean>(defaultSettings.showSettings);
const tagsView = ref<boolean>(
localStorage.get('tagsView') || defaultSettings.tagsView
);
const fixedHeader = ref<boolean>(defaultSettings.fixedHeader);
const sidebarLogo = ref<boolean>(defaultSettings.sidebarLogo);
// auction
function changeSetting(param: { key: string; value: any }) {
const { key, value } = param;
switch (key) {
case 'theme':
this.theme = value;
theme.value = value;
break;
case 'showSettings':
this.showSettings = value;
showSettings.value = value;
break;
case 'fixedHeader':
this.fixedHeader = value;
break;
case 'tagsView':
this.tagsView = value;
fixedHeader.value = value;
localStorage.set('tagsView', value);
break;
case 'sidebarLogo':
this.sidebarLogo = value;
case 'tagsView':
tagsView.value = value;
break;
case 'sidevarLogo':
sidebarLogo.value = value;
break;
default:
break;
}
},
},
});
}
export default useSettingStore;
return {
theme,
showSettings,
tagsView,
fixedHeader,
sidebarLogo,
changeSetting
};
});

View File

@@ -1,181 +1,214 @@
import { defineStore } from 'pinia';
import { TagsViewState } from './types';
import { ref } from 'vue';
import { RouteLocationNormalized } from 'vue-router';
const useTagsViewStore = defineStore({
id: 'tagsView',
state: (): TagsViewState => ({
visitedViews: [],
cachedViews: [], // keepAlive 缓存页面
}),
actions: {
addVisitedView(view: any) {
if (this.visitedViews.some((v) => v.path === view.path)) return;
export interface TagView extends Partial<RouteLocationNormalized> {
title?: string;
}
// setup
export const useTagsViewStore = defineStore('tagsView', () => {
// state
const visitedViews = ref<TagView[]>([]);
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) {
this.visitedViews.unshift(
visitedViews.value.unshift(
Object.assign({}, view, {
title: view.meta?.title || 'no-name',
title: view.meta?.title || 'no-name'
})
);
} else {
this.visitedViews.push(
visitedViews.value.push(
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) {
return new Promise((resolve) => {
for (const [i, v] of this.visitedViews.entries()) {
function addCachedView(view: TagView) {
const viewName = view.name as string;
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) {
this.visitedViews.splice(i, 1);
visitedViews.value.splice(i, 1);
break;
}
}
resolve([...this.visitedViews]);
resolve([...visitedViews.value]);
});
},
delCachedView(view: any) {
return new Promise((resolve) => {
const index = this.cachedViews.indexOf(view.name);
index > -1 && this.cachedViews.splice(index, 1);
resolve([...this.cachedViews]);
}
function delCachedView(view: TagView) {
const viewName = view.name as string;
return new Promise(resolve => {
const index = cachedViews.value.indexOf(viewName);
index > -1 && cachedViews.value.splice(index, 1);
resolve([...cachedViews.value]);
});
},
delOtherVisitedViews(view: any) {
return new Promise((resolve) => {
this.visitedViews = this.visitedViews.filter((v) => {
}
function delOtherVisitedViews(view: TagView) {
return new Promise(resolve => {
visitedViews.value = visitedViews.value.filter(v => {
return v.meta?.affix || v.path === view.path;
});
resolve([...this.visitedViews]);
resolve([...visitedViews.value]);
});
},
delOtherCachedViews(view: any) {
return new Promise((resolve) => {
const index = this.cachedViews.indexOf(view.name);
}
function delOtherCachedViews(view: TagView) {
const viewName = view.name as string;
return new Promise(resolve => {
const index = cachedViews.value.indexOf(viewName);
if (index > -1) {
this.cachedViews = this.cachedViews.slice(index, index + 1);
cachedViews.value = cachedViews.value.slice(index, index + 1);
} else {
// if index = -1, there is no cached tags
this.cachedViews = [];
cachedViews.value = [];
}
resolve([...this.cachedViews]);
resolve([...cachedViews.value]);
});
},
}
updateVisitedView(view: any) {
for (let v of this.visitedViews) {
function updateVisitedView(view: TagView) {
for (let v of visitedViews.value) {
if (v.path === view.path) {
v = Object.assign(v, view);
break;
}
}
},
addView(view: any) {
this.addVisitedView(view);
this.addCachedView(view);
},
delView(view: any) {
return new Promise((resolve) => {
this.delVisitedView(view);
this.delCachedView(view);
}
function addView(view: TagView) {
addVisitedView(view);
addCachedView(view);
}
function delView(view: TagView) {
return new Promise(resolve => {
delVisitedView(view);
delCachedView(view);
resolve({
visitedViews: [...this.visitedViews],
cachedViews: [...this.cachedViews],
visitedViews: [...visitedViews.value],
cachedViews: [...cachedViews.value]
});
});
},
delOtherViews(view: any) {
return new Promise((resolve) => {
this.delOtherVisitedViews(view);
this.delOtherCachedViews(view);
}
function delOtherViews(view: TagView) {
return new Promise(resolve => {
delOtherVisitedViews(view);
delOtherCachedViews(view);
resolve({
visitedViews: [...this.visitedViews],
cachedViews: [...this.cachedViews],
visitedViews: [...visitedViews.value],
cachedViews: [...cachedViews.value]
});
});
},
delLeftViews(view: any) {
return new Promise((resolve) => {
const currIndex = this.visitedViews.findIndex(
(v) => v.path === view.path
);
}
function delLeftViews(view: TagView) {
return new Promise(resolve => {
const currIndex = visitedViews.value.findIndex(v => v.path === view.path);
if (currIndex === -1) {
return;
}
this.visitedViews = this.visitedViews.filter((item, index) => {
visitedViews.value = visitedViews.value.filter((item, index) => {
// affix:true 固定tag例如“首页”
if (index >= currIndex || (item.meta && item.meta.affix)) {
return true;
}
const cacheIndex = this.cachedViews.indexOf(item.name as string);
const cacheIndex = cachedViews.value.indexOf(item.name as string);
if (cacheIndex > -1) {
this.cachedViews.splice(cacheIndex, 1);
cachedViews.value.splice(cacheIndex, 1);
}
return false;
});
resolve({
visitedViews: [...this.visitedViews],
visitedViews: [...visitedViews.value]
});
});
},
delRightViews(view: any) {
return new Promise((resolve) => {
const currIndex = this.visitedViews.findIndex(
(v) => v.path === view.path
);
}
function delRightViews(view: TagView) {
return new Promise(resolve => {
const currIndex = visitedViews.value.findIndex(v => v.path === view.path);
if (currIndex === -1) {
return;
}
this.visitedViews = this.visitedViews.filter((item, index) => {
visitedViews.value = visitedViews.value.filter((item, index) => {
// affix:true 固定tag例如“首页”
if (index <= currIndex || (item.meta && item.meta.affix)) {
return true;
}
const cacheIndex = this.cachedViews.indexOf(item.name as string);
const cacheIndex = cachedViews.value.indexOf(item.name as string);
if (cacheIndex > -1) {
this.cachedViews.splice(cacheIndex, 1);
cachedViews.value.splice(cacheIndex, 1);
}
return false;
});
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 { UserState } from './types';
import { localStorage } from '@/utils/storage';
import { getToken, setToken, removeToken } from '@/utils/auth';
import { loginApi, logoutApi } from '@/api/auth';
import { getUserInfo } from '@/api/user';
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({
id: 'user',
state: (): UserState => ({
token: localStorage.get('token') || '',
nickname: '',
avatar: '',
roles: [],
perms: []
}),
actions: {
async RESET_STATE() {
this.$reset();
},
/**
* 登录
*/
login(data: LoginForm) {
const { username, password } = data;
return new Promise((resolve, reject) => {
loginApi({
grant_type: 'password',
username: username.trim(),
password: password
})
export const useUserStore = defineStore('user', () => {
// state
const token = ref<string>(getToken() || '');
const nickname = ref<string>('');
const avatar = ref<string>('');
const roles = ref<Array<string>>([]); // 用户角色编码集合 → 判断路由权限
const perms = ref<Array<string>>([]); // 用户权限编码集合 → 判断按钮权限
// auctions
// 登录
function login(loginData: LoginData) {
return new Promise<void>((resolve, reject) => {
loginApi(loginData)
.then(response => {
console.log('response.data', response.data);
const accessToken = response.data;
localStorage.set('token', accessToken);
this.token = accessToken;
resolve(accessToken);
const { accessToken } = response.data;
token.value = accessToken;
setToken(accessToken);
resolve();
})
.catch(error => {
reject(error);
});
});
},
/**
* 获取用户信息(昵称、头像、角色集合、权限集合
*/
getUserInfo() {
return new Promise((resolve, reject) => {
}
// 获取信息(用户昵称、头像、角色集合、权限集合)
function getInfo() {
return new Promise<UserInfo>((resolve, reject) => {
getUserInfo()
.then(({ data }) => {
if (!data) {
return reject('Verification failed, please Login again.');
}
const { nickname, avatar, roles, perms } = data;
if (!roles || roles.length <= 0) {
if (!data.roles || data.roles.length <= 0) {
reject('getUserInfo: roles must be a non-null array!');
}
this.nickname = nickname;
this.avatar = avatar;
this.roles = roles;
this.perms = perms;
nickname.value = data.nickname;
avatar.value = data.avatar;
roles.value = data.roles;
perms.value = data.perms;
resolve(data);
})
.catch(error => {
reject(error);
});
});
},
}
/**
* 注销
*/
logout() {
return new Promise((resolve, reject) => {
// 注销
function logout() {
return new Promise<void>((resolve, reject) => {
logoutApi()
.then(() => {
localStorage.remove('token');
this.RESET_STATE();
resetRouter();
resolve(null);
resetToken();
resolve();
})
.catch(error => {
reject(error);
});
});
},
}
/**
* 清除 Token
*/
resetToken() {
return new Promise(resolve => {
localStorage.remove('token');
this.RESET_STATE();
resolve(null);
});
}
// 重置
function resetToken() {
removeToken();
token.value = '';
nickname.value = '';
avatar.value = '';
roles.value = [];
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">
<el-form
ref="loginFormRef"
:model="loginForm"
:model="loginData"
:rules="loginRules"
class="login-form"
auto-complete="on"
@@ -19,7 +19,7 @@
</span>
<el-input
ref="username"
v-model="loginForm.username"
v-model="loginData.username"
:placeholder="$t('login.username')"
name="username"
type="text"
@@ -40,7 +40,7 @@
<el-input
ref="passwordRef"
:key="passwordType"
v-model="loginForm.password"
v-model="loginData.password"
:type="passwordType"
placeholder="Password"
name="password"
@@ -95,13 +95,13 @@ import LangSelect from '@/components/LangSelect/index.vue';
import SvgIcon from '@/components/SvgIcon/index.vue';
// 状态管理依赖
import useStore from '@/store';
import { useUserStore } from '@/store/modules/user';
// API依赖
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 loginFormRef = ref(ElForm);
@@ -109,10 +109,10 @@ const passwordRef = ref(ElInput);
const state = reactive({
redirect: '',
loginForm: {
loginData: {
username: 'admin',
password: '123456'
} as LoginForm,
} as LoginData,
loginRules: {
username: [{ required: true, trigger: 'blur' }],
password: [{ required: true, trigger: 'blur', validator: validatePassword }]
@@ -136,7 +136,7 @@ function validatePassword(rule: any, value: any, callback: any) {
}
const {
loginForm,
loginData,
loginRules,
loading,
passwordType,
@@ -162,14 +162,14 @@ function showPwd() {
}
/**
* 登录处理
* 登录
*/
function handleLogin() {
loginFormRef.value.validate((valid: boolean) => {
if (valid) {
state.loading = true;
user
.login(state.loginForm)
userStore
.login(state.loginData)
.then(() => {
router.push({ path: state.redirect || '/', query: state.otherQuery });
state.loading = false;

View File

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

View File

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