feat: 新增关闭右侧菜单动画

This commit is contained in:
何嘉悦 2025-05-20 23:49:15 +08:00
parent 1f5eeb2146
commit 7535bbb591
15 changed files with 438 additions and 112 deletions

View File

@ -24,6 +24,8 @@ export interface DesignConfigState {
collapseType: CollapseType;
// 是否折叠菜单
isCollapse: boolean;
// 头部折叠按钮是否被悬停
isCollapseHover: boolean;
}
export const themeColorList: string[] = [
@ -65,6 +67,8 @@ const design: DesignConfigState = {
collapseType: 'followSystem',
// 是否折叠菜单
isCollapse: false,
// 头部折叠按钮是否被悬停
isCollapseHover: false,
};
export default design;

View File

@ -1,6 +1,6 @@
import { COLLAPSE_THRESHOLD } from '@/config/index';
import { useWindowWidthObserver } from '@/hooks/useWindowWidthObserver';
import { useDesignStore } from '@/store/modules/design';
import { useDesignStore } from '@/store';
/**
* 便

86
src/hooks/useSafeArea.ts Normal file
View File

@ -0,0 +1,86 @@
import type { Ref } from 'vue';
type SafeAreaDirection = 'left' | 'right' | 'top' | 'bottom';
interface SafeAreaOptions {
direction: SafeAreaDirection; // 必选方向
size: number; // 安全区域尺寸(像素)
onChange?: (isInArea: boolean, e: MouseEvent) => void; // 状态变化回调
enabled?: Ref<boolean>; // 改为接收响应式变量Ref 类型)
}
export function useSafeArea(options: SafeAreaOptions): { isInSafeArea: Ref<boolean> } {
const { direction, size, onChange, enabled = ref(true) } = options; // 默认值改为 ref(true)
const isInSafeArea = ref(false);
let isThrottling = false;
// 方向检测逻辑(未修改)
const checkInArea = (e: MouseEvent) => {
const { clientX, clientY } = e;
const w = window.innerWidth;
const h = window.innerHeight;
return direction === 'left'
? clientX <= size
: direction === 'right'
? clientX >= w - size
: direction === 'top'
? clientY <= size
: direction === 'bottom'
? clientY >= h - size
: false;
};
// 鼠标移动处理函数(未修改)
const handleMouseMove = (e: MouseEvent) => {
if (!enabled.value || isThrottling)
return; // 直接使用 enabled.value 判断
requestAnimationFrame(() => {
const current = checkInArea(e);
if (current !== isInSafeArea.value) {
isInSafeArea.value = current;
onChange?.(current, e);
}
isThrottling = false;
});
isThrottling = true;
};
// 动态绑定/解绑事件(简化逻辑)
const toggleListener = (enable: boolean) => {
if (enable) {
window.addEventListener('mousemove', handleMouseMove);
}
else {
window.removeEventListener('mousemove', handleMouseMove);
isInSafeArea.value = false; // 关闭时重置状态
}
};
// 监听 enabled 的变化(直接监听传入的 ref
watch(
enabled,
(newVal) => {
toggleListener(newVal);
},
{ immediate: true },
); // 立即执行一次,处理初始状态
// 组件卸载时强制解绑
onUnmounted(() => {
toggleListener(false);
});
return { isInSafeArea };
}
// 使用示例
// 外部组件中
// const isListening = ref(false); // 响应式开关
// const { isInSafeArea } = useSafeArea({
// direction: 'left',
// size: 50,
// onChange: (isIn, e) => console.log('状态变化:', isIn),
// enabled: isListening // 直接传入响应式变量(无需 .value
// });

View File

@ -1,7 +1,7 @@
import type { MaybeRef } from 'vue';
import { onBeforeUnmount, ref, unref, watch } from 'vue';
import { COLLAPSE_THRESHOLD, SIDE_BAR_WIDTH } from '@/config/index';
import { useDesignStore } from '@/store/modules/design';
import { useDesignStore } from '@/store';
/**
*
@ -28,7 +28,6 @@ export function useWindowWidthObserver(
designStore.setCollapseFinal(true);
break;
case 'followSystem':
designStore.setCollapseFinal(!isAbove);
designStore.setCollapseFinal(!isAbove);
break;
case 'alwaysExpanded':
@ -44,7 +43,6 @@ export function useWindowWidthObserver(
break;
case 'narrowExpandWideCollapse':
designStore.setCollapseFinal(isAbove);
designStore.setCollapseFinal(isAbove);
}
console.log('最终的折叠状态:', designStore.isCollapse);

View File

@ -1,14 +1,27 @@
<!-- 纵向布局作为基础布局 -->
<script setup lang="ts">
import { useSafeArea } from '@/hooks/useSafeArea';
import { useWindowWidthObserver } from '@/hooks/useWindowWidthObserver';
import Aside from '@/layouts/components/Aside/index.vue';
import Header from '@/layouts/components/Header/index.vue';
import Main from '@/layouts/components/Main/index.vue';
import { useDesignStore } from '@/store/modules/design';
import { useDesignStore } from '@/store';
const designStore = useDesignStore();
console.log('每次加载全局的折叠状态', designStore.collapseType);
const isCollapse = computed(() => designStore.isCollapse);
/* 是否移入了安全区 */
useSafeArea({
direction: 'left',
size: 50,
onChange(isInSafeArea) {
// console.log('', isInSafeArea, isCollapse.value);
// true
designStore.isCollapseHover = isInSafeArea;
},
enabled: isCollapse, //
});
/** 监听窗口大小变化,折叠侧边栏 */
useWindowWidthObserver();
@ -20,9 +33,7 @@ useWindowWidthObserver();
<Header />
</el-header>
<el-container class="layout-container-main">
<Transition :class="designStore.pageAnimateType">
<Aside v-if="!designStore.isCollapse" class="layout-aside transition-all" />
</Transition>
<Aside />
<el-main class="layout-main">
<!-- 路由页面 -->
<Main />
@ -35,21 +46,12 @@ useWindowWidthObserver();
.layout-container {
width: 100%;
height: 100%;
position: relative;
.layout-header {
padding: 0;
}
.layout-aside {
overflow: hidden;
width: var(--sidebar-left-container-default-width, 0px);
height: 100%;
position: absolute;
z-index: 10;
top: 0;
left: 0;
}
.layout-main {
padding: 0;
}

View File

@ -1,7 +1,11 @@
<!-- Aside 侧边栏 -->
<script setup lang="ts">
import type { GroupableOptions } from 'vue-element-plus-x/types/Conversations';
import logo from '@/assets/images/logo.png';
import Collapse from '@/layouts/components/Header/components/Collapse.vue';
import { useDesignStore } from '@/store';
const designStore = useDesignStore();
/* 创建会话 开始 */
function handleCreatChat() {
@ -10,19 +14,125 @@ function handleCreatChat() {
/* 创建会话 结束 */
/* 会话组件 开始 */
const active = ref();
const conversationsList = ref([]);
const active = ref('m1');
const conversationsList = ref([
{
key: 'm1',
label: '菜单测试项目 1',
group: '工作',
},
{
key: 'm2',
label: '菜单测试项目 2',
disabled: true,
group: '工作',
},
{
key: 'm3',
label: '菜单测试项目 3',
group: '工作',
},
{
key: 'm4',
label: '菜单测试项目 4',
group: '学习',
},
{
key: 'm5',
label: '菜单测试项目 5',
group: '学习',
},
{
key: 'm6',
label: '菜单测试项目 6',
group: '学习',
},
{
key: 'm7',
label: '菜单测试项目 7',
group: '学习',
},
{
key: 'm8',
label: '菜单测试项目 8',
group: '个人',
},
{
key: 'm9',
label: '菜单测试项目 9',
group: '个人',
},
{
key: 'm10',
label: '菜单测试项目 10',
group: '个人',
},
{
key: 'm11',
label: '菜单测试项目 11',
group: '个人',
},
{
key: 'm12',
label: '菜单测试项目 12',
},
{
key: 'm13',
label: '菜单测试项目 13',
},
{
key: 'm14',
label: '菜单测试项目 14',
},
]);
//
const customGroupOptions: GroupableOptions = {
// > > >
sort: (a: any, b: any) => {
const order: Record<string, number> = { 学习: 0, 工作: 1, 个人: 2, 未分组: 3 };
const orderA = order[a] !== undefined ? order[a] : 999;
const orderB = order[b] !== undefined ? order[b] : 999;
return orderA - orderB;
},
};
function handleChange() {
console.log('点击了会话');
}
/* 会话组件 结束 */
/* 鼠标事件 开始 */
//
const isHoverSelf = ref(false);
function handleChangeMouse(type: string) {
if (designStore.isCollapse) {
if (type === 'enter') {
isHoverSelf.value = !!designStore.isCollapse;
}
else {
isHoverSelf.value = false;
}
}
else {
isHoverSelf.value = false;
}
}
/* 鼠标事件 结束 */
</script>
<template>
<div class="aside-container">
<div
class="aside-container"
:class="{
'aside-container-suspended': designStore.isCollapseHover,
'aside-container-collapse': designStore.isCollapse,
}"
@mouseenter.stop="handleChangeMouse('enter')"
@mouseleave.stop="handleChangeMouse('leave')"
>
<div class="aside-wrapper">
<div class="aside-header">
<div v-if="!designStore.isCollapse" class="aside-header">
<div class="flex items-center gap-8px hover:cursor-pointer" @click="handleCreatChat">
<el-image :src="logo" alt="logo" fit="cover" class="logo-img" />
<span class="logo-text max-w-150px text-overflow">Elemennt-Plus-X</span>
@ -30,6 +140,7 @@ function handleChange() {
<Collapse class="ml-auto" />
</div>
<div class="aside-body">
<div class="creat-chat-btn-wrapper">
<div class="creat-chat-btn" @click="handleCreatChat">
<el-icon class="add-icon">
@ -40,13 +151,17 @@ function handleChange() {
</div>
</div>
<div class="aside-body">
<div v-if="conversationsList.length > 0" class="flex h-full">
<div class="aside-content">
<div v-if="conversationsList.length > 0" class="conversations-wrap overflow-hidden">
<Conversations
:active="active"
v-model:active="active"
:items="conversationsList"
row-key="id"
label-key="sessionTitle"
:label-max-width="200"
:show-tooltip="true"
:tooltip-offset="35"
show-built-in-menu
:groupable="customGroupOptions"
row-key="key"
@change="handleChange"
/>
</div>
@ -55,22 +170,28 @@ function handleChange() {
</div>
</div>
</div>
</div>
</template>
<style scoped lang="scss">
//
.aside-container {
height: 100%;
position: relative;
user-select: none;
box-sizing: border-box;
left: 0;
top: 0;
position: absolute;
width: var(--sidebar-default-width);
pointer-events: auto;
background-color: var(--sidebar-background-color);
border-right: 1px solid rgba(0, 0, 0, 0.08);
border-right: 0.5px solid var(--s-color-border-tertiary, rgba(0, 0, 0, 0.08));
z-index: 11;
.aside-wrapper {
display: flex;
flex-direction: column;
height: 100%;
//
.aside-header {
height: 36px;
margin: 10px 12px 0px;
@ -103,6 +224,8 @@ function handleChange() {
}
}
//
.aside-body {
.creat-chat-btn-wrapper {
padding: 0 12px;
@ -112,7 +235,6 @@ function handleChange() {
padding: 8px 6px;
margin-top: 16px;
margin-bottom: 6px;
color: #0057ff;
background-color: rgba(0, 87, 255, 0.06);
border-radius: 12px;
border: 1px solid rgba(0, 102, 255, 0.15);
@ -143,13 +265,81 @@ function handleChange() {
}
}
.aside-body {
.aside-content {
display: flex;
flex-direction: column;
height: 100%;
flex: 1;
min-height: 0;
// -
.conversations-wrap {
height: calc(100vh - 110px);
}
}
}
}
}
//
.aside-container-collapse {
position: absolute;
opacity: 0;
//
transform: translateX(-100%);
top: 54px;
border-radius: 15px;
border: 1px solid rgba(0, 0, 0, 0.08);
box-shadow:
0px 10px 20px 0px rgba(0, 0, 0, 0.1),
0px 0px 1px 0px rgba(0, 0, 0, 0.15);
height: auto;
z-index: 22;
overflow: hidden;
max-height: calc(100% - 110px);
padding-bottom: 12px;
//
transition-property: opacity, transform;
transition-duration: 0.2s, 0.2s;
transition-timing-function: ease, ease;
transition-delay: 0.2s, 0s;
}
//
.hover-delay {
transition-delay: 0.2s;
}
//
.aside-container-suspended {
position: absolute;
opacity: 1;
transform: translateX(15px);
top: 54px;
border-radius: 15px;
border: 1px solid rgba(0, 0, 0, 0.08);
box-shadow:
0px 10px 20px 0px rgba(0, 0, 0, 0.1),
0px 0px 1px 0px rgba(0, 0, 0, 0.15);
height: auto;
z-index: 22;
overflow: hidden;
max-height: calc(100% - 110px);
padding-bottom: 12px;
transition: all 0.3s ease;
}
// 穿
:deep() {
//
.conversations-list {
background-color: transparent !important;
}
//
.conversation-group-title {
background-color: var(--sidebar-background-color) !important;
}
}
</style>

View File

@ -3,13 +3,14 @@
import SvgIcon from '@/components/SvgIcon/index.vue';
import { SIDE_BAR_WIDTH } from '@/config/index';
import { useCollapseToggle } from '@/hooks/useCollapseToggle';
import { useDesignStore } from '@/store/modules/design';
import { useDesignStore } from '@/store';
const { changeCollapse } = useCollapseToggle();
const designStore = useDesignStore();
function handleChangeCollapse() {
changeCollapse();
designStore.isCollapseHover = false;
if (!designStore.isCollapse) {
document.documentElement.style.setProperty(
`--sidebar-left-container-default-width`,

View File

@ -30,24 +30,34 @@ function handleClickTitle() {
</script>
<template>
<div class="w-full h-full flex flex-col justify-center">
<div class="box-border mr-20px">
<div
class="title-editing-container hover:bg-[rgba(0,0,0,.04)] cursor-pointer rounded-md p-4px flex items-center"
class="title-editing-container p-4px w-fit max-w-full flex items-center justify-start cursor-pointer select-none hover:bg-[rgba(0,0,0,.04)] cursor-pointer rounded-md font-size-14px"
@click="handleClickTitle"
>
<span class="font-size-14px text-overflow max-w-320px">标题编辑标题编辑标题编辑标题编辑标题编辑标题编辑标题编辑标题编辑</span>
<SvgIcon name="draft-line" size="14" />
<div class="text-overflow select-none pr-8px">
标题编辑标题编辑标题编辑标题编辑标题编辑标题编辑标题编辑标题编辑
</div>
<SvgIcon name="draft-line" size="14" class="flex-none c-gray-500" />
</div>
</div>
</div>
</template>
<style scoped lang="scss">
.title-editing-container {
transition: all 0.3s ease;
&:hover {
.svg-icon {
opacity: 1;
display: block;
}
}
.svg-icon {
transition: all 0.3s ease;
opacity: 0.5;
display: none;
}
}

View File

@ -1,7 +1,7 @@
<!-- Header 头部 -->
<script setup lang="ts">
import { SIDE_BAR_WIDTH } from '@/config/index';
import { useUserStore } from '@/store';
import { useDesignStore, useUserStore } from '@/store';
import Avatar from './components/Avatar.vue';
import Collapse from './components/Collapse.vue';
import CreateChat from './components/CreateChat.vue';
@ -9,13 +9,21 @@ import LoginBtn from './components/LoginBtn.vue';
import TitleEditing from './components/TitleEditing.vue';
const userStore = useUserStore();
const designStore = useDesignStore();
console.log('userStore', userStore.token);
onMounted(() => {
// ()
document.documentElement.style.setProperty(`--sidebar-default-width`, `${SIDE_BAR_WIDTH}px`);
if (designStore.isCollapse) {
document.documentElement.style.setProperty(`--sidebar-left-container-default-width`, ``);
}
else {
document.documentElement.style.setProperty(
`--sidebar-left-container-default-width`,
`${SIDE_BAR_WIDTH}px`,
);
}
});
</script>
@ -23,17 +31,26 @@ onMounted(() => {
<div class="header-container">
<div class="header-box relative z-10 top-0 left-0 right-0">
<div class="absolute left-0 right-0 top-0 bottom-0 flex items-center flex-row">
<div
class="overflow-hidden flex h-full items-center flex-row flex-1 w-fit flex-shrink-0 min-w-0"
>
<div class="w-full flex items-center flex-row">
<!-- 左边 -->
<div class="left-box flex h-full items-center pl-20px gap-12px flex-shrink-0 flex-row">
<div
v-if="designStore.isCollapse"
class="left-box flex h-full items-center pl-20px gap-12px flex-shrink-0 flex-row"
>
<Collapse />
<CreateChat />
<div class="w-0.5px h-30px bg-[rgba(217,217,217)]" />
</div>
<!-- 中间 -->
<div class="middle-box flex h-full items-center gap-12px flex-1 pl-12px">
<div class="w-0.5px h-30px bg-[rgba(217,217,217)]" />
<div class="middle-box flex-1 min-w-0 ml-12px">
<TitleEditing />
</div>
</div>
</div>
<!-- 右边 -->
<div class="right-box flex h-full items-center pr-20px flex-shrink-0 mr-auto flex-row">
@ -54,6 +71,7 @@ onMounted(() => {
display: flex;
flex-direction: column;
flex-shrink: 0;
.header-box {
height: var(--header-container-default-heigth);
width: 100%;

View File

@ -1,6 +1,6 @@
<!-- Main -->
<script setup lang="ts">
import { useDesignStore } from '@/store/modules/design';
import { useDesignStore } from '@/store';
import { useKeepAliveStore } from '@/store/modules/keepAlive';
const designStore = useDesignStore();

View File

@ -3,7 +3,7 @@
import type { LayoutType } from '@/config/design';
// import { useScreenStore } from '@/hooks/useScreen';
import LayoutVertical from '@/layouts/LayoutVertical/index.vue';
import { useDesignStore } from '@/store/modules/design';
import { useDesignStore } from '@/store';
//
const LayoutComponent: Record<LayoutType, Component> = {

View File

@ -30,7 +30,7 @@ function handleSend() {
@submit="handleSend"
>
<template #prefix>
<div class="flex items-center gap-8px">
<div class="flex-1 flex items-center gap-8px flex-none w-fit overflow-hidden">
<div
class="flex items-center gap-4px px-12px py-8px rounded-15px cursor-pointer font-size-12px border-1px border-gray border-solid hover:bg-[rgba(0,0,0,.04)]"
>
@ -58,7 +58,8 @@ function handleSend() {
<style scoped lang="scss">
.chat-home-container {
width: 100%;
padding: 0 16px;
width: calc(100% - 32px);
display: flex;
position: relative;
flex-direction: column;

View File

@ -7,4 +7,5 @@ store.use(piniaPluginPersistedstate);
export default store;
export * from './modules/design';
export * from './modules/user';

View File

@ -11,6 +11,7 @@ const {
layout: reLayout,
collapseType: reCollapseType,
isCollapse: reisCollapse,
isCollapseHover: reisCollapseHover,
} = designSetting;
export const useDesignStore = defineStore(
@ -47,6 +48,13 @@ export const useDesignStore = defineStore(
isCollapse.value = collapseFinal;
};
// 折叠按钮是否被悬停
const isCollapseHover = ref<boolean>(reisCollapseHover);
const setCollapseHover = (hover: boolean) => {
isCollapseHover.value = hover;
};
return {
darkMode,
setDarkMode,
@ -60,6 +68,8 @@ export const useDesignStore = defineStore(
setCollapseType,
isCollapse,
setCollapseFinal,
isCollapseHover,
setCollapseHover,
};
},
{

View File

@ -3,18 +3,23 @@
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
// biome-ignore lint: disable
export {};
export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
ElAside: typeof import('element-plus/es')['ElAside']
ElAvatar: typeof import('element-plus/es')['ElAvatar']
ElContainer: typeof import('element-plus/es')['ElContainer']
ElEmpty: typeof import('element-plus/es')['ElEmpty']
ElHeader: typeof import('element-plus/es')['ElHeader']
ElIcon: typeof import('element-plus/es')['ElIcon']
ElImage: typeof import('element-plus/es')['ElImage']
ElInput: typeof import('element-plus/es')['ElInput']
ElMain: typeof import('element-plus/es')['ElMain']
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
ElTabPane: typeof import('element-plus/es')['ElTabPane']
ElTabs: typeof import('element-plus/es')['ElTabs']
IconSelect: typeof import('./../src/components/IconSelect/index.vue')['default']
Popover: typeof import('./../src/components/Popover/index.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']