Merge branch 'master' into master
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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: () => [],
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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); // 跳转到左侧第一个菜单
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user