feat: 前端搭建
This commit is contained in:
61
Yi.Ai.Vue3/src/hooks/useCollapseToggle.ts
Normal file
61
Yi.Ai.Vue3/src/hooks/useCollapseToggle.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { COLLAPSE_THRESHOLD } from '@/config/index';
|
||||
import { useWindowWidthObserver } from '@/hooks/useWindowWidthObserver';
|
||||
import { useDesignStore } from '@/stores';
|
||||
|
||||
/**
|
||||
* 侧边栏折叠切换逻辑组合式函数 方便多个页面调用
|
||||
* @param threshold 折叠阈值(可选,默认使用全局配置)
|
||||
*/
|
||||
export function useCollapseToggle(threshold: number = COLLAPSE_THRESHOLD) {
|
||||
const designStore = useDesignStore();
|
||||
// 获取当前视口宽度是否大于阈值,但不做响应式处理,传入一个空函数执行
|
||||
const { isAboveThreshold } = useWindowWidthObserver(threshold, () => {});
|
||||
|
||||
/** 核心折叠切换方法 */
|
||||
const changeCollapse = () => {
|
||||
// 切换最终折叠状态
|
||||
designStore.setCollapse(!designStore.isCollapse);
|
||||
|
||||
if (isAboveThreshold.value) {
|
||||
// 宽屏逻辑
|
||||
if (designStore.isCollapse) {
|
||||
designStore.setCollapseType('alwaysCollapsed');
|
||||
}
|
||||
else {
|
||||
designStore.setCollapseType(
|
||||
designStore.collapseType === 'narrowExpandWideCollapse'
|
||||
? 'alwaysExpanded'
|
||||
: 'followSystem',
|
||||
);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// 窄屏逻辑
|
||||
if (designStore.isCollapse) {
|
||||
designStore.setCollapseType('followSystem');
|
||||
}
|
||||
else {
|
||||
designStore.setCollapseType(
|
||||
designStore.collapseType === 'alwaysCollapsed'
|
||||
? 'narrowExpandWideCollapse'
|
||||
: 'alwaysExpanded',
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
changeCollapse,
|
||||
};
|
||||
}
|
||||
|
||||
// 使用示例与特性说明
|
||||
// <script setup lang="ts">
|
||||
// import { useCollapseToggle } from '@/hooks/useCollapseToggle';
|
||||
// import { COLLAPSE_THRESHOLD } from '@/config/index'; (可传,不传入全局配置走)
|
||||
// const { changeCollapseIcon } = useCollapseToggle(designStore, COLLAPSE_THRESHOLD);
|
||||
// </script>
|
||||
// <template>
|
||||
// <!-- 其他页面的按钮 -->
|
||||
// <button @click="changeCollapseIcon">切换侧边栏</button>
|
||||
// </template>
|
||||
87
Yi.Ai.Vue3/src/hooks/useSafeArea.ts
Normal file
87
Yi.Ai.Vue3/src/hooks/useSafeArea.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
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 };
|
||||
}
|
||||
|
||||
// 使用示例
|
||||
// 外部组件中
|
||||
// import { useSafeArea } from '@/hooks/useSafeArea';
|
||||
|
||||
// const isListening = ref(false); // 响应式开关
|
||||
// const { isInSafeArea } = useSafeArea({
|
||||
// direction: 'left',
|
||||
// size: 50,
|
||||
// onChange: (isIn, e) => console.log('状态变化:', isIn),
|
||||
// enabled: isListening // 直接传入响应式变量(无需 .value)
|
||||
// });
|
||||
36
Yi.Ai.Vue3/src/hooks/useScreen.ts
Normal file
36
Yi.Ai.Vue3/src/hooks/useScreen.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { useBreakpoints } from '@vueuse/core';
|
||||
import { reactive } from 'vue';
|
||||
|
||||
export const breakpointsEnum = {
|
||||
xl: 1600,
|
||||
lg: 1199,
|
||||
md: 991,
|
||||
sm: 767,
|
||||
xs: 575,
|
||||
};
|
||||
|
||||
export function useScreenStore() {
|
||||
const breakpoints = reactive(useBreakpoints(breakpointsEnum));
|
||||
// 手机端
|
||||
const isMobile = breakpoints.smaller('sm');
|
||||
// pad端
|
||||
const isPad = breakpoints.between('sm', 'md');
|
||||
// pc端
|
||||
const isDesktop = breakpoints.greater('md');
|
||||
// 登录页面一
|
||||
const isScreen = breakpoints.smaller('lg');
|
||||
return {
|
||||
breakpoints,
|
||||
isMobile,
|
||||
isPad,
|
||||
isDesktop,
|
||||
isScreen,
|
||||
};
|
||||
}
|
||||
|
||||
// 使用步骤
|
||||
/** 适配移动端开始 */
|
||||
// import { useScreenStore } from "@/hooks/useScreen";
|
||||
// 获取当前为[移动端、IPad、PC端]仓库,可以使用 watchEffect(() => {}) 进行监听。
|
||||
// const { isMobile } = useScreenStore();
|
||||
/** 适配移动端结束 */
|
||||
36
Yi.Ai.Vue3/src/hooks/useTimeGreeting.ts
Normal file
36
Yi.Ai.Vue3/src/hooks/useTimeGreeting.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { ref } from 'vue';
|
||||
|
||||
type TimeGreeting = '凌晨' | '清晨' | '早上' | '中午' | '下午' | '傍晚' | '晚上';
|
||||
|
||||
// 时间分段配置(按顺序排列,find会返回第一个匹配项)
|
||||
const timeRanges: Array<[start: number, end: number, label: TimeGreeting]> = [
|
||||
[0, 5, '凌晨'],
|
||||
[5, 7, '清晨'],
|
||||
[7, 12, '早上'],
|
||||
[12, 14, '中午'],
|
||||
[14, 18, '下午'],
|
||||
[18, 21, '傍晚'],
|
||||
[21, 24, '晚上'],
|
||||
];
|
||||
|
||||
/**
|
||||
* 获取当前时段问候语(Vue组合式函数)
|
||||
* @returns 响应式的时段问候语
|
||||
*/
|
||||
export function useTimeGreeting() {
|
||||
// 直接计算初始值(合并初始化逻辑)
|
||||
const currentHour = new Date().getHours();
|
||||
const greeting = timeRanges.find(([s, e]) => currentHour >= s && currentHour < e)?.[2] || '晚上';
|
||||
|
||||
// 使用ref保持响应式(即使不更新,组件仍可正确绑定)
|
||||
return ref<TimeGreeting>(greeting);
|
||||
}
|
||||
|
||||
// 示例用法(在Vue组件中)
|
||||
// <script setup lang="ts">
|
||||
// import { useTimeGreeting } from '@/hooks/useTimeGreeting';
|
||||
// const timeGreeting = useTimeGreeting();
|
||||
// </script>
|
||||
// <template>
|
||||
// <div>{{ timeGreeting }}好,欢迎~</div>
|
||||
// </template>
|
||||
150
Yi.Ai.Vue3/src/hooks/useWindowWidthObserver.ts
Normal file
150
Yi.Ai.Vue3/src/hooks/useWindowWidthObserver.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
import type { MaybeRef } from 'vue';
|
||||
import { onBeforeUnmount, ref, unref, watch } from 'vue';
|
||||
import { COLLAPSE_THRESHOLD, SIDE_BAR_WIDTH } from '@/config/index';
|
||||
import { useDesignStore } from '@/stores';
|
||||
|
||||
/**
|
||||
* 这里逻辑是研究豆包的折叠逻辑后,设计的折叠方法
|
||||
* 基于ResizeObserver的窗口宽度监听hooks(高性能实时监控)
|
||||
* @param threshold 宽度阈值(默认600px,支持响应式)
|
||||
* @param onChange 自定义回调(传入则覆盖默认逻辑,参数:当前视口宽度是否超过阈值)
|
||||
* @returns {object} 包含卸载监听的方法及当前状态
|
||||
*/
|
||||
export function useWindowWidthObserver(
|
||||
threshold: MaybeRef<number> = COLLAPSE_THRESHOLD,
|
||||
onChange?: (isAboveThreshold: boolean) => void,
|
||||
) {
|
||||
const designStore = useDesignStore();
|
||||
const currentWidth = ref(window.innerWidth);
|
||||
const isAboveThreshold = ref(false);
|
||||
const thresholdRef = ref(threshold);
|
||||
let prevIsAbove = false; // 记录上一次状态,避免重复触发
|
||||
|
||||
// 默认逻辑:修改全局折叠状态
|
||||
const updateCollapseState = (isAbove: boolean) => {
|
||||
// 判断当前的折叠状态
|
||||
switch (designStore.collapseType) {
|
||||
case 'alwaysCollapsed':
|
||||
designStore.setCollapse(true);
|
||||
break;
|
||||
case 'followSystem':
|
||||
designStore.setCollapse(!isAbove);
|
||||
break;
|
||||
case 'alwaysExpanded':
|
||||
designStore.setCollapse(false);
|
||||
if (isAbove) {
|
||||
// 大于的时候执行关闭动画 (豆包是有的,第一版本暂未添加)
|
||||
console.log('执行关闭动画');
|
||||
}
|
||||
else {
|
||||
// 小于的时候执行打开动画 (豆包是有的,第一版本暂未添加)
|
||||
console.log('小于的时候执行打开动画');
|
||||
}
|
||||
break;
|
||||
case 'narrowExpandWideCollapse':
|
||||
designStore.setCollapse(isAbove);
|
||||
}
|
||||
// console.log('最终的折叠状态:', designStore.isCollapse);
|
||||
|
||||
if (!designStore.isCollapse) {
|
||||
document.documentElement.style.setProperty(
|
||||
`--sidebar-left-container-default-width`,
|
||||
`${SIDE_BAR_WIDTH}px`,
|
||||
);
|
||||
}
|
||||
else {
|
||||
document.documentElement.style.setProperty(`--sidebar-left-container-default-width`, ``);
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化状态计算
|
||||
const updateState = () => {
|
||||
const thresholdVal = unref(threshold);
|
||||
const current = unref(currentWidth);
|
||||
const newIsAbove = current > thresholdVal; // 正确比较两个 number 类型
|
||||
|
||||
if (newIsAbove !== prevIsAbove) {
|
||||
isAboveThreshold.value = newIsAbove;
|
||||
prevIsAbove = newIsAbove;
|
||||
|
||||
// 触发用户回调或默认逻辑
|
||||
if (onChange) {
|
||||
onChange(newIsAbove);
|
||||
}
|
||||
else {
|
||||
updateCollapseState(newIsAbove);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 创建ResizeObserver监听根元素宽度(直接反映视口宽度)
|
||||
const observer = new ResizeObserver(([entry]) => {
|
||||
currentWidth.value = entry.contentRect.width; // 实时获取最新宽度
|
||||
updateState(); // 立即更新状态并触发逻辑
|
||||
});
|
||||
|
||||
// 监听根元素(HTML元素)的尺寸变化
|
||||
observer.observe(document.documentElement);
|
||||
|
||||
// 监听阈值变化(支持响应式阈值)
|
||||
watch(thresholdRef, () => {
|
||||
updateState(); // 阈值变化时重新计算状态
|
||||
});
|
||||
|
||||
// 卸载监听的方法
|
||||
const unmount = () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
|
||||
// 组件卸载时自动清理
|
||||
onBeforeUnmount(unmount);
|
||||
|
||||
return {
|
||||
unmount,
|
||||
currentWidth, // 实时当前宽度(Ref)
|
||||
isAboveThreshold, // 当前是否超过阈值(Ref)
|
||||
};
|
||||
}
|
||||
|
||||
// 使用示例与特性说明
|
||||
// 1. 基础使用(保留默认折叠逻辑)
|
||||
// <script setup lang="ts">
|
||||
// import { useWindowWidthObserver } from '@/hooks/useWindowWidthObserver';
|
||||
// // 使用默认阈值600px,自动修改全局折叠状态
|
||||
// useWindowWidthObserver();
|
||||
// </script>
|
||||
|
||||
// 2. 自定义阈值(例如 768px)
|
||||
// <script setup lang="ts">
|
||||
// import { useWindowWidthObserver } from '@/hooks/useWindowWidthObserver';
|
||||
// // 自定义阈值768px,仍使用默认折叠逻辑
|
||||
// useWindowWidthObserver(768);
|
||||
// </script>
|
||||
|
||||
// 3. 自定义回调(覆盖默认逻辑)
|
||||
// <script setup lang="ts">
|
||||
// import { useWindowWidthObserver } from '@/hooks/useWindowWidthObserver';
|
||||
// // 传入自定义回调,例如控制其他组件显隐
|
||||
// const { unmount, isAboveThreshold } = useWindowWidthObserver(992, (isAbove) => {
|
||||
// console.log(`当前窗口是否大于992px:${isAbove}`);
|
||||
// // 可在此处执行任意自定义逻辑(如修改其他状态、控制组件等)
|
||||
// });
|
||||
// </script>
|
||||
|
||||
// 4. 响应式阈值(动态调整阈值)
|
||||
// <script setup lang="ts">
|
||||
// import { ref } from 'vue';
|
||||
// import { useWindowWidthObserver } from '@/hooks/useWindowWidthObserver';
|
||||
// // 动态阈值(例如根据路由参数变化)
|
||||
// const dynamicThreshold = ref(600);
|
||||
|
||||
// // 监听阈值变化,自动触发逻辑更新
|
||||
// useWindowWidthObserver(dynamicThreshold, (isAbove) => {
|
||||
// console.log(`当前阈值${dynamicThreshold.value},是否超过:${isAbove}`);
|
||||
// });
|
||||
|
||||
// // 模拟阈值变化(实际场景中可能由用户操作或路由变化触发)
|
||||
// setTimeout(() => {
|
||||
// dynamicThreshold.value = 768;
|
||||
// }, 3000);
|
||||
// </script>
|
||||
Reference in New Issue
Block a user