import { computed, inject, provide, ref, shallowRef, type ComputedRef, type Ref } from "vue"; import { DefaultDarkTheme, DefaultTheme } from "./Theme"; import type { DynamicVarType } from "./ThemeTools"; import { ObjectUtils } from "@imengyu/imengyu-utils"; export const ThemeKey = Symbol("NanaThemeKey"); let defaultSizeUnit = 'rpx'; /** * 配置默认的尺寸单位 * @param unit */ export function configDefaultSizeUnit(unit: string) { defaultSizeUnit = unit; } export type ViewStyle = Record export type TextStyle = Record export type ThemeSizeType = number|string; export type ThemePaddingOrMarginType = number|Array|ThemePaddingMargin|string|string[]; export interface ThemeConfig { varOverrides: Record, colorConfigs: Record>, textConfigs: Record>, } function popInjectTheme() : ThemeConfig { return inject>(ThemeKey)?.value ?? DefaultTheme; } /** * 在PropDefault回调中使用主题默认值的函数 * @param key * @param defaultValue * @returns */ export function propGetThemeVar(key: string, defaultValue?: T) : T { const theme = popInjectTheme(); return theme?.varOverrides[key] ?? defaultValue; } /** * 在PropDefault回调中使用主题默认值的函数(回调) * @param cb 回调函数,参数为获取主题变量的函数和主题配置 * @returns 回调函数的返回值 */ export function propGetThemeVar2(cb: (getVar: (key: string, defaultValue: any) => any, theme: ThemeConfig) => any) { const theme = popInjectTheme(); return cb((key, defaultValue) => getVar(theme, key, defaultValue), theme); } export function provideSomeThemeColor(record: Record) { const topTheme = inject>(ThemeKey); const newTheme = computed(() => { const topThemeValue = topTheme?.value ?? DefaultTheme; const v = { ...topThemeValue, colorConfigs: { ...topThemeValue.colorConfigs, ...record } } as ThemeConfig; return v; }); provide(ThemeKey, newTheme); } export function provideSomeThemeVar(record: Record) { const topTheme = inject>(ThemeKey); const newTheme = computed(() => { const topThemeValue = topTheme?.value ?? DefaultTheme; const v = { ...topThemeValue, varOverrides: { ...topThemeValue.varOverrides, ...record } } as ThemeConfig; return v; }); provide(ThemeKey, newTheme); } export function getVar(theme : ThemeConfig, key: string, defaultValue: T) : T { return theme.varOverrides[key] ?? defaultValue; } /** * 主题的组合代码 */ export function useTheme() { const topTheme = inject>(ThemeKey); const theme = computed(() => topTheme?.value ?? DefaultTheme); function resolveThemeSize(inValue?: ThemeSizeType, defaultValue?: ThemeSizeType) : string|undefined { const preResolve = resolveSize(inValue); if (preResolve !== undefined) return preResolve; if (defaultValue && !isRealSize(defaultValue)) defaultValue = resolveThemeSize(defaultValue); if (inValue === undefined) inValue = defaultValue; if (inValue === undefined) return undefined; if (typeof inValue === 'string') { const v = getSize(inValue, undefined); if (v === undefined) inValue = defaultValue; else inValue = v; } return resolveSize(inValue); } function resolveThemeColor(inValue?: string, defaultValue?: string) : string|undefined { if (isSpecialColor(inValue)) return inValue; if (inValue === undefined) inValue = defaultValue; else defaultValue = resolveThemeColor(defaultValue); if (inValue === undefined) return undefined; if (inValue.startsWith('#') || inValue.startsWith('rgb')) return inValue; return getColor(inValue, defaultValue); } function resolveThemeSizes>(defaults: T) { const result : T = {} as T; for (let key in defaults) result[key] = resolveThemeSize(key, defaults[key]) as any; return result; } function resolveThemeColors>(defaults: T) { const result : T = {} as T; for (let key in defaults) result[key] = resolveThemeColor(key, defaults[key]) as any; return result; } function isSpecialColor(key?: string) { return key === 'transparent' || key === 'currentColor'; } function getColor(key: string, defaultValue?: string) { if (key === undefined) return defaultValue; let type = ''; let keyResolve = getVar(key, key); if (typeof keyResolve === 'string') key = keyResolve; if (key.includes('.')) [type, key] = key.split('.'); if (isSpecialColor(keyResolve)) return keyResolve; let group = theme.value.colorConfigs[type || 'default']; if (!group) group = theme.value.colorConfigs['default']; return group?.[key] ?? defaultValue; } function getSize(key: string, defaultValue?: string|number) { if (key === undefined) return defaultValue; let type = ''; if (key.includes('.')) [type, key] = key.split('.'); let v: any = undefined; if (type) { const group = theme.value.varOverrides[type]; v = group?.[key]; } else v = theme.value.varOverrides[key]; return resolveSize(v ?? defaultValue); } function getText(key: string, defaultValue?: Record) { return theme.value.textConfigs[key]?? defaultValue; } function getVar(key: string, defaultValue?: T) : T { let rs = undefined; let type = ''; if (key.includes('.')) [type, key] = key.split('.'); if (type) { const group = theme.value.varOverrides[type]; rs = group?.[key]; } else rs = theme.value.varOverrides[key]; return rs ?? defaultValue; } function getVars>(defaults: T) { const result : T = {} as T; for (let key in defaults) { result[key] = getVar(key, defaults[key]); } return result; } function getColors>(defaults: T) { const result : T = {} as T; for (let key in defaults) { result[key] = getColor(key, defaults[key]) as any; } return result; } function useThemeStyle(style: Record) { const mapDynamicVars = new Map(); for (const key in style) { const v = style[key]; if (typeof v === 'object' && typeof v['type'] === 'string') mapDynamicVars.set(key, v) } return computed(() => { for (const [key, v] of mapDynamicVars) { switch (v.type) { case 'var': style[key] = getVar(v.name, v.defaultValue); break; case 'color': style[key] = resolveThemeColor(v.name, v.defaultValue); break; case 'size': style[key] = resolveThemeSize(v.name, v.defaultValue); break; case 'size2': style[key] = `${resolveThemeSize(v.name, v.defaultValue)} ${resolveThemeSize(v.name2, v.defaultValue2)}`; break; } } return style; }) } function useThemeStyles>> (style: Record>) : { [key in keyof T]: ComputedRef> } { const result = {} as Record>; for (const key in style) result[key] = useThemeStyle(style[key]); return result as any; } return { theme, resolveThemeSizes, resolveThemeSize, resolveThemeColors, resolveThemeColor, resolveSize, getSize, getVar, getVars, getText, getColor, getColors, useThemeStyle, useThemeStyles, } } export type ThemeContext = ReturnType; function isNumbrSize(inValue: ThemeSizeType|undefined) { if (inValue == undefined) return false; return typeof inValue === 'number' || !isNaN(Number(inValue)); } function isRealSize(inValue: ThemeSizeType|undefined) { if (inValue == undefined) return false; return typeof inValue === 'number' || (typeof inValue === 'string' && ( inValue.startsWith('calc(') || inValue.endsWith('px') || inValue.endsWith('%') || inValue.endsWith('em') || inValue.endsWith('vh') || inValue.endsWith('vw') )); } /** * 数字单位的处理 * @param inValue 输入值 * @param forceUnit 强制单位,默认为defaultSizeUnit即系统配置单位 * @returns */ export function resolveSize(inValue: ThemeSizeType|undefined, forceUnit = defaultSizeUnit) : string|undefined { if (inValue == undefined) return undefined; if (isNumbrSize(inValue)) return `${inValue}${forceUnit}`; if (isRealSize(inValue as string)) return inValue as string; if (inValue == 'fill') return '100%'; if (inValue == 'half' || inValue == '1/2') return '50%'; if (inValue == '1/3') return '33%'; if (inValue == '1/4') return '25%'; return undefined; } export interface ThemePaddingMargin { l?: number|string, r?: number|string, t?: number|string, b?: number|string, } /** * 配置主题变量,建议在App.vue中调用 * @param cb 回调函数,参数为默认主题配置,返回值为新的主题配置 * @returns 主题配置对象,返回currentTheme为当前主题,可以修改为其他主题对象,但请注意主题对象为shallowRef, * 直接修改主题对象的属性不会触发主题更新,需要调用currentTheme.value = newTheme新的对象来更新主题。 */ export function configTheme( /** * 是否自动匹配系统深色主题 * @default true */ autoMatchSystemDark?: boolean, /** * 回调函数,用于配置修改主题,参数为默认主题配置,返回值为新的主题配置 * @default undefined * @returns 可以返回新的主题对象或者是传入的对象,第一个为亮色主题,第二个为深色主题 */ cb?: (defaultTheme: ThemeConfig, defaultDarkTheme: ThemeConfig) => [ThemeConfig,ThemeConfig] ) { let defaultTheme = ObjectUtils.clone(DefaultTheme); let defaultDarkTheme = ObjectUtils.clone(DefaultDarkTheme); const [theme, darkTheme] = cb?.(defaultTheme, defaultDarkTheme) ?? [defaultTheme, defaultDarkTheme]; const currentSystemDark = ref(autoMatchSystemDark !== false && uni.getAppBaseInfo().theme === 'dark'); const currentTheme = shallowRef(currentSystemDark.value ? darkTheme : theme); provide(ThemeKey, currentTheme); if (autoMatchSystemDark !== false) { // 监听系统主题变化 uni.onThemeChange((res) => { currentSystemDark.value = res.theme === 'dark'; currentTheme.value = currentSystemDark.value ? darkTheme : theme; }); } return { currentTheme, } } /** * 克隆默认主题,用于自定义主题 * @param cb 回调函数,参数为默认主题配置,返回值为新的主题配置 * @param dark 是否为深色主题,默认false为亮色主题 * @returns 新的主题配置对象 */ export function cloneTheme(dark?: boolean, cb?: (defaultTheme: ThemeConfig) => ThemeConfig) { return cb?.(ObjectUtils.clone(dark ? DefaultDarkTheme : DefaultTheme)) ?? ObjectUtils.clone(dark ? DefaultDarkTheme : DefaultTheme); }