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

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>