Merge branch 'master' into master

This commit is contained in:
Ray Hao
2025-02-19 13:43:33 +08:00
committed by GitHub
78 changed files with 1906 additions and 2145 deletions

View File

@@ -2,7 +2,7 @@
<section class="app-main" :style="{ height: appMainHeight }">
<router-view>
<template #default="{ Component, route }">
<transition name="el-fade-in-linear" mode="out-in">
<transition enter-active-class="animate__animated animate__fadeIn" mode="out-in">
<keep-alive :include="cachedViews">
<component :is="Component" :key="route.path" />
</keep-alive>

View File

@@ -1,7 +1,7 @@
<template>
<div class="navbar__right" :class="navbarRightClass">
<!-- 非手机设备窄屏显示 -->
<template v-if="!isMobile">
<!-- 桌面端显示 -->
<template v-if="isDesktop">
<!-- 搜索 -->
<MenuSearch />
@@ -19,28 +19,52 @@
</template>
<!-- 用户头像个人中心注销登录等 -->
<UserProfile />
<el-dropdown trigger="click">
<div class="user-profile">
<img class="user-profile__avatar" :src="userStore.userInfo.avatar" />
<span class="user-profile__name">{{ userStore.userInfo.username }}</span>
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="handleProfileClick">
{{ $t("navbar.profile") }}
</el-dropdown-item>
<el-dropdown-item divided @click="logout">
{{ $t("navbar.logout") }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<!-- 设置面板 -->
<div v-if="defaultSettings.showSettings" @click="settingStore.settingsVisible = true">
<SvgIcon icon-class="setting" />
<div class="i-svg:setting" />
</div>
</div>
</template>
<script setup lang="ts">
import defaultSettings from "@/settings";
import { DeviceEnum } from "@/enums/DeviceEnum";
import { useAppStore, useSettingsStore, useUserStore, useTagsViewStore } from "@/store";
import { useAppStore, useSettingsStore } from "@/store";
import UserProfile from "./UserProfile.vue";
import Notification from "./Notification.vue";
import { SidebarLightThemeEnum } from "@/enums/ThemeEnum";
const appStore = useAppStore();
const settingStore = useSettingsStore();
const userStore = useUserStore();
const tagsViewStore = useTagsViewStore();
const isMobile = computed(() => appStore.device === DeviceEnum.MOBILE);
const route = useRoute();
const router = useRouter();
const isDesktop = computed(() => appStore.device === DeviceEnum.DESKTOP);
/**
* 打开个人中心页面
*/
function handleProfileClick() {
router.push({ name: "Profile" });
}
// 根据浅色模式-侧边栏颜色配置选择navbar右侧的样式类
const navbarRightClass = computed(() => {
@@ -48,6 +72,27 @@ const navbarRightClass = computed(() => {
? "navbar__right--darkBlue"
: "navbar__right--white";
});
/**
* 注销登出
*/
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>
@@ -69,14 +114,23 @@ const navbarRightClass = computed(() => {
background: rgb(0 0 0 / 10%);
}
}
}
.user-profile {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
padding: 0 13px;
:deep(.el-divider--horizontal) {
margin: 10px 0;
}
&__avatar {
width: 32px;
height: 32px;
border-radius: 50%;
}
.dark .navbar__right > *:hover {
background: rgb(255 255 255 / 20%);
&__name {
margin-left: 10px;
}
}
}
.layout-top .navbar__right--darkBlue > *,
@@ -84,8 +138,13 @@ const navbarRightClass = computed(() => {
color: #fff;
}
.layout-top .navbar__right--white > *,
.layout-mix .navbar__right--white > * {
color: #000;
}
.dark .navbar__right > *:hover {
color: #ccc;
}
</style>

View File

@@ -1,162 +1,61 @@
<template>
<div>
<el-dropdown class="wh-full">
<el-badge v-if="notices.length > 0" :offset="[-10, 15]" :value="notices.length" :max="99">
<el-dropdown class="h-full items-center justify-center" trigger="click">
<el-badge v-if="notices.length > 0" :offset="[0, 15]" :value="notices.length" :max="99">
<el-icon>
<Bell />
</el-icon>
</el-badge>
<el-badge v-else>
<div v-else>
<el-icon>
<Bell />
</el-icon>
</el-badge>
</div>
<template #dropdown>
<div class="p-2">
<el-tabs v-model="activeTab">
<el-tab-pane label="通知" name="notice">
<template v-if="notices.length > 0">
<div v-for="(item, index) in notices" :key="index" class="w500px py-3">
<div class="flex-y-center">
<DictLabel v-model="item.type" code="notice_type" size="small" />
<el-text
size="small"
class="w200px cursor-pointer !ml-2 !flex-1"
truncated
@click="handleReadNotice(item.id)"
>
{{ item.title }}
</el-text>
<div class="p-5">
<template v-if="notices.length > 0">
<div v-for="(item, index) in notices" :key="index" class="w500px py-3">
<div class="flex-y-center">
<DictLabel v-model="item.type" code="notice_type" size="small" />
<el-text
size="small"
class="w200px cursor-pointer !ml-2 !flex-1"
truncated
@click="readNotice(item.id)"
>
{{ item.title }}
</el-text>
<div class="text-xs text-gray">
{{ item.publishTime }}
</div>
</div>
<div class="text-xs text-gray">
{{ item.publishTime }}
</div>
</template>
<template v-else>
<div class="flex-center h150px w350px">
<el-empty :image-size="50" description="暂无通知" />
</div>
</template>
<el-divider />
<div class="flex-x-between">
<el-link type="primary" :underline="false" @click="handleViewMore">
<span class="text-xs">查看更多</span>
<el-icon class="text-xs">
<ArrowRight />
</el-icon>
</el-link>
<el-link
v-if="notices.length > 0"
type="primary"
:underline="false"
@click="markAllAsRead"
>
<span class="text-xs">全部已读</span>
</el-link>
</div>
</el-tab-pane>
<el-tab-pane label="消息" name="message">
<template v-if="messages.length > 0">
<div
v-for="(item, index) in messages"
:key="index"
class="w400px flex-x-between p-1"
>
<div class="flex-y-center">
<DictLabel v-model="item.type" code="notice_type" size="small" />
<el-link
type="primary"
class="w200px cursor-pointer !ml-2 !flex-1"
@click="handleReadNotice(item.id)"
>
{{ item.title }}
</el-link>
<div class="text-xs text-gray-400">
{{ item.publishTime }}
</div>
</div>
</div>
</template>
<template v-else>
<div class="flex-center h150px w350px">
<el-empty :image-size="50" description="暂无消息" />
</div>
</template>
<el-divider />
<div class="flex-x-between">
<el-link
v-if="tasks.length > 5"
type="primary"
:underline="false"
@click="handleViewMore"
>
<span class="text-xs">查看更多</span>
<el-icon class="text-xs">
<ArrowRight />
</el-icon>
</el-link>
<el-link
v-if="messages.length > 0"
type="primary"
:underline="false"
@click="markAllAsRead"
>
<span class="text-xs">全部已读</span>
</el-link>
</div>
</el-tab-pane>
<el-tab-pane label="待办" name="task">
<template v-if="tasks.length > 0">
<div v-for="(item, index) in tasks" :key="index" class="w500px py-3">
<div class="flex-y-center">
<DictLabel v-model="item.type" code="notice_type" size="small" />
<el-link
type="primary"
class="w200px cursor-pointer !ml-2 !flex-1"
@click="handleReadNotice(item.id)"
>
{{ item.title }}
</el-link>
<div class="text-xs text-gray-400">
{{ item.publishTime }}
</div>
</div>
</div>
</template>
<template v-else>
<div class="flex-center h150px w350px">
<el-empty :image-size="50" description="暂无待办" />
</div>
</template>
<el-divider />
<div class="flex-x-between">
<el-link
v-if="tasks.length > 5"
type="primary"
:underline="false"
@click="handleViewMore"
>
<span class="text-xs">查看更多</span>
<el-icon class="text-xs">
<ArrowRight />
</el-icon>
</el-link>
<el-link
v-if="tasks.length > 0"
type="primary"
:underline="false"
@click="markAllAsRead"
>
<span class="text-xs">全部已读</span>
</el-link>
</div>
</el-tab-pane>
</el-tabs>
</div>
<el-divider />
<div class="flex-x-between">
<el-link type="primary" :underline="false" @click="viewMoreNotice">
<span class="text-xs">查看更多</span>
<el-icon class="text-xs">
<ArrowRight />
</el-icon>
</el-link>
<el-link
v-if="notices.length > 0"
type="primary"
:underline="false"
@click="markAllAsRead"
>
<span class="text-xs">全部已读</span>
</el-link>
</div>
</template>
<template v-else>
<div class="flex-center h150px w350px">
<el-empty :image-size="50" description="暂无消息" />
</div>
</template>
</div>
</template>
</el-dropdown>
@@ -167,45 +66,53 @@
<script setup lang="ts">
import NoticeAPI, { NoticePageVO } from "@/api/system/notice";
import WebSocketManager from "@/utils/websocket";
import router from "@/router";
const activeTab = ref("notice");
const notices = ref<NoticePageVO[]>([]);
const messages = ref<any[]>([]);
const tasks = ref<any[]>([]);
const noticeDetailRef = ref();
// 获取未读消息列表并连接 WebSocket
onMounted(() => {
import { useStomp } from "@/hooks/useStomp";
const { subscribe, unsubscribe, isConnected } = useStomp();
watch(
() => isConnected.value,
(connected) => {
if (connected) {
subscribe("/user/queue/message", (message) => {
console.log("收到通知消息:", message);
const data = JSON.parse(message.body);
const id = data.id;
if (!notices.value.some((notice) => notice.id == id)) {
notices.value.unshift({
id,
title: data.title,
type: data.type,
publishTime: data.publishTime,
});
ElNotification({
title: "您收到一条新的通知消息!",
message: data.title,
type: "success",
position: "bottom-right",
});
}
});
}
}
);
/**
* 获取我的通知公告
*/
function featchMyNotice() {
NoticeAPI.getMyNoticePage({ pageNum: 1, pageSize: 5, isRead: 0 }).then((data) => {
notices.value = data.list;
});
WebSocketManager.subscribeToTopic("/user/queue/message", (message) => {
console.log("收到消息:", message);
const data = JSON.parse(message);
const id = data.id;
if (!notices.value.some((notice) => notice.id == id)) {
notices.value.unshift({
id,
title: data.title,
type: data.type,
publishTime: data.publishTime,
});
ElNotification({
title: "您收到一条新的通知消息!",
message: data.title,
type: "success",
position: "bottom-right",
});
}
});
});
}
// 阅读通知公告
function handleReadNotice(id: string) {
function readNotice(id: string) {
noticeDetailRef.value.openNotice(id);
const index = notices.value.findIndex((notice) => notice.id === id);
if (index >= 0) {
@@ -214,7 +121,7 @@ function handleReadNotice(id: string) {
}
// 查看更多
function handleViewMore() {
function viewMoreNotice() {
router.push({ path: "/myNotice" });
}
@@ -224,6 +131,14 @@ function markAllAsRead() {
notices.value = [];
});
}
onMounted(() => {
featchMyNotice();
});
onBeforeUnmount(() => {
unsubscribe("/user/queue/message");
});
</script>
<style lang="scss" scoped>
@@ -234,4 +149,7 @@ function markAllAsRead() {
width: 100%;
height: 100%;
}
:deep(.el-dropdown) {
color: currentColor;
}
</style>

View File

@@ -1,62 +0,0 @@
<template>
<el-dropdown trigger="click">
<div class="flex-center h100% p13px">
<img :src="userStore.userInfo.avatar" class="rounded-full mr-10px w24px h24px" />
<span>{{ userStore.userInfo.username }}</span>
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="handleOpenUserProfile">
{{ $t("navbar.profile") }}
</el-dropdown-item>
<el-dropdown-item divided @click="logout">
{{ $t("navbar.logout") }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
<script setup lang="ts">
defineOptions({
name: "UserProfile",
});
import { useTagsViewStore, useUserStore } from "@/store";
const tagsViewStore = useTagsViewStore();
const userStore = useUserStore();
const route = useRoute();
const router = useRouter();
/**
* 打开个人中心页面
*/
function handleOpenUserProfile() {
router.push({ name: "Profile" });
}
/**
* 注销登出
*/
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></style>

View File

@@ -1,35 +1,20 @@
<template>
<div class="flex flex-wrap justify-around w-full h-12">
<el-tooltip content="左侧模式" placement="bottom">
<div class="layout-select">
<el-tooltip
v-for="item in layoutOptions"
:key="item.value"
:content="item.label"
placement="bottom"
>
<div
class="layout-item left"
:class="{ 'is-active': modelValue === LayoutEnum.LEFT }"
@click="updateValue(LayoutEnum.LEFT)"
role="button"
tabindex="0"
:class="['layout-item', item.className, { 'is-active': modelValue === item.value }]"
@click="handleLayoutChange(item.value)"
@keydown.enter.space="handleLayoutChange(item.value)"
>
<div />
<div />
</div>
</el-tooltip>
<el-tooltip content="顶部模式" placement="bottom">
<div
class="layout-item top"
:class="{ 'is-active': modelValue === LayoutEnum.TOP }"
@click="updateValue(LayoutEnum.TOP)"
>
<div />
<div />
</div>
</el-tooltip>
<el-tooltip content="混合模式" placement="bottom">
<div
class="layout-item mix"
:class="{ 'is-active': modelValue === LayoutEnum.MIX }"
@click="updateValue(LayoutEnum.MIX)"
>
<div />
<div />
<div class="layout-item-part" />
<div class="layout-item-part" />
</div>
</el-tooltip>
</div>
@@ -38,71 +23,118 @@
<script lang="ts" setup>
import { LayoutEnum } from "@/enums/LayoutEnum";
const props = defineProps({
modelValue: String,
interface LayoutOption {
value: LayoutEnum;
label: string;
className: string;
}
const layoutOptions: LayoutOption[] = [
{ value: LayoutEnum.LEFT, label: "左侧模式", className: "left" },
{ value: LayoutEnum.TOP, label: "顶部模式", className: "top" },
{ value: LayoutEnum.MIX, label: "混合模式", className: "mix" },
];
const modelValue = defineModel<LayoutEnum>("modelValue", {
required: true,
default: () => LayoutEnum.LEFT,
});
const emit = defineEmits(["update:modelValue"]);
function updateValue(layout: string) {
emit("update:modelValue", layout);
function handleLayoutChange(layout: LayoutEnum) {
modelValue.value = layout;
}
</script>
<style scoped>
.layout-selector {
<style scoped lang="scss">
.layout-select {
display: flex;
flex-wrap: wrap;
justify-content: space-around;
width: 100%;
height: 50px;
gap: 10px;
justify-content: space-evenly;
padding: 10px 0;
--layout-primary: #1b2a47;
--layout-background: #f0f2f5;
--layout-shadow: 0 0 8px rgba(0, 0, 0, 0.1);
--layout-hover: #e3f1f9;
}
.layout-item {
position: relative;
width: 18%;
height: 45px;
overflow: hidden;
height: 50px;
cursor: pointer;
background: #f0f2f5;
border-radius: 4px;
background: var(--layout-background);
border-radius: 8px;
box-shadow: var(--layout-shadow);
&.mix div:nth-child(1),
&.top div:nth-child(1) {
width: 100%;
height: 30%;
background: #1b2a47;
box-shadow: 0 0 1px #888;
transition:
transform 0.2s ease,
border-color 0.2s ease,
box-shadow 0.2s ease;
&:hover {
background-color: var(--layout-hover);
transform: scale(1.02); /* 稍微放大,避免过于夸张 */
}
&.mix div:nth-child(2) {
&:focus-visible {
outline: 2px solid var(--el-color-primary);
}
&-part {
position: absolute;
bottom: 0;
left: 0;
width: 30%;
height: 70%;
background: #1b2a47;
box-shadow: 0 0 1px #888;
background: var(--layout-primary);
border-radius: 4px; /* 保持和父容器一致的圆角 */
box-shadow: var(--layout-shadow);
transition: all 0.3s ease;
}
&.left div:nth-child(1) {
width: 30%;
height: 100%;
background: #1b2a47;
&.left {
.layout-item-part {
&:first-child {
width: 30%;
height: 100%;
border-radius: 4px 0 0 4px; /* 左边部分圆角 */
}
&:last-child {
top: 0;
right: 0;
width: 70%;
height: 30%;
background: #fff;
border-radius: 0 4px 4px 0; /* 右边部分圆角 */
}
}
}
&.left div:nth-child(2) {
position: absolute;
top: 0;
right: 0;
width: 70%;
height: 30%;
background: #fff;
box-shadow: 0 0 1px #888;
&.top {
.layout-item-part:first-child {
width: 100%;
height: 30%;
border-radius: 4px 4px 0 0; /* 顶部部分圆角 */
}
}
&.mix {
.layout-item-part {
&:first-child {
width: 100%;
height: 30%;
border-radius: 4px 4px 0 0; /* 顶部部分圆角 */
}
&:last-child {
bottom: 0;
left: 0;
width: 30%;
height: 70%;
border-radius: 0 0 4px 4px; /* 底部部分圆角 */
}
}
}
}
.layout-item.is-active {
.is-active {
background-color: var(--layout-hover);
border: 2px solid var(--el-color-primary);
transform: scale(1.05); /* 轻微放大 */
}
</style>

View File

@@ -1,6 +1,6 @@
<template>
<div class="logo">
<transition name="el-fade-in-linear" mode="out-in">
<transition enter-active-class="animate__animated animate__fadeInLeft">
<router-link :key="+collapse" class="wh-full flex-center" to="/">
<img :src="logo" class="w20px h20px" />
<span v-if="!collapse" class="title">

View File

@@ -28,7 +28,7 @@
>
<!-- 菜单项 -->
<SidebarMenuItem
v-for="route in menuList"
v-for="route in data"
:key="route.path"
:item="route"
:base-path="resolveFullPath(route.path)"
@@ -39,6 +39,7 @@
<script lang="ts" setup>
import path from "path-browserify";
import type { MenuInstance } from "element-plus";
import type { RouteRecordRaw } from "vue-router";
import { LayoutEnum } from "@/enums/LayoutEnum";
import { SidebarLightThemeEnum } from "@/enums/ThemeEnum";
@@ -48,8 +49,8 @@ import { isExternal } from "@/utils/index";
import variables from "@/styles/variables.module.scss";
const props = defineProps({
menuList: {
type: Array<any>,
data: {
type: Array<RouteRecordRaw>,
required: true,
default: () => [],
},

View File

@@ -149,10 +149,10 @@ function resolvePath(routePath: string) {
& > span {
display: inline-block;
visibility: hidden;
width: 0;
height: 0;
overflow: hidden;
visibility: hidden;
}
}
@@ -178,10 +178,10 @@ function resolvePath(routePath: string) {
.el-sub-menu {
& > .el-sub-menu__title > span {
display: inline-block;
visibility: hidden;
width: 0;
height: 0;
overflow: hidden;
visibility: hidden;
}
}
}

View File

@@ -1,10 +1,14 @@
<template>
<!-- 根据 icon 类型决定使用的不同类型的图标组件 -->
<el-icon v-if="icon && icon.startsWith('el-icon')" class="sub-el-icon">
<component :is="icon.replace('el-icon-', '')" />
</el-icon>
<svg-icon v-else-if="icon" :icon-class="icon" />
<svg-icon v-else icon-class="menu" />
<!-- 菜单图标 -->
<template v-if="icon">
<el-icon v-if="isElIcon" class="el-icon">
<component :is="iconComponent" />
</el-icon>
<div v-else :class="`i-svg:${icon}`" />
</template>
<template v-else>
<div class="i-svg:menu" />
</template>
<!-- 菜单标题 -->
<span v-if="title" class="ml-1">{{ translateRouteTitle(title) }}</span>
</template>
@@ -12,32 +16,38 @@
<script setup lang="ts">
import { translateRouteTitle } from "@/utils/i18n";
defineProps({
icon: {
type: String,
default: "",
},
title: {
type: String,
default: "",
},
});
const props = defineProps<{
icon?: string;
title?: string;
}>();
const isElIcon = computed(() => props.icon?.startsWith("el-icon"));
const iconComponent = computed(() => props.icon?.replace("el-icon-", ""));
</script>
<style lang="scss" scoped>
.sub-el-icon {
.el-icon {
width: 14px !important;
margin-right: 0 !important;
color: currentcolor;
}
[class^="i-svg:"] {
width: 14px;
height: 14px;
color: currentcolor !important;
}
.hideSidebar {
.el-sub-menu,
.el-menu-item {
.svg-icon,
.sub-el-icon {
.el-icon {
margin-left: 20px;
}
}
[class^="i-svg:"] {
margin-left: 20px;
}
}
</style>

View File

@@ -27,7 +27,7 @@
<el-icon v-if="route.meta.icon.startsWith('el-icon')" class="sub-el-icon">
<component :is="route.meta.icon.replace('el-icon-', '')" />
</el-icon>
<svg-icon v-else :icon-class="route.meta.icon" />
<div v-else :class="`i-svg:${route.meta.icon}`" />
</template>
<span v-if="route.path === '/'">首页</span>
<span v-else-if="route.meta && route.meta.title" class="ml-1">
@@ -40,18 +40,12 @@
</template>
<script lang="ts" setup>
/**
* 导入模块:先外部库,再内部模块,最后导入样式和工具类
*/
import { LocationQueryRaw, RouteRecordRaw } from "vue-router";
import { usePermissionStore, useAppStore, useSettingsStore } from "@/store";
import { translateRouteTitle } from "@/utils/i18n";
import variables from "@/styles/variables.module.scss";
import { SidebarLightThemeEnum } from "@/enums/ThemeEnum";
/**
* 定义状态:先定义 reactive、ref 或 computed 状态
*/
const router = useRouter();
const appStore = useAppStore();
const permissionStore = usePermissionStore();
@@ -84,8 +78,8 @@ appStore.activeTopMenu(activeTopMenuPath);
*/
const handleMenuSelect = (routePath: string) => {
appStore.activeTopMenu(routePath); // 设置激活的顶部菜单
permissionStore.setMixLeftMenus(routePath); // 更新左侧菜单
navigateToFirstLeftMenu(permissionStore.mixLeftMenus); // 跳转到左侧第一个菜单
permissionStore.setMixedLayoutLeftRoutes(routePath); // 更新左侧菜单
navigateToFirstLeftMenu(permissionStore.mixedLayoutLeftRoutes); // 跳转到左侧第一个菜单
};
/**

View File

@@ -1,21 +1,21 @@
<template>
<div :class="{ 'has-logo': sidebarLogo }">
<!-- 混合布局顶部 -->
<div v-if="isMixLayout" class="flex w-full">
<!-- 混合布局 -->
<div v-if="layout == LayoutEnum.MIX" class="flex w-full">
<SidebarLogo v-if="sidebarLogo" :collapse="isSidebarCollapsed" />
<SidebarMixTopMenu class="flex-1" />
<NavbarRight />
</div>
<!-- 顶部布局顶部 || 左侧布局左侧 -->
<!-- 顶部布局 || 左侧布局 -->
<template v-else>
<SidebarLogo v-if="sidebarLogo" :collapse="isSidebarCollapsed" />
<el-scrollbar>
<SidebarMenu :menu-list="permissionStore.routes" base-path="" />
<SidebarMenu :data="permissionStore.routes" base-path="" />
</el-scrollbar>
<!-- 顶部布局导航 -->
<NavbarRight v-if="isTopLayout" />
<!-- 顶部导航 -->
<NavbarRight v-if="layout == LayoutEnum.TOP" />
</template>
</div>
</template>
@@ -33,8 +33,6 @@ const permissionStore = usePermissionStore();
const sidebarLogo = computed(() => settingsStore.sidebarLogo);
const layout = computed(() => settingsStore.layout);
const isMixLayout = computed(() => layout.value === LayoutEnum.MIX);
const isTopLayout = computed(() => layout.value === LayoutEnum.TOP);
const isSidebarCollapsed = computed(() => !appStore.sidebar.opened);
</script>

View File

@@ -28,27 +28,27 @@
:style="{ left: left + 'px', top: top + 'px' }"
>
<li @click="refreshSelectedTag(selectedTag)">
<svg-icon icon-class="refresh" />
<div class="i-svg:refresh" />
刷新
</li>
<li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)">
<svg-icon icon-class="close" />
<div class="i-svg:close" />
关闭
</li>
<li @click="closeOtherTags">
<svg-icon icon-class="close_other" />
<div class="i-svg:close_other" />
关闭其它
</li>
<li v-if="!isFirstView()" @click="closeLeftTags">
<svg-icon icon-class="close_left" />
<div class="i-svg:close_left" />
关闭左侧
</li>
<li v-if="!isLastView()" @click="closeRightTags">
<svg-icon icon-class="close_right" />
<div class="i-svg:close_right" />
关闭右侧
</li>
<li @click="closeAllTags(selectedTag)">
<svg-icon icon-class="close_all" />
<div class="i-svg:close_all" />
关闭所有
</li>
</ul>
@@ -187,25 +187,17 @@ function isAffix(tag: TagView) {
}
function isFirstView() {
try {
return (
selectedTag.value.path === "/dashboard" ||
selectedTag.value.fullPath === tagsViewStore.visitedViews[1].fullPath
);
} catch (err) {
return false;
}
return (
selectedTag.value.path === "/dashboard" ||
selectedTag.value.fullPath === tagsViewStore.visitedViews[1]?.fullPath
);
}
function isLastView() {
try {
return (
selectedTag.value.fullPath ===
tagsViewStore.visitedViews[tagsViewStore.visitedViews.length - 1].fullPath
);
} catch (err) {
return false;
}
return (
selectedTag.value.fullPath ===
tagsViewStore.visitedViews[tagsViewStore.visitedViews.length - 1]?.fullPath
);
}
function refreshSelectedTag(view: TagView) {

View File

@@ -14,33 +14,33 @@
<div v-if="layout === LayoutEnum.MIX" class="mix-container">
<div class="mix-container-sidebar">
<el-scrollbar>
<SidebarMenu :menu-list="mixLeftMenus" :base-path="activeTopMenuPath" />
<SidebarMenu :data="mixedLayoutLeftRoutes" :base-path="activeTopMenuPath" />
</el-scrollbar>
<div class="sidebar-toggle">
<hamburger :is-active="appStore.sidebar.opened" @toggle-click="toggleSidebar" />
</div>
</div>
<div :class="{ hasTagsView: showTagsView }" class="main-container">
<TagsView v-if="showTagsView" />
<div :class="{ hasTagsView: isShowTagsView }" class="main-container">
<TagsView v-if="isShowTagsView" />
<AppMain />
<Settings v-if="defaultSettings.showSettings" />
<!-- 返回顶部 -->
<el-backtop target=".app-main">
<svg-icon icon-class="backtop" size="24px" />
<div class="i-svg:backtop w-6 h-6" />
</el-backtop>
</div>
</div>
<!-- 左侧和顶部布局 -->
<div v-else :class="{ hasTagsView: showTagsView }" class="main-container">
<div v-else :class="{ hasTagsView: isShowTagsView }" class="main-container">
<NavBar v-if="layout === LayoutEnum.LEFT" />
<TagsView v-if="showTagsView" />
<TagsView v-if="isShowTagsView" />
<AppMain />
<Settings v-if="defaultSettings.showSettings" />
<!-- 返回顶部 -->
<el-backtop target=".app-main">
<svg-icon icon-class="backtop" size="24px" />
<div class="i-svg:backtop w-6 h-6" />
</el-backtop>
</div>
</div>
@@ -62,15 +62,15 @@ const width = useWindowSize().width;
const WIDTH_DESKTOP = 992; // 响应式布局容器固定宽度 大屏(>=1200px 中屏(>=992px 小屏(>=768px
const isMobile = computed(() => appStore.device === DeviceEnum.MOBILE);
const isOpenSidebar = computed(() => appStore.sidebar.opened);
const showTagsView = computed(() => settingsStore.tagsView); // 是否显示tagsView
const isShowTagsView = computed(() => settingsStore.tagsView); // 是否显示tagsView
const layout = computed(() => settingsStore.layout); // 布局模式 left top mix
const activeTopMenuPath = computed(() => appStore.activeTopMenuPath); // 顶部菜单激活path
const mixLeftMenus = computed(() => permissionStore.mixLeftMenus); // 混合布局左侧菜单
const mixedLayoutLeftRoutes = computed(() => permissionStore.mixedLayoutLeftRoutes); // 混合布局左侧菜单
watch(
() => activeTopMenuPath.value,
(newVal: string) => {
permissionStore.setMixLeftMenus(newVal);
permissionStore.setMixedLayoutLeftRoutes(newVal);
},
{
deep: true,
@@ -245,8 +245,10 @@ watch(route, () => {
}
.hideSidebar {
.main-container {
margin-left: $sidebar-width-collapsed;
&.layout-left {
.main-container {
margin-left: $sidebar-width-collapsed;
}
}
&.layout-top {
@@ -280,8 +282,8 @@ watch(route, () => {
&.mobile {
.sidebar-container {
pointer-events: none;
transform: translate3d(-$sidebar-width, 0, 0);
transition-duration: 0.3s;
transform: translate3d(-210px, 0, 0);
}
.main-container {
@@ -291,13 +293,10 @@ watch(route, () => {
}
.mobile {
.main-container {
.layout-mix,
.layout-top,
.layout-left {
margin-left: 0;
}
&.layout-top {
// 顶部模式全局变量修改
--el-menu-item-height: $navbar-height;
}
}
</style>