refactor: 项目重构
This commit is contained in:
parent
cf8a76c203
commit
56f5ac3802
|
|
@ -3,6 +3,7 @@ node_modules
|
|||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
.history
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
|
|
|
|||
|
|
@ -36,7 +36,14 @@ module.exports = {
|
|||
"property-no-unknown": [
|
||||
true,
|
||||
{
|
||||
ignoreProperties: ["menuBg", "menuText", "menuActiveText"],
|
||||
ignoreProperties: [],
|
||||
},
|
||||
],
|
||||
// 允许未知规则
|
||||
"at-rule-no-unknown": [
|
||||
true,
|
||||
{
|
||||
ignoreAtRules: ["apply"],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<el-breadcrumb class="h-[50px] flex items-center">
|
||||
<transition-group name="breadcrumb">
|
||||
<transition-group name="breadcrumb-transition">
|
||||
<el-breadcrumb-item v-for="(item, index) in breadcrumbs" :key="item.path">
|
||||
<span
|
||||
v-if="
|
||||
|
|
|
|||
|
|
@ -1,6 +1,25 @@
|
|||
<script setup lang="ts">
|
||||
import { onBeforeUnmount, onMounted, ref, watch } from "vue";
|
||||
<template>
|
||||
<div ref="rightPanel" :class="{ show: show }">
|
||||
<div class="right-panel-overlay"></div>
|
||||
<div class="right-panel-container">
|
||||
<div
|
||||
class="right-panel-btn"
|
||||
:style="{
|
||||
top: buttonTop + 'px',
|
||||
}"
|
||||
@click="show = !show"
|
||||
>
|
||||
<i-ep-close v-show="show" />
|
||||
<i-ep-setting v-show="!show" />
|
||||
</div>
|
||||
<div>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { addClass, removeClass } from "@/utils/index";
|
||||
|
||||
const show = ref(false);
|
||||
|
|
@ -52,27 +71,6 @@ onBeforeUnmount(() => {
|
|||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="rightPanel" :class="{ show: show }">
|
||||
<div class="right-panel-overlay"></div>
|
||||
<div class="right-panel-container">
|
||||
<div
|
||||
class="right-panel-btn"
|
||||
:style="{
|
||||
top: buttonTop + 'px',
|
||||
}"
|
||||
@click="show = !show"
|
||||
>
|
||||
<i-ep-close v-show="show" />
|
||||
<i-ep-setting v-show="!show" />
|
||||
</div>
|
||||
<div>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.showRightPanel {
|
||||
position: relative;
|
||||
|
|
@ -91,7 +89,7 @@ onBeforeUnmount(() => {
|
|||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 999;
|
||||
z-index: 1000;
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
height: 100vh;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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,
|
||||
let tags: TagView[] = [];
|
||||
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 (tag.affix) {
|
||||
tags.push(tag);
|
||||
});
|
||||
}
|
||||
|
||||
if (route.children) {
|
||||
route.children.forEach(processRoute);
|
||||
const tempTags = filterAffixTags(route.children, basePath + route.path);
|
||||
if (tempTags.length >= 1) {
|
||||
tags = [...tags, ...tempTags];
|
||||
}
|
||||
};
|
||||
|
||||
let tags: TagView[] = [];
|
||||
routes.forEach(processRoute);
|
||||
|
||||
}
|
||||
});
|
||||
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 {
|
||||
.sidebar-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 999;
|
||||
width: $sidebar-width;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background-color: $menu-background;
|
||||
transition: width 0.28s;
|
||||
|
||||
:deep(.el-menu) {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
.drawer__background {
|
||||
position: absolute;
|
||||
.fixed-header {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
z-index: 999;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #000;
|
||||
opacity: 0.3;
|
||||
right: 0;
|
||||
z-index: 9;
|
||||
width: calc(100% - $sidebar-width);
|
||||
transition: width 0.28s;
|
||||
}
|
||||
// 导航栏顶部显示
|
||||
.isTop {
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.mix-wrapper {
|
||||
: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;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
:deep(.svg-icon) {
|
||||
margin-top: -1px;
|
||||
margin-right: 5px;
|
||||
.fixed-header {
|
||||
width: calc(100% - $sidebar-width-collapsed);
|
||||
}
|
||||
|
||||
.el-menu {
|
||||
border: none;
|
||||
&.mobile {
|
||||
.fixed-header {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.layout-top {
|
||||
.sidebar-container {
|
||||
z-index: 999;
|
||||
display: flex;
|
||||
width: 100% !important;
|
||||
height: $navbar-height;
|
||||
|
||||
: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>
|
||||
|
|
@ -6,6 +6,8 @@ import { setupDirective } from "@/directive";
|
|||
|
||||
import "@/permission";
|
||||
|
||||
import * as ElementPlusIconsVue from "@element-plus/icons-vue";
|
||||
|
||||
// 本地SVG图标
|
||||
import "virtual:svg-icons-register";
|
||||
|
||||
|
|
@ -23,4 +25,8 @@ setupDirective(app);
|
|||
// 全局注册 状态管理(store)
|
||||
setupStore(app);
|
||||
|
||||
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||
app.component(key, component);
|
||||
}
|
||||
|
||||
app.use(router).use(i18n).mount("#app");
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ const whiteList = ["/login"];
|
|||
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
NProgress.start();
|
||||
const hasToken = localStorage.getItem("accessToken");
|
||||
const hasToken = localStorage.getItem("token");
|
||||
if (hasToken) {
|
||||
if (to.path === "/login") {
|
||||
// 如果已登录,跳转首页
|
||||
|
|
|
|||
|
|
@ -8,4 +8,9 @@ export function setupStore(app: App<Element>) {
|
|||
app.use(store);
|
||||
}
|
||||
|
||||
export * from "./modules/app";
|
||||
export * from "./modules/permission";
|
||||
export * from "./modules/settings";
|
||||
export * from "./modules/tagsView";
|
||||
export * from "./modules/user";
|
||||
export { store };
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import { defineStore } from "pinia";
|
||||
import { useStorage } from "@vueuse/core";
|
||||
import defaultSettings from "@/settings";
|
||||
|
||||
// 导入 Element Plus 中英文语言包
|
||||
|
|
@ -19,7 +17,7 @@ export const useAppStore = defineStore("app", () => {
|
|||
opened: sidebarStatus.value !== "closed",
|
||||
withoutAnimation: false,
|
||||
});
|
||||
const activeTopMenu = useStorage("activeTop", "");
|
||||
const activeTopMenuPath = useStorage("activeTopMenuPath", "");
|
||||
/**
|
||||
* 根据语言标识读取对应的语言包
|
||||
*/
|
||||
|
|
@ -72,8 +70,8 @@ export const useAppStore = defineStore("app", () => {
|
|||
/**
|
||||
* 混合模式顶部切换
|
||||
*/
|
||||
function changeTopActive(val: string) {
|
||||
activeTopMenu.value = val;
|
||||
function activeTopMenu(val: string) {
|
||||
activeTopMenuPath.value = val;
|
||||
}
|
||||
return {
|
||||
device,
|
||||
|
|
@ -88,6 +86,6 @@ export const useAppStore = defineStore("app", () => {
|
|||
toggleSidebar,
|
||||
closeSideBar,
|
||||
openSideBar,
|
||||
changeTopActive,
|
||||
activeTopMenuPath,
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -100,17 +100,24 @@ export const usePermissionStore = defineStore("permission", () => {
|
|||
}
|
||||
|
||||
/**
|
||||
* 混合模式左侧菜单
|
||||
* 获取与激活的顶部菜单项相关的混合模式左侧菜单集合
|
||||
*/
|
||||
const mixLeftMenu = ref<RouteRecordRaw[]>([]);
|
||||
function getMixLeftMenu(activeTop: string) {
|
||||
routes.value.forEach((item) => {
|
||||
if (item.path === activeTop) {
|
||||
mixLeftMenu.value = item.children || [];
|
||||
const mixLeftMenus = ref<RouteRecordRaw[]>([]);
|
||||
function setMixLeftMenus(activeTopMenu: string) {
|
||||
const matchedItem = routes.value.find(
|
||||
(item) => item.path === activeTopMenu
|
||||
);
|
||||
if (matchedItem && matchedItem.children) {
|
||||
mixLeftMenus.value = matchedItem.children;
|
||||
}
|
||||
});
|
||||
}
|
||||
return { routes, setRoutes, generateRoutes, getMixLeftMenu, mixLeftMenu };
|
||||
return {
|
||||
routes,
|
||||
setRoutes,
|
||||
generateRoutes,
|
||||
mixLeftMenus,
|
||||
setMixLeftMenus,
|
||||
};
|
||||
});
|
||||
|
||||
// 非setup
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import { defineStore } from "pinia";
|
||||
|
||||
export const useTagsViewStore = defineStore("tagsView", () => {
|
||||
const visitedViews = ref<TagView[]>([]);
|
||||
const cachedViews = ref<string[]>([]);
|
||||
|
|
@ -9,7 +7,7 @@ export const useTagsViewStore = defineStore("tagsView", () => {
|
|||
*/
|
||||
function addVisitedView(view: TagView) {
|
||||
// 如果已经存在于已访问的视图列表中,则不再添加
|
||||
if (visitedViews.value.some((v) => v.fullPath === view.fullPath)) {
|
||||
if (visitedViews.value.some((v) => v.path === view.path)) {
|
||||
return;
|
||||
}
|
||||
// 如果视图是固定的(affix),则在已访问的视图列表的开头添加
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import { defineStore } from "pinia";
|
||||
|
||||
import { loginApi, logoutApi } from "@/api/auth";
|
||||
import { getUserInfoApi } from "@/api/user";
|
||||
import { resetRouter } from "@/router";
|
||||
|
|
@ -8,16 +6,12 @@ import { store } from "@/store";
|
|||
import { LoginData } from "@/api/auth/types";
|
||||
import { UserInfo } from "@/api/user/types";
|
||||
|
||||
import { useStorage } from "@vueuse/core";
|
||||
|
||||
export const useUserStore = defineStore("user", () => {
|
||||
const user: UserInfo = {
|
||||
roles: [],
|
||||
perms: [],
|
||||
};
|
||||
|
||||
const token = useStorage("accessToken", "");
|
||||
|
||||
/**
|
||||
* 登录
|
||||
*
|
||||
|
|
@ -29,7 +23,7 @@ export const useUserStore = defineStore("user", () => {
|
|||
loginApi(loginData)
|
||||
.then((response) => {
|
||||
const { tokenType, accessToken } = response.data;
|
||||
token.value = tokenType + " " + accessToken; // Bearer eyJhbGciOiJIUzI1NiJ9.xxx.xxx
|
||||
localStorage.setItem("token", tokenType + " " + accessToken); // Bearer eyJhbGciOiJIUzI1NiJ9.xxx.xxx
|
||||
resolve();
|
||||
})
|
||||
.catch((error) => {
|
||||
|
|
@ -65,7 +59,7 @@ export const useUserStore = defineStore("user", () => {
|
|||
return new Promise<void>((resolve, reject) => {
|
||||
logoutApi()
|
||||
.then(() => {
|
||||
token.value = "";
|
||||
localStorage.setItem("token", "");
|
||||
location.reload(); // 清空路由
|
||||
resolve();
|
||||
})
|
||||
|
|
@ -77,15 +71,15 @@ export const useUserStore = defineStore("user", () => {
|
|||
|
||||
// remove token
|
||||
function resetToken() {
|
||||
console.log("resetToken");
|
||||
return new Promise<void>((resolve) => {
|
||||
token.value = "";
|
||||
localStorage.setItem("token", "");
|
||||
resetRouter();
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
token,
|
||||
user,
|
||||
login,
|
||||
getUserInfo,
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
html.dark {
|
||||
--menuBg: var(--el-bg-color-overlay);
|
||||
--menuText: #fff;
|
||||
--menu-background: var(--el-bg-color-overlay);
|
||||
--menu-text: #fff;
|
||||
--menuActiveText: var(--el-menu-active-color);
|
||||
--menuHover: rgb(0 0 0 / 20%);
|
||||
--subMenuBg: var(--el-menu-bg-color);
|
||||
--sub-menu-background: var(--el-menu-bg-color);
|
||||
--subMenuActiveText: var(--el-menu-active-color);
|
||||
--subMenuHover: rgb(0 0 0 / 20%);
|
||||
|
||||
|
|
@ -38,10 +38,4 @@ html.dark {
|
|||
.right-panel-btn {
|
||||
background-color: var(--el-color-primary-dark);
|
||||
}
|
||||
|
||||
.sidebar-container {
|
||||
.el-menu-item.is-active .svg-icon {
|
||||
fill: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,89 +1,4 @@
|
|||
.app {
|
||||
.main-container {
|
||||
position: relative;
|
||||
min-height: 100%;
|
||||
margin-left: $sideBarWidth;
|
||||
transition: margin-left 0.28s;
|
||||
}
|
||||
|
||||
.sidebar-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 1001;
|
||||
width: $sideBarWidth !important;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background-color: $menuBg;
|
||||
transition: width 0.28s;
|
||||
|
||||
// reset element-ui css
|
||||
.horizontal-collapse-transition {
|
||||
transition: 0s width ease-in-out, 0s padding-left ease-in-out,
|
||||
0s padding-right ease-in-out;
|
||||
}
|
||||
|
||||
.scrollbar-wrapper {
|
||||
overflow-x: hidden !important;
|
||||
}
|
||||
|
||||
.el-scrollbar__bar.is-vertical {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.el-scrollbar {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&.has-logo {
|
||||
.el-scrollbar {
|
||||
height: calc(100% - 50px);
|
||||
}
|
||||
}
|
||||
|
||||
.is-horizontal {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.svg-icon {
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.sub-el-icon {
|
||||
margin-right: 12px;
|
||||
margin-left: -2px;
|
||||
}
|
||||
|
||||
.el-menu {
|
||||
width: 100% !important;
|
||||
height: 100%;
|
||||
border: none;
|
||||
}
|
||||
|
||||
// menu hover
|
||||
.submenu-title-noDropdown,
|
||||
.el-sub-menu__title {
|
||||
&:hover {
|
||||
background-color: $menuHover !important;
|
||||
}
|
||||
}
|
||||
|
||||
.is-active > .el-sub-menu__title {
|
||||
color: $subMenuActiveText !important;
|
||||
}
|
||||
|
||||
& .nest-menu .el-sub-menu > .el-sub-menu__title,
|
||||
& .el-sub-menu .el-menu-item {
|
||||
min-width: $sideBarWidth !important;
|
||||
background-color: $subMenuBg !important;
|
||||
|
||||
&:hover {
|
||||
background-color: $subMenuHover !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.hideSidebar {
|
||||
.mix-wrapper__left {
|
||||
width: 54px;
|
||||
|
|
@ -111,20 +26,11 @@
|
|||
.el-tooltip {
|
||||
padding: 0 !important;
|
||||
|
||||
.svg-icon {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.sub-el-icon {
|
||||
margin-left: 19px;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO
|
||||
& > .svg-icon {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
& > span {
|
||||
display: inline-block;
|
||||
width: 0;
|
||||
|
|
@ -140,10 +46,6 @@
|
|||
& > .el-sub-menu__title {
|
||||
padding: 0 !important;
|
||||
|
||||
.svg-icon {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.sub-el-icon {
|
||||
margin-left: 19px;
|
||||
}
|
||||
|
|
@ -170,7 +72,7 @@
|
|||
}
|
||||
|
||||
.el-menu--collapse .el-menu .el-sub-menu {
|
||||
min-width: $sideBarWidth !important;
|
||||
min-width: $sidebar-width !important;
|
||||
}
|
||||
|
||||
// mobile responsive
|
||||
|
|
@ -180,15 +82,15 @@
|
|||
}
|
||||
|
||||
.sidebar-container {
|
||||
width: $sideBarWidth !important;
|
||||
width: $sidebar-width !important;
|
||||
transition: transform 0.28s;
|
||||
}
|
||||
|
||||
&.hideSidebar:not(.isMix, .isTop) {
|
||||
&.hideSidebar:not(.layout-mix, .layout-top) {
|
||||
.sidebar-container {
|
||||
pointer-events: none;
|
||||
transition-duration: 0.3s;
|
||||
transform: translate3d(-$sideBarWidth, 0, 0);
|
||||
transform: translate3d(-$sidebar-width, 0, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -204,10 +106,6 @@
|
|||
// when menu collapsed
|
||||
.el-menu--vertical {
|
||||
& > .el-menu {
|
||||
.svg-icon {
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.sub-el-icon {
|
||||
margin-right: 12px;
|
||||
margin-left: -2px;
|
||||
|
|
@ -218,7 +116,7 @@
|
|||
.el-menu-item {
|
||||
&:hover {
|
||||
// you can use $subMenuHover
|
||||
background-color: $menuHover !important;
|
||||
background-color: $menu-hover !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,53 +1,29 @@
|
|||
// global transition css
|
||||
/* 简化和统一后的命名示例 */
|
||||
|
||||
/* fade */
|
||||
/* 淡入淡出 */
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.28s;
|
||||
}
|
||||
|
||||
.fade-enter,
|
||||
.fade-leave-active {
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* fade-transform */
|
||||
.fade-transform-leave-active,
|
||||
.fade-transform-enter-active {
|
||||
transition: all 0.5s;
|
||||
/* 平移和淡入淡出 */
|
||||
.fade-translate-enter-active,
|
||||
.fade-translate-leave-active {
|
||||
transition: opacity 0.3s, transform 0.3s;
|
||||
}
|
||||
|
||||
.fade-transform-enter {
|
||||
.fade-translate-enter-from,
|
||||
.fade-translate-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(-30px);
|
||||
}
|
||||
|
||||
.fade-transform-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(30px);
|
||||
}
|
||||
|
||||
/* breadcrumb transition */
|
||||
.breadcrumb-enter-active,
|
||||
.breadcrumb-leave-active {
|
||||
transition: all 0.5s;
|
||||
}
|
||||
|
||||
.breadcrumb-enter,
|
||||
.breadcrumb-leave-active {
|
||||
opacity: 0;
|
||||
transform: translateX(20px);
|
||||
}
|
||||
|
||||
.breadcrumb-move {
|
||||
transition: all 0.5s;
|
||||
}
|
||||
|
||||
.breadcrumb-leave-active {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
/* 缩放过渡 */
|
||||
/* 缩放 */
|
||||
.fade-scale-enter-active,
|
||||
.fade-scale-leave-active {
|
||||
transition: transform 0.3s ease-in-out;
|
||||
|
|
@ -58,22 +34,7 @@
|
|||
transform: scale(0);
|
||||
}
|
||||
|
||||
.fade-slide-leave-active,
|
||||
.fade-slide-enter-active {
|
||||
transition: opacity 0.3s, transform 0.3s;
|
||||
}
|
||||
|
||||
.fade-slide-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateX(-30px);
|
||||
}
|
||||
|
||||
.fade-slide-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(30px);
|
||||
}
|
||||
|
||||
/* 旋转过渡 */
|
||||
/* 旋转 */
|
||||
.fade-rotate-enter-active,
|
||||
.fade-rotate-leave-active {
|
||||
transition: transform 0.3s ease-in-out;
|
||||
|
|
@ -83,3 +44,28 @@
|
|||
.fade-rotate-leave-to {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
/* 面包屑 */
|
||||
.breadcrumb-transition-enter-active,
|
||||
.breadcrumb-transition-leave-active {
|
||||
transition: all 0.5s;
|
||||
}
|
||||
|
||||
.breadcrumb-transition-enter-from,
|
||||
.breadcrumb-transition-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(20px);
|
||||
}
|
||||
|
||||
/* Logo动画 */
|
||||
|
||||
// 进入动画,2s内过渡到完全不透明(opacity默认值为1,不需要显示声明)
|
||||
.logo-transition-enter-active {
|
||||
transition: opacity 2s;
|
||||
}
|
||||
|
||||
// 进入动画开始时和离开动画的样式状态(opacity)
|
||||
.logo-transition-enter-from,
|
||||
.logo-transition-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,13 @@
|
|||
// 导出 variables.module.scss 变量提供给TypeScript使用
|
||||
/* stylelint-disable property-no-unknown */
|
||||
:export {
|
||||
menuBg: $menuBg;
|
||||
menuText: $menuText;
|
||||
menuActiveText: $menuActiveText;
|
||||
sidebar-width: $sidebar-width;
|
||||
navbar-height: $navbar-height;
|
||||
menu-background: $menu-background;
|
||||
menu-text: $menu-text;
|
||||
menu-active-text: $menu-active-text;
|
||||
menu-hover: $menu-hover;
|
||||
sub-menu-background: $sub-menu-background;
|
||||
sub-menu-active-text: $sub-menu-active-text;
|
||||
sub-menu-hover: $sub-menu-hover;
|
||||
}
|
||||
/* stylelint-enable property-no-unknown */
|
||||
|
|
|
|||
|
|
@ -1,27 +1,37 @@
|
|||
// 全局SCSS变量
|
||||
/* 全局SCSS变量 */
|
||||
|
||||
:root {
|
||||
--menuBg: #304156;
|
||||
--menuText: #bfcbd9;
|
||||
--menuActiveText: #409eff;
|
||||
--menuHover: #263445;
|
||||
--subMenuBg: #1f2d3d;
|
||||
--subMenuActiveText: #f4f4f5;
|
||||
--subMenuHover: #001528;
|
||||
|
||||
// wang-editor textarea
|
||||
--w-e-textarea-slight-border-color: var(--el-color-primary);
|
||||
--w-e-textarea-slight-bg-color: rgb(var(--el-color-primary-rgb) 0.1);
|
||||
--w-e-textarea-selected-border-color: var(--el-color-primary);
|
||||
--menu-background: #304156;
|
||||
--menu-text: #bfcbd9;
|
||||
--menu-active-text: #409eff;
|
||||
--menu-hover: #263445;
|
||||
--sub-menu-background: #1f2d3d;
|
||||
--sub-menu-active-text: #f4f4f5;
|
||||
--sub-menu-hover: #001528;
|
||||
--sidebar-logo-background: #2d3748;
|
||||
}
|
||||
|
||||
$menuBg: var(--menuBg);
|
||||
$menuText: var(--menuText);
|
||||
$menuActiveText: var(--menuActiveText);
|
||||
$menuHover: var(--menuHover);
|
||||
html.dark {
|
||||
--menu-background: var(--el-bg-color-overlay);
|
||||
--menu-text: #fff;
|
||||
--menu-active-text: var(--el-menu-active-color);
|
||||
--menu-hover: rgb(0 0 0 / 20%);
|
||||
--sub-menu-background: var(--el-menu-bg-color);
|
||||
--sub-menu-active-text: var(--el-menu-active-color);
|
||||
--sub-menu-hover: rgb(0 0 0 / 20%);
|
||||
--sidebar-logo-background: rgb(0 0 0 / 20%);
|
||||
}
|
||||
|
||||
$subMenuBg: var(--subMenuBg);
|
||||
$subMenuActiveText: var(--subMenuActiveText);
|
||||
$subMenuHover: var(--subMenuHover);
|
||||
$menu-background: var(--menu-background);
|
||||
$menu-text: var(--menu-text);
|
||||
$menu-active-text: var(--menu-active-text);
|
||||
$menu-hover: var(--menu-hover);
|
||||
$sub-menu-background: var(--sub-menu-background);
|
||||
$sub-menu-active-text: var(--sub-menu-active-text);
|
||||
$sub-menu-hover: var(--sub-menu-hover);
|
||||
$sidebar-logo-background: var(--sidebar-logo-background); // 侧边栏 Logo 背景色
|
||||
|
||||
$sideBarWidth: 210px;
|
||||
$sidebar-width: 210px; // 侧边栏宽度
|
||||
$sidebar-width-collapsed: 54px; // 侧边栏收缩宽度
|
||||
$navbar-height: 50px; // 导航栏高度
|
||||
$tags-view-height: 34px; // TagsView 高度
|
||||
|
|
|
|||
|
|
@ -188,6 +188,7 @@ declare global {
|
|||
const useFullscreen: typeof import("@vueuse/core")["useFullscreen"];
|
||||
const useGamepad: typeof import("@vueuse/core")["useGamepad"];
|
||||
const useGeolocation: typeof import("@vueuse/core")["useGeolocation"];
|
||||
const useI18n: typeof import("vue-i18n")["useI18n"];
|
||||
const useIdle: typeof import("@vueuse/core")["useIdle"];
|
||||
const useImage: typeof import("@vueuse/core")["useImage"];
|
||||
const useInfiniteScroll: typeof import("@vueuse/core")["useInfiniteScroll"];
|
||||
|
|
@ -741,6 +742,7 @@ declare module "vue" {
|
|||
readonly useGeolocation: UnwrapRef<
|
||||
typeof import("@vueuse/core")["useGeolocation"]
|
||||
>;
|
||||
readonly useI18n: UnwrapRef<typeof import("vue-i18n")["useI18n"]>;
|
||||
readonly useIdle: UnwrapRef<typeof import("@vueuse/core")["useIdle"]>;
|
||||
readonly useImage: UnwrapRef<typeof import("@vueuse/core")["useImage"]>;
|
||||
readonly useInfiniteScroll: UnwrapRef<
|
||||
|
|
@ -1427,6 +1429,7 @@ declare module "@vue/runtime-core" {
|
|||
readonly useGeolocation: UnwrapRef<
|
||||
typeof import("@vueuse/core")["useGeolocation"]
|
||||
>;
|
||||
readonly useI18n: UnwrapRef<typeof import("vue-i18n")["useI18n"]>;
|
||||
readonly useIdle: UnwrapRef<typeof import("@vueuse/core")["useIdle"]>;
|
||||
readonly useImage: UnwrapRef<typeof import("@vueuse/core")["useImage"]>;
|
||||
readonly useInfiniteScroll: UnwrapRef<
|
||||
|
|
|
|||
|
|
@ -9,9 +9,11 @@ export {};
|
|||
|
||||
declare module "@vue/runtime-core" {
|
||||
export interface GlobalComponents {
|
||||
AppMain: typeof import("./../layout/components/AppMain.vue")["default"];
|
||||
AppLink: typeof import("./../layout/components/Sidebar/components/SidebarMenuItem/components/AppLink.vue")["default"];
|
||||
AppMain: typeof import("./../layout/components/AppMain/index.vue")["default"];
|
||||
BarChart: typeof import("./../views/dashboard/components/BarChart.vue")["default"];
|
||||
Breadcrumb: typeof import("./../components/Breadcrumb/index.vue")["default"];
|
||||
Components: typeof import("./../layout/components/Sidebar/components/SidebarMenuItem/components/index.vue")["default"];
|
||||
DeptTree: typeof import("./../views/system/user/components/dept-tree.vue")["default"];
|
||||
Dictionary: typeof import("./../components/Dictionary/index.vue")["default"];
|
||||
DictItem: typeof import("./../views/system/dict/components/dict-item.vue")["default"];
|
||||
|
|
@ -23,6 +25,7 @@ declare module "@vue/runtime-core" {
|
|||
ElCheckbox: typeof import("element-plus/es")["ElCheckbox"];
|
||||
ElCheckboxGroup: typeof import("element-plus/es")["ElCheckboxGroup"];
|
||||
ElCol: typeof import("element-plus/es")["ElCol"];
|
||||
ElConfigProvider: typeof import("element-plus/es")["ElConfigProvider"];
|
||||
ElDatePicker: typeof import("element-plus/es")["ElDatePicker"];
|
||||
ElDialog: typeof import("element-plus/es")["ElDialog"];
|
||||
ElDivider: typeof import("element-plus/es")["ElDivider"];
|
||||
|
|
@ -48,6 +51,7 @@ declare module "@vue/runtime-core" {
|
|||
ElRow: typeof import("element-plus/es")["ElRow"];
|
||||
ElScrollbar: typeof import("element-plus/es")["ElScrollbar"];
|
||||
ElSelect: typeof import("element-plus/es")["ElSelect"];
|
||||
ElStatistic: typeof import("element-plus/es")["ElStatistic"];
|
||||
ElSubMenu: typeof import("element-plus/es")["ElSubMenu"];
|
||||
ElSwitch: typeof import("element-plus/es")["ElSwitch"];
|
||||
ElTable: typeof import("element-plus/es")["ElTable"];
|
||||
|
|
@ -55,6 +59,7 @@ declare module "@vue/runtime-core" {
|
|||
ElTabPane: typeof import("element-plus/es")["ElTabPane"];
|
||||
ElTabs: typeof import("element-plus/es")["ElTabs"];
|
||||
ElTag: typeof import("element-plus/es")["ElTag"];
|
||||
ElText: typeof import("element-plus/es")["ElText"];
|
||||
ElTooltip: typeof import("element-plus/es")["ElTooltip"];
|
||||
ElTree: typeof import("element-plus/es")["ElTree"];
|
||||
ElTreeSelect: typeof import("element-plus/es")["ElTreeSelect"];
|
||||
|
|
@ -64,9 +69,11 @@ declare module "@vue/runtime-core" {
|
|||
GithubCorner: typeof import("./../components/GithubCorner/index.vue")["default"];
|
||||
Hamburger: typeof import("./../components/Hamburger/index.vue")["default"];
|
||||
IconSelect: typeof import("./../components/IconSelect/index.vue")["default"];
|
||||
IconTitle: typeof import("./../layout/components/Sidebar/components/SidebarMenuItem/components/IconTitle.vue")["default"];
|
||||
IEpArrowDown: typeof import("~icons/ep/arrow-down")["default"];
|
||||
IEpCaretBottom: typeof import("~icons/ep/caret-bottom")["default"];
|
||||
IEpCaretTop: typeof import("~icons/ep/caret-top")["default"];
|
||||
IEpCheck: typeof import("~icons/ep/check")["default"];
|
||||
IEpClose: typeof import("~icons/ep/close")["default"];
|
||||
IEpCollection: typeof import("~icons/ep/collection")["default"];
|
||||
IEpDelete: typeof import("~icons/ep/delete")["default"];
|
||||
|
|
@ -75,6 +82,7 @@ declare module "@vue/runtime-core" {
|
|||
IEpPicture: typeof import("~icons/ep/picture")["default"];
|
||||
IEpPlus: typeof import("~icons/ep/plus")["default"];
|
||||
IEpPosition: typeof import("~icons/ep/position")["default"];
|
||||
IEpQuestionFilled: typeof import("~icons/ep/question-filled")["default"];
|
||||
IEpRefresh: typeof import("~icons/ep/refresh")["default"];
|
||||
IEpRefreshLeft: typeof import("~icons/ep/refresh-left")["default"];
|
||||
IEpSearch: typeof import("~icons/ep/search")["default"];
|
||||
|
|
@ -83,14 +91,25 @@ declare module "@vue/runtime-core" {
|
|||
IEpSortUp: typeof import("~icons/ep/sort-up")["default"];
|
||||
IEpTop: typeof import("~icons/ep/top")["default"];
|
||||
IEpUploadFilled: typeof import("~icons/ep/upload-filled")["default"];
|
||||
Item: typeof import("./../layout/components/Sidebar/Item.vue")["default"];
|
||||
Item: typeof import("./../layout/components/Sidebar/components/Item.vue")["default"];
|
||||
LangSelect: typeof import("./../components/LangSelect/index.vue")["default"];
|
||||
LeftMenu: typeof import("./../layout/components/Sidebar/LeftMenu.vue")["default"];
|
||||
LeftMenu: typeof import("./../layout/components/Sidebar/components/LeftMenu.vue")["default"];
|
||||
Link: typeof import("./../layout/components/Sidebar/Link.vue")["default"];
|
||||
Logo: typeof import("./../layout/components/Sidebar/Logo.vue")["default"];
|
||||
Logo: typeof import("./../layout/components/Sidebar/components/Logo.vue")["default"];
|
||||
Menu: typeof import("./../layout/components/Sidebar/components/Menu/index.vue")["default"];
|
||||
MenuIconTitle: typeof import("./../layout/components/Sidebar/components/SidebarMenuItem/components/MenuIconTitle.vue")["default"];
|
||||
MenuItem: typeof import("./../layout/components/Sidebar/components/MenuItem/index.vue")["default"];
|
||||
MenuItemContent: typeof import("./../layout/components/Sidebar/components/SidebarMenuItem/components/MenuItemContent.vue")["default"];
|
||||
MenuItemLink: typeof import("./../layout/components/Sidebar/components/SidebarMenuItem/components/MenuItemLink.vue")["default"];
|
||||
MenuItemTitle: typeof import("./../layout/components/Sidebar/components/SidebarMenuItem/components/MenuItemTitle.vue")["default"];
|
||||
MenuLink: typeof import("./../components/AppLink/MenuLink.vue")["default"];
|
||||
MenuTitle: typeof import("./../layout/components/Sidebar/components/SidebarMenuItem/components/MenuTitle.vue")["default"];
|
||||
MixLayoutTopMenu: typeof import("./../layout/components/Sidebar/components/MixLayoutTopMenu.vue")["default"];
|
||||
MultiUpload: typeof import("./../components/Upload/MultiUpload.vue")["default"];
|
||||
Navbar: typeof import("./../layout/components/Navbar/index.vue")["default"];
|
||||
NavBar: typeof import("./../layout/components/NavBar/index.vue")["default"];
|
||||
NavRight: typeof import("./../layout/components/NavBar/NavRight.vue")["default"];
|
||||
NavbarLeft: typeof import("./../layout/components/Navbar/components/NavbarLeft.vue")["default"];
|
||||
NavbarRight: typeof import("./../layout/components/Navbar/components/NavbarRight.vue")["default"];
|
||||
Pagination: typeof import("./../components/Pagination/index.vue")["default"];
|
||||
PieChart: typeof import("./../views/dashboard/components/PieChart.vue")["default"];
|
||||
RadarChart: typeof import("./../views/dashboard/components/RadarChart.vue")["default"];
|
||||
|
|
@ -98,16 +117,27 @@ declare module "@vue/runtime-core" {
|
|||
RouterLink: typeof import("vue-router")["RouterLink"];
|
||||
RouterView: typeof import("vue-router")["RouterView"];
|
||||
ScrollPane: typeof import("./../layout/components/TagsView/ScrollPane.vue")["default"];
|
||||
Setting: typeof import("./../layout/components/Setting/index.vue")["default"];
|
||||
Settings: typeof import("./../layout/components/Settings/index.vue")["default"];
|
||||
Sidebar: typeof import("./../layout/components/Sidebar/index.vue")["default"];
|
||||
SidebarItem: typeof import("./../layout/components/Sidebar/SidebarItem.vue")["default"];
|
||||
SidebarItem: typeof import("./../layout/components/Sidebar/components/SidebarItem/index.vue")["default"];
|
||||
SidebarLeft: typeof import("./../layout/components/Sidebar/components/SidebarLeft.vue")["default"];
|
||||
SidebarLeftMenu: typeof import("./../layout/components/Sidebar/components/SidebarLeftMenu.vue")["default"];
|
||||
SidebarLogo: typeof import("./../layout/components/Sidebar/components/SidebarLogo.vue")["default"];
|
||||
SidebarMenu: typeof import("./../layout/components/Sidebar/components/SidebarMenu.vue")["default"];
|
||||
SidebarMenuItem: typeof import("./../layout/components/Sidebar/components/SidebarMenuItem/index.vue")["default"];
|
||||
SidebarMenuItemLink: typeof import("./../layout/components/Sidebar/components/SidebarMenuItem/SidebarMenuItemLink.vue")["default"];
|
||||
SidebarMenuItemTitle: typeof import("./../layout/components/Sidebar/components/SidebarMenuItem/SidebarMenuItemTitle.vue")["default"];
|
||||
SidebarMixTopMenu: typeof import("./../layout/components/Sidebar/components/SidebarMixTopMenu.vue")["default"];
|
||||
SidebarTop: typeof import("./../layout/components/Sidebar/components/SidebarTop.vue")["default"];
|
||||
SidebarTopMenu: typeof import("./../layout/components/Sidebar/components/SidebarTopMenu.vue")["default"];
|
||||
SingleUpload: typeof import("./../components/Upload/SingleUpload.vue")["default"];
|
||||
SizeSelect: typeof import("./../components/SizeSelect/index.vue")["default"];
|
||||
SvgIcon: typeof import("./../components/SvgIcon/index.vue")["default"];
|
||||
SwitchRoles: typeof import("./../views/demo/permission/components/SwitchRoles.vue")["default"];
|
||||
TagInput: typeof import("./../components/TagInput/index.vue")["default"];
|
||||
TagsView: typeof import("./../layout/components/TagsView/index.vue")["default"];
|
||||
TopMenu: typeof import("./../layout/components/Sidebar/TopMenu.vue")["default"];
|
||||
TopMenu: typeof import("./../layout/components/Sidebar/components/TopMenu.vue")["default"];
|
||||
UnfixedThead: typeof import("./../views/demo/table/dynamic-table/components/UnfixedThead.vue")["default"];
|
||||
WangEditor: typeof import("./../components/WangEditor/index.vue")["default"];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,9 +11,9 @@ const service = axios.create({
|
|||
// 请求拦截器
|
||||
service.interceptors.request.use(
|
||||
(config: InternalAxiosRequestConfig) => {
|
||||
const userStore = useUserStoreHook();
|
||||
if (userStore.token) {
|
||||
config.headers.Authorization = userStore.token;
|
||||
const accessToken = localStorage.getItem("token");
|
||||
if (accessToken) {
|
||||
config.headers.Authorization = accessToken;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
|
|
@ -44,6 +44,7 @@ service.interceptors.response.use(
|
|||
if (code === "A0230") {
|
||||
ElMessageBox.confirm("当前页面已失效,请重新登录", "提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
}).then(() => {
|
||||
const userStore = useUserStoreHook();
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@
|
|||
</el-tooltip>
|
||||
|
||||
<!-- 验证码 -->
|
||||
<el-form-item prop="captchaCode" class="flex justify-between">
|
||||
<el-form-item prop="captchaCode" class="flex-x-between">
|
||||
<div class="flex">
|
||||
<span class="p-2">
|
||||
<svg-icon icon-class="captcha" />
|
||||
|
|
@ -85,14 +85,10 @@
|
|||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex justify-end h-full items-center !w-[128px] cursor-pointer"
|
||||
@click="getCaptcha"
|
||||
>
|
||||
<div class="flex-x-end w-[120px] cursor-pointer" @click="getCaptcha">
|
||||
<el-image
|
||||
:src="captchaBase64"
|
||||
height="48px"
|
||||
class="rounded-tr-md rounded-br-md"
|
||||
class="rounded-tr-md rounded-br-md h-[48px]"
|
||||
>
|
||||
<template #error>
|
||||
<el-icon><Picture /></el-icon>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,11 @@ import {
|
|||
export default defineConfig({
|
||||
shortcuts: {
|
||||
"flex-center": "flex justify-center items-center",
|
||||
"flex-x-center": "flex justify-center",
|
||||
"flex-y-center": "flex items-center",
|
||||
"wh-full": "w-full h-full",
|
||||
"flex-x-between": "flex items-center justify-between",
|
||||
"flex-x-end": "flex items-center justify-end",
|
||||
},
|
||||
theme: {
|
||||
colors: {
|
||||
|
|
|
|||
|
|
@ -52,9 +52,9 @@ export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
|
|||
[env.VITE_APP_BASE_API]: {
|
||||
changeOrigin: true,
|
||||
// 线上接口地址
|
||||
target: "http://vapi.youlai.tech",
|
||||
// target: "http://vapi.youlai.tech",
|
||||
// 开发接口地址
|
||||
//target: "http://localhost:8989",
|
||||
target: "http://localhost:8989",
|
||||
rewrite: (path) =>
|
||||
path.replace(new RegExp("^" + env.VITE_APP_BASE_API), ""),
|
||||
},
|
||||
|
|
@ -71,7 +71,7 @@ export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
|
|||
// 自动导入参考: https://github.com/sxzz/element-plus-best-practices/blob/main/vite.config.ts
|
||||
AutoImport({
|
||||
// 自动导入 Vue 相关函数,如:ref, reactive, toRef 等
|
||||
imports: ["vue", "@vueuse/core", "pinia", "vue-router"],
|
||||
imports: ["vue", "@vueuse/core", "pinia", "vue-router", "vue-i18n"],
|
||||
// 自动导入 Element Plus 相关函数,如:ElMessage, ElMessageBox... (带样式)
|
||||
resolvers: [ElementPlusResolver(), IconsResolver({})],
|
||||
eslintrc: {
|
||||
|
|
@ -81,8 +81,8 @@ export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
|
|||
},
|
||||
vueTemplate: true,
|
||||
// 配置文件生成位置(false:关闭自动生成)
|
||||
dts: false,
|
||||
// dts: "src/typings/auto-imports.d.ts",
|
||||
//dts: false,
|
||||
dts: "src/typings/auto-imports.d.ts",
|
||||
}),
|
||||
|
||||
Components({
|
||||
|
|
@ -95,8 +95,8 @@ export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
|
|||
// 指定自定义组件位置(默认:src/components)
|
||||
dirs: ["src/components", "src/**/components"],
|
||||
// 配置文件位置 (false:关闭自动生成)
|
||||
dts: false,
|
||||
// dts: "src/typings/components.d.ts",
|
||||
//dts: false,
|
||||
dts: "src/typings/components.d.ts",
|
||||
}),
|
||||
|
||||
Icons({
|
||||
|
|
@ -172,6 +172,8 @@ export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
|
|||
"@wangeditor/editor",
|
||||
"@wangeditor/editor-for-vue",
|
||||
"vue-i18n",
|
||||
"element-plus/es/components/text/style/css",
|
||||
"path-browserify",
|
||||
],
|
||||
},
|
||||
// 构建配置
|
||||
|
|
|
|||
Loading…
Reference in New Issue