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
dist-ssr dist-ssr
*.local *.local
.history
# Editor directories and files # Editor directories and files
.idea .idea

View File

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

View File

@ -1,6 +1,6 @@
<template> <template>
<el-breadcrumb class="h-[50px] flex items-center"> <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"> <el-breadcrumb-item v-for="(item, index) in breadcrumbs" :key="item.path">
<span <span
v-if=" v-if="

View File

@ -1,6 +1,25 @@
<script setup lang="ts"> <template>
import { onBeforeUnmount, onMounted, ref, watch } from "vue"; <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"; import { addClass, removeClass } from "@/utils/index";
const show = ref(false); const show = ref(false);
@ -52,27 +71,6 @@ onBeforeUnmount(() => {
}); });
</script> </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> <style lang="scss" scoped>
.showRightPanel { .showRightPanel {
position: relative; position: relative;
@ -91,7 +89,7 @@ onBeforeUnmount(() => {
position: fixed; position: fixed;
top: 0; top: 0;
right: 0; right: 0;
z-index: 999; z-index: 1000;
width: 100%; width: 100%;
max-width: 300px; max-width: 300px;
height: 100vh; 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> <template>
<!-- 顶部导航栏 --> <!-- 顶部导航栏 -->
<div class="navbar"> <div class="navbar-container">
<!-- 左侧面包屑 --> <!-- 导航栏左侧 -->
<div class="flex"> <NavbarLeft />
<hamburger <!-- 导航栏右侧 -->
:is-active="appStore.sidebar.opened" <NavbarRight />
@toggle-click="toggleSideBar"
/>
<breadcrumb />
</div>
<!-- 右侧导航设置 -->
<div class="flex">
<NavRight />
</div>
</div> </div>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
.navbar { .navbar-container {
display: flex; @apply flex-x-between;
align-items: center;
justify-content: space-between; height: $navbar-height;
height: 50px; background: var(--el-bg-color);
background-color: #fff;
box-shadow: 0 0 1px #0003;
} }
</style> </style>

View File

@ -1,19 +1,19 @@
<template> <template>
<div class="settings-container"> <div class="setting-container">
<h3 class="text-base font-bold">项目配置</h3> <h3 class="text-base font-bold">项目配置</h3>
<el-divider>主题设置</el-divider> <el-divider>主题设置</el-divider>
<div class="flex-center"> <div class="flex-center">
<el-switch <el-switch
v-model="isDark" v-model="isDark"
:active-icon="IconEpMoon" :active-icon="Moon"
:inactive-icon="IconEpSunny" :inactive-icon="Sunny"
@change="handleThemeChange" @change="handleThemeChange"
/> />
</div> </div>
<el-divider>界面设置</el-divider> <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-text>开启 Tags-View</el-text>
<el-switch v-model="settingsStore.tagsView" /> <el-switch v-model="settingsStore.tagsView" />
</div> </div>
@ -34,7 +34,7 @@
<li <li
v-for="(color, index) in themeColors" v-for="(color, index) in themeColors"
:key="index" :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 }" :style="{ background: color }"
@click="changeThemeColor(color)" @click="changeThemeColor(color)"
> >
@ -86,12 +86,8 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useSettingsStore } from "@/store/modules/settings"; import { useSettingsStore, usePermissionStore, useAppStore } from "@/store";
import { usePermissionStore } from "@/store/modules/permission"; import { Sunny, Moon } from "@element-plus/icons-vue";
import { useAppStore } from "@/store/modules/app";
import { useRoute } from "vue-router";
import IconEpSunny from "~icons/ep/sunny";
import IconEpMoon from "~icons/ep/moon";
const route = useRoute(); const route = useRoute();
@ -129,7 +125,7 @@ function findOutermostParent(tree: any[], findName: string) {
const againActiveTop = (newVal: string) => { const againActiveTop = (newVal: string) => {
const parent = findOutermostParent(permissionStore.routes, newVal); const parent = findOutermostParent(permissionStore.routes, newVal);
if (appStore.activeTopMenu !== parent.path) { 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) { function changeLayout(layout: string) {
settingsStore.changeSetting({ key: "layout", value: layout }); settingsStore.changeSetting({ key: "layout", value: layout });
window.document.body.setAttribute("layout", settingsStore.layout);
if (layout === "mix") { if (layout === "mix") {
route.name && againActiveTop(route.name as string); route.name && againActiveTop(route.name as string);
} }
@ -193,7 +188,7 @@ onMounted(() => {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.settings-container { .setting-container {
padding: 16px; padding: 16px;
.layout { .layout {
@ -257,12 +252,5 @@ onMounted(() => {
box-shadow: 0 0 1px #888; box-shadow: 0 0 1px #888;
} }
} }
.theme-wrap {
display: flex;
align-items: center;
justify-content: center;
color: #fff;
}
} }
</style> </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> <!-- 侧边菜单包括左侧布局(all)顶部布局(all)混合布局(left) -->
import { useRoute } from "vue-router"; <template>
import SidebarItem from "./SidebarItem.vue"; <el-menu
import { useSettingsStore } from "@/store/modules/settings"; :default-active="currRoute.path"
import { useAppStore } from "@/store/modules/app"; :collapse="!appStore.sidebar.opened"
import variables from "@/styles/variables.module.scss"; :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 { isExternal } from "@/utils/index";
import path from "path-browserify";
import variables from "@/styles/variables.module.scss";
const settingsStore = useSettingsStore(); const settingsStore = useSettingsStore();
const appStore = useAppStore(); const appStore = useAppStore();
@ -44,23 +62,3 @@ function resolvePath(routePath: string) {
return fullPath; return fullPath;
} }
</script> </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> <template>
<el-icon v-if="icon && icon.includes('el-icon')" class="sub-el-icon" /> <el-icon v-if="icon && icon.includes('el-icon')" class="sub-el-icon" />
<SvgIcon v-else-if="icon" :icon-class="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> </template>
<script setup lang="ts"> <script setup lang="ts">
@ -19,10 +19,19 @@ defineProps({
}); });
</script> </script>
<style scoped> <style lang="scss" scoped>
.sub-el-icon { .sub-el-icon {
width: 1em; width: 1em;
height: 1em; height: 1em;
color: currentcolor; color: currentcolor;
} }
.hideSidebar {
.el-sub-menu,
.el-menu-item {
.svg-icon {
margin-left: 20px;
}
}
}
</style> </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"> <script setup lang="ts">
defineOptions({
name: "SidebarMenuItem",
inheritAttrs: false,
});
import path from "path-browserify"; import path from "path-browserify";
import { isExternal } from "@/utils/index"; import { isExternal } from "@/utils/index";
import AppLink from "./Link.vue";
import { RouteRecordRaw } from "vue-router"; import { RouteRecordRaw } from "vue-router";
import Item from "./Item.vue";
const props = defineProps({ const props = defineProps({
/** /**
* 路由(eg:user) * 路由(eg:user)
@ -87,52 +132,25 @@ function resolvePath(routePath: string) {
return fullPath; return fullPath;
} }
</script> </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> <style lang="scss" scoped>
:deep(.el-menu-item .el-menu-tooltip__trigger) { .submenu-title-noDropdown {
width: auto !important; 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> </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"> <script setup lang="ts">
import TopMenu from "./TopMenu.vue"; import { useSettingsStore, usePermissionStore, useAppStore } from "@/store";
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";
const settingsStore = useSettingsStore(); const settingsStore = useSettingsStore();
const permissionStore = usePermissionStore(); const permissionStore = usePermissionStore();
const appStore = useAppStore(); const appStore = useAppStore();
const { sidebarLogo } = storeToRefs(settingsStore); const { sidebarLogo } = settingsStore;
const layout = computed(() => settingsStore.layout); const layout = computed(() => settingsStore.layout);
const showContent = ref(true);
watch(
() => layout.value,
() => {
showContent.value = false;
nextTick(() => {
showContent.value = true;
});
}
);
</script> </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> <template>
<div class="tags-container"> <div class="tags-container">
<scroll-pane ref="scrollPaneRef" @scroll="handleScroll"> <el-scrollbar
class="scroll-container"
:vertical="false"
@wheel.prevent="handleScroll"
>
<router-link <router-link
ref="tagRef" ref="tagRef"
v-for="tag in visitedViews" v-for="tag in visitedViews"
:key="tag.fullPath" :key="tag.fullPath"
:class="'tags-item ' + (isActive(tag) ? 'active' : '')" :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) : ''" @click.middle="!isAffix(tag) ? closeSelectedTag(tag) : ''"
@contextmenu.prevent="openTagMenu(tag, $event)" @contextmenu.prevent="openContentMenu(tag, $event)"
> >
{{ translateRouteTitle(tag.title) }} {{ translateRouteTitle(tag.title) }}
<i-ep-close <i-ep-close
size="12px" size="12px"
v-if="!isAffix(tag)" v-if="!isAffix(tag)"
@click.prevent.stop="closeSelectedTag(tag)" @click.prevent.stop="closeSelectedTag(tag)"
/> />
</router-link> </router-link>
</scroll-pane> </el-scrollbar>
<!-- tag标签操作菜单 --> <!-- tag标签操作菜单 -->
<ul <ul
v-show="tagMenuVisible" v-show="contentMenuVisible"
class="tag-menu" class="contextmenu"
:style="{ left: left + 'px', top: top + 'px' }" :style="{ left: left + 'px', top: top + 'px' }"
> >
<li @click="refreshSelectedTag(selectedTag)"> <li @click="refreshSelectedTag(selectedTag)">
@ -55,18 +58,16 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { storeToRefs } from "pinia";
import { useRoute, useRouter, RouteRecordRaw } from "vue-router"; import { useRoute, useRouter, RouteRecordRaw } from "vue-router";
import { resolve } from "path-browserify"; import { resolve } from "path-browserify";
import { translateRouteTitle } from "@/utils/i18n"; import { translateRouteTitle } from "@/utils/i18n";
import { usePermissionStore } from "@/store/modules/permission"; import {
import { useTagsViewStore } from "@/store/modules/tagsView"; usePermissionStore,
import { useSettingsStore } from "@/store/modules/settings"; useTagsViewStore,
import { useAppStore } from "@/store/modules/app"; useSettingsStore,
useAppStore,
import ScrollPane from "./ScrollPane.vue"; } from "@/store";
const { proxy } = getCurrentInstance()!; const { proxy } = getCurrentInstance()!;
const router = useRouter(); const router = useRouter();
@ -90,7 +91,6 @@ const selectedTag = ref<TagView>({
}); });
const affixTags = ref<TagView[]>([]); const affixTags = ref<TagView[]>([]);
const scrollPaneRef = ref();
const left = ref(0); const left = ref(0);
const top = ref(0); const top = ref(0);
@ -105,40 +105,39 @@ watch(
} }
); );
const tagMenuVisible = ref(false); // const contentMenuVisible = ref(false); //
watch(tagMenuVisible, (value) => { watch(contentMenuVisible, (value) => {
if (value) { if (value) {
document.body.addEventListener("click", closeTagMenu); document.body.addEventListener("click", closeContentMenu);
} else { } else {
document.body.removeEventListener("click", closeTagMenu); document.body.removeEventListener("click", closeContentMenu);
} }
}); });
/**
* 过滤出需要固定的标签
*/
function filterAffixTags(routes: RouteRecordRaw[], basePath = "/") { function filterAffixTags(routes: RouteRecordRaw[], basePath = "/") {
const processRoute = (route: RouteRecordRaw) => { let tags: TagView[] = [];
const fullPath = resolve(basePath, route.path); routes.forEach((route: RouteRecordRaw) => {
const tagPath = resolve(basePath, route.path);
const tag: TagView = { if (route.meta?.affix) {
path: route.path, tags.push({
fullPath, path: tagPath,
fullPath: tagPath,
name: String(route.name), name: String(route.name),
title: route.meta?.title || "no-name", title: route.meta?.title || "no-name",
affix: route.meta?.affix, affix: route.meta?.affix,
keepAlive: route.meta?.keepAlive, keepAlive: route.meta?.keepAlive,
}; });
if (tag.affix) {
tags.push(tag);
} }
if (route.children) { 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; return tags;
} }
@ -171,7 +170,6 @@ function moveToCurrentTag() {
nextTick(() => { nextTick(() => {
for (const tag of visitedViews.value) { for (const tag of visitedViews.value) {
if (tag.path === route.path) { if (tag.path === route.path) {
scrollPaneRef.value.moveToTarget(tag);
// when query is different then update // when query is different then update
// route.query = { ...route.query, ...tag.query }; // route.query = { ...route.query, ...tag.query };
if (tag.fullPath !== route.fullPath) { if (tag.fullPath !== route.fullPath) {
@ -190,7 +188,7 @@ function moveToCurrentTag() {
} }
function isActive(tag: TagView) { function isActive(tag: TagView) {
return tag.fullPath === route.fullPath; return tag.path === route.path;
} }
function isAffix(tag: TagView) { function isAffix(tag: TagView) {
@ -200,7 +198,7 @@ function isAffix(tag: TagView) {
function isFirstView() { function isFirstView() {
try { try {
return ( return (
selectedTag.value.fullPath === "/dashboard" || selectedTag.value.path === "/dashboard" ||
selectedTag.value.fullPath === tagsViewStore.visitedViews[1].fullPath selectedTag.value.fullPath === tagsViewStore.visitedViews[1].fullPath
); );
} catch (err) { } catch (err) {
@ -253,18 +251,14 @@ function closeSelectedTag(view: TagView) {
function closeLeftTags() { function closeLeftTags() {
tagsViewStore.delLeftViews(selectedTag.value).then((res: any) => { tagsViewStore.delLeftViews(selectedTag.value).then((res: any) => {
if ( if (!res.visitedViews.find((item: any) => item.path === route.path)) {
!res.visitedViews.find((item: any) => item.fullPath === route.fullPath)
) {
toLastView(res.visitedViews); toLastView(res.visitedViews);
} }
}); });
} }
function closeRightTags() { function closeRightTags() {
tagsViewStore.delRightViews(selectedTag.value).then((res: any) => { tagsViewStore.delRightViews(selectedTag.value).then((res: any) => {
if ( if (!res.visitedViews.find((item: any) => item.path === route.path)) {
!res.visitedViews.find((item: any) => item.fullPath === route.fullPath)
) {
toLastView(res.visitedViews); 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 menuMinWidth = 105;
const offsetLeft = proxy?.$el.getBoundingClientRect().left; // container margin left const offsetLeft = proxy?.$el.getBoundingClientRect().left; // container margin left
@ -304,17 +301,24 @@ function openTagMenu(tag: TagView, e: MouseEvent) {
top.value = e.clientY; top.value = e.clientY;
} }
tagMenuVisible.value = true; contentMenuVisible.value = true;
selectedTag.value = tag; selectedTag.value = tag;
} }
function closeTagMenu() { /**
tagMenuVisible.value = false; * 关闭右键菜单
*/
function closeContentMenu() {
contentMenuVisible.value = false;
} }
/**
* 滚动事件
*/
function handleScroll() { function handleScroll() {
closeTagMenu(); closeContentMenu();
} }
function findOutermostParent(tree: any[], findName: string) { function findOutermostParent(tree: any[], findName: string) {
let parentMap: any = {}; let parentMap: any = {};
@ -342,11 +346,12 @@ function findOutermostParent(tree: any[], findName: string) {
return null; return null;
} }
const againActiveTop = (newVal: string) => { const againActiveTop = (newVal: string) => {
if (layout.value !== "mix") return; if (layout.value !== "mix") return;
const parent = findOutermostParent(permissionStore.routes, newVal); const parent = findOutermostParent(permissionStore.routes, newVal);
if (appStore.activeTopMenu !== parent.path) { if (appStore.activeTopMenu !== parent.path) {
appStore.changeTopActive(parent.path); appStore.activeTopMenu(parent.path);
} }
}; };
// selectedTagactiveTop // selectedTagactiveTop
@ -412,7 +417,7 @@ onMounted(() => {
} }
} }
.tag-menu { .contextmenu {
position: absolute; position: absolute;
z-index: 99; z-index: 99;
font-size: 12px; 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> </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"> <template>
import Main from "./main.vue"; <div class="wh-full" :class="classObj">
import { computed, watchEffect } from "vue"; <!-- 遮罩层 -->
import { useWindowSize } from "@vueuse/core"; <div
import Sidebar from "./components/Sidebar/index.vue"; v-if="classObj.mobile && classObj.openSidebar"
import LeftMenu from "./components/Sidebar/LeftMenu.vue"; class="fixed z-1000 bg-black bg-opacity-20"
@click="handleOutsideClick"
></div>
import { useAppStore } from "@/store/modules/app"; <Sidebar class="sidebar-container" />
import { useSettingsStore } from "@/store/modules/settings";
import { usePermissionStore } from "@/store/modules/permission"; <!-- 混合布局 -->
<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 permissionStore = usePermissionStore();
const { width } = useWindowSize(); const { width } = useWindowSize();
/**
* 响应式布局容器固定宽度 const WIDTH = 992; // >=1200px >=992px >=768px
*
* 大屏>=1200px
* 中屏>=992px
* 小屏>=768px
*/
const WIDTH = 992;
const appStore = useAppStore(); const appStore = useAppStore();
const settingsStore = useSettingsStore(); 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(() => { const activeTopMenu = computed(() => {
return appStore.activeTopMenu; return appStore.activeTopMenu;
}); });
// //
const mixLeftMenu = computed(() => { const mixLeftMenus = computed(() => {
return permissionStore.mixLeftMenu; return permissionStore.mixLeftMenus;
}); });
const layout = computed(() => settingsStore.layout);
const watermarkEnabled = computed(() => settingsStore.watermark.enabled);
watch( watch(
() => activeTopMenu.value, () => activeTopMenu.value,
(newVal) => { (newVal) => {
if (layout.value !== "mix") return; if (layout.value !== "mix") return;
permissionStore.getMixLeftMenu(newVal); permissionStore.setMixLeftMenus(newVal);
}, },
{ {
deep: true, deep: true,
@ -49,8 +86,8 @@ const classObj = computed(() => ({
openSidebar: appStore.sidebar.opened, openSidebar: appStore.sidebar.opened,
withoutAnimation: appStore.sidebar.withoutAnimation, withoutAnimation: appStore.sidebar.withoutAnimation,
mobile: appStore.device === "mobile", mobile: appStore.device === "mobile",
isTop: layout.value === "top", "layout-top": layout.value === "top",
isMix: layout.value === "mix", "layout-mix": layout.value === "mix",
})); }));
watchEffect(() => { watchEffect(() => {
@ -61,7 +98,6 @@ watchEffect(() => {
appStore.toggleDevice("desktop"); appStore.toggleDevice("desktop");
if (width.value >= 1200) { if (width.value >= 1200) {
//
appStore.openSideBar(true); appStore.openSideBar(true);
} else { } else {
appStore.closeSideBar(true); appStore.closeSideBar(true);
@ -73,123 +109,117 @@ function handleOutsideClick() {
appStore.closeSideBar(false); appStore.closeSideBar(false);
} }
function toggleSideBar() { function toggleSidebar() {
appStore.toggleSidebar(); appStore.toggleSidebar();
} }
</script> </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> <style lang="scss" scoped>
.app-wrapper { .sidebar-container {
&::after {
display: table;
clear: both;
content: "";
}
position: relative;
width: 100%;
height: 100%;
&.mobile.openSidebar {
position: fixed; position: fixed;
top: 0; 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 { .fixed-header {
position: absolute; position: fixed;
top: 0; top: 0;
z-index: 999; right: 0;
width: 100%; z-index: 9;
height: 100%; width: calc(100% - $sidebar-width);
background: #000; transition: width 0.28s;
opacity: 0.3;
} }
//
.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 { .sidebar-container {
z-index: 800; z-index: 999;
display: flex; display: flex;
width: 100% !important; width: 100% !important;
height: 50px; height: $navbar-height;
:deep(.logo-wrap) {
width: $sideBarWidth;
}
:deep(.el-scrollbar) { :deep(.el-scrollbar) {
flex: 1; flex: 1;
min-width: 0; height: $navbar-height;
height: 50px; }
:deep(.el-menu-item),
:deep(.el-sub-menu__title) {
height: $navbar-height;
line-height: $navbar-height;
} }
} }
.main-container { .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; 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; display: flex;
height: 100%; height: 100%;
padding-top: 50px; padding-top: $navbar-height;
.mix-wrapper__left { &__left {
position: relative; position: relative;
width: $sidebar-width;
height: 100%; height: 100%;
.el-menu { :deep(.el-menu) {
height: 100%; height: 100%;
border: none;
} }
.toggle-sidebar { .sidebar-toggle {
position: absolute; position: absolute;
bottom: 0; bottom: 0;
display: flex; display: flex;
@ -201,11 +231,11 @@ function toggleSideBar() {
box-shadow: 0 0 6px -2px var(--el-color-primary); box-shadow: 0 0 6px -2px var(--el-color-primary);
div:hover { div:hover {
background-color: var(--menuBg); background-color: var(--menu-background);
} }
:deep(svg) { :deep(svg) {
color: #409eff !important; color: var(--el-color-primary) !important;
} }
} }
} }
@ -213,23 +243,47 @@ function toggleSideBar() {
.main-container { .main-container {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
margin-left: 0;
} }
} }
} }
.openSidebar { .hideSidebar {
.mix-wrapper { .mix-container__left {
.mix-wrapper__left { width: $sidebar-width-collapsed;
width: $sideBarWidth;
} }
:deep(.svg-icon) { .fixed-header {
margin-top: -1px; width: calc(100% - $sidebar-width-collapsed);
margin-right: 5px;
} }
.el-menu { &.mobile {
border: none; .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 "@/permission";
import * as ElementPlusIconsVue from "@element-plus/icons-vue";
// 本地SVG图标 // 本地SVG图标
import "virtual:svg-icons-register"; import "virtual:svg-icons-register";
@ -23,4 +25,8 @@ setupDirective(app);
// 全局注册 状态管理(store) // 全局注册 状态管理(store)
setupStore(app); setupStore(app);
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component);
}
app.use(router).use(i18n).mount("#app"); app.use(router).use(i18n).mount("#app");

View File

@ -14,7 +14,7 @@ const whiteList = ["/login"];
router.beforeEach(async (to, from, next) => { router.beforeEach(async (to, from, next) => {
NProgress.start(); NProgress.start();
const hasToken = localStorage.getItem("accessToken"); const hasToken = localStorage.getItem("token");
if (hasToken) { if (hasToken) {
if (to.path === "/login") { if (to.path === "/login") {
// 如果已登录,跳转首页 // 如果已登录,跳转首页

View File

@ -8,4 +8,9 @@ export function setupStore(app: App<Element>) {
app.use(store); 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 }; export { store };

View File

@ -1,5 +1,3 @@
import { defineStore } from "pinia";
import { useStorage } from "@vueuse/core";
import defaultSettings from "@/settings"; import defaultSettings from "@/settings";
// 导入 Element Plus 中英文语言包 // 导入 Element Plus 中英文语言包
@ -19,7 +17,7 @@ export const useAppStore = defineStore("app", () => {
opened: sidebarStatus.value !== "closed", opened: sidebarStatus.value !== "closed",
withoutAnimation: false, withoutAnimation: false,
}); });
const activeTopMenu = useStorage("activeTop", ""); const activeTopMenuPath = useStorage("activeTopMenuPath", "");
/** /**
* *
*/ */
@ -72,8 +70,8 @@ export const useAppStore = defineStore("app", () => {
/** /**
* *
*/ */
function changeTopActive(val: string) { function activeTopMenu(val: string) {
activeTopMenu.value = val; activeTopMenuPath.value = val;
} }
return { return {
device, device,
@ -88,6 +86,6 @@ export const useAppStore = defineStore("app", () => {
toggleSidebar, toggleSidebar,
closeSideBar, closeSideBar,
openSideBar, openSideBar,
changeTopActive, activeTopMenuPath,
}; };
}); });

View File

@ -100,17 +100,24 @@ export const usePermissionStore = defineStore("permission", () => {
} }
/** /**
* *
*/ */
const mixLeftMenu = ref<RouteRecordRaw[]>([]); const mixLeftMenus = ref<RouteRecordRaw[]>([]);
function getMixLeftMenu(activeTop: string) { function setMixLeftMenus(activeTopMenu: string) {
routes.value.forEach((item) => { const matchedItem = routes.value.find(
if (item.path === activeTop) { (item) => item.path === activeTopMenu
mixLeftMenu.value = item.children || []; );
if (matchedItem && matchedItem.children) {
mixLeftMenus.value = matchedItem.children;
} }
});
} }
return { routes, setRoutes, generateRoutes, getMixLeftMenu, mixLeftMenu }; return {
routes,
setRoutes,
generateRoutes,
mixLeftMenus,
setMixLeftMenus,
};
}); });
// 非setup // 非setup

View File

@ -1,5 +1,3 @@
import { defineStore } from "pinia";
export const useTagsViewStore = defineStore("tagsView", () => { export const useTagsViewStore = defineStore("tagsView", () => {
const visitedViews = ref<TagView[]>([]); const visitedViews = ref<TagView[]>([]);
const cachedViews = ref<string[]>([]); const cachedViews = ref<string[]>([]);
@ -9,7 +7,7 @@ export const useTagsViewStore = defineStore("tagsView", () => {
*/ */
function addVisitedView(view: TagView) { function addVisitedView(view: TagView) {
// 如果已经存在于已访问的视图列表中,则不再添加 // 如果已经存在于已访问的视图列表中,则不再添加
if (visitedViews.value.some((v) => v.fullPath === view.fullPath)) { if (visitedViews.value.some((v) => v.path === view.path)) {
return; return;
} }
// 如果视图是固定的affix则在已访问的视图列表的开头添加 // 如果视图是固定的affix则在已访问的视图列表的开头添加

View File

@ -1,5 +1,3 @@
import { defineStore } from "pinia";
import { loginApi, logoutApi } from "@/api/auth"; import { loginApi, logoutApi } from "@/api/auth";
import { getUserInfoApi } from "@/api/user"; import { getUserInfoApi } from "@/api/user";
import { resetRouter } from "@/router"; import { resetRouter } from "@/router";
@ -8,16 +6,12 @@ import { store } from "@/store";
import { LoginData } from "@/api/auth/types"; import { LoginData } from "@/api/auth/types";
import { UserInfo } from "@/api/user/types"; import { UserInfo } from "@/api/user/types";
import { useStorage } from "@vueuse/core";
export const useUserStore = defineStore("user", () => { export const useUserStore = defineStore("user", () => {
const user: UserInfo = { const user: UserInfo = {
roles: [], roles: [],
perms: [], perms: [],
}; };
const token = useStorage("accessToken", "");
/** /**
* *
* *
@ -29,7 +23,7 @@ export const useUserStore = defineStore("user", () => {
loginApi(loginData) loginApi(loginData)
.then((response) => { .then((response) => {
const { tokenType, accessToken } = response.data; const { tokenType, accessToken } = response.data;
token.value = tokenType + " " + accessToken; // Bearer eyJhbGciOiJIUzI1NiJ9.xxx.xxx localStorage.setItem("token", tokenType + " " + accessToken); // Bearer eyJhbGciOiJIUzI1NiJ9.xxx.xxx
resolve(); resolve();
}) })
.catch((error) => { .catch((error) => {
@ -65,7 +59,7 @@ export const useUserStore = defineStore("user", () => {
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
logoutApi() logoutApi()
.then(() => { .then(() => {
token.value = ""; localStorage.setItem("token", "");
location.reload(); // 清空路由 location.reload(); // 清空路由
resolve(); resolve();
}) })
@ -77,15 +71,15 @@ export const useUserStore = defineStore("user", () => {
// remove token // remove token
function resetToken() { function resetToken() {
console.log("resetToken");
return new Promise<void>((resolve) => { return new Promise<void>((resolve) => {
token.value = ""; localStorage.setItem("token", "");
resetRouter(); resetRouter();
resolve(); resolve();
}); });
} }
return { return {
token,
user, user,
login, login,
getUserInfo, getUserInfo,

View File

@ -1,9 +1,9 @@
html.dark { html.dark {
--menuBg: var(--el-bg-color-overlay); --menu-background: var(--el-bg-color-overlay);
--menuText: #fff; --menu-text: #fff;
--menuActiveText: var(--el-menu-active-color); --menuActiveText: var(--el-menu-active-color);
--menuHover: rgb(0 0 0 / 20%); --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); --subMenuActiveText: var(--el-menu-active-color);
--subMenuHover: rgb(0 0 0 / 20%); --subMenuHover: rgb(0 0 0 / 20%);
@ -38,10 +38,4 @@ html.dark {
.right-panel-btn { .right-panel-btn {
background-color: var(--el-color-primary-dark); 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 { .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 { .hideSidebar {
.mix-wrapper__left { .mix-wrapper__left {
width: 54px; width: 54px;
@ -111,20 +26,11 @@
.el-tooltip { .el-tooltip {
padding: 0 !important; padding: 0 !important;
.svg-icon {
margin-left: 20px;
}
.sub-el-icon { .sub-el-icon {
margin-left: 19px; margin-left: 19px;
} }
} }
// TODO
& > .svg-icon {
margin-left: 20px;
}
& > span { & > span {
display: inline-block; display: inline-block;
width: 0; width: 0;
@ -140,10 +46,6 @@
& > .el-sub-menu__title { & > .el-sub-menu__title {
padding: 0 !important; padding: 0 !important;
.svg-icon {
margin-left: 20px;
}
.sub-el-icon { .sub-el-icon {
margin-left: 19px; margin-left: 19px;
} }
@ -170,7 +72,7 @@
} }
.el-menu--collapse .el-menu .el-sub-menu { .el-menu--collapse .el-menu .el-sub-menu {
min-width: $sideBarWidth !important; min-width: $sidebar-width !important;
} }
// mobile responsive // mobile responsive
@ -180,15 +82,15 @@
} }
.sidebar-container { .sidebar-container {
width: $sideBarWidth !important; width: $sidebar-width !important;
transition: transform 0.28s; transition: transform 0.28s;
} }
&.hideSidebar:not(.isMix, .isTop) { &.hideSidebar:not(.layout-mix, .layout-top) {
.sidebar-container { .sidebar-container {
pointer-events: none; pointer-events: none;
transition-duration: 0.3s; transition-duration: 0.3s;
transform: translate3d(-$sideBarWidth, 0, 0); transform: translate3d(-$sidebar-width, 0, 0);
} }
} }
} }
@ -204,10 +106,6 @@
// when menu collapsed // when menu collapsed
.el-menu--vertical { .el-menu--vertical {
& > .el-menu { & > .el-menu {
.svg-icon {
margin-right: 12px;
}
.sub-el-icon { .sub-el-icon {
margin-right: 12px; margin-right: 12px;
margin-left: -2px; margin-left: -2px;
@ -218,7 +116,7 @@
.el-menu-item { .el-menu-item {
&:hover { &:hover {
// you can use $subMenuHover // 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-enter-active,
.fade-leave-active { .fade-leave-active {
transition: opacity 0.28s; transition: opacity 0.28s;
} }
.fade-enter, .fade-enter-from,
.fade-leave-active { .fade-leave-to {
opacity: 0; opacity: 0;
} }
/* fade-transform */ /* 平移和淡入淡出 */
.fade-transform-leave-active, .fade-translate-enter-active,
.fade-transform-enter-active { .fade-translate-leave-active {
transition: all 0.5s; transition: opacity 0.3s, transform 0.3s;
} }
.fade-transform-enter { .fade-translate-enter-from,
.fade-translate-leave-to {
opacity: 0; opacity: 0;
transform: translateX(-30px); 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-enter-active,
.fade-scale-leave-active { .fade-scale-leave-active {
transition: transform 0.3s ease-in-out; transition: transform 0.3s ease-in-out;
@ -58,22 +34,7 @@
transform: scale(0); 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-enter-active,
.fade-rotate-leave-active { .fade-rotate-leave-active {
transition: transform 0.3s ease-in-out; transition: transform 0.3s ease-in-out;
@ -83,3 +44,28 @@
.fade-rotate-leave-to { .fade-rotate-leave-to {
transform: rotate(90deg); 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 { :export {
menuBg: $menuBg; sidebar-width: $sidebar-width;
menuText: $menuText; navbar-height: $navbar-height;
menuActiveText: $menuActiveText; 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 { :root {
--menuBg: #304156; --menu-background: #304156;
--menuText: #bfcbd9; --menu-text: #bfcbd9;
--menuActiveText: #409eff; --menu-active-text: #409eff;
--menuHover: #263445; --menu-hover: #263445;
--subMenuBg: #1f2d3d; --sub-menu-background: #1f2d3d;
--subMenuActiveText: #f4f4f5; --sub-menu-active-text: #f4f4f5;
--subMenuHover: #001528; --sub-menu-hover: #001528;
--sidebar-logo-background: #2d3748;
// 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);
} }
$menuBg: var(--menuBg); html.dark {
$menuText: var(--menuText); --menu-background: var(--el-bg-color-overlay);
$menuActiveText: var(--menuActiveText); --menu-text: #fff;
$menuHover: var(--menuHover); --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); $menu-background: var(--menu-background);
$subMenuActiveText: var(--subMenuActiveText); $menu-text: var(--menu-text);
$subMenuHover: var(--subMenuHover); $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 useFullscreen: typeof import("@vueuse/core")["useFullscreen"];
const useGamepad: typeof import("@vueuse/core")["useGamepad"]; const useGamepad: typeof import("@vueuse/core")["useGamepad"];
const useGeolocation: typeof import("@vueuse/core")["useGeolocation"]; const useGeolocation: typeof import("@vueuse/core")["useGeolocation"];
const useI18n: typeof import("vue-i18n")["useI18n"];
const useIdle: typeof import("@vueuse/core")["useIdle"]; const useIdle: typeof import("@vueuse/core")["useIdle"];
const useImage: typeof import("@vueuse/core")["useImage"]; const useImage: typeof import("@vueuse/core")["useImage"];
const useInfiniteScroll: typeof import("@vueuse/core")["useInfiniteScroll"]; const useInfiniteScroll: typeof import("@vueuse/core")["useInfiniteScroll"];
@ -741,6 +742,7 @@ declare module "vue" {
readonly useGeolocation: UnwrapRef< readonly useGeolocation: UnwrapRef<
typeof import("@vueuse/core")["useGeolocation"] typeof import("@vueuse/core")["useGeolocation"]
>; >;
readonly useI18n: UnwrapRef<typeof import("vue-i18n")["useI18n"]>;
readonly useIdle: UnwrapRef<typeof import("@vueuse/core")["useIdle"]>; readonly useIdle: UnwrapRef<typeof import("@vueuse/core")["useIdle"]>;
readonly useImage: UnwrapRef<typeof import("@vueuse/core")["useImage"]>; readonly useImage: UnwrapRef<typeof import("@vueuse/core")["useImage"]>;
readonly useInfiniteScroll: UnwrapRef< readonly useInfiniteScroll: UnwrapRef<
@ -1427,6 +1429,7 @@ declare module "@vue/runtime-core" {
readonly useGeolocation: UnwrapRef< readonly useGeolocation: UnwrapRef<
typeof import("@vueuse/core")["useGeolocation"] typeof import("@vueuse/core")["useGeolocation"]
>; >;
readonly useI18n: UnwrapRef<typeof import("vue-i18n")["useI18n"]>;
readonly useIdle: UnwrapRef<typeof import("@vueuse/core")["useIdle"]>; readonly useIdle: UnwrapRef<typeof import("@vueuse/core")["useIdle"]>;
readonly useImage: UnwrapRef<typeof import("@vueuse/core")["useImage"]>; readonly useImage: UnwrapRef<typeof import("@vueuse/core")["useImage"]>;
readonly useInfiniteScroll: UnwrapRef< readonly useInfiniteScroll: UnwrapRef<

View File

@ -9,9 +9,11 @@ export {};
declare module "@vue/runtime-core" { declare module "@vue/runtime-core" {
export interface GlobalComponents { 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"]; BarChart: typeof import("./../views/dashboard/components/BarChart.vue")["default"];
Breadcrumb: typeof import("./../components/Breadcrumb/index.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"]; DeptTree: typeof import("./../views/system/user/components/dept-tree.vue")["default"];
Dictionary: typeof import("./../components/Dictionary/index.vue")["default"]; Dictionary: typeof import("./../components/Dictionary/index.vue")["default"];
DictItem: typeof import("./../views/system/dict/components/dict-item.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"]; ElCheckbox: typeof import("element-plus/es")["ElCheckbox"];
ElCheckboxGroup: typeof import("element-plus/es")["ElCheckboxGroup"]; ElCheckboxGroup: typeof import("element-plus/es")["ElCheckboxGroup"];
ElCol: typeof import("element-plus/es")["ElCol"]; ElCol: typeof import("element-plus/es")["ElCol"];
ElConfigProvider: typeof import("element-plus/es")["ElConfigProvider"];
ElDatePicker: typeof import("element-plus/es")["ElDatePicker"]; ElDatePicker: typeof import("element-plus/es")["ElDatePicker"];
ElDialog: typeof import("element-plus/es")["ElDialog"]; ElDialog: typeof import("element-plus/es")["ElDialog"];
ElDivider: typeof import("element-plus/es")["ElDivider"]; ElDivider: typeof import("element-plus/es")["ElDivider"];
@ -48,6 +51,7 @@ declare module "@vue/runtime-core" {
ElRow: typeof import("element-plus/es")["ElRow"]; ElRow: typeof import("element-plus/es")["ElRow"];
ElScrollbar: typeof import("element-plus/es")["ElScrollbar"]; ElScrollbar: typeof import("element-plus/es")["ElScrollbar"];
ElSelect: typeof import("element-plus/es")["ElSelect"]; ElSelect: typeof import("element-plus/es")["ElSelect"];
ElStatistic: typeof import("element-plus/es")["ElStatistic"];
ElSubMenu: typeof import("element-plus/es")["ElSubMenu"]; ElSubMenu: typeof import("element-plus/es")["ElSubMenu"];
ElSwitch: typeof import("element-plus/es")["ElSwitch"]; ElSwitch: typeof import("element-plus/es")["ElSwitch"];
ElTable: typeof import("element-plus/es")["ElTable"]; ElTable: typeof import("element-plus/es")["ElTable"];
@ -55,6 +59,7 @@ declare module "@vue/runtime-core" {
ElTabPane: typeof import("element-plus/es")["ElTabPane"]; ElTabPane: typeof import("element-plus/es")["ElTabPane"];
ElTabs: typeof import("element-plus/es")["ElTabs"]; ElTabs: typeof import("element-plus/es")["ElTabs"];
ElTag: typeof import("element-plus/es")["ElTag"]; ElTag: typeof import("element-plus/es")["ElTag"];
ElText: typeof import("element-plus/es")["ElText"];
ElTooltip: typeof import("element-plus/es")["ElTooltip"]; ElTooltip: typeof import("element-plus/es")["ElTooltip"];
ElTree: typeof import("element-plus/es")["ElTree"]; ElTree: typeof import("element-plus/es")["ElTree"];
ElTreeSelect: typeof import("element-plus/es")["ElTreeSelect"]; ElTreeSelect: typeof import("element-plus/es")["ElTreeSelect"];
@ -64,9 +69,11 @@ declare module "@vue/runtime-core" {
GithubCorner: typeof import("./../components/GithubCorner/index.vue")["default"]; GithubCorner: typeof import("./../components/GithubCorner/index.vue")["default"];
Hamburger: typeof import("./../components/Hamburger/index.vue")["default"]; Hamburger: typeof import("./../components/Hamburger/index.vue")["default"];
IconSelect: typeof import("./../components/IconSelect/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"]; IEpArrowDown: typeof import("~icons/ep/arrow-down")["default"];
IEpCaretBottom: typeof import("~icons/ep/caret-bottom")["default"]; IEpCaretBottom: typeof import("~icons/ep/caret-bottom")["default"];
IEpCaretTop: typeof import("~icons/ep/caret-top")["default"]; IEpCaretTop: typeof import("~icons/ep/caret-top")["default"];
IEpCheck: typeof import("~icons/ep/check")["default"];
IEpClose: typeof import("~icons/ep/close")["default"]; IEpClose: typeof import("~icons/ep/close")["default"];
IEpCollection: typeof import("~icons/ep/collection")["default"]; IEpCollection: typeof import("~icons/ep/collection")["default"];
IEpDelete: typeof import("~icons/ep/delete")["default"]; IEpDelete: typeof import("~icons/ep/delete")["default"];
@ -75,6 +82,7 @@ declare module "@vue/runtime-core" {
IEpPicture: typeof import("~icons/ep/picture")["default"]; IEpPicture: typeof import("~icons/ep/picture")["default"];
IEpPlus: typeof import("~icons/ep/plus")["default"]; IEpPlus: typeof import("~icons/ep/plus")["default"];
IEpPosition: typeof import("~icons/ep/position")["default"]; IEpPosition: typeof import("~icons/ep/position")["default"];
IEpQuestionFilled: typeof import("~icons/ep/question-filled")["default"];
IEpRefresh: typeof import("~icons/ep/refresh")["default"]; IEpRefresh: typeof import("~icons/ep/refresh")["default"];
IEpRefreshLeft: typeof import("~icons/ep/refresh-left")["default"]; IEpRefreshLeft: typeof import("~icons/ep/refresh-left")["default"];
IEpSearch: typeof import("~icons/ep/search")["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"]; IEpSortUp: typeof import("~icons/ep/sort-up")["default"];
IEpTop: typeof import("~icons/ep/top")["default"]; IEpTop: typeof import("~icons/ep/top")["default"];
IEpUploadFilled: typeof import("~icons/ep/upload-filled")["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"]; 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"]; 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"]; 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"]; 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"]; Pagination: typeof import("./../components/Pagination/index.vue")["default"];
PieChart: typeof import("./../views/dashboard/components/PieChart.vue")["default"]; PieChart: typeof import("./../views/dashboard/components/PieChart.vue")["default"];
RadarChart: typeof import("./../views/dashboard/components/RadarChart.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"]; RouterLink: typeof import("vue-router")["RouterLink"];
RouterView: typeof import("vue-router")["RouterView"]; RouterView: typeof import("vue-router")["RouterView"];
ScrollPane: typeof import("./../layout/components/TagsView/ScrollPane.vue")["default"]; 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"]; Settings: typeof import("./../layout/components/Settings/index.vue")["default"];
Sidebar: typeof import("./../layout/components/Sidebar/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"]; SingleUpload: typeof import("./../components/Upload/SingleUpload.vue")["default"];
SizeSelect: typeof import("./../components/SizeSelect/index.vue")["default"]; SizeSelect: typeof import("./../components/SizeSelect/index.vue")["default"];
SvgIcon: typeof import("./../components/SvgIcon/index.vue")["default"]; SvgIcon: typeof import("./../components/SvgIcon/index.vue")["default"];
SwitchRoles: typeof import("./../views/demo/permission/components/SwitchRoles.vue")["default"]; SwitchRoles: typeof import("./../views/demo/permission/components/SwitchRoles.vue")["default"];
TagInput: typeof import("./../components/TagInput/index.vue")["default"]; TagInput: typeof import("./../components/TagInput/index.vue")["default"];
TagsView: typeof import("./../layout/components/TagsView/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"]; UnfixedThead: typeof import("./../views/demo/table/dynamic-table/components/UnfixedThead.vue")["default"];
WangEditor: typeof import("./../components/WangEditor/index.vue")["default"]; WangEditor: typeof import("./../components/WangEditor/index.vue")["default"];
} }

View File

@ -11,9 +11,9 @@ const service = axios.create({
// 请求拦截器 // 请求拦截器
service.interceptors.request.use( service.interceptors.request.use(
(config: InternalAxiosRequestConfig) => { (config: InternalAxiosRequestConfig) => {
const userStore = useUserStoreHook(); const accessToken = localStorage.getItem("token");
if (userStore.token) { if (accessToken) {
config.headers.Authorization = userStore.token; config.headers.Authorization = accessToken;
} }
return config; return config;
}, },
@ -44,6 +44,7 @@ service.interceptors.response.use(
if (code === "A0230") { if (code === "A0230") {
ElMessageBox.confirm("当前页面已失效,请重新登录", "提示", { ElMessageBox.confirm("当前页面已失效,请重新登录", "提示", {
confirmButtonText: "确定", confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning", type: "warning",
}).then(() => { }).then(() => {
const userStore = useUserStoreHook(); const userStore = useUserStoreHook();

View File

@ -69,7 +69,7 @@
</el-tooltip> </el-tooltip>
<!-- 验证码 --> <!-- 验证码 -->
<el-form-item prop="captchaCode" class="flex justify-between"> <el-form-item prop="captchaCode" class="flex-x-between">
<div class="flex"> <div class="flex">
<span class="p-2"> <span class="p-2">
<svg-icon icon-class="captcha" /> <svg-icon icon-class="captcha" />
@ -85,14 +85,10 @@
/> />
</div> </div>
<div <div class="flex-x-end w-[120px] cursor-pointer" @click="getCaptcha">
class="flex justify-end h-full items-center !w-[128px] cursor-pointer"
@click="getCaptcha"
>
<el-image <el-image
:src="captchaBase64" :src="captchaBase64"
height="48px" class="rounded-tr-md rounded-br-md h-[48px]"
class="rounded-tr-md rounded-br-md"
> >
<template #error> <template #error>
<el-icon><Picture /></el-icon> <el-icon><Picture /></el-icon>

View File

@ -13,6 +13,11 @@ import {
export default defineConfig({ export default defineConfig({
shortcuts: { shortcuts: {
"flex-center": "flex justify-center items-center", "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: { theme: {
colors: { colors: {

View File

@ -52,9 +52,9 @@ export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
[env.VITE_APP_BASE_API]: { [env.VITE_APP_BASE_API]: {
changeOrigin: true, changeOrigin: true,
// 线上接口地址 // 线上接口地址
target: "http://vapi.youlai.tech", // target: "http://vapi.youlai.tech",
// 开发接口地址 // 开发接口地址
//target: "http://localhost:8989", target: "http://localhost:8989",
rewrite: (path) => rewrite: (path) =>
path.replace(new RegExp("^" + env.VITE_APP_BASE_API), ""), 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 // 自动导入参考: https://github.com/sxzz/element-plus-best-practices/blob/main/vite.config.ts
AutoImport({ AutoImport({
// 自动导入 Vue 相关函数ref, reactive, toRef 等 // 自动导入 Vue 相关函数ref, reactive, toRef 等
imports: ["vue", "@vueuse/core", "pinia", "vue-router"], imports: ["vue", "@vueuse/core", "pinia", "vue-router", "vue-i18n"],
// 自动导入 Element Plus 相关函数ElMessage, ElMessageBox... (带样式) // 自动导入 Element Plus 相关函数ElMessage, ElMessageBox... (带样式)
resolvers: [ElementPlusResolver(), IconsResolver({})], resolvers: [ElementPlusResolver(), IconsResolver({})],
eslintrc: { eslintrc: {
@ -81,8 +81,8 @@ export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
}, },
vueTemplate: true, vueTemplate: true,
// 配置文件生成位置(false:关闭自动生成) // 配置文件生成位置(false:关闭自动生成)
dts: false, //dts: false,
// dts: "src/typings/auto-imports.d.ts", dts: "src/typings/auto-imports.d.ts",
}), }),
Components({ Components({
@ -95,8 +95,8 @@ export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
// 指定自定义组件位置(默认:src/components) // 指定自定义组件位置(默认:src/components)
dirs: ["src/components", "src/**/components"], dirs: ["src/components", "src/**/components"],
// 配置文件位置 (false:关闭自动生成) // 配置文件位置 (false:关闭自动生成)
dts: false, //dts: false,
// dts: "src/typings/components.d.ts", dts: "src/typings/components.d.ts",
}), }),
Icons({ Icons({
@ -172,6 +172,8 @@ export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
"@wangeditor/editor", "@wangeditor/editor",
"@wangeditor/editor-for-vue", "@wangeditor/editor-for-vue",
"vue-i18n", "vue-i18n",
"element-plus/es/components/text/style/css",
"path-browserify",
], ],
}, },
// 构建配置 // 构建配置