feat: 添加自定义导航栏组件和相关 composables
This commit is contained in:
141
src/composables/useLoading.ts
Normal file
141
src/composables/useLoading.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
import { ref, computed } from "vue"
|
||||
|
||||
/**
|
||||
* 加载状态管理
|
||||
*
|
||||
* 提供全局或局部的加载状态管理,支持多个加载任务的并发控制。
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // 基本使用
|
||||
* const loading = useLoading()
|
||||
* loading.start()
|
||||
* await fetchData()
|
||||
* loading.stop()
|
||||
*
|
||||
* // 带消息
|
||||
* loading.start('正在加载数据...')
|
||||
*
|
||||
* // 异步包装
|
||||
* const result = await loading.wrap(fetchData())
|
||||
*
|
||||
* // 独立实例
|
||||
* const localLoading = useLoading({ singleton: false })
|
||||
* ```
|
||||
*/
|
||||
|
||||
export interface UseLoadingOptions {
|
||||
/** 是否使用全局单例,默认 true */
|
||||
singleton?: boolean
|
||||
/** 初始加载状态,默认 false */
|
||||
initial?: boolean
|
||||
/** 加载提示消息 */
|
||||
message?: string
|
||||
}
|
||||
|
||||
export interface UseLoadingReturn {
|
||||
/** 是否处于加载状态 */
|
||||
isLoading: ReturnType<typeof computed<boolean>>
|
||||
/** 加载计数(支持并发) */
|
||||
loadingCount: ReturnType<typeof ref<number>>
|
||||
/** 当前加载消息 */
|
||||
message: ReturnType<typeof ref<string>>
|
||||
/** 开始加载 */
|
||||
start: (msg?: string) => void
|
||||
/** 结束加载 */
|
||||
stop: () => void
|
||||
/** 包装异步函数,自动管理加载状态 */
|
||||
wrap: <T>(promise: Promise<T>) => Promise<T>
|
||||
/** 设置加载消息 */
|
||||
setMessage: (msg: string) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建加载状态实例
|
||||
*/
|
||||
function createLoadingState(options: UseLoadingOptions = {}) {
|
||||
const { initial = false, message: initialMessage = "" } = options
|
||||
|
||||
const loadingCount = ref(0)
|
||||
const message = ref(initialMessage)
|
||||
|
||||
const isLoading = computed(() => loadingCount.value > 0)
|
||||
|
||||
/**
|
||||
* 开始加载
|
||||
* @param msg 加载提示消息
|
||||
*/
|
||||
const start = (msg?: string): void => {
|
||||
loadingCount.value++
|
||||
if (msg) {
|
||||
message.value = msg
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 结束加载
|
||||
*/
|
||||
const stop = (): void => {
|
||||
if (loadingCount.value > 0) {
|
||||
loadingCount.value--
|
||||
}
|
||||
if (loadingCount.value === 0) {
|
||||
message.value = ""
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 包装异步函数,自动管理加载状态
|
||||
* @param promise 异步 Promise
|
||||
*/
|
||||
const wrap = async <T>(promise: Promise<T>): Promise<T> => {
|
||||
try {
|
||||
start()
|
||||
return await promise
|
||||
} finally {
|
||||
stop()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置加载消息
|
||||
* @param msg 消息内容
|
||||
*/
|
||||
const setMessage = (msg: string): void => {
|
||||
message.value = msg
|
||||
}
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
loadingCount,
|
||||
message,
|
||||
start,
|
||||
stop,
|
||||
wrap,
|
||||
setMessage,
|
||||
}
|
||||
}
|
||||
|
||||
/** 全局单例实例 */
|
||||
let loadingInstance: UseLoadingReturn | null = null
|
||||
|
||||
/**
|
||||
* 加载状态管理 Hook
|
||||
*
|
||||
* 默认使用全局单例模式,确保整个应用共享同一加载状态。
|
||||
* 如需独立状态(如组件内部的局部加载),可传入 `{ singleton: false }`。
|
||||
*
|
||||
* @param options 配置选项
|
||||
*/
|
||||
export function useLoading(options?: UseLoadingOptions): UseLoadingReturn {
|
||||
const { singleton = true } = options ?? {}
|
||||
|
||||
if (singleton) {
|
||||
if (!loadingInstance) {
|
||||
loadingInstance = createLoadingState(options)
|
||||
}
|
||||
return loadingInstance
|
||||
}
|
||||
|
||||
return createLoadingState(options)
|
||||
}
|
||||
242
src/composables/useNavbar.ts
Normal file
242
src/composables/useNavbar.ts
Normal file
@@ -0,0 +1,242 @@
|
||||
import { ref, computed, onMounted } 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, hasTabbar = false } = 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 返回值无效,使用默认值");
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("获取胶囊按钮位置失败,使用默认值", e);
|
||||
}
|
||||
};
|
||||
// #endif
|
||||
|
||||
// 初始化
|
||||
const init = () => {
|
||||
try {
|
||||
// 优先使用新版 API(uni.getWindowInfo),降级到 uni.getSystemInfoSync
|
||||
let systemInfo: UniApp.GetSystemInfoSyncSuccess;
|
||||
try {
|
||||
// @ts-expect-error uni.getWindowInfo 是新版 API
|
||||
systemInfo = uni.getWindowInfo?.() || uni.getSystemInfoSync();
|
||||
} catch {
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
// 在 onMounted 中调用,确保页面渲染完成
|
||||
onMounted(() => {
|
||||
init();
|
||||
});
|
||||
|
||||
return {
|
||||
// 高度信息
|
||||
statusBarHeight,
|
||||
navBarHeight,
|
||||
totalHeight,
|
||||
contentPaddingTop,
|
||||
safeAreaBottom,
|
||||
|
||||
// 胶囊按钮信息
|
||||
menuButton,
|
||||
menuButtonRightGap,
|
||||
menuButtonLeft,
|
||||
menuButtonWidth,
|
||||
contentWidth,
|
||||
|
||||
// 平台信息
|
||||
platform,
|
||||
windowWidth,
|
||||
|
||||
// 方法
|
||||
init,
|
||||
};
|
||||
}
|
||||
|
||||
// 类型导出
|
||||
type Ref<T> = import("vue").Ref<T>;
|
||||
type ComputedRef<T> = import("vue").ComputedRef;
|
||||
302
src/composables/useRequest.ts
Normal file
302
src/composables/useRequest.ts
Normal file
@@ -0,0 +1,302 @@
|
||||
import { ref, computed, shallowRef, type Ref, type ComputedRef } from "vue"
|
||||
|
||||
/**
|
||||
* 请求状态管理
|
||||
*
|
||||
* 封装异步请求的加载状态、错误处理和数据缓存,提供统一的请求管理方式。
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const { data, loading, error, execute } = useRequest(
|
||||
* () => UserAPI.getUserInfo()
|
||||
* )
|
||||
*
|
||||
* // 立即执行
|
||||
* const { data } = useRequest(() => UserAPI.getUserInfo(), { immediate: true })
|
||||
*
|
||||
* // 手动执行
|
||||
* await execute()
|
||||
*
|
||||
* // 刷新数据
|
||||
* await refresh()
|
||||
* ```
|
||||
*/
|
||||
|
||||
export interface UseRequestOptions<T> {
|
||||
/** 是否立即执行,默认 false */
|
||||
immediate?: boolean
|
||||
/** 初始数据 */
|
||||
initialData?: T | null
|
||||
/** 请求前的回调 */
|
||||
onBefore?: () => void
|
||||
/** 请求成功的回调 */
|
||||
onSuccess?: (data: T) => void
|
||||
/** 请求失败的回调 */
|
||||
onError?: (error: Error) => void
|
||||
/** 请求完成的回调(无论成功失败) */
|
||||
onFinally?: () => void
|
||||
/** 是否重置数据在重新请求时,默认 false */
|
||||
resetOnExecute?: boolean
|
||||
}
|
||||
|
||||
export interface UseRequestReturn<T> {
|
||||
/** 响应数据 */
|
||||
data: Ref<T | null>
|
||||
/** 是否正在加载 */
|
||||
loading: ComputedRef<boolean>
|
||||
/** 错误信息 */
|
||||
error: Ref<Error | null>
|
||||
/** 是否已请求过(用于区分初始状态和请求后状态) */
|
||||
hasExecuted: Ref<boolean>
|
||||
/** 执行请求 */
|
||||
execute: () => Promise<T | null>
|
||||
/** 刷新数据(重新执行请求) */
|
||||
refresh: () => Promise<T | null>
|
||||
/** 重置状态 */
|
||||
reset: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求状态管理 Hook
|
||||
*
|
||||
* @param requestFn 请求函数
|
||||
* @param options 配置选项
|
||||
*/
|
||||
export function useRequest<T>(
|
||||
requestFn: () => Promise<T>,
|
||||
options: UseRequestOptions<T> = {}
|
||||
): UseRequestReturn<T> {
|
||||
const {
|
||||
immediate = false,
|
||||
initialData = null,
|
||||
onBefore,
|
||||
onSuccess,
|
||||
onError,
|
||||
onFinally,
|
||||
resetOnExecute = false,
|
||||
} = options
|
||||
|
||||
// 使用 shallowRef 避免深层响应式,提升性能
|
||||
const data = shallowRef<T | null>(initialData) as Ref<T | null>
|
||||
const error = ref<Error | null>(null)
|
||||
const loadingCount = ref(0)
|
||||
const hasExecuted = ref(false)
|
||||
|
||||
const loading = computed(() => loadingCount.value > 0)
|
||||
|
||||
/**
|
||||
* 执行请求
|
||||
*/
|
||||
const execute = async (): Promise<T | null> => {
|
||||
// 重置状态
|
||||
if (resetOnExecute) {
|
||||
data.value = initialData
|
||||
error.value = null
|
||||
}
|
||||
|
||||
loadingCount.value++
|
||||
hasExecuted.value = true
|
||||
|
||||
try {
|
||||
onBefore?.()
|
||||
const result = await requestFn()
|
||||
data.value = result
|
||||
error.value = null
|
||||
onSuccess?.(result)
|
||||
return result
|
||||
} catch (e) {
|
||||
const err = e instanceof Error ? e : new Error(String(e))
|
||||
error.value = err
|
||||
onError?.(err)
|
||||
return null
|
||||
} finally {
|
||||
loadingCount.value--
|
||||
onFinally?.()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新数据
|
||||
*/
|
||||
const refresh = async (): Promise<T | null> => {
|
||||
return execute()
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置状态
|
||||
*/
|
||||
const reset = (): void => {
|
||||
data.value = initialData
|
||||
error.value = null
|
||||
loadingCount.value = 0
|
||||
hasExecuted.value = false
|
||||
}
|
||||
|
||||
// 立即执行
|
||||
if (immediate) {
|
||||
execute()
|
||||
}
|
||||
|
||||
return {
|
||||
data,
|
||||
loading,
|
||||
error,
|
||||
hasExecuted,
|
||||
execute,
|
||||
refresh,
|
||||
reset,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页请求状态管理
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const { list, loading, loadMore, refresh, hasMore } = usePagination(
|
||||
* (params) => UserAPI.getUserPage({ ...params, status: 1 }),
|
||||
* { pageSize: 20 }
|
||||
* )
|
||||
*
|
||||
* // 加载更多
|
||||
* await loadMore()
|
||||
*
|
||||
* // 刷新
|
||||
* await refresh()
|
||||
* ```
|
||||
*/
|
||||
|
||||
export interface PaginationParams {
|
||||
pageNum: number
|
||||
pageSize: number
|
||||
}
|
||||
|
||||
export interface UsePaginationOptions<T> {
|
||||
/** 每页条数,默认 10 */
|
||||
pageSize?: number
|
||||
/** 初始数据 */
|
||||
initialData?: T[]
|
||||
/** 请求成功的回调 */
|
||||
onSuccess?: (list: T[], total: number) => void
|
||||
/** 请求失败的回调 */
|
||||
onError?: (error: Error) => void
|
||||
}
|
||||
|
||||
export interface UsePaginationReturn<T> {
|
||||
/** 数据列表 */
|
||||
list: Ref<T[]>
|
||||
/** 是否正在加载 */
|
||||
loading: ComputedRef<boolean>
|
||||
/** 错误信息 */
|
||||
error: Ref<Error | null>
|
||||
/** 是否还有更多数据 */
|
||||
hasMore: ComputedRef<boolean>
|
||||
/** 当前页码 */
|
||||
pageNum: Ref<number>
|
||||
/** 总条数 */
|
||||
total: Ref<number>
|
||||
/** 是否为空 */
|
||||
isEmpty: ComputedRef<boolean>
|
||||
/** 加载更多 */
|
||||
loadMore: () => Promise<T[]>
|
||||
/** 刷新(从第一页开始) */
|
||||
refresh: () => Promise<T[]>
|
||||
/** 重置 */
|
||||
reset: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页请求 Hook
|
||||
*
|
||||
* @param requestFn 分页请求函数
|
||||
* @param options 配置选项
|
||||
*/
|
||||
export function usePagination<T>(
|
||||
requestFn: (params: PaginationParams) => Promise<PageResult<T[]>>,
|
||||
options: UsePaginationOptions<T> = {}
|
||||
): UsePaginationReturn<T> {
|
||||
const { pageSize = 10, initialData = [], onSuccess, onError } = options
|
||||
|
||||
const list = ref<T[]>(initialData) as Ref<T[]>
|
||||
const loadingCount = ref(0)
|
||||
const error = ref<Error | null>(null)
|
||||
const pageNum = ref(1)
|
||||
const total = ref(0)
|
||||
|
||||
const loading = computed(() => loadingCount.value > 0)
|
||||
const hasMore = computed(() => list.value.length < total.value)
|
||||
const isEmpty = computed(() => list.value.length === 0 && !loading.value)
|
||||
|
||||
/**
|
||||
* 加载更多
|
||||
*/
|
||||
const loadMore = async (): Promise<T[]> => {
|
||||
// 如果正在加载或没有更多数据,直接返回
|
||||
if (loading.value || !hasMore.value) {
|
||||
return list.value
|
||||
}
|
||||
|
||||
loadingCount.value++
|
||||
|
||||
try {
|
||||
const result = await requestFn({
|
||||
pageNum: pageNum.value,
|
||||
pageSize,
|
||||
})
|
||||
|
||||
// 如果是第一页,替换数据;否则追加数据
|
||||
if (pageNum.value === 1) {
|
||||
list.value = result.list
|
||||
} else {
|
||||
list.value = [...list.value, ...result.list]
|
||||
}
|
||||
|
||||
total.value = result.total
|
||||
pageNum.value++
|
||||
onSuccess?.(result.list, result.total)
|
||||
|
||||
return result.list
|
||||
} catch (e) {
|
||||
const err = e instanceof Error ? e : new Error(String(e))
|
||||
error.value = err
|
||||
onError?.(err)
|
||||
return []
|
||||
} finally {
|
||||
loadingCount.value--
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新(从第一页开始)
|
||||
*/
|
||||
const refresh = async (): Promise<T[]> => {
|
||||
pageNum.value = 1
|
||||
total.value = 0
|
||||
list.value = []
|
||||
return loadMore()
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置
|
||||
*/
|
||||
const reset = (): void => {
|
||||
list.value = initialData
|
||||
loadingCount.value = 0
|
||||
error.value = null
|
||||
pageNum.value = 1
|
||||
total.value = 0
|
||||
}
|
||||
|
||||
return {
|
||||
list,
|
||||
loading,
|
||||
error,
|
||||
hasMore,
|
||||
pageNum,
|
||||
total,
|
||||
isEmpty,
|
||||
loadMore,
|
||||
refresh,
|
||||
reset,
|
||||
}
|
||||
}
|
||||
@@ -1,46 +1,104 @@
|
||||
export interface TabbarItem {
|
||||
name: string;
|
||||
value: number | null;
|
||||
active: boolean;
|
||||
title: string;
|
||||
icon: string;
|
||||
import { ref, computed } from "vue"
|
||||
|
||||
/**
|
||||
* TabBar 状态管理
|
||||
*
|
||||
* 提供底部导航栏的状态管理功能,包括激活状态切换、徽标数字设置等。
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const { tabbarList, setTabbarItemActive, setTabbarItem } = useTabbar()
|
||||
*
|
||||
* // 切换到工作台
|
||||
* setTabbarItemActive('work')
|
||||
*
|
||||
* // 设置徽标数字
|
||||
* setTabbarItem('work', 5)
|
||||
* ```
|
||||
*/
|
||||
|
||||
export interface TabbarItem {
|
||||
/** 唯一标识 */
|
||||
name: string
|
||||
/** 徽标数字,null 表示不显示 */
|
||||
value: number | null
|
||||
/** 是否激活 */
|
||||
active: boolean
|
||||
/** 显示标题 */
|
||||
title: string
|
||||
/** 图标名称 */
|
||||
icon: string
|
||||
}
|
||||
|
||||
const tabbarItems = ref<TabbarItem[]>([
|
||||
{ name: "home", value: null, active: true, title: "首页", icon: "home" },
|
||||
{ name: "work", value: null, active: false, title: "工作台", icon: "laptop" },
|
||||
{ name: "mine", value: null, active: false, title: "我的", icon: "user" },
|
||||
]);
|
||||
/** 默认 TabBar 配置 */
|
||||
const DEFAULT_TABBAR_ITEMS: Omit<TabbarItem, "active">[] = [
|
||||
{ name: "home", value: null, title: "首页", icon: "home" },
|
||||
{ name: "work", value: null, title: "工作台", icon: "laptop" },
|
||||
{ name: "mine", value: null, title: "我的", icon: "user" },
|
||||
]
|
||||
|
||||
export function useTabbar() {
|
||||
const tabbarList = computed(() => tabbarItems.value);
|
||||
/**
|
||||
* 创建 TabBar 状态实例
|
||||
*
|
||||
* 使用工厂函数创建独立的状态实例,避免模块级别的状态污染。
|
||||
* 每个调用者都可以获得独立的状态管理。
|
||||
*/
|
||||
function createTabbarState() {
|
||||
const items = ref<TabbarItem[]>(
|
||||
DEFAULT_TABBAR_ITEMS.map((item, index) => ({
|
||||
...item,
|
||||
active: index === 0,
|
||||
}))
|
||||
)
|
||||
|
||||
/** TabBar 列表 */
|
||||
const tabbarList = computed(() => items.value)
|
||||
|
||||
/** 当前激活的 TabBar 项 */
|
||||
const activeTabbar = computed(() => {
|
||||
const item = tabbarItems.value.find((item) => item.active);
|
||||
return item || tabbarItems.value[0];
|
||||
});
|
||||
return items.value.find((item) => item.active) || items.value[0]
|
||||
})
|
||||
|
||||
const getTabbarItemValue = (name: string) => {
|
||||
const item = tabbarItems.value.find((item) => item.name === name);
|
||||
return item && item.value ? item.value : null;
|
||||
};
|
||||
/**
|
||||
* 获取指定 TabBar 项的徽标数字
|
||||
* @param name TabBar 项名称
|
||||
*/
|
||||
const getTabbarItemValue = (name: string): number | null => {
|
||||
const item = items.value.find((item) => item.name === name)
|
||||
return item?.value ?? null
|
||||
}
|
||||
|
||||
const setTabbarItem = (name: string, value: number) => {
|
||||
const tabbarItem = tabbarItems.value.find((item) => item.name === name);
|
||||
if (tabbarItem) {
|
||||
tabbarItem.value = value;
|
||||
/**
|
||||
* 设置 TabBar 项的徽标数字
|
||||
* @param name TabBar 项名称
|
||||
* @param value 徽标数字
|
||||
*/
|
||||
const setTabbarItem = (name: string, value: number): void => {
|
||||
const item = items.value.find((item) => item.name === name)
|
||||
if (item) {
|
||||
item.value = value
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const setTabbarItemActive = (name: string) => {
|
||||
tabbarItems.value.forEach((item) => {
|
||||
if (item.name === name) {
|
||||
item.active = true;
|
||||
} else {
|
||||
item.active = false;
|
||||
}
|
||||
});
|
||||
};
|
||||
/**
|
||||
* 设置激活的 TabBar 项
|
||||
* @param name TabBar 项名称
|
||||
*/
|
||||
const setTabbarItemActive = (name: string): void => {
|
||||
items.value.forEach((item) => {
|
||||
item.active = item.name === name
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置 TabBar 状态
|
||||
*/
|
||||
const resetTabbar = (): void => {
|
||||
items.value = DEFAULT_TABBAR_ITEMS.map((item, index) => ({
|
||||
...item,
|
||||
active: index === 0,
|
||||
}))
|
||||
}
|
||||
|
||||
return {
|
||||
tabbarList,
|
||||
@@ -48,5 +106,31 @@ export function useTabbar() {
|
||||
getTabbarItemValue,
|
||||
setTabbarItem,
|
||||
setTabbarItemActive,
|
||||
};
|
||||
resetTabbar,
|
||||
}
|
||||
}
|
||||
|
||||
/** 全局单例实例 */
|
||||
let tabbarInstance: ReturnType<typeof createTabbarState> | null = null
|
||||
|
||||
/**
|
||||
* TabBar 状态管理 Hook
|
||||
*
|
||||
* 默认使用全局单例模式,确保整个应用共享同一状态。
|
||||
* 如需独立状态,可传入 `{ singleton: false }` 创建新实例。
|
||||
*
|
||||
* @param options 配置选项
|
||||
* @param options.singleton 是否使用单例模式,默认 true
|
||||
*/
|
||||
export function useTabbar(options?: { singleton?: boolean }) {
|
||||
const { singleton = true } = options ?? {}
|
||||
|
||||
if (singleton) {
|
||||
if (!tabbarInstance) {
|
||||
tabbarInstance = createTabbarState()
|
||||
}
|
||||
return tabbarInstance
|
||||
}
|
||||
|
||||
return createTabbarState()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user