This commit is contained in:
2026-03-25 14:54:15 +08:00
commit ab4646b43a
827 changed files with 123812 additions and 0 deletions
+10
View File
@@ -0,0 +1,10 @@
export * from './useEmitter';
export * from './useRect';
export * from './useCompRelation';
export * from './useTheme';
export * from './useColor';
export * from './useLocale';
export * from './useDebounce';
export * from './useThrottle';
export * from './useToast';
export * from './useModal';
@@ -0,0 +1,61 @@
/**
* 响应式颜色管理 composable
* 提供响应式的颜色访问,支持主题切换和暗黑模式
*
* 使用方式:
* const { color, getColor } = useColor()
* // color 是响应式的,可以直接在模板中使用
* // getColor('primary') 返回响应式的颜色值
*/
import { computed, type ComputedRef } from 'vue';
import type { ColorType } from '../../types/global';
import configProvider from '../util/config-provider';
/**
* 响应式颜色 composable
* 返回响应式的颜色对象和获取颜色的方法
*/
export function useColor() {
// 从 configProvider 获取当前主题的响应式引用
const currentTheme = configProvider.currentThemeRef;
const darkModeRef = configProvider.darkModeRef;
/**
* 获取当前激活模式下的颜色对象(响应式)
*/
const color = computed(() => {
const theme = currentTheme.value;
if (!theme) return {} as Record<ColorType, string>;
const isDark = configProvider.isInDarkMode();
const palette =
isDark && theme.darkColor && Object.keys(theme.darkColor).length ? theme.darkColor : theme.color || {};
// 合并默认值,确保所有颜色都有值
const defaultPalette = isDark
? (configProvider as any).baseDarkColorTokens || {}
: (configProvider as any).baseColorTokens || {};
return {
...defaultPalette,
...palette
} as Record<ColorType, string>;
});
/**
* 获取指定颜色(响应式)
* @param name 颜色名称
* @returns 颜色值
*/
const getColor = (name: ColorType): ComputedRef<string> => {
return computed(() => {
return color.value[name] || '';
});
};
return {
color,
getColor
};
}
@@ -0,0 +1,421 @@
import {
ref,
reactive,
getCurrentInstance,
onUnmounted,
nextTick,
computed,
onMounted,
type ComponentInternalInstance
} from 'vue';
import { logger } from '../util/logger';
// 类型定义
interface ParentContext {
name: string;
addChild: (child: ChildContext) => void;
removeChild: (childId: string) => void;
broadcast: (event: string, data?: any, childIds?: string | string[]) => void;
broadcastToChildren: (componentName: string, event: string, data?: any) => void;
getChildren: () => ChildContext[];
getExposed: () => Record<string, any>;
getChildExposed: (childId: string) => Record<string, any>;
getChildrenExposed: () => Array<{ id: string; name: string; exposed: Record<string, any> }>;
getInstance: () => any;
}
interface ChildContext {
id: string;
name: string;
getChildIndex: () => number;
emitToParent: (event: string, data?: any) => void;
getParentExposed: () => Record<string, any>;
getInstance: () => any;
getExposed: () => Record<string, any>;
}
// 符号定义
const PARENT_CONTEXT_SYMBOL = Symbol('parent_context');
/**
* 生成实例唯一ID
*/
function generateInstanceId(componentName: string): string {
return `${componentName}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
/**
* 查找父组件实例
*/
function findParentInstance(name: string, instance: any): any {
if (!instance) return null;
let parent = instance.parent;
while (parent) {
const parentName = parent.type?.name || parent.type?.__name;
if (parentName === name) {
return parent;
}
parent = parent.parent;
}
return null;
}
/**
* 获取父组件上下文
*/
function getParentContext(name: string, instance: any): ParentContext | null {
const parentInstance = findParentInstance(name, instance);
return parentInstance?.proxy?.[PARENT_CONTEXT_SYMBOL] || null;
}
/**
* 递归查找所有指定名称的子组件
*/
function findAllChildComponents(componentName: string, instance: any): any[] {
const components: any[] = [];
function traverse(currentInstance: any) {
if (!currentInstance?.subTree) return;
const subTree = currentInstance.subTree?.children || [];
const children = Array.isArray(subTree) ? subTree : [subTree];
children.forEach((vnode: any) => {
const child = vnode.component;
if (!child) return;
const name = child.type?.name || child.type?.__name;
if (name === componentName) {
components.push(child);
}
traverse(child);
});
}
traverse(instance);
logger.log(`Found ${components.length} ${componentName} components`);
return components;
}
/**
* 父组件 Hook
*/
export function useParent(componentName?: string) {
const instance = getCurrentInstance();
if (!instance) {
throw new Error('useParent must be called within setup function');
}
const name = componentName || instance.type.name || instance.type.__name;
if (!name) {
throw new Error('Component name is required for useParent');
}
const children = reactive<ChildContext[]>([]);
const childrenMap = new Map<string, ChildContext>();
const broadcast = (event: string, data?: any, childIds?: string | string[]) => {
const targetChildren = childIds
? ((Array.isArray(childIds) ? childIds : [childIds])
.map(id => childrenMap.get(id))
.filter(Boolean) as ChildContext[])
: Array.from(childrenMap.values());
logger.log(`Parent ${name} broadcasting event: ${event} to ${targetChildren.length} children`);
targetChildren.forEach(child => {
const exposed = child.getExposed();
if (exposed && typeof exposed[event] === 'function') {
try {
exposed[event](data);
} catch (error) {
logger.warn(`Error calling child method ${event}:`, error);
}
}
});
};
const broadcastToChildren = (componentName: string, event: string, data?: any) => {
logger.log(`Parent ${name} broadcasting event: ${event} to all ${componentName} components`);
const childComponents = findAllChildComponents(componentName, instance);
let successCount = 0;
childComponents.forEach(childComponent => {
const exposed = childComponent.exposed || childComponent.proxy;
if (exposed && typeof exposed[event] === 'function') {
try {
exposed[event](data);
successCount++;
} catch (error) {
logger.warn(`Error calling ${componentName} method ${event}:`, error);
}
}
});
logger.log(
`Parent ${name} successfully called ${successCount} of ${childComponents.length} ${componentName} components`
);
};
const parentContext: ParentContext = {
name,
addChild(child: ChildContext) {
if (!childrenMap.has(child.id)) {
childrenMap.set(child.id, child);
children.push(child);
logger.log(`Parent ${name} added child: ${child.name}`);
}
},
removeChild(childId: string) {
if (childrenMap.has(childId)) {
const child = childrenMap.get(childId)!;
childrenMap.delete(childId);
const index = children.findIndex(c => c.id === childId);
if (index > -1) children.splice(index, 1);
logger.log(`Parent ${name} removed child: ${childId}`);
}
},
broadcast,
broadcastToChildren,
getChildren: () => Array.from(childrenMap.values()),
getExposed: () => instance.exposed || {},
getChildExposed(childId: string) {
const child = childrenMap.get(childId);
return child?.getExposed?.() || {};
},
getChildrenExposed() {
return Array.from(childrenMap.values())
.filter(child => child.getExposed)
.map(child => ({
id: child.id,
name: child.name,
exposed: child.getExposed()
}))
.filter(item => Object.keys(item.exposed).length > 0);
},
getInstance: () => instance
};
if (instance.proxy) {
(instance.proxy as any)[PARENT_CONTEXT_SYMBOL] = parentContext;
}
onUnmounted(() => {
childrenMap.forEach((_, childId) => parentContext.removeChild(childId));
if (instance.proxy) {
delete (instance.proxy as any)[PARENT_CONTEXT_SYMBOL];
}
logger.log(`Parent ${name} unmounted and cleaned up`);
});
return {
parentName: name,
children,
broadcast,
broadcastToChildren,
getChildren: parentContext.getChildren,
getChildExposed: parentContext.getChildExposed,
getChildrenExposed: parentContext.getChildrenExposed,
getExposed: parentContext.getExposed,
getInstance: parentContext.getInstance
};
}
/**
* 子组件 Hook
*/
export function useChildren(componentName?: string, parentName?: string) {
const instance = getCurrentInstance();
if (!instance) {
throw new Error('useChildren must be called within setup function');
}
const name = componentName || instance.type.name || instance.type.__name;
const instanceId = generateInstanceId(name || 'anonymous');
const parentRef = ref<any | null>(null);
const parentExposed = ref<Record<string, any>>({});
const createSimulatedParentContext = (parentInstance: any): ParentContext => ({
name: parentInstance?.type?.name || parentInstance?.type?.__name || 'unknown',
addChild: () => logger.log('Simulated parent added child'),
removeChild: () => logger.log('Simulated parent removed child'),
broadcast: () => logger.log('Simulated parent broadcasting'),
broadcastToChildren: () => logger.log('Simulated parent broadcasting to children'),
getChildren: () => [],
getExposed: () => parentInstance?.exposed || {},
getChildExposed: () => ({}),
getChildrenExposed: () => [],
getInstance: () => parentInstance
});
const getParentExposed = (): Record<string, any> => {
if (parentRef.value) {
const exposed = parentRef.value.getExposed();
parentExposed.value = exposed;
return exposed;
}
return {};
};
const getExposed = (): Record<string, any> => instance.exposed || {};
const findParent = (): ParentContext | null => {
if (parentName) {
const parentContext = getParentContext(parentName, instance);
if (parentContext) {
if (!parentContext.getInstance) {
parentContext.getInstance = () => findParentInstance(parentName, instance);
}
return parentContext;
}
const parentInstance = findParentInstance(parentName, instance);
if (parentInstance) {
return createSimulatedParentContext(parentInstance);
}
}
let current = instance.parent;
while (current) {
const context = (current.proxy as any)?.[PARENT_CONTEXT_SYMBOL];
if (context) {
if (!context.getInstance) {
context.getInstance = () => current;
}
return context;
}
current = current.parent;
}
return instance.parent ? createSimulatedParentContext(instance.parent) : null;
};
const linkParent = (): boolean => {
const parent = findParent();
if (parent) {
parentRef.value = parent;
if (parent.addChild && childContext) {
parent.addChild(childContext);
}
getParentExposed();
logger.log(`Child ${name || 'anonymous'} linked to parent ${parent.name}`);
return true;
}
logger.log(`Child ${name || 'anonymous'} no parent found, working in standalone mode`);
return false;
};
const emitToParent = (event: string, data?: any) => {
if (parentRef.value) {
const exposed = getParentExposed();
if (exposed && typeof exposed[event] === 'function') {
try {
exposed[event](data, instanceId, name);
} catch (error) {
logger.warn(`Error calling parent method ${event}:`, error);
}
}
}
};
const getChildIndex = () => {
if (!parentRef.value) return -1;
try {
const children = parentRef.value.getChildren();
return children.findIndex((child: ChildContext) => child.id === instanceId);
} catch (error) {
return -1;
}
};
const childContext: ChildContext = {
id: instanceId,
name: name || 'anonymous',
getChildIndex,
emitToParent,
getParentExposed,
getInstance: () => instance,
getExposed
};
logger.log(`Child ${name || 'anonymous'} registered, looking for parent`);
onMounted(() => {
let connected = linkParent();
nextTick(() => {
connected = linkParent();
if (!connected) {
setTimeout(linkParent, 500);
}
});
});
onUnmounted(() => {
if (parentRef.value?.removeChild) {
parentRef.value.removeChild(instanceId);
}
logger.log(`Child ${name || 'anonymous'} unmounted`);
});
return {
childId: instanceId,
childName: name || 'anonymous',
childIndex: computed(getChildIndex),
parent: parentRef,
emitToParent,
getParentExposed,
parentExposed: computed(() => parentExposed.value),
getExposed
};
}
/**
* 检查父组件是否存在
*/
export function hasParent(parentName?: string): boolean {
const instance = getCurrentInstance();
if (!instance) return false;
if (parentName) {
return getParentContext(parentName, instance) !== null;
}
let current = instance.parent;
while (current) {
if ((current.proxy as any)?.[PARENT_CONTEXT_SYMBOL]) return true;
current = current.parent;
}
return false;
}
/**
* 获取父组件上下文
*/
export function getParentContextByName(parentName: string): ParentContext | null {
const instance = getCurrentInstance();
return instance ? getParentContext(parentName, instance) : null;
}
/**
* 热更新清理函数
*/
export function cleanupComponentRelations(): void {
logger.log('Cleaning up component relations for hot reload');
}
// 热更新处理
if (import.meta.hot) {
import.meta.hot.accept(() => {
logger.log('Hot reload detected, relations will be automatically reconnected');
});
}
export default {
useParent,
useChildren,
hasParent,
getParentContextByName,
cleanupComponentRelations
};
@@ -0,0 +1,15 @@
export function useDebounce(delay: number = 500) {
let timeout: ReturnType<typeof setTimeout> | null = null;
// 防抖函数
function debounce(callback: () => void, debounceTime?: number) {
debounceTime = debounceTime || delay;
if (timeout) clearTimeout(timeout);
timeout = setTimeout(() => {
callback();
}, debounceTime);
}
return {
debounce
};
}
@@ -0,0 +1,79 @@
import { type ComponentInternalInstance, getCurrentInstance } from 'vue';
/**
* 将事件名转换为驼峰格式
* @param str 需要转换的字符串
* @description 例如:on-form-change -> onFormChange
* @returns
*/
function formatToCamelCase(str: string): string {
return str.replace(/-([a-z])/g, function (g) {
return g[1].toUpperCase();
});
}
export function useEmitter(name: string) {
const instance: ComponentInternalInstance | null | undefined = getCurrentInstance();
/** * 向上查找父组件并派发事件
* @param instance 当前组件实例(setup中可用getCurrentInstance()
* @param componentName 目标组件名
* @param eventName 事件名
* @param params 参数
*/
function dispatch(componentName: string, eventName: string, ...params: any[]) {
let parent = instance && (instance.parent as ComponentInternalInstance | null | undefined);
while (parent) {
const name = (parent.type as any)?.name as string | undefined;
if (name === componentName) {
// 找到目标组件,派发事件
// Vue3未解决,目标组件事件监听失效,待优化,暂时使用下面的方式解决,如果你有好的方式也可以告诉我或者提PR
parent.emit && parent.emit(eventName, ...params);
// 如果有对应的方法,执行方法
// 这里可以考虑将 eventName 转换为驼峰格式
// 例如:on-form-change -> onFormChange
parent.exposed?.[formatToCamelCase(eventName)] &&
parent.exposed[formatToCamelCase(eventName)](...params);
break;
}
parent = parent.parent;
}
}
/**
* 向下递归查找子组件并广播事件
* @param instance 当前组件实例(setup中可用getCurrentInstance()
* @param componentName 目标组件名
* @param eventName 事件名
* @param params 参数
*/
function broadcast(componentName: string, eventName: string, ...params: any[]) {
if (!instance) return;
const subTree = (instance.subTree as any)?.children || [];
const children = Array.isArray(subTree) ? subTree : [subTree];
children.forEach((vnode: any) => {
const child = vnode.component as ComponentInternalInstance | undefined;
if (child) {
const name = (child.type as any)?.name as string | undefined;
if (name === componentName) {
// 找到目标组件,广播事件
// Vue3未解决,目标组件事件监听失效,待优化,暂时使用下面的方式解决,如果你有好的方式也可以告诉我或者提PR
child.emit && child.emit(eventName, ...params);
// 如果有对应的方法,执行方法
// 这里可以考虑将 eventName 转换为驼峰格式
// 例如:on-form-change -> onFormChange
child.exposed?.[formatToCamelCase(eventName)] &&
child.exposed[formatToCamelCase(eventName)](...params);
} else {
broadcast.call(child, componentName, eventName, ...params);
}
}
});
}
return {
dispatch,
broadcast
};
}
@@ -0,0 +1,34 @@
import configProvider from '../util/config-provider';
/**
* 国际化 composable
* 提供:
* - currentLocale / locales 响应式引用
* - t(key, replacements) 翻译方法
* - setLocale(name) 切换语言
* - initLocales(locales?, defaultName?) 初始化语言包
*/
export function useLocale(namespace?: string) {
// 创建翻译函数,支持命名空间
const createTranslateFunction = (ns?: string) => {
return (key: string, replacements?: any, localeName?: string): string => {
// 如果有命名空间,自动添加前缀
const fullKey = ns ? `${ns}.${key}` : key;
return configProvider.t(fullKey, replacements, localeName);
};
};
return {
// 响应式引用
currentLocale: configProvider.currentLocaleRef,
locales: configProvider.localesRef,
// 方法
t: createTranslateFunction(namespace),
setLocale: (name: string) => configProvider.setLocale(name),
getLocales: () => configProvider.getLocales(),
getCurrentLocale: () => configProvider.getCurrentLocale(),
initLocales: (locales?: any[], defaultLocaleName?: string, isForce?: boolean) =>
configProvider.initLocales(locales, defaultLocaleName, isForce)
};
}
@@ -0,0 +1,131 @@
import {
U_MODAL_EVENT_SHOW,
U_MODAL_EVENT_HIDE,
U_MODAL_EVENT_CLEAR_LOADING,
U_MODAL_GLOBAL_EVENT_SHOW,
U_MODAL_GLOBAL_EVENT_HIDE,
U_MODAL_GLOBAL_EVENT_CLEAR_LOADING,
getEventWithCurrentPage,
type ModalPayload
} from '../../components/u-modal/service';
export type UseModalShowOptions = ModalPayload;
export type UseModal = {
/**
* 显示 modal
* - show('标题')
* - show({ title, content, showCancelButton, ... })
*/
show: (contentOrOptions: string | UseModalShowOptions) => void;
/**
* 显示 confirm 类型的 modal(带确认和取消按钮)
* - confirm('标题')
* - confirm({ title, content, onConfirm, onCancel })
*/
confirm: (contentOrOptions: string | UseModalShowOptions) => void;
/** 关闭 modal */
close: () => void;
/** 清除 loading 状态 */
clearLoading: () => void;
};
export type UseModalOptions = {
/** 是否使用全局根部 <u-modal global /> */
global?: boolean;
/** 是否使用页面级 <u-modal page /> 或某个页面的 <u-modal page="pageId" /> */
page?: boolean | string;
};
function normalize(contentOrOptions: string | UseModalShowOptions): UseModalShowOptions {
if (typeof contentOrOptions === 'string') {
// 如果是简单的字符串,可能是标题或内容
return { content: contentOrOptions };
}
return contentOrOptions || {};
}
function getPage(optionsOrGlobal: UseModalOptions | boolean): string {
if (typeof optionsOrGlobal === 'boolean') {
return '';
}
if (optionsOrGlobal.page && typeof optionsOrGlobal.page === 'string' && optionsOrGlobal.page !== '') {
return optionsOrGlobal.page;
}
return '';
}
/**
* Modal 函数式调用
* @description 需要页面/应用中至少存在一个 <u-modal global /> 或 <u-modal page /> 实例用于承接事件;不影响原 ref 调用方式。
*
* 支持两种调用方式:
* - 应用级 useModal() / useModal(true) / useModal({ global: true })
* - 页面级 useModal(false) / useModal({ page: true }) / useModal({ page: 'pageId' }) pageId 应和页面中 <u-modal page="pageId" /> 的 page 属性一致)
*/
export function useModal(optionsOrGlobal: UseModalOptions | boolean = true): UseModal {
const isGlobal = typeof optionsOrGlobal === 'boolean' ? optionsOrGlobal === true : !!optionsOrGlobal.global;
const isPage = typeof optionsOrGlobal === 'boolean' ? optionsOrGlobal === false : !!optionsOrGlobal.page;
const showEvent = isGlobal
? U_MODAL_GLOBAL_EVENT_SHOW
: isPage
? getEventWithCurrentPage(U_MODAL_EVENT_SHOW, getPage(optionsOrGlobal))
: '';
const hideEvent = isGlobal
? U_MODAL_GLOBAL_EVENT_HIDE
: isPage
? getEventWithCurrentPage(U_MODAL_EVENT_HIDE, getPage(optionsOrGlobal))
: '';
const clearLoadingEvent = isGlobal
? U_MODAL_GLOBAL_EVENT_CLEAR_LOADING
: isPage
? getEventWithCurrentPage(U_MODAL_EVENT_CLEAR_LOADING, getPage(optionsOrGlobal))
: '';
function emitShow(payload: UseModalShowOptions) {
if (showEvent) {
uni?.$emit && uni.$emit(showEvent, payload);
}
}
function emitHide() {
if (hideEvent) {
uni?.$emit && uni.$emit(hideEvent);
}
}
function emitClearLoading() {
if (clearLoadingEvent) {
uni?.$emit && uni.$emit(clearLoadingEvent);
}
}
function show(contentOrOptions: string | UseModalShowOptions) {
emitShow(normalize(contentOrOptions));
}
function confirm(contentOrOptions: string | UseModalShowOptions) {
const options = normalize(contentOrOptions);
emitShow({
...options,
showCancelButton: options.showCancelButton ?? true,
showConfirmButton: options.showConfirmButton ?? true
});
}
function clearLoading() {
emitClearLoading();
}
function close() {
emitHide();
}
return {
show,
confirm,
close,
clearLoading
};
}
@@ -0,0 +1,40 @@
import { getCurrentInstance, nextTick, ref } from 'vue';
/**
* useRect - 获取元素的位置信息(响应式,原生实现)
* @param selector 选择器(如 #id 或 .class
* @param all 是否获取所有匹配元素
* @returns rect 响应式的节点信息,refresh 主动刷新方法
*/
export function useRect(selector: string | null = null, all = false) {
const rect = ref<any>(all ? [] : null);
const instance = getCurrentInstance();
async function getRect(realSelector: string | null = null, delay = 0): Promise<any> {
realSelector = realSelector || selector;
if (!realSelector) return rect.value;
await nextTick();
return new Promise(resolve => {
setTimeout(() => {
uni.createSelectorQuery()
.in(instance?.proxy)
[all ? 'selectAll' : 'select'](realSelector as string)
.boundingClientRect((res: any) => {
rect.value = res;
resolve(res);
})
.exec();
}, delay);
});
}
function refresh(selector?: string | null, delay?: number): Promise<any> {
return getRect(selector, delay);
}
return {
rect,
getRect,
refresh
};
}
@@ -0,0 +1,173 @@
/**
* 路由跳转 hooks,基于 route.ts 的 Router 类实现
* 提供 Vue Composition API 风格的路由跳转方法
*/
import { ref } from 'vue';
interface RouterConfig {
type?: string;
url?: string;
delta?: number;
params?: Record<string, any>;
animationType?: string;
animationDuration?: number;
intercept?: boolean;
}
declare const uni: any;
const config: RouterConfig = {
type: 'navigateTo',
url: '',
delta: 1,
params: {},
animationType: 'pop-in',
animationDuration: 300,
intercept: false
};
// 响应式配置,支持动态修改
const configRef = ref({ ...config });
/**
* 判断 url 前面是否有 "/",如果没有则加上
*/
const addRootPath = (url: string): string => {
return url[0] === '/' ? url : `/${url}`;
};
/**
* 整合路由参数
*/
const mixinParam = (url: string, params: Record<string, any>): string => {
url = url && addRootPath(url);
let query = '';
if (/.*\/.*\?.*=.*/.test(url)) {
query = uni.$u.queryParams(params, false);
return url + '&' + query;
} else {
query = uni.$u.queryParams(params);
return url + query;
}
};
/**
* 执行路由跳转
*/
const openPage = (cfg: RouterConfig): void => {
const { url = '', type = '', delta = 1, animationDuration = 300 } = cfg;
if (type === 'navigateTo' || type === 'to') {
uni.navigateTo({ url, animationDuration });
}
if (type === 'redirectTo' || type === 'redirect') {
uni.redirectTo({ url });
}
if (type === 'switchTab' || type === 'tab') {
uni.switchTab({ url });
}
if (type === 'reLaunch' || type === 'launch') {
uni.reLaunch({ url });
}
if (type === 'navigateBack' || type === 'back') {
uni.navigateBack({ delta });
}
};
/**
* 路由跳转主方法
*/
const route = async (options: string | RouterConfig = {}, params: Record<string, any> = {}): Promise<void> => {
let mergeConfig: RouterConfig = {};
if (typeof options === 'string') {
mergeConfig.url = mixinParam(options, params);
mergeConfig.type = 'navigateTo';
} else {
mergeConfig = uni.$u.deepMerge(configRef.value, options);
mergeConfig.url = mixinParam(options.url || '', options.params || {});
}
if (params.intercept !== undefined) {
configRef.value.intercept = params.intercept;
}
mergeConfig.params = params;
mergeConfig = uni.$u.deepMerge(configRef.value, mergeConfig);
if (uni.$u.routeIntercept && typeof uni.$u.routeIntercept === 'function') {
const isNext = await new Promise<boolean>(resolve => {
uni.$u.routeIntercept(mergeConfig, resolve);
});
isNext && openPage(mergeConfig);
} else {
openPage(mergeConfig);
}
};
/**
* 跳转到指定页面
*/
const to = (url: string, params?: Record<string, any>): Promise<void> => {
return route({ url, type: 'navigateTo' }, params || {});
};
/**
* 关闭当前页面,跳转到指定页面
*/
const redirect = (url: string, params?: Record<string, any>): Promise<void> => {
return route({ url, type: 'redirectTo' }, params || {});
};
/**
* 跳转到 tabBar 页面
*/
const tab = (url: string, params?: Record<string, any>): Promise<void> => {
return route({ url, type: 'switchTab' }, params || {});
};
/**
* 关闭所有页面,跳转到指定页面
*/
const reLaunch = (url: string, params?: Record<string, any>): Promise<void> => {
return route({ url, type: 'reLaunch' }, params || {});
};
/**
* 返回上一页
*/
const back = (delta: number = 1): Promise<void> => {
return route({ type: 'navigateBack', delta });
};
/**
* 设置默认路由配置
*/
const setConfig = (newConfig: Partial<RouterConfig>): void => {
configRef.value = uni.$u.deepMerge(configRef.value, newConfig);
};
/**
* 获取当前路由配置
*/
const getConfig = (): RouterConfig => {
return { ...configRef.value };
};
export function useRouter() {
return {
// 核心跳转方法
route,
// 便捷方法
to,
redirect,
tab,
reLaunch,
back,
// 配置方法
setConfig,
getConfig
};
}
export default useRouter;
@@ -0,0 +1,173 @@
/**
* 主题管理 composable
* 提供主题切换、持久化、CSS 变量注入、暗黑模式等功能
*
* 使用方式:
* const { currentTheme, themes, setTheme, getDarkMode, setDarkMode, isInDarkMode, getAvailableThemes, initTheme } = useTheme()
*/
import type { DarkMode, Theme } from '../../types/global';
import configProvider, { type DefaultThemeConfig } from '../util/config-provider';
import { defaultThemes } from '../config/theme-tokens';
const THEME_STORAGE_KEY = 'uview-pro-theme';
const DARK_MODE_STORAGE_KEY = 'uview-pro-dark-mode';
const themesRef = configProvider.themesRef;
const currentTheme = configProvider.currentThemeRef;
const darkModeRef = configProvider.darkModeRef;
/**
* 保存主题到 Storage
*/
function saveThemeToStorage(themeName: string) {
try {
uni.setStorageSync(THEME_STORAGE_KEY, themeName);
} catch (e) {
console.warn('[useTheme] failed to write storage', e);
}
}
/**
* 保存暗黑模式设置到 Storage
*/
function saveDarkModeToStorage(mode: DarkMode) {
try {
uni.setStorageSync(DARK_MODE_STORAGE_KEY, mode);
} catch (e) {
console.warn('[useTheme] failed to write storage', e);
}
}
/**
* 设置主题
*/
function setTheme(themeName: string) {
configProvider.setTheme(themeName);
currentTheme.value = configProvider.getCurrentTheme();
saveThemeToStorage(themeName);
}
/**
* 获取当前主题
*/
function getCurrentTheme(): Theme | null {
return currentTheme.value || configProvider.getCurrentTheme();
}
/**
* 获取所有可用主题
*/
function getAvailableThemes() {
return configProvider.getThemes();
}
/**
* 初始化主题系统
* @param themes 可选的主题列表,如果未提供则尝试从 uni.$u.themes 读取
* @param defaultConfig 可选的默认主题配置,支持字符串(默认主题名)或对象({ defaultTheme?, defaultDarkMode? }
*/
export function initTheme(themes?: Theme[], defaultConfig?: string | DefaultThemeConfig, isForce?: boolean) {
// 如果有传入主题列表,使用传入的
if (Array.isArray(themes) && themes.length > 0) {
configProvider.initTheme(themes, defaultConfig, isForce);
return;
}
// // 若已通过插件或其他方式完成初始化,则不再覆盖,最多按需切换默认主题
// const existingThemes = configProvider.getThemes();
// if (existingThemes.length > 0) {
// if (typeof defaultConfig === 'string') {
// configProvider.setTheme(defaultConfig);
// } else if (defaultConfig && typeof defaultConfig === 'object' && (defaultConfig as any).defaultTheme) {
// configProvider.setTheme(defaultConfig.defaultTheme);
// } else if (!configProvider.getCurrentTheme()) {
// configProvider.setTheme(existingThemes[0].name);
// } else {
// // 触发一次 apply,便于初始化 CSS 变量
// configProvider.setTheme(configProvider.getCurrentTheme()!.name);
// }
// return;
// }
// // 初始化 configProvider(如果运行时提供了内置主题)
// try {
// const builtin = (typeof uni !== 'undefined' && (uni as any).$u && (uni as any).$u.themes) || [];
// if (Array.isArray(builtin) && builtin.length > 0) {
// configProvider.initTheme(builtin as Theme[], defaultConfig);
// return;
// }
// } catch (e) {
// // ignore
// }
// 回退到内置默认主题
configProvider.initTheme(defaultThemes as Theme[], defaultConfig);
}
/**
* 初始化暗黑模式
* @param darkMode 暗黑模式设置
* @param isForce 是否强制初始化
*/
function initDarkMode(darkMode?: DarkMode, isForce?: boolean) {
configProvider.initDarkMode(darkMode, isForce);
}
/**
* 获取当前暗黑模式设置
*/
function getDarkMode(): DarkMode {
return configProvider.getDarkMode();
}
/**
* 设置暗黑模式
* @param mode 'auto' (跟随系统) | 'light' (强制亮色) | 'dark' (强制暗黑)
*/
function setDarkMode(mode: DarkMode) {
configProvider.setDarkMode(mode);
darkModeRef.value = mode;
saveDarkModeToStorage(mode);
}
/**
* 检查当前是否处于暗黑模式
*/
function isInDarkMode(): boolean {
return configProvider.isInDarkMode();
}
/**
* 切换暗黑模式(在当前模式的基础上切换)
*/
function toggleDarkMode() {
const current = getDarkMode();
const nextMode = current === 'dark' ? 'light' : 'dark';
setDarkMode(nextMode);
}
/**
* 使用主题的 composable
* 返回所有主题相关的响应式引用和方法
*/
export function useTheme() {
return {
// 响应式引用
currentTheme,
themes: themesRef,
darkMode: darkModeRef,
cssVars: configProvider.cssVarsRef,
// 主题相关方法
initTheme,
setTheme,
getCurrentTheme,
getAvailableThemes,
// 暗黑模式相关方法
initDarkMode,
getDarkMode,
setDarkMode,
isInDarkMode,
toggleDarkMode
};
}
@@ -0,0 +1,16 @@
export function useThrottle(delay: number = 500) {
let previous: number = 0;
// 节流函数
function throttle(callback: () => void, throttleTime?: number) {
throttleTime = throttleTime || delay;
let now = Date.now();
if (now - previous > throttleTime) {
callback();
previous = now;
}
}
return {
throttle
};
}
@@ -0,0 +1,115 @@
import {
U_TOAST_EVENT_HIDE,
U_TOAST_EVENT_SHOW,
U_TOAST_GLOBAL_EVENT_HIDE,
U_TOAST_GLOBAL_EVENT_SHOW,
getEventWithCurrentPage,
type ToastPayload
} from '../../components/u-toast/service';
import type { ThemeType } from '../../types/global';
export type UseToastShowOptions = ToastPayload;
export type UseToast = {
/**
* 显示 toast
* - show('文本')
* - show({ title, type, duration, ... })
*/
show: (titleOrOptions: string | UseToastShowOptions) => void;
/** 关闭 toast */
close: () => void;
/** 成功 */
success: (titleOrOptions: string | UseToastShowOptions) => void;
/** 错误 */
error: (titleOrOptions: string | UseToastShowOptions) => void;
/** 警告 */
warning: (titleOrOptions: string | UseToastShowOptions) => void;
/** 信息 */
info: (titleOrOptions: string | UseToastShowOptions) => void;
/** 加载中(默认常驻,需 close 关闭) */
loading: (titleOrOptions: string | UseToastShowOptions) => void;
};
export type UseToastOptions = {
/** 是否使用全局根部 <u-toast global />,默认 true;为 false 时走页面级 <u-toast /> */
global?: boolean;
/** 是否使用页面级 <u-toast page /> 或某个页面的 <u-toast page="pageId" /> */
page?: boolean | string;
};
function normalize(titleOrOptions: string | UseToastShowOptions): UseToastShowOptions {
if (typeof titleOrOptions === 'string') return { title: titleOrOptions };
return titleOrOptions || {};
}
function getPage(optionsOrGlobal: UseToastOptions | boolean): string {
if (typeof optionsOrGlobal === 'boolean') {
return '';
}
if (optionsOrGlobal.page && typeof optionsOrGlobal.page === 'string' && optionsOrGlobal.page !== '') {
return optionsOrGlobal.page;
}
return '';
}
/**
* Toast 函数式调用
* @description 需要页面/应用中至少存在一个 <u-toast global /> 或 <u-toast page /> 实例用于承接事件;不影响原 ref 调用方式。
*
* 支持两种调用方式:
* - 应用级 useToast() / useToast(true) / useToast({ global: true })
* - 页面级 useToast(false) / useToast({ page: true }) / useToast({ page: 'pageId' }) pageId 应和页面中 <u-toast page="pageId" /> 的 page 属性一致)
*/
export function useToast(optionsOrGlobal: UseToastOptions | boolean = true): UseToast {
const isGlobal = typeof optionsOrGlobal === 'boolean' ? optionsOrGlobal === true : !!optionsOrGlobal.global;
const isPage = typeof optionsOrGlobal === 'boolean' ? optionsOrGlobal === false : !!optionsOrGlobal.page;
const showEvent = isGlobal
? U_TOAST_GLOBAL_EVENT_SHOW
: isPage
? getEventWithCurrentPage(U_TOAST_EVENT_SHOW, getPage(optionsOrGlobal))
: '';
const hideEvent = isGlobal
? U_TOAST_GLOBAL_EVENT_HIDE
: isPage
? getEventWithCurrentPage(U_TOAST_EVENT_HIDE, getPage(optionsOrGlobal))
: '';
function emitShow(payload: UseToastShowOptions) {
if (showEvent) {
uni?.$emit && uni.$emit(showEvent, payload);
}
}
function emitHide() {
if (hideEvent) {
uni?.$emit && uni.$emit(hideEvent);
}
}
function show(titleOrOptions: string | UseToastShowOptions) {
emitShow(normalize(titleOrOptions));
}
function close() {
emitHide();
}
function withType(type: ThemeType, titleOrOptions: string | UseToastShowOptions) {
const options = normalize(titleOrOptions);
emitShow({ ...options, type });
}
return {
show,
close,
success: (v: any) => withType('success', v),
error: (v: any) => withType('error', v),
warning: (v: any) => withType('warning', v),
info: (v: any) => withType('info', v),
loading: (v: any) => {
const options = normalize(v);
// loading 通常需要常驻,除非用户显式传 duration
emitShow({ ...options, loading: true, duration: options.duration ?? 0 });
}
};
}