refactor: 项目重构
This commit is contained in:
@@ -1,74 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { useTagsViewStore } from "@/store/modules/tagsView";
|
||||
|
||||
const tagsViewStore = useTagsViewStore();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="app-main">
|
||||
<router-view>
|
||||
<template #default="{ Component, route }">
|
||||
<transition name="fade-slide" mode="out-in">
|
||||
<keep-alive :include="tagsViewStore.cachedViews">
|
||||
<component :is="Component" :key="route.fullPath" />
|
||||
</keep-alive>
|
||||
</transition>
|
||||
</template>
|
||||
</router-view>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.app-main {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
/* 50= navbar 50 */
|
||||
min-height: calc(100vh - 50px);
|
||||
overflow: hidden;
|
||||
background-color: var(--el-bg-color-page);
|
||||
}
|
||||
|
||||
.fixed-header + .app-main {
|
||||
padding-top: 34px;
|
||||
}
|
||||
|
||||
.hasTagsView {
|
||||
.app-main {
|
||||
/* 84 = navbar + tags-view = 50 + 34 */
|
||||
min-height: calc(100vh - 84px);
|
||||
}
|
||||
|
||||
.fixed-header + .app-main {
|
||||
min-height: 100vh;
|
||||
padding-top: 84px;
|
||||
}
|
||||
}
|
||||
|
||||
.isMix {
|
||||
.app-main {
|
||||
height: calc(100vh - 50px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.hasTagsView {
|
||||
.app-main {
|
||||
/* 84 = navbar + tags-view = 50 + 34 */
|
||||
height: calc(100vh - 84px);
|
||||
}
|
||||
|
||||
.fixed-header + .app-main {
|
||||
min-height: calc(100vh - 50px);
|
||||
padding-top: 34px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.isTop {
|
||||
.hasTagsView {
|
||||
.fixed-header + .app-main {
|
||||
padding-top: 34px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
73
src/layout/components/AppMain/index.vue
Normal file
73
src/layout/components/AppMain/index.vue
Normal file
@@ -0,0 +1,73 @@
|
||||
<template>
|
||||
<section class="app-main">
|
||||
<router-view>
|
||||
<template #default="{ Component, route }">
|
||||
<transition name="fade-translate" mode="out-in">
|
||||
<keep-alive :include="cachedViews">
|
||||
<component :is="Component" :key="route.path" />
|
||||
</keep-alive>
|
||||
</transition>
|
||||
</template>
|
||||
</router-view>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useTagsViewStore } from "@/store";
|
||||
const tagsViewStore = useTagsViewStore();
|
||||
|
||||
const cachedViews = computed(() => tagsViewStore.cachedViews); // 缓存页面集合
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.app-main {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
min-height: calc(100vh - $navbar-height);
|
||||
overflow: hidden;
|
||||
background-color: var(--el-bg-color-page);
|
||||
}
|
||||
|
||||
.fixed-header + .app-main {
|
||||
min-height: 100vh;
|
||||
padding-top: $navbar-height;
|
||||
}
|
||||
|
||||
.hasTagsView {
|
||||
.app-main {
|
||||
/* 84 = navbar + tags-view = 50 + 34 */
|
||||
min-height: calc(100vh - $navbar-height - $tags-view-height);
|
||||
}
|
||||
|
||||
.fixed-header + .app-main {
|
||||
min-height: 100vh;
|
||||
padding-top: $navbar-height + $tags-view-height;
|
||||
}
|
||||
}
|
||||
|
||||
.layout-mix {
|
||||
.app-main {
|
||||
height: calc(100vh - $navbar-height);
|
||||
min-height: calc(100vh - $navbar-height);
|
||||
padding-top: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.fixed-header + .app-main {
|
||||
min-height: calc(100vh - $navbar-height);
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.hasTagsView {
|
||||
.app-main {
|
||||
height: calc(100vh - $navbar-height - $tags-view-height);
|
||||
min-height: calc(100vh - $navbar-height - $tags-view-height);
|
||||
}
|
||||
|
||||
.fixed-header + .app-main {
|
||||
min-height: calc(100vh - $navbar-height);
|
||||
padding-top: $tags-view-height;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,120 +0,0 @@
|
||||
<template>
|
||||
<!-- 导航栏设置(窄屏隐藏)-->
|
||||
<div v-if="device !== 'mobile'" class="setting-container">
|
||||
<!--全屏 -->
|
||||
<div class="setting-item" @click="toggle">
|
||||
<svg-icon :icon-class="isFullscreen ? 'exit-fullscreen' : 'fullscreen'" />
|
||||
</div>
|
||||
<!-- 布局大小 -->
|
||||
<el-tooltip content="布局大小" effect="dark" placement="bottom">
|
||||
<size-select class="setting-item" />
|
||||
</el-tooltip>
|
||||
</div>
|
||||
|
||||
<!-- 用户头像 -->
|
||||
<el-dropdown trigger="click">
|
||||
<div class="avatar-container">
|
||||
<img :src="userStore.user.avatar + '?imageView2/1/w/80/h/80'" />
|
||||
<i-ep-caret-bottom class="w-3 h-3" />
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<router-link to="/">
|
||||
<el-dropdown-item>{{ $t("navbar.dashboard") }}</el-dropdown-item>
|
||||
</router-link>
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://github.com/youlaitech/vue3-element-admin"
|
||||
>
|
||||
<el-dropdown-item>Github</el-dropdown-item>
|
||||
</a>
|
||||
<a target="_blank" href="https://gitee.com/haoxr">
|
||||
<el-dropdown-item>{{ $t("navbar.gitee") }}</el-dropdown-item>
|
||||
</a>
|
||||
<a target="_blank" href="https://juejin.cn/post/7228990409909108793">
|
||||
<el-dropdown-item>{{ $t("navbar.document") }}</el-dropdown-item>
|
||||
</a>
|
||||
<el-dropdown-item divided @click="logout">
|
||||
{{ $t("navbar.logout") }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from "pinia";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { useAppStore } 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 } = storeToRefs(appStore); // 设备类型:desktop-宽屏设备 || mobile-窄屏设备
|
||||
|
||||
/**
|
||||
* vueUse 全屏
|
||||
*/
|
||||
const { isFullscreen, toggle } = useFullscreen();
|
||||
|
||||
/**
|
||||
* 注销
|
||||
*/
|
||||
function logout() {
|
||||
ElMessageBox.confirm("确定注销并退出系统吗?", "提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
lockScroll: false,
|
||||
})
|
||||
.then(() => {
|
||||
userStore
|
||||
.logout()
|
||||
.then(() => {
|
||||
tagsViewStore.delAllViews();
|
||||
})
|
||||
.then(() => {
|
||||
router.push(`/login?redirect=${route.fullPath}`);
|
||||
});
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.setting-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.setting-item {
|
||||
display: inline-block;
|
||||
width: 30px;
|
||||
height: 50px;
|
||||
line-height: 50px;
|
||||
color: var(--el-text-color-regular);
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: var(--el-disabled-bg-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-container {
|
||||
display: flex;
|
||||
place-items: center center;
|
||||
margin: 0 5px;
|
||||
cursor: pointer;
|
||||
|
||||
img {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
19
src/layout/components/NavBar/components/NavbarLeft.vue
Normal file
19
src/layout/components/NavBar/components/NavbarLeft.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<template>
|
||||
<div class="flex">
|
||||
<hamburger
|
||||
:is-active="appStore.sidebar.opened"
|
||||
@toggle-click="toggleSideBar"
|
||||
/>
|
||||
<breadcrumb />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useAppStore } from "@/store";
|
||||
|
||||
const appStore = useAppStore();
|
||||
|
||||
function toggleSideBar() {
|
||||
appStore.toggleSidebar();
|
||||
}
|
||||
</script>
|
||||
117
src/layout/components/NavBar/components/NavbarRight.vue
Normal file
117
src/layout/components/NavBar/components/NavbarRight.vue
Normal file
@@ -0,0 +1,117 @@
|
||||
<template>
|
||||
<div class="flex">
|
||||
<div v-if="device !== 'mobile'" class="flex-center">
|
||||
<!--全屏 -->
|
||||
<div class="navbar-item" @click="toggle">
|
||||
<svg-icon
|
||||
:icon-class="isFullscreen ? 'exit-fullscreen' : 'fullscreen'"
|
||||
/>
|
||||
</div>
|
||||
<!-- 布局大小 -->
|
||||
<el-tooltip content="布局大小" effect="dark" placement="bottom">
|
||||
<size-select class="navbar-item" />
|
||||
</el-tooltip>
|
||||
</div>
|
||||
|
||||
<!-- 用户头像 -->
|
||||
<el-dropdown trigger="click">
|
||||
<div class="flex-center ml-1">
|
||||
<img
|
||||
:src="userStore.user.avatar + '?imageView2/1/w/80/h/80'"
|
||||
width="40px"
|
||||
height="40px"
|
||||
class="rounded-md cursor-pointer"
|
||||
/>
|
||||
|
||||
<el-icon class="cursor-pointer">
|
||||
<CaretBottom />
|
||||
</el-icon>
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<router-link to="/">
|
||||
<el-dropdown-item>{{ $t("navbar.dashboard") }}</el-dropdown-item>
|
||||
</router-link>
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://github.com/youlaitech/vue3-element-admin"
|
||||
>
|
||||
<el-dropdown-item>Github</el-dropdown-item>
|
||||
</a>
|
||||
<a target="_blank" href="https://gitee.com/haoxr">
|
||||
<el-dropdown-item>{{ $t("navbar.gitee") }}</el-dropdown-item>
|
||||
</a>
|
||||
<a target="_blank" href="https://juejin.cn/post/7228990409909108793">
|
||||
<el-dropdown-item>{{ $t("navbar.document") }}</el-dropdown-item>
|
||||
</a>
|
||||
<el-dropdown-item divided @click="logout">
|
||||
{{ $t("navbar.logout") }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { useAppStore, useTagsViewStore, useUserStore } from "@/store";
|
||||
|
||||
const appStore = useAppStore();
|
||||
const tagsViewStore = useTagsViewStore();
|
||||
const userStore = useUserStore();
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
// 设备类型:desktop-宽屏设备 || mobile-窄屏设备
|
||||
const device = computed(() => appStore.device);
|
||||
|
||||
const { isFullscreen, toggle } = useFullscreen();
|
||||
|
||||
/**
|
||||
* 注销
|
||||
*/
|
||||
function logout() {
|
||||
ElMessageBox.confirm("确定注销并退出系统吗?", "提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
lockScroll: false,
|
||||
}).then(() => {
|
||||
userStore
|
||||
.logout()
|
||||
.then(() => {
|
||||
tagsViewStore.delAllViews();
|
||||
})
|
||||
.then(() => {
|
||||
router.push(`/login?redirect=${route.fullPath}`);
|
||||
});
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.navbar-item {
|
||||
display: inline-block;
|
||||
width: 30px;
|
||||
height: $navbar-height;
|
||||
line-height: $navbar-height;
|
||||
color: var(--el-text-color);
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: rgb(0 0 0 / 10%);
|
||||
}
|
||||
}
|
||||
|
||||
.layout-top,
|
||||
.layout-mix {
|
||||
.navbar-item,
|
||||
.el-icon {
|
||||
color: var(--el-color-white);
|
||||
}
|
||||
}
|
||||
|
||||
.dark .navbar-item:hover {
|
||||
background: rgb(255 255 255 / 20%);
|
||||
}
|
||||
</style>
|
||||
@@ -1,42 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import { useAppStore } from "@/store/modules/app";
|
||||
|
||||
const appStore = useAppStore();
|
||||
|
||||
/**
|
||||
* 左侧菜单栏显示/隐藏
|
||||
*/
|
||||
function toggleSideBar() {
|
||||
appStore.toggleSidebar();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 顶部导航栏 -->
|
||||
<div class="navbar">
|
||||
<!-- 左侧面包屑 -->
|
||||
<div class="flex">
|
||||
<hamburger
|
||||
:is-active="appStore.sidebar.opened"
|
||||
@toggle-click="toggleSideBar"
|
||||
/>
|
||||
<breadcrumb />
|
||||
</div>
|
||||
|
||||
<!-- 右侧导航设置 -->
|
||||
<div class="flex">
|
||||
<NavRight />
|
||||
</div>
|
||||
<div class="navbar-container">
|
||||
<!-- 导航栏左侧 -->
|
||||
<NavbarLeft />
|
||||
<!-- 导航栏右侧 -->
|
||||
<NavbarRight />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.navbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 50px;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 0 1px #0003;
|
||||
.navbar-container {
|
||||
@apply flex-x-between;
|
||||
|
||||
height: $navbar-height;
|
||||
background: var(--el-bg-color);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
<template>
|
||||
<div class="settings-container">
|
||||
<div class="setting-container">
|
||||
<h3 class="text-base font-bold">项目配置</h3>
|
||||
<el-divider>主题设置</el-divider>
|
||||
|
||||
<div class="flex-center">
|
||||
<el-switch
|
||||
v-model="isDark"
|
||||
:active-icon="IconEpMoon"
|
||||
:inactive-icon="IconEpSunny"
|
||||
:active-icon="Moon"
|
||||
:inactive-icon="Sunny"
|
||||
@change="handleThemeChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<el-divider>界面设置</el-divider>
|
||||
<div class="py-[8px] flex justify-between">
|
||||
<div class="py-[8px] flex-x-between">
|
||||
<el-text>开启 Tags-View</el-text>
|
||||
<el-switch v-model="settingsStore.tagsView" />
|
||||
</div>
|
||||
@@ -34,7 +34,7 @@
|
||||
<li
|
||||
v-for="(color, index) in themeColors"
|
||||
:key="index"
|
||||
class="inline-block w-[30px] h-[30px] cursor-pointer theme-wrap"
|
||||
class="w-[30px] h-[30px] cursor-pointer flex-center color-white"
|
||||
:style="{ background: color }"
|
||||
@click="changeThemeColor(color)"
|
||||
>
|
||||
@@ -86,12 +86,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useSettingsStore } from "@/store/modules/settings";
|
||||
import { usePermissionStore } from "@/store/modules/permission";
|
||||
import { useAppStore } from "@/store/modules/app";
|
||||
import { useRoute } from "vue-router";
|
||||
import IconEpSunny from "~icons/ep/sunny";
|
||||
import IconEpMoon from "~icons/ep/moon";
|
||||
import { useSettingsStore, usePermissionStore, useAppStore } from "@/store";
|
||||
import { Sunny, Moon } from "@element-plus/icons-vue";
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
@@ -129,7 +125,7 @@ function findOutermostParent(tree: any[], findName: string) {
|
||||
const againActiveTop = (newVal: string) => {
|
||||
const parent = findOutermostParent(permissionStore.routes, newVal);
|
||||
if (appStore.activeTopMenu !== parent.path) {
|
||||
appStore.changeTopActive(parent.path);
|
||||
appStore.activeTopMenu(parent.path);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -138,7 +134,6 @@ const againActiveTop = (newVal: string) => {
|
||||
*/
|
||||
function changeLayout(layout: string) {
|
||||
settingsStore.changeSetting({ key: "layout", value: layout });
|
||||
window.document.body.setAttribute("layout", settingsStore.layout);
|
||||
if (layout === "mix") {
|
||||
route.name && againActiveTop(route.name as string);
|
||||
}
|
||||
@@ -193,7 +188,7 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.settings-container {
|
||||
.setting-container {
|
||||
padding: 16px;
|
||||
|
||||
.layout {
|
||||
@@ -257,12 +252,5 @@ onMounted(() => {
|
||||
box-shadow: 0 0 1px #888;
|
||||
}
|
||||
}
|
||||
|
||||
.theme-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { isExternal } from "@/utils/index";
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
import { 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 === "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></slot>
|
||||
</a>
|
||||
<div v-else @click="push">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,57 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import defaultSettings from "@/settings";
|
||||
import { useSettingsStore } from "@/store/modules/settings";
|
||||
|
||||
const settingsStore = useSettingsStore();
|
||||
|
||||
defineProps({
|
||||
collapse: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const logo = ref(new URL(`../../../assets/logo.png`, import.meta.url).href);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="w-full h-[50px] bg-gray-800 dark:bg-[var(--el-bg-color-overlay)] logo-wrap"
|
||||
>
|
||||
<transition name="sidebarLogoFade">
|
||||
<router-link
|
||||
v-if="collapse"
|
||||
key="collapse"
|
||||
class="h-full w-full flex items-center justify-center"
|
||||
to="/"
|
||||
>
|
||||
<img v-if="settingsStore.sidebarLogo" :src="logo" class="w-5 h-5" />
|
||||
</router-link>
|
||||
|
||||
<router-link
|
||||
v-else
|
||||
key="expand"
|
||||
class="h-full w-full flex items-center justify-center"
|
||||
to="/"
|
||||
>
|
||||
<img v-if="settingsStore.sidebarLogo" :src="logo" class="w-5 h-5" />
|
||||
<span class="ml-3 text-white text-sm font-bold">
|
||||
{{ defaultSettings.title }}</span
|
||||
>
|
||||
</router-link>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
// https://cn.vuejs.org/guide/built-ins/transition.html#the-transition-component
|
||||
.sidebarLogoFade-enter-active {
|
||||
transition: opacity 2s;
|
||||
}
|
||||
|
||||
.sidebarLogoFade-leave-active,
|
||||
.sidebarLogoFade-enter-from,
|
||||
.sidebarLogoFade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -1,71 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { usePermissionStore } from "@/store/modules/permission";
|
||||
import variables from "@/styles/variables.module.scss";
|
||||
import { useAppStore } from "@/store/modules/app";
|
||||
import { translateRouteTitle } from "@/utils/i18n";
|
||||
import { useRouter } from "vue-router";
|
||||
const appStore = useAppStore();
|
||||
const activePath = computed(() => appStore.activeTopMenu);
|
||||
const router = useRouter();
|
||||
// 递归跳转
|
||||
const goFirst = (menu: any[]) => {
|
||||
if (!menu.length) return;
|
||||
const [first] = menu;
|
||||
if (first.children) {
|
||||
goFirst(first.children);
|
||||
} else {
|
||||
router.push({
|
||||
name: first.name,
|
||||
});
|
||||
}
|
||||
};
|
||||
const selectMenu = (index: string) => {
|
||||
appStore.changeTopActive(index);
|
||||
permissionStore.getMixLeftMenu(index);
|
||||
const { mixLeftMenu } = permissionStore;
|
||||
goFirst(mixLeftMenu);
|
||||
};
|
||||
const permissionStore = usePermissionStore();
|
||||
const topMenu = ref<any[]>([]);
|
||||
onMounted(() => {
|
||||
topMenu.value = permissionStore.routes.filter(
|
||||
(item) => !item.meta || !item.meta.hidden
|
||||
);
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<el-scrollbar>
|
||||
<el-menu
|
||||
mode="horizontal"
|
||||
:default-active="activePath"
|
||||
:background-color="variables.menuBg"
|
||||
:text-color="variables.menuText"
|
||||
:active-text-color="variables.menuActiveText"
|
||||
@select="selectMenu"
|
||||
>
|
||||
<el-menu-item
|
||||
v-for="route in topMenu"
|
||||
:key="route.path"
|
||||
:index="route.path"
|
||||
>
|
||||
<template #title>
|
||||
<svg-icon
|
||||
v-if="route.meta && route.meta.icon"
|
||||
:icon-class="route.meta.icon"
|
||||
/>
|
||||
<span v-if="route.path === '/'"> 首页 </span>
|
||||
<template v-else>
|
||||
<span v-if="route.meta && route.meta.title">
|
||||
{{ translateRouteTitle(route.meta.title) }}
|
||||
</span>
|
||||
</template>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
</el-scrollbar>
|
||||
</template>
|
||||
<style lang="scss" scoped>
|
||||
.el-menu {
|
||||
height: 50px !important;
|
||||
}
|
||||
</style>
|
||||
61
src/layout/components/Sidebar/components/SidebarLogo.vue
Normal file
61
src/layout/components/Sidebar/components/SidebarLogo.vue
Normal file
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<div class="logo-container">
|
||||
<transition name="logo-transition">
|
||||
<router-link v-if="collapse" class="wh-full flex-center" to="/">
|
||||
<img v-if="settingsStore.sidebarLogo" :src="logo" class="logo-image" />
|
||||
</router-link>
|
||||
|
||||
<router-link v-else class="wh-full flex-center" to="/">
|
||||
<img v-if="settingsStore.sidebarLogo" :src="logo" class="logo-image" />
|
||||
<span class="logo-title"> {{ defaultSettings.title }}</span>
|
||||
</router-link>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import defaultSettings from "@/settings";
|
||||
import { useSettingsStore } from "@/store";
|
||||
|
||||
const settingsStore = useSettingsStore();
|
||||
|
||||
defineProps({
|
||||
collapse: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const logo = ref(new URL(`../../../../assets/logo.png`, import.meta.url).href);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.logo-container {
|
||||
width: 100%;
|
||||
height: $navbar-height;
|
||||
background-color: $sidebar-logo-background;
|
||||
|
||||
.logo-image {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.logo-title {
|
||||
margin-left: 10px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.layout-top,
|
||||
.layout-mix {
|
||||
.logo-container {
|
||||
width: $sidebar-width;
|
||||
}
|
||||
}
|
||||
|
||||
.mobile .logo-container {
|
||||
width: $sidebar-width-collapsed;
|
||||
}
|
||||
</style>
|
||||
@@ -1,12 +1,30 @@
|
||||
<script lang="ts" setup>
|
||||
import { useRoute } from "vue-router";
|
||||
import SidebarItem from "./SidebarItem.vue";
|
||||
import { useSettingsStore } from "@/store/modules/settings";
|
||||
import { useAppStore } from "@/store/modules/app";
|
||||
import variables from "@/styles/variables.module.scss";
|
||||
<!-- 侧边菜单:包括左侧布局(all)、顶部布局(all)、混合布局(left) -->
|
||||
<template>
|
||||
<el-menu
|
||||
:default-active="currRoute.path"
|
||||
:collapse="!appStore.sidebar.opened"
|
||||
:background-color="variables['menu-background']"
|
||||
:text-color="variables['menu-text']"
|
||||
:active-text-color="variables['menu-active-text']"
|
||||
:unique-opened="false"
|
||||
:collapse-transition="false"
|
||||
:mode="layout === 'top' ? 'horizontal' : 'vertical'"
|
||||
>
|
||||
<SidebarMenuItem
|
||||
v-for="route in menuList"
|
||||
:key="route.path"
|
||||
:item="route"
|
||||
:base-path="resolvePath(route.path)"
|
||||
:is-collapse="!appStore.sidebar.opened"
|
||||
/>
|
||||
</el-menu>
|
||||
</template>
|
||||
|
||||
import path from "path-browserify";
|
||||
<script lang="ts" setup>
|
||||
import { useSettingsStore, useAppStore } from "@/store";
|
||||
import { isExternal } from "@/utils/index";
|
||||
import path from "path-browserify";
|
||||
import variables from "@/styles/variables.module.scss";
|
||||
|
||||
const settingsStore = useSettingsStore();
|
||||
const appStore = useAppStore();
|
||||
@@ -44,23 +62,3 @@ function resolvePath(routePath: string) {
|
||||
return fullPath;
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<el-menu
|
||||
:default-active="currRoute.path"
|
||||
:collapse="!appStore.sidebar.opened"
|
||||
:background-color="variables.menuBg"
|
||||
:text-color="variables.menuText"
|
||||
:active-text-color="variables.menuActiveText"
|
||||
:unique-opened="false"
|
||||
:collapse-transition="false"
|
||||
:mode="layout === 'top' ? 'horizontal' : 'vertical'"
|
||||
>
|
||||
<sidebar-item
|
||||
v-for="route in menuList"
|
||||
:key="route.path"
|
||||
:item="route"
|
||||
:base-path="resolvePath(route.path)"
|
||||
:is-collapse="!appStore.sidebar.opened"
|
||||
/>
|
||||
</el-menu>
|
||||
</template>
|
||||
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<component :is="type" v-bind="linkProps(to)">
|
||||
<slot></slot>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineOptions({
|
||||
name: "AppLink",
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
import { isExternal } from "@/utils/index";
|
||||
|
||||
const props = defineProps({
|
||||
to: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const isExternalLink = computed(() => isExternal(props.to));
|
||||
|
||||
const type = computed(() => {
|
||||
return isExternalLink.value ? "a" : "router-link";
|
||||
});
|
||||
|
||||
const linkProps = (to: string) => {
|
||||
if (isExternalLink.value) {
|
||||
return {
|
||||
href: to,
|
||||
target: "_blank",
|
||||
rel: "noopener noreferrer",
|
||||
};
|
||||
}
|
||||
return {
|
||||
to: to,
|
||||
};
|
||||
};
|
||||
</script>
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<el-icon v-if="icon && icon.includes('el-icon')" class="sub-el-icon" />
|
||||
<SvgIcon v-else-if="icon" :icon-class="icon" />
|
||||
<span v-if="title">{{ translateRouteTitle(title) }}</span>
|
||||
<span v-if="title" class="ml-1">{{ translateRouteTitle(title) }}</span>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -19,10 +19,19 @@ defineProps({
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
<style lang="scss" scoped>
|
||||
.sub-el-icon {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
color: currentcolor;
|
||||
}
|
||||
|
||||
.hideSidebar {
|
||||
.el-sub-menu,
|
||||
.el-menu-item {
|
||||
.svg-icon {
|
||||
margin-left: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,11 +1,56 @@
|
||||
<template>
|
||||
<div v-if="!item.meta || !item.meta.hidden">
|
||||
<!-- 显示具有单个子路由的菜单项或没有子路由的父路由 -->
|
||||
<template
|
||||
v-if="
|
||||
hasOneShowingChild(item.children, item as RouteRecordRaw) &&
|
||||
(!onlyOneChild.children || onlyOneChild.noShowingChildren) &&
|
||||
!item.meta?.alwaysShow
|
||||
"
|
||||
>
|
||||
<AppLink v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path)">
|
||||
<el-menu-item
|
||||
:index="resolvePath(onlyOneChild.path)"
|
||||
:class="{ 'submenu-title-noDropdown': !isNest }"
|
||||
>
|
||||
<MenuIconTitle
|
||||
:icon="onlyOneChild.meta.icon || (item.meta && item.meta.icon)"
|
||||
:title="onlyOneChild.meta.title"
|
||||
/>
|
||||
</el-menu-item>
|
||||
</AppLink>
|
||||
</template>
|
||||
|
||||
<!-- 显示具有多个子路由的父菜单项 -->
|
||||
<el-sub-menu v-else :index="resolvePath(item.path)" teleported>
|
||||
<template #title>
|
||||
<MenuIconTitle
|
||||
v-if="item.meta"
|
||||
:icon="item.meta && item.meta.icon"
|
||||
:title="item.meta.title"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<SidebarMenuItem
|
||||
v-for="child in item.children"
|
||||
:key="child.path"
|
||||
:is-nest="true"
|
||||
:item="child"
|
||||
:base-path="resolvePath(child.path)"
|
||||
/>
|
||||
</el-sub-menu>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
defineOptions({
|
||||
name: "SidebarMenuItem",
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
import path from "path-browserify";
|
||||
import { isExternal } from "@/utils/index";
|
||||
import AppLink from "./Link.vue";
|
||||
import { RouteRecordRaw } from "vue-router";
|
||||
|
||||
import Item from "./Item.vue";
|
||||
|
||||
const props = defineProps({
|
||||
/**
|
||||
* 路由(eg:user)
|
||||
@@ -87,52 +132,25 @@ function resolvePath(routePath: string) {
|
||||
return fullPath;
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div v-if="!item.meta || !item.meta.hidden">
|
||||
<!-- 无子路由 || 目录只有一个子路由并配置始终显示为否(alwaysShow=false) -->
|
||||
<template
|
||||
v-if="
|
||||
hasOneShowingChild(item.children, item as RouteRecordRaw) &&
|
||||
(!onlyOneChild.children || onlyOneChild.noShowingChildren) &&
|
||||
!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 }"
|
||||
>
|
||||
<item
|
||||
:icon="onlyOneChild.meta.icon || (item.meta && item.meta.icon)"
|
||||
:title="onlyOneChild.meta.title"
|
||||
/>
|
||||
</el-menu-item>
|
||||
</app-link>
|
||||
</template>
|
||||
|
||||
<!-- 有子路由 -->
|
||||
<el-sub-menu v-else :index="resolvePath(item.path)" teleported>
|
||||
<template #title>
|
||||
<item
|
||||
v-if="item.meta"
|
||||
:icon="item.meta && item.meta.icon"
|
||||
:title="item.meta.title"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<sidebar-item
|
||||
v-for="child in item.children"
|
||||
:key="child.path"
|
||||
:is-nest="true"
|
||||
:item="child"
|
||||
:base-path="resolvePath(child.path)"
|
||||
/>
|
||||
</el-sub-menu>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-menu-item .el-menu-tooltip__trigger) {
|
||||
width: auto !important;
|
||||
.submenu-title-noDropdown {
|
||||
position: relative;
|
||||
|
||||
.el-tooltip {
|
||||
padding: 0 !important;
|
||||
|
||||
.sub-el-icon {
|
||||
margin-left: 19px;
|
||||
}
|
||||
}
|
||||
|
||||
& > span {
|
||||
display: inline-block;
|
||||
width: 0;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,83 @@
|
||||
<!-- 混合布局菜单(top) -->
|
||||
<template>
|
||||
<el-scrollbar>
|
||||
<el-menu
|
||||
mode="horizontal"
|
||||
:default-active="activePath"
|
||||
:background-color="variables['menu-background']"
|
||||
:text-color="variables['menu-text']"
|
||||
:active-text-color="variables['menu-active-text']"
|
||||
@select="handleMenuSelect"
|
||||
>
|
||||
<el-menu-item
|
||||
v-for="route in mixTopMenus"
|
||||
:key="route.path"
|
||||
:index="route.path"
|
||||
>
|
||||
<template #title>
|
||||
<svg-icon
|
||||
v-if="route.meta && route.meta.icon"
|
||||
:icon-class="route.meta.icon"
|
||||
/>
|
||||
<span v-if="route.path === '/'"> 首页 </span>
|
||||
<template v-else>
|
||||
<span v-if="route.meta && route.meta.title" class="ml-1">
|
||||
{{ translateRouteTitle(route.meta.title) }}
|
||||
</span>
|
||||
</template>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
</el-scrollbar>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { RouteRecordRaw } from "vue-router";
|
||||
import { usePermissionStore, useAppStore } from "@/store";
|
||||
import { translateRouteTitle } from "@/utils/i18n";
|
||||
import variables from "@/styles/variables.module.scss";
|
||||
|
||||
const appStore = useAppStore();
|
||||
const permissionStore = usePermissionStore();
|
||||
const router = useRouter();
|
||||
|
||||
const activePath = computed(() => appStore.activeTopMenu);
|
||||
|
||||
// 顶部菜单集合
|
||||
const mixTopMenus = ref<RouteRecordRaw[]>([]);
|
||||
|
||||
/**
|
||||
* 菜单选择事件
|
||||
*/
|
||||
const handleMenuSelect = (selectedRoutePath: string) => {
|
||||
appStore.activeTopMenu(selectedRoutePath);
|
||||
permissionStore.setMixLeftMenus(selectedRoutePath);
|
||||
// 获取左侧菜单集合,并跳转到第一个菜单
|
||||
const mixLeftMenus = permissionStore.mixLeftMenus;
|
||||
goToFirstMenu(mixLeftMenus);
|
||||
};
|
||||
|
||||
/**
|
||||
* 默认跳转到左侧第一个菜单
|
||||
*/
|
||||
const goToFirstMenu = (menus: RouteRecordRaw[]) => {
|
||||
if (menus.length === 0) return;
|
||||
|
||||
const [first] = menus;
|
||||
|
||||
if (first.children && first.children.length > 0) {
|
||||
goToFirstMenu(first.children as RouteRecordRaw[]);
|
||||
} else if (first.name) {
|
||||
router.push({
|
||||
name: first.name,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化顶部菜单
|
||||
onMounted(() => {
|
||||
mixTopMenus.value = permissionStore.routes.filter(
|
||||
(item) => !item.meta || !item.meta.hidden
|
||||
);
|
||||
});
|
||||
</script>
|
||||
@@ -1,111 +1,28 @@
|
||||
<template>
|
||||
<div :class="{ 'has-logo': sidebarLogo }">
|
||||
<!--混合布局-->
|
||||
<div class="flex w-full" v-if="layout == 'mix'">
|
||||
<SidebarLogo v-if="sidebarLogo" :collapse="!appStore.sidebar.opened" />
|
||||
<SidebarMixTopMenu class="flex-1" />
|
||||
<NavbarRight />
|
||||
</div>
|
||||
<!--左侧布局 || 顶部布局 -->
|
||||
<template v-else>
|
||||
<SidebarLogo v-if="sidebarLogo" :collapse="!appStore.sidebar.opened" />
|
||||
<el-scrollbar>
|
||||
<SidebarMenu :menu-list="permissionStore.routes" base-path="" />
|
||||
</el-scrollbar>
|
||||
<NavbarRight v-if="layout === 'top'" />
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import TopMenu from "./TopMenu.vue";
|
||||
import LeftMenu from "./LeftMenu.vue";
|
||||
import Logo from "./Logo.vue";
|
||||
import { useSettingsStore } from "@/store/modules/settings";
|
||||
import { usePermissionStore } from "@/store/modules/permission";
|
||||
import { useAppStore } from "@/store/modules/app";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { useSettingsStore, usePermissionStore, useAppStore } from "@/store";
|
||||
|
||||
const settingsStore = useSettingsStore();
|
||||
const permissionStore = usePermissionStore();
|
||||
const appStore = useAppStore();
|
||||
const { sidebarLogo } = storeToRefs(settingsStore);
|
||||
const { sidebarLogo } = settingsStore;
|
||||
const layout = computed(() => settingsStore.layout);
|
||||
const showContent = ref(true);
|
||||
watch(
|
||||
() => layout.value,
|
||||
() => {
|
||||
showContent.value = false;
|
||||
nextTick(() => {
|
||||
showContent.value = true;
|
||||
});
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="{ 'has-logo': sidebarLogo }"
|
||||
class="menu-wrap"
|
||||
v-if="layout !== 'mix'"
|
||||
>
|
||||
<logo v-if="sidebarLogo" :collapse="!appStore.sidebar.opened" />
|
||||
<el-scrollbar v-if="showContent">
|
||||
<LeftMenu :menu-list="permissionStore.routes" base-path="" />
|
||||
</el-scrollbar>
|
||||
<NavRight v-if="layout === 'top'" />
|
||||
</div>
|
||||
<template v-else>
|
||||
<div :class="{ 'has-logo': sidebarLogo }" class="menu-wrap">
|
||||
<div class="header">
|
||||
<logo v-if="sidebarLogo" :collapse="!appStore.sidebar.opened" />
|
||||
<TopMenu />
|
||||
<NavRight />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
<style lang="scss" scoped>
|
||||
:deep(.setting-container) {
|
||||
.setting-item {
|
||||
color: #fff;
|
||||
|
||||
.svg-icon {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.isMix {
|
||||
.menu-wrap {
|
||||
z-index: 99;
|
||||
width: 100% !important;
|
||||
height: 50px;
|
||||
background-color: $menuBg;
|
||||
|
||||
:deep(.header) {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
// 顶部模式全局变量修改
|
||||
--el-menu-item-height: 50px;
|
||||
|
||||
.logo-wrap {
|
||||
width: $sideBarWidth;
|
||||
}
|
||||
|
||||
.el-menu {
|
||||
background-color: $menuBg;
|
||||
|
||||
.el-menu-item {
|
||||
color: $menuText;
|
||||
}
|
||||
}
|
||||
|
||||
.el-scrollbar {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
height: 50px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.left-menu {
|
||||
display: inline-block;
|
||||
width: $sideBarWidth;
|
||||
background-color: $menuBg;
|
||||
|
||||
:deep(.el-menu) {
|
||||
background-color: $menuBg;
|
||||
|
||||
.el-menu-item {
|
||||
color: $menuText;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { useTagsViewStore } from "@/store/modules/tagsView";
|
||||
|
||||
const tagAndTagSpacing = ref(4);
|
||||
const { proxy } = getCurrentInstance() as any;
|
||||
|
||||
const emits = defineEmits(["scroll"]);
|
||||
const emitScroll = () => {
|
||||
emits("scroll");
|
||||
};
|
||||
|
||||
const tagsViewStore = useTagsViewStore();
|
||||
|
||||
const scrollWrapper = computed(
|
||||
() => proxy?.$refs.scrollContainer.$refs.wrapRef
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
scrollWrapper.value.addEventListener("scroll", emitScroll, true);
|
||||
});
|
||||
onBeforeUnmount(() => {
|
||||
scrollWrapper.value.removeEventListener("scroll", emitScroll);
|
||||
});
|
||||
|
||||
function handleScroll(e: WheelEvent) {
|
||||
const eventDelta = (e as any).wheelDelta || -e.deltaY * 40;
|
||||
scrollWrapper.value.scrollLeft =
|
||||
scrollWrapper.value.scrollLeft + eventDelta / 4;
|
||||
}
|
||||
|
||||
function moveToTarget(currentTag: TagView) {
|
||||
const $container = proxy.$refs.scrollContainer.$el;
|
||||
const $containerWidth = $container.offsetWidth;
|
||||
const $scrollWrapper = scrollWrapper.value;
|
||||
|
||||
let firstTag = null;
|
||||
let lastTag = null;
|
||||
|
||||
// find first tag and last tag
|
||||
if (tagsViewStore.visitedViews.length > 0) {
|
||||
firstTag = tagsViewStore.visitedViews[0];
|
||||
lastTag = tagsViewStore.visitedViews[tagsViewStore.visitedViews.length - 1];
|
||||
}
|
||||
|
||||
if (firstTag === currentTag) {
|
||||
$scrollWrapper.scrollLeft = 0;
|
||||
} else if (lastTag === currentTag) {
|
||||
$scrollWrapper.scrollLeft = $scrollWrapper.scrollWidth - $containerWidth;
|
||||
} else {
|
||||
const tagListDom = document.getElementsByClassName("tags-item");
|
||||
const currentIndex = tagsViewStore.visitedViews.findIndex(
|
||||
(item) => item === currentTag
|
||||
);
|
||||
let prevTag = null;
|
||||
let nextTag = null;
|
||||
for (const k in tagListDom) {
|
||||
if (k !== "length" && Object.hasOwnProperty.call(tagListDom, k)) {
|
||||
if (
|
||||
(tagListDom[k] as any).dataset.path ===
|
||||
tagsViewStore.visitedViews[currentIndex - 1].path
|
||||
) {
|
||||
prevTag = tagListDom[k];
|
||||
}
|
||||
if (
|
||||
(tagListDom[k] as any).dataset.path ===
|
||||
tagsViewStore.visitedViews[currentIndex + 1].path
|
||||
) {
|
||||
nextTag = tagListDom[k];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// the tag's offsetLeft after of nextTag
|
||||
const afterNextTagOffsetLeft =
|
||||
(nextTag as any).offsetLeft +
|
||||
(nextTag as any).offsetWidth +
|
||||
tagAndTagSpacing.value;
|
||||
|
||||
// the tag's offsetLeft before of prevTag
|
||||
const beforePrevTagOffsetLeft =
|
||||
(prevTag as any).offsetLeft - tagAndTagSpacing.value;
|
||||
if (afterNextTagOffsetLeft > $scrollWrapper.scrollLeft + $containerWidth) {
|
||||
$scrollWrapper.scrollLeft = afterNextTagOffsetLeft - $containerWidth;
|
||||
} else if (beforePrevTagOffsetLeft < $scrollWrapper.scrollLeft) {
|
||||
$scrollWrapper.scrollLeft = beforePrevTagOffsetLeft;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
moveToTarget,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-scrollbar
|
||||
ref="scrollContainer"
|
||||
class="scroll-container"
|
||||
:vertical="false"
|
||||
@wheel.prevent="handleScroll"
|
||||
>
|
||||
<slot></slot>
|
||||
</el-scrollbar>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.scroll-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
|
||||
.el-scrollbar__bar {
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.el-scrollbar__wrap {
|
||||
height: 49px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,29 +1,32 @@
|
||||
<template>
|
||||
<div class="tags-container">
|
||||
<scroll-pane ref="scrollPaneRef" @scroll="handleScroll">
|
||||
<el-scrollbar
|
||||
class="scroll-container"
|
||||
:vertical="false"
|
||||
@wheel.prevent="handleScroll"
|
||||
>
|
||||
<router-link
|
||||
ref="tagRef"
|
||||
v-for="tag in visitedViews"
|
||||
:key="tag.fullPath"
|
||||
:class="'tags-item ' + (isActive(tag) ? 'active' : '')"
|
||||
:to="{ path: tag.fullPath, query: tag.query }"
|
||||
:to="{ path: tag.path, query: tag.query }"
|
||||
@click.middle="!isAffix(tag) ? closeSelectedTag(tag) : ''"
|
||||
@contextmenu.prevent="openTagMenu(tag, $event)"
|
||||
@contextmenu.prevent="openContentMenu(tag, $event)"
|
||||
>
|
||||
{{ translateRouteTitle(tag.title) }}
|
||||
|
||||
<i-ep-close
|
||||
size="12px"
|
||||
v-if="!isAffix(tag)"
|
||||
@click.prevent.stop="closeSelectedTag(tag)"
|
||||
/>
|
||||
</router-link>
|
||||
</scroll-pane>
|
||||
</el-scrollbar>
|
||||
|
||||
<!-- tag标签操作菜单 -->
|
||||
<ul
|
||||
v-show="tagMenuVisible"
|
||||
class="tag-menu"
|
||||
v-show="contentMenuVisible"
|
||||
class="contextmenu"
|
||||
:style="{ left: left + 'px', top: top + 'px' }"
|
||||
>
|
||||
<li @click="refreshSelectedTag(selectedTag)">
|
||||
@@ -55,18 +58,16 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from "pinia";
|
||||
import { useRoute, useRouter, RouteRecordRaw } from "vue-router";
|
||||
import { resolve } from "path-browserify";
|
||||
|
||||
import { translateRouteTitle } from "@/utils/i18n";
|
||||
|
||||
import { usePermissionStore } from "@/store/modules/permission";
|
||||
import { useTagsViewStore } from "@/store/modules/tagsView";
|
||||
import { useSettingsStore } from "@/store/modules/settings";
|
||||
import { useAppStore } from "@/store/modules/app";
|
||||
|
||||
import ScrollPane from "./ScrollPane.vue";
|
||||
import {
|
||||
usePermissionStore,
|
||||
useTagsViewStore,
|
||||
useSettingsStore,
|
||||
useAppStore,
|
||||
} from "@/store";
|
||||
|
||||
const { proxy } = getCurrentInstance()!;
|
||||
const router = useRouter();
|
||||
@@ -90,7 +91,6 @@ const selectedTag = ref<TagView>({
|
||||
});
|
||||
|
||||
const affixTags = ref<TagView[]>([]);
|
||||
const scrollPaneRef = ref();
|
||||
const left = ref(0);
|
||||
const top = ref(0);
|
||||
|
||||
@@ -105,40 +105,39 @@ watch(
|
||||
}
|
||||
);
|
||||
|
||||
const tagMenuVisible = ref(false); // 标签操作菜单显示状态
|
||||
watch(tagMenuVisible, (value) => {
|
||||
const contentMenuVisible = ref(false); // 右键菜单是否显示
|
||||
watch(contentMenuVisible, (value) => {
|
||||
if (value) {
|
||||
document.body.addEventListener("click", closeTagMenu);
|
||||
document.body.addEventListener("click", closeContentMenu);
|
||||
} else {
|
||||
document.body.removeEventListener("click", closeTagMenu);
|
||||
document.body.removeEventListener("click", closeContentMenu);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 过滤出需要固定的标签
|
||||
*/
|
||||
function filterAffixTags(routes: RouteRecordRaw[], basePath = "/") {
|
||||
const processRoute = (route: RouteRecordRaw) => {
|
||||
const fullPath = resolve(basePath, route.path);
|
||||
|
||||
const tag: TagView = {
|
||||
path: route.path,
|
||||
fullPath,
|
||||
name: String(route.name),
|
||||
title: route.meta?.title || "no-name",
|
||||
affix: route.meta?.affix,
|
||||
keepAlive: route.meta?.keepAlive,
|
||||
};
|
||||
|
||||
if (tag.affix) {
|
||||
tags.push(tag);
|
||||
}
|
||||
|
||||
if (route.children) {
|
||||
route.children.forEach(processRoute);
|
||||
}
|
||||
};
|
||||
|
||||
let tags: TagView[] = [];
|
||||
routes.forEach(processRoute);
|
||||
|
||||
routes.forEach((route: RouteRecordRaw) => {
|
||||
const tagPath = resolve(basePath, route.path);
|
||||
if (route.meta?.affix) {
|
||||
tags.push({
|
||||
path: tagPath,
|
||||
fullPath: tagPath,
|
||||
name: String(route.name),
|
||||
title: route.meta?.title || "no-name",
|
||||
affix: route.meta?.affix,
|
||||
keepAlive: route.meta?.keepAlive,
|
||||
});
|
||||
}
|
||||
if (route.children) {
|
||||
const tempTags = filterAffixTags(route.children, basePath + route.path);
|
||||
if (tempTags.length >= 1) {
|
||||
tags = [...tags, ...tempTags];
|
||||
}
|
||||
}
|
||||
});
|
||||
return tags;
|
||||
}
|
||||
|
||||
@@ -171,7 +170,6 @@ function moveToCurrentTag() {
|
||||
nextTick(() => {
|
||||
for (const tag of visitedViews.value) {
|
||||
if (tag.path === route.path) {
|
||||
scrollPaneRef.value.moveToTarget(tag);
|
||||
// when query is different then update
|
||||
// route.query = { ...route.query, ...tag.query };
|
||||
if (tag.fullPath !== route.fullPath) {
|
||||
@@ -190,7 +188,7 @@ function moveToCurrentTag() {
|
||||
}
|
||||
|
||||
function isActive(tag: TagView) {
|
||||
return tag.fullPath === route.fullPath;
|
||||
return tag.path === route.path;
|
||||
}
|
||||
|
||||
function isAffix(tag: TagView) {
|
||||
@@ -200,7 +198,7 @@ function isAffix(tag: TagView) {
|
||||
function isFirstView() {
|
||||
try {
|
||||
return (
|
||||
selectedTag.value.fullPath === "/dashboard" ||
|
||||
selectedTag.value.path === "/dashboard" ||
|
||||
selectedTag.value.fullPath === tagsViewStore.visitedViews[1].fullPath
|
||||
);
|
||||
} catch (err) {
|
||||
@@ -253,18 +251,14 @@ function closeSelectedTag(view: TagView) {
|
||||
|
||||
function closeLeftTags() {
|
||||
tagsViewStore.delLeftViews(selectedTag.value).then((res: any) => {
|
||||
if (
|
||||
!res.visitedViews.find((item: any) => item.fullPath === route.fullPath)
|
||||
) {
|
||||
if (!res.visitedViews.find((item: any) => item.path === route.path)) {
|
||||
toLastView(res.visitedViews);
|
||||
}
|
||||
});
|
||||
}
|
||||
function closeRightTags() {
|
||||
tagsViewStore.delRightViews(selectedTag.value).then((res: any) => {
|
||||
if (
|
||||
!res.visitedViews.find((item: any) => item.fullPath === route.fullPath)
|
||||
) {
|
||||
if (!res.visitedViews.find((item: any) => item.path === route.path)) {
|
||||
toLastView(res.visitedViews);
|
||||
}
|
||||
});
|
||||
@@ -283,7 +277,10 @@ function closeAllTags(view: TagView) {
|
||||
});
|
||||
}
|
||||
|
||||
function openTagMenu(tag: TagView, e: MouseEvent) {
|
||||
/**
|
||||
* 打开右键菜单
|
||||
*/
|
||||
function openContentMenu(tag: TagView, e: MouseEvent) {
|
||||
const menuMinWidth = 105;
|
||||
|
||||
const offsetLeft = proxy?.$el.getBoundingClientRect().left; // container margin left
|
||||
@@ -304,17 +301,24 @@ function openTagMenu(tag: TagView, e: MouseEvent) {
|
||||
top.value = e.clientY;
|
||||
}
|
||||
|
||||
tagMenuVisible.value = true;
|
||||
contentMenuVisible.value = true;
|
||||
selectedTag.value = tag;
|
||||
}
|
||||
|
||||
function closeTagMenu() {
|
||||
tagMenuVisible.value = false;
|
||||
/**
|
||||
* 关闭右键菜单
|
||||
*/
|
||||
function closeContentMenu() {
|
||||
contentMenuVisible.value = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 滚动事件
|
||||
*/
|
||||
function handleScroll() {
|
||||
closeTagMenu();
|
||||
closeContentMenu();
|
||||
}
|
||||
|
||||
function findOutermostParent(tree: any[], findName: string) {
|
||||
let parentMap: any = {};
|
||||
|
||||
@@ -342,11 +346,12 @@ function findOutermostParent(tree: any[], findName: string) {
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const againActiveTop = (newVal: string) => {
|
||||
if (layout.value !== "mix") return;
|
||||
const parent = findOutermostParent(permissionStore.routes, newVal);
|
||||
if (appStore.activeTopMenu !== parent.path) {
|
||||
appStore.changeTopActive(parent.path);
|
||||
appStore.activeTopMenu(parent.path);
|
||||
}
|
||||
};
|
||||
// 如果是混合模式,更改selectedTag,需要对应高亮的activeTop
|
||||
@@ -412,7 +417,7 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
|
||||
.tag-menu {
|
||||
.contextmenu {
|
||||
position: absolute;
|
||||
z-index: 99;
|
||||
font-size: 12px;
|
||||
@@ -429,4 +434,19 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.scroll-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
|
||||
.el-scrollbar__bar {
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.el-scrollbar__wrap {
|
||||
height: 49px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
export { default as Navbar } from "./NavBar/index.vue";
|
||||
export { default as AppMain } from "./AppMain.vue";
|
||||
export { default as Settings } from "./Settings/index.vue";
|
||||
export { default as TagsView } from "./TagsView/index.vue";
|
||||
@@ -1,42 +1,79 @@
|
||||
<script setup lang="ts">
|
||||
import Main from "./main.vue";
|
||||
import { computed, watchEffect } from "vue";
|
||||
import { useWindowSize } from "@vueuse/core";
|
||||
import Sidebar from "./components/Sidebar/index.vue";
|
||||
import LeftMenu from "./components/Sidebar/LeftMenu.vue";
|
||||
<template>
|
||||
<div class="wh-full" :class="classObj">
|
||||
<!-- 遮罩层 -->
|
||||
<div
|
||||
v-if="classObj.mobile && classObj.openSidebar"
|
||||
class="fixed z-1000 bg-black bg-opacity-20"
|
||||
@click="handleOutsideClick"
|
||||
></div>
|
||||
|
||||
import { useAppStore } from "@/store/modules/app";
|
||||
import { useSettingsStore } from "@/store/modules/settings";
|
||||
import { usePermissionStore } from "@/store/modules/permission";
|
||||
<Sidebar class="sidebar-container" />
|
||||
|
||||
<!-- 混合布局 -->
|
||||
<div v-if="layout === 'mix'" class="mix-container">
|
||||
<div class="mix-container__left">
|
||||
<SidebarMenu :menu-list="mixLeftMenus" :base-path="activeTopMenu" />
|
||||
<div class="sidebar-toggle">
|
||||
<hamburger
|
||||
:is-active="appStore.sidebar.opened"
|
||||
@toggle-click="toggleSidebar"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div :class="{ hasTagsView: showTagsView }" class="main-container">
|
||||
<div :class="{ 'fixed-header': fixedHeader }">
|
||||
<TagsView v-if="showTagsView" />
|
||||
</div>
|
||||
<AppMain />
|
||||
<RightPanel v-if="showSettings">
|
||||
<Settings />
|
||||
</RightPanel>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 左侧布局|| 顶部布局 -->
|
||||
<div v-else :class="{ hasTagsView: showTagsView }" class="main-container">
|
||||
<div :class="{ 'fixed-header': fixedHeader }">
|
||||
<Navbar v-if="layout === 'left'" />
|
||||
<TagsView v-if="showTagsView" />
|
||||
</div>
|
||||
<AppMain />
|
||||
<RightPanel v-if="showSettings">
|
||||
<Settings />
|
||||
</RightPanel>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useAppStore, useSettingsStore, usePermissionStore } from "@/store";
|
||||
const permissionStore = usePermissionStore();
|
||||
const { width } = useWindowSize();
|
||||
/**
|
||||
* 响应式布局容器固定宽度
|
||||
*
|
||||
* 大屏(>=1200px)
|
||||
* 中屏(>=992px)
|
||||
* 小屏(>=768px)
|
||||
*/
|
||||
const WIDTH = 992;
|
||||
|
||||
const WIDTH = 992; // 响应式布局容器固定宽度 大屏(>=1200px) 中屏(>=992px) 小屏(>=768px)
|
||||
|
||||
const appStore = useAppStore();
|
||||
const settingsStore = useSettingsStore();
|
||||
|
||||
const fixedHeader = computed(() => settingsStore.fixedHeader);
|
||||
const showTagsView = computed(() => settingsStore.tagsView);
|
||||
const showSettings = computed(() => settingsStore.showSettings);
|
||||
const layout = computed(() => settingsStore.layout);
|
||||
|
||||
const activeTopMenu = computed(() => {
|
||||
return appStore.activeTopMenu;
|
||||
});
|
||||
// 混合模式左侧菜单
|
||||
const mixLeftMenu = computed(() => {
|
||||
return permissionStore.mixLeftMenu;
|
||||
const mixLeftMenus = computed(() => {
|
||||
return permissionStore.mixLeftMenus;
|
||||
});
|
||||
const layout = computed(() => settingsStore.layout);
|
||||
const watermarkEnabled = computed(() => settingsStore.watermark.enabled);
|
||||
|
||||
watch(
|
||||
() => activeTopMenu.value,
|
||||
(newVal) => {
|
||||
if (layout.value !== "mix") return;
|
||||
permissionStore.getMixLeftMenu(newVal);
|
||||
permissionStore.setMixLeftMenus(newVal);
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
@@ -49,8 +86,8 @@ const classObj = computed(() => ({
|
||||
openSidebar: appStore.sidebar.opened,
|
||||
withoutAnimation: appStore.sidebar.withoutAnimation,
|
||||
mobile: appStore.device === "mobile",
|
||||
isTop: layout.value === "top",
|
||||
isMix: layout.value === "mix",
|
||||
"layout-top": layout.value === "top",
|
||||
"layout-mix": layout.value === "mix",
|
||||
}));
|
||||
|
||||
watchEffect(() => {
|
||||
@@ -61,7 +98,6 @@ watchEffect(() => {
|
||||
appStore.toggleDevice("desktop");
|
||||
|
||||
if (width.value >= 1200) {
|
||||
//大屏
|
||||
appStore.openSideBar(true);
|
||||
} else {
|
||||
appStore.closeSideBar(true);
|
||||
@@ -73,123 +109,117 @@ function handleOutsideClick() {
|
||||
appStore.closeSideBar(false);
|
||||
}
|
||||
|
||||
function toggleSideBar() {
|
||||
function toggleSidebar() {
|
||||
appStore.toggleSidebar();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="classObj" class="app-wrapper">
|
||||
<!-- 手机设备侧边栏打开遮罩层 -->
|
||||
<div
|
||||
v-if="classObj.mobile && classObj.openSidebar"
|
||||
class="drawer__background"
|
||||
@click="handleOutsideClick"
|
||||
></div>
|
||||
|
||||
<Sidebar class="sidebar-container" />
|
||||
|
||||
<div v-if="layout === 'mix'" class="mix-wrapper">
|
||||
<div class="mix-wrapper__left">
|
||||
<LeftMenu :menu-list="mixLeftMenu" :base-path="activeTopMenu" />
|
||||
<!-- 展开/收缩侧边栏菜单 -->
|
||||
<div class="toggle-sidebar">
|
||||
<hamburger
|
||||
:is-active="appStore.sidebar.opened"
|
||||
@toggle-click="toggleSideBar"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Main />
|
||||
</div>
|
||||
|
||||
<Main v-else />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.app-wrapper {
|
||||
&::after {
|
||||
display: table;
|
||||
clear: both;
|
||||
content: "";
|
||||
}
|
||||
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
&.mobile.openSidebar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.drawer__background {
|
||||
position: absolute;
|
||||
.sidebar-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 999;
|
||||
width: 100%;
|
||||
width: $sidebar-width;
|
||||
height: 100%;
|
||||
background: #000;
|
||||
opacity: 0.3;
|
||||
overflow: hidden;
|
||||
background-color: $menu-background;
|
||||
transition: width 0.28s;
|
||||
|
||||
:deep(.el-menu) {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
// 导航栏顶部显示
|
||||
.isTop {
|
||||
|
||||
.fixed-header {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 9;
|
||||
width: calc(100% - $sidebar-width);
|
||||
transition: width 0.28s;
|
||||
}
|
||||
|
||||
.main-container {
|
||||
position: relative;
|
||||
min-height: 100%;
|
||||
margin-left: $sidebar-width;
|
||||
transition: margin-left 0.28s;
|
||||
}
|
||||
|
||||
.layout-top {
|
||||
.fixed-header {
|
||||
top: $navbar-height;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sidebar-container {
|
||||
z-index: 800;
|
||||
z-index: 999;
|
||||
display: flex;
|
||||
width: 100% !important;
|
||||
height: 50px;
|
||||
|
||||
:deep(.logo-wrap) {
|
||||
width: $sideBarWidth;
|
||||
}
|
||||
height: $navbar-height;
|
||||
|
||||
:deep(.el-scrollbar) {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
height: 50px;
|
||||
height: $navbar-height;
|
||||
}
|
||||
|
||||
:deep(.el-menu-item),
|
||||
:deep(.el-sub-menu__title) {
|
||||
height: $navbar-height;
|
||||
line-height: $navbar-height;
|
||||
}
|
||||
}
|
||||
|
||||
.main-container {
|
||||
padding-top: 50px;
|
||||
margin-left: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
// 顶部模式全局变量修改
|
||||
--el-menu-item-height: 50px;
|
||||
}
|
||||
|
||||
.mobile.isTop {
|
||||
:deep(.logo-wrap) {
|
||||
width: 63px;
|
||||
}
|
||||
}
|
||||
|
||||
.isMix {
|
||||
:deep(.main-container) {
|
||||
display: inline-block;
|
||||
width: calc(100% - #{$sideBarWidth});
|
||||
margin-left: 0;
|
||||
.layout-mix {
|
||||
.sidebar-container {
|
||||
width: 100% !important;
|
||||
height: $navbar-height;
|
||||
|
||||
:deep(.el-scrollbar) {
|
||||
flex: 1;
|
||||
height: $navbar-height;
|
||||
}
|
||||
|
||||
:deep(.el-menu-item),
|
||||
:deep(.el-sub-menu__title),
|
||||
:deep(.el-menu--horizontal) {
|
||||
height: $navbar-height;
|
||||
line-height: $navbar-height;
|
||||
}
|
||||
|
||||
:deep(.el-menu--horizontal.el-menu) {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
.mix-wrapper {
|
||||
.fixed-header {
|
||||
top: $navbar-height;
|
||||
width: calc(100% - $sidebar-width);
|
||||
}
|
||||
|
||||
.mix-container {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
padding-top: 50px;
|
||||
padding-top: $navbar-height;
|
||||
|
||||
.mix-wrapper__left {
|
||||
&__left {
|
||||
position: relative;
|
||||
width: $sidebar-width;
|
||||
height: 100%;
|
||||
|
||||
.el-menu {
|
||||
:deep(.el-menu) {
|
||||
height: 100%;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.toggle-sidebar {
|
||||
.sidebar-toggle {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
@@ -201,11 +231,11 @@ function toggleSideBar() {
|
||||
box-shadow: 0 0 6px -2px var(--el-color-primary);
|
||||
|
||||
div:hover {
|
||||
background-color: var(--menuBg);
|
||||
background-color: var(--menu-background);
|
||||
}
|
||||
|
||||
:deep(svg) {
|
||||
color: #409eff !important;
|
||||
color: var(--el-color-primary) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -213,23 +243,47 @@ function toggleSideBar() {
|
||||
.main-container {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.openSidebar {
|
||||
.mix-wrapper {
|
||||
.mix-wrapper__left {
|
||||
width: $sideBarWidth;
|
||||
.hideSidebar {
|
||||
.mix-container__left {
|
||||
width: $sidebar-width-collapsed;
|
||||
}
|
||||
|
||||
.fixed-header {
|
||||
width: calc(100% - $sidebar-width-collapsed);
|
||||
}
|
||||
|
||||
&.mobile {
|
||||
.fixed-header {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.svg-icon) {
|
||||
margin-top: -1px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
.layout-top {
|
||||
.sidebar-container {
|
||||
z-index: 999;
|
||||
display: flex;
|
||||
width: 100% !important;
|
||||
height: $navbar-height;
|
||||
|
||||
.el-menu {
|
||||
border: none;
|
||||
:deep(.el-scrollbar) {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
height: $navbar-height;
|
||||
}
|
||||
}
|
||||
|
||||
.main-container {
|
||||
padding-top: $navbar-height;
|
||||
margin-left: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
// 顶部模式全局变量修改
|
||||
--el-menu-item-height: $navbar-height;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
<template>
|
||||
<div :class="{ hasTagsView: showTagsView }" class="main-container">
|
||||
<div :class="{ 'fixed-header': fixedHeader, device: device }">
|
||||
<navbar v-if="layout === 'left'" />
|
||||
<tags-view v-if="showTagsView" />
|
||||
</div>
|
||||
<!--主页面-->
|
||||
<app-main />
|
||||
<!-- 设置面板 -->
|
||||
<RightPanel v-if="showSettings">
|
||||
<settings />
|
||||
</RightPanel>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, watchEffect } from "vue";
|
||||
import { useWindowSize } from "@vueuse/core";
|
||||
import { AppMain, Navbar, Settings, TagsView } from "./components/index";
|
||||
import RightPanel from "@/components/RightPanel/index.vue";
|
||||
|
||||
import { 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 layout = computed(() => settingsStore.layout);
|
||||
const device = computed(() => appStore.device);
|
||||
|
||||
watchEffect(() => {
|
||||
if (width.value < WIDTH) {
|
||||
appStore.toggleDevice("mobile");
|
||||
appStore.closeSideBar(true);
|
||||
} else {
|
||||
appStore.toggleDevice("desktop");
|
||||
|
||||
if (width.value >= 1200) {
|
||||
//大屏
|
||||
appStore.openSideBar(true);
|
||||
} else {
|
||||
appStore.closeSideBar(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.fixed-header {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 9;
|
||||
width: calc(100% - #{$sideBarWidth});
|
||||
transition: width 0.28s;
|
||||
}
|
||||
|
||||
.hideSidebar .fixed-header {
|
||||
width: calc(100% - 54px);
|
||||
}
|
||||
|
||||
.hideSidebar.mobile .fixed-header {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
body[layout="top"] .fixed-header {
|
||||
top: 50px;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
body[layout="mix"] .fixed-header {
|
||||
top: 50px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user