refactor: 项目重构

This commit is contained in:
郝先瑞 2024-02-07 21:33:51 +08:00
parent cf8a76c203
commit 56f5ac3802
44 changed files with 1005 additions and 1257 deletions

1
.gitignore vendored
View File

@ -3,6 +3,7 @@ node_modules
dist
dist-ssr
*.local
.history
# Editor directories and files
.idea

View File

@ -36,7 +36,14 @@ module.exports = {
"property-no-unknown": [
true,
{
ignoreProperties: ["menuBg", "menuText", "menuActiveText"],
ignoreProperties: [],
},
],
// 允许未知规则
"at-rule-no-unknown": [
true,
{
ignoreAtRules: ["apply"],
},
],
},

View File

@ -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="

View File

@ -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;

View File

@ -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>

View File

@ -0,0 +1,73 @@
<template>
<section class="app-main">
<router-view>
<template #default="{ Component, route }">
<transition name="fade-translate" mode="out-in">
<keep-alive :include="cachedViews">
<component :is="Component" :key="route.path" />
</keep-alive>
</transition>
</template>
</router-view>
</section>
</template>
<script setup lang="ts">
import { useTagsViewStore } from "@/store";
const tagsViewStore = useTagsViewStore();
const cachedViews = computed(() => tagsViewStore.cachedViews); //
</script>
<style lang="scss" scoped>
.app-main {
position: relative;
width: 100%;
min-height: calc(100vh - $navbar-height);
overflow: hidden;
background-color: var(--el-bg-color-page);
}
.fixed-header + .app-main {
min-height: 100vh;
padding-top: $navbar-height;
}
.hasTagsView {
.app-main {
/* 84 = navbar + tags-view = 50 + 34 */
min-height: calc(100vh - $navbar-height - $tags-view-height);
}
.fixed-header + .app-main {
min-height: 100vh;
padding-top: $navbar-height + $tags-view-height;
}
}
.layout-mix {
.app-main {
height: calc(100vh - $navbar-height);
min-height: calc(100vh - $navbar-height);
padding-top: 0;
overflow-y: auto;
}
.fixed-header + .app-main {
min-height: calc(100vh - $navbar-height);
padding-top: 0;
}
.hasTagsView {
.app-main {
height: calc(100vh - $navbar-height - $tags-view-height);
min-height: calc(100vh - $navbar-height - $tags-view-height);
}
.fixed-header + .app-main {
min-height: calc(100vh - $navbar-height);
padding-top: $tags-view-height;
}
}
}
</style>

View File

@ -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>

View File

@ -0,0 +1,19 @@
<template>
<div class="flex">
<hamburger
:is-active="appStore.sidebar.opened"
@toggle-click="toggleSideBar"
/>
<breadcrumb />
</div>
</template>
<script setup lang="ts">
import { useAppStore } from "@/store";
const appStore = useAppStore();
function toggleSideBar() {
appStore.toggleSidebar();
}
</script>

View File

@ -0,0 +1,117 @@
<template>
<div class="flex">
<div v-if="device !== 'mobile'" class="flex-center">
<!--全屏 -->
<div class="navbar-item" @click="toggle">
<svg-icon
:icon-class="isFullscreen ? 'exit-fullscreen' : 'fullscreen'"
/>
</div>
<!-- 布局大小 -->
<el-tooltip content="布局大小" effect="dark" placement="bottom">
<size-select class="navbar-item" />
</el-tooltip>
</div>
<!-- 用户头像 -->
<el-dropdown trigger="click">
<div class="flex-center ml-1">
<img
:src="userStore.user.avatar + '?imageView2/1/w/80/h/80'"
width="40px"
height="40px"
class="rounded-md cursor-pointer"
/>
<el-icon class="cursor-pointer">
<CaretBottom />
</el-icon>
</div>
<template #dropdown>
<el-dropdown-menu>
<router-link to="/">
<el-dropdown-item>{{ $t("navbar.dashboard") }}</el-dropdown-item>
</router-link>
<a
target="_blank"
href="https://github.com/youlaitech/vue3-element-admin"
>
<el-dropdown-item>Github</el-dropdown-item>
</a>
<a target="_blank" href="https://gitee.com/haoxr">
<el-dropdown-item>{{ $t("navbar.gitee") }}</el-dropdown-item>
</a>
<a target="_blank" href="https://juejin.cn/post/7228990409909108793">
<el-dropdown-item>{{ $t("navbar.document") }}</el-dropdown-item>
</a>
<el-dropdown-item divided @click="logout">
{{ $t("navbar.logout") }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</template>
<script setup lang="ts">
import { useAppStore, useTagsViewStore, useUserStore } from "@/store";
const appStore = useAppStore();
const tagsViewStore = useTagsViewStore();
const userStore = useUserStore();
const route = useRoute();
const router = useRouter();
// desktop- || mobile-
const device = computed(() => appStore.device);
const { isFullscreen, toggle } = useFullscreen();
/**
* 注销
*/
function logout() {
ElMessageBox.confirm("确定注销并退出系统吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
lockScroll: false,
}).then(() => {
userStore
.logout()
.then(() => {
tagsViewStore.delAllViews();
})
.then(() => {
router.push(`/login?redirect=${route.fullPath}`);
});
});
}
</script>
<style lang="scss" scoped>
.navbar-item {
display: inline-block;
width: 30px;
height: $navbar-height;
line-height: $navbar-height;
color: var(--el-text-color);
text-align: center;
cursor: pointer;
&:hover {
background: rgb(0 0 0 / 10%);
}
}
.layout-top,
.layout-mix {
.navbar-item,
.el-icon {
color: var(--el-color-white);
}
}
.dark .navbar-item:hover {
background: rgb(255 255 255 / 20%);
}
</style>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -0,0 +1,61 @@
<template>
<div class="logo-container">
<transition name="logo-transition">
<router-link v-if="collapse" class="wh-full flex-center" to="/">
<img v-if="settingsStore.sidebarLogo" :src="logo" class="logo-image" />
</router-link>
<router-link v-else class="wh-full flex-center" to="/">
<img v-if="settingsStore.sidebarLogo" :src="logo" class="logo-image" />
<span class="logo-title"> {{ defaultSettings.title }}</span>
</router-link>
</transition>
</div>
</template>
<script lang="ts" setup>
import defaultSettings from "@/settings";
import { useSettingsStore } from "@/store";
const settingsStore = useSettingsStore();
defineProps({
collapse: {
type: Boolean,
required: true,
},
});
const logo = ref(new URL(`../../../../assets/logo.png`, import.meta.url).href);
</script>
<style lang="scss" scoped>
.logo-container {
width: 100%;
height: $navbar-height;
background-color: $sidebar-logo-background;
.logo-image {
width: 20px;
height: 20px;
}
.logo-title {
margin-left: 10px;
font-size: 14px;
font-weight: bold;
color: white;
}
}
.layout-top,
.layout-mix {
.logo-container {
width: $sidebar-width;
}
}
.mobile .logo-container {
width: $sidebar-width-collapsed;
}
</style>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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);
}
};
// selectedTagactiveTop
@ -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>

View File

@ -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";

View File

@ -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;
}
}
.mix-wrapper {
.layout-mix {
.sidebar-container {
width: 100% !important;
height: $navbar-height;
:deep(.el-scrollbar) {
flex: 1;
height: $navbar-height;
}
:deep(.el-menu-item),
:deep(.el-sub-menu__title),
:deep(.el-menu--horizontal) {
height: $navbar-height;
line-height: $navbar-height;
}
:deep(.el-menu--horizontal.el-menu) {
border: none;
}
}
.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;
}
}
}

View File

@ -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>

View File

@ -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");

View File

@ -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") {
// 如果已登录,跳转首页

View File

@ -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 };

View File

@ -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,
};
});

View File

@ -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

View File

@ -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则在已访问的视图列表的开头添加

View File

@ -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,

View File

@ -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);
}
}
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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 */

View File

@ -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 高度

View File

@ -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<

View File

@ -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"];
}

View File

@ -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();

View File

@ -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>

View File

@ -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: {

View File

@ -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",
],
},
// 构建配置