Files
youlai-app/src/composables/useNavbar.ts
Mickey wu 325316d0b0 fix: 修复类型定义及多处逻辑缺陷
- 补充字典项备注字段及部门排序必填约束
- 使用 Ref/ComputedRef 替代 ReturnType 简化类型
- 兜底胶囊按钮默认尺寸,避免获取失败时导航栏异常
- 移除不可靠的新版 API 降级调用,统一使用 getSystemInfoSync
- 修复防抖函数定时器未清空及类型不严谨问题
2026-05-25 21:55:04 +08:00

248 lines
7.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { ref, computed, type Ref, type ComputedRef } from "vue";
/**
* 微信小程序导航栏高度计算
*
* 参考 uni-ui 官方实现https://github.com/dcloudio/uni-ui
*
* 重要说明:
* 1. CSS 变量 --status-bar-height 在某些情况下不准确(小程序端固定 25px
* 2. 推荐使用 uni.getSystemInfoSync().statusBarHeight 动态获取
* 3. 微信小程序需要考虑胶囊按钮位置
* 4. H5 没有状态栏statusBarHeight 为 0
*
* 导航栏结构:
* ┌─────────────────────────────┐
* │ 状态栏区域 │ statusBarHeight
* ├─────────────────────────────┤
* │ 上边距 │ menu.top - statusBarHeight
* ├─────────────────────────────┤
* │ 胶囊按钮 │ menu.height
* ├─────────────────────────────┤
* │ 下边距(与上边距相等) │
* └─────────────────────────────┘
*
* 导航栏高度计算公式uni-ui 官方):
* navBarHeight = menu.height + (menu.top - statusBarHeight) * 2
* totalHeight = statusBarHeight + navBarHeight
*/
interface MenuButtonRect {
width: number;
height: number;
top: number;
right: number;
bottom: number;
left: number;
}
export interface UseNavbarOptions {
/** 导航栏内容高度,默认 44 */
navBarHeight?: number;
/** 是否有 TabBar默认 false */
hasTabbar?: boolean;
}
export interface UseNavbarReturn {
// 高度信息
statusBarHeight: Ref<number>;
navBarHeight: number;
totalHeight: ComputedRef<number>;
contentPaddingTop: ComputedRef<string>;
safeAreaBottom: Ref<number>;
// 胶囊按钮信息
menuButton: Ref<MenuButtonRect | null>;
menuButtonRightGap: ComputedRef<number>;
menuButtonLeft: ComputedRef<number>;
menuButtonWidth: ComputedRef<number>;
contentWidth: ComputedRef<number>;
// 平台信息
platform: Ref<string>;
windowWidth: Ref<number>;
// 方法
init: () => void;
}
/**
* 导航栏高度计算 Hook
*
* @param options 配置选项
*/
export function useNavbar(options: UseNavbarOptions = {}): UseNavbarReturn {
const { navBarHeight = 44 } = options;
// 状态栏高度
const statusBarHeight = ref(0);
// 胶囊按钮信息
const menuButton = ref<MenuButtonRect | null>(null);
// 安全区域底部高度
const safeAreaBottom = ref(0);
// 窗口宽度
const windowWidth = ref(375);
// 平台标识
const platform = ref("");
// 计算导航栏总高度(参考 uni-ui 官方实现)
const totalHeight = computed(() => {
// #ifdef MP-WEIXIN
// 微信小程序:使用胶囊按钮精确计算
if (menuButton.value && menuButton.value.height > 0 && menuButton.value.top > 0) {
const { top, height } = menuButton.value;
// 导航栏高度 = 胶囊按钮高度 + 上下边距
// 上下边距 = (胶囊按钮top - 状态栏高度) * 2
const spaceHeight = top - statusBarHeight.value;
return statusBarHeight.value + height + spaceHeight * 2;
}
// #endif
// H5 / App状态栏高度 + 导航栏高度
return statusBarHeight.value + navBarHeight;
});
// 页面内容区域的 paddingTop用于避开固定导航栏
const contentPaddingTop = computed(() => `${totalHeight.value}px`);
// 胶囊按钮右侧距离屏幕右边的距离(用于避开胶囊按钮)
const menuButtonRightGap = computed(() => {
// #ifdef MP-WEIXIN
if (menuButton.value && menuButton.value.right > 0) {
return windowWidth.value - menuButton.value.right;
}
// #endif
// H5 / App 不需要避让胶囊
return 10;
});
// 胶囊按钮左侧距离屏幕左边的距离
const menuButtonLeft = computed(() => {
// #ifdef MP-WEIXIN
if (menuButton.value) {
return menuButton.value.left;
}
// #endif
return 0;
});
// 胶囊按钮宽度
const menuButtonWidth = computed(() => {
// #ifdef MP-WEIXIN
if (menuButton.value) {
return menuButton.value.width;
}
// #endif
return 0;
});
// 页面内容区域可用宽度(避开胶囊按钮)
const contentWidth = computed(() => {
// #ifdef MP-WEIXIN
if (menuButton.value) {
// 内容区域宽度 = 胶囊按钮左侧距离 - 间距
const gap = menuButtonRightGap.value;
return menuButton.value.left - gap;
}
// #endif
return windowWidth.value;
});
// #ifdef MP-WEIXIN
const isValidMenuButtonRect = (rect: ReturnType<typeof uni.getMenuButtonBoundingClientRect>) => {
return !!rect && rect.width > 0 && rect.height > 0 && rect.top > 0 && rect.right > 0;
};
const setMenuButtonRectWithRetry = (maxRetry = 6, delayMs = 50) => {
try {
const rect = uni.getMenuButtonBoundingClientRect();
if (isValidMenuButtonRect(rect)) {
menuButton.value = {
width: rect.width,
height: rect.height,
top: rect.top,
right: rect.right,
bottom: rect.bottom,
left: rect.left,
};
return;
}
if (maxRetry > 0) {
setTimeout(() => {
setMenuButtonRectWithRetry(maxRetry - 1, delayMs);
}, delayMs);
} else {
console.warn("getMenuButtonBoundingClientRect 返回值无效,使用默认值");
// 兜底:设置合理的默认胶囊按钮尺寸,避免导航栏高度为 0
menuButton.value = {
width: 87,
height: 32,
top: statusBarHeight.value + 6,
right: windowWidth.value - 10,
bottom: statusBarHeight.value + 38,
left: windowWidth.value - 97,
};
}
} catch (e) {
console.warn("获取胶囊按钮位置失败,使用默认值", e);
// 兜底:设置合理的默认胶囊按钮尺寸
menuButton.value = {
width: 87,
height: 32,
top: statusBarHeight.value + 6,
right: windowWidth.value - 10,
bottom: statusBarHeight.value + 38,
left: windowWidth.value - 97,
};
}
};
// #endif
// 初始化
const init = () => {
try {
const systemInfo = uni.getSystemInfoSync();
statusBarHeight.value = systemInfo.statusBarHeight || 0;
safeAreaBottom.value = systemInfo.safeAreaInsets?.bottom || 0;
windowWidth.value = systemInfo.windowWidth || 375;
platform.value = systemInfo.platform || "";
// #ifdef MP-WEIXIN
// 微信小程序:获取胶囊按钮位置
setMenuButtonRectWithRetry();
// #endif
} catch (e) {
console.warn("获取系统信息失败", e);
}
};
// setup 阶段同步调用,避免首次渲染闪烁
init();
return {
// 高度信息
statusBarHeight,
navBarHeight,
totalHeight,
contentPaddingTop,
safeAreaBottom,
// 胶囊按钮信息
menuButton,
menuButtonRightGap,
menuButtonLeft,
menuButtonWidth,
contentWidth,
// 平台信息
platform,
windowWidth,
// 方法
init,
};
}