看板
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* async-validator 类型声明文件
|
||||
* 适用于 uView Pro 内置 async-validator.js
|
||||
* 支持 Schema、规则、校验回调、辅助类型等
|
||||
*/
|
||||
|
||||
declare type ValidateError = {
|
||||
message: string;
|
||||
field?: string;
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
declare type ValidateCallback = (
|
||||
errors?: ValidateError[] | null,
|
||||
fields?: Record<string, ValidateError[]> | null
|
||||
) => void;
|
||||
|
||||
declare interface ValidateRule {
|
||||
type?: string;
|
||||
required?: boolean;
|
||||
message?: string;
|
||||
validator?: (
|
||||
rule: ValidateRule,
|
||||
value: any,
|
||||
callback: ValidateCallback,
|
||||
source: Record<string, any>,
|
||||
options: ValidateOptions
|
||||
) => void | boolean | string | Error | string[] | Promise<any>;
|
||||
asyncValidator?: (
|
||||
rule: ValidateRule,
|
||||
value: any,
|
||||
callback: ValidateCallback,
|
||||
source: Record<string, any>,
|
||||
options: ValidateOptions
|
||||
) => Promise<any>;
|
||||
enum?: any[];
|
||||
len?: number;
|
||||
min?: number;
|
||||
max?: number;
|
||||
pattern?: RegExp | string;
|
||||
whitespace?: boolean;
|
||||
fields?: Record<string, ValidateRule | ValidateRule[]>;
|
||||
defaultField?: ValidateRule | ValidateRule[];
|
||||
transform?: (value: any) => any;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
declare interface ValidateOptions {
|
||||
messages?: Record<string, any>;
|
||||
suppressWarning?: boolean;
|
||||
first?: boolean;
|
||||
firstFields?: boolean | string[];
|
||||
keys?: string[];
|
||||
error?: (rule: ValidateRule, msg: string) => ValidateError;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
declare class Schema {
|
||||
constructor(descriptor: Record<string, ValidateRule | ValidateRule[]>);
|
||||
messages(messages?: Record<string, any>): Record<string, any>;
|
||||
define(rules: Record<string, ValidateRule | ValidateRule[]>): void;
|
||||
validate(
|
||||
source: Record<string, any>,
|
||||
options?: ValidateOptions | ValidateCallback,
|
||||
callback?: ValidateCallback
|
||||
): Promise<void>;
|
||||
getType(rule: ValidateRule): string;
|
||||
getValidationMethod(rule: ValidateRule): Function | false;
|
||||
static register(type: string, validator: Function): void;
|
||||
static warning: (...args: any[]) => void;
|
||||
static messages: Record<string, any>;
|
||||
}
|
||||
|
||||
export = Schema;
|
||||
export as namespace Schema;
|
||||
File diff suppressed because one or more lines are too long
+57
@@ -0,0 +1,57 @@
|
||||
// Type definitions for calendar.js (农历/公历互转)
|
||||
// Project: https://github.com/jjonline/calendar.js
|
||||
|
||||
export interface Solar2LunarResult {
|
||||
lYear: number; // 农历年
|
||||
lMonth: number; // 农历月
|
||||
lDay: number; // 农历日
|
||||
Animal: string; // 生肖
|
||||
IMonthCn: string; // 农历月中文
|
||||
IDayCn: string; // 农历日中文
|
||||
cYear: number; // 公历年
|
||||
cMonth: number; // 公历月
|
||||
cDay: number; // 公历日
|
||||
gzYear: string; // 干支年
|
||||
gzMonth: string; // 干支月
|
||||
gzDay: string; // 干支日
|
||||
isToday: boolean; // 是否今天
|
||||
isLeap: boolean; // 是否闰月
|
||||
nWeek: number; // 星期几(1-7,周一为1)
|
||||
ncWeek: string; // 星期几中文
|
||||
isTerm: boolean; // 是否节气
|
||||
Term: string | null; // 节气名
|
||||
astro: string; // 星座
|
||||
}
|
||||
|
||||
export interface Lunar2SolarResult extends Solar2LunarResult {}
|
||||
|
||||
export interface Calendar {
|
||||
lunarInfo: number[];
|
||||
solarMonth: number[];
|
||||
Gan: string[];
|
||||
Zhi: string[];
|
||||
Animals: string[];
|
||||
solarTerm: string[];
|
||||
sTermInfo: string[];
|
||||
nStr1: string[];
|
||||
nStr2: string[];
|
||||
nStr3: string[];
|
||||
|
||||
lYearDays(y: number): number;
|
||||
leapMonth(y: number): number;
|
||||
leapDays(y: number): number;
|
||||
monthDays(y: number, m: number): number;
|
||||
solarDays(y: number, m: number): number;
|
||||
toGanZhiYear(lYear: number): string;
|
||||
toAstro(cMonth: number, cDay: number): string;
|
||||
toGanZhi(offset: number): string;
|
||||
getTerm(y: number, n: number): number;
|
||||
toChinaMonth(m: number): string;
|
||||
toChinaDay(d: number): string;
|
||||
getAnimal(y: number): string;
|
||||
solar2lunar(y: number, m: number, d: number): Solar2LunarResult;
|
||||
lunar2solar(y: number, m: number, d: number, isLeapMonth?: boolean): Lunar2SolarResult;
|
||||
}
|
||||
|
||||
declare const calendar: Calendar;
|
||||
export default calendar;
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* 适配 canvas 2d 上下文
|
||||
* @param ctx canvas 2d 上下文
|
||||
* @returns
|
||||
*/
|
||||
export function canvas2d(ctx: CanvasRenderingContext2D): UniApp.CanvasContext {
|
||||
return Object.assign(ctx, {
|
||||
setFillStyle(color: string | CanvasGradient) {
|
||||
ctx.fillStyle = color;
|
||||
},
|
||||
setStrokeStyle(color: string | CanvasGradient | CanvasPattern) {
|
||||
ctx.strokeStyle = color;
|
||||
},
|
||||
setLineWidth(lineWidth: number) {
|
||||
ctx.lineWidth = lineWidth;
|
||||
},
|
||||
setLineCap(lineCap: 'butt' | 'round' | 'square') {
|
||||
ctx.lineCap = lineCap;
|
||||
},
|
||||
|
||||
setFontSize(font: string) {
|
||||
ctx.font = font;
|
||||
},
|
||||
setGlobalAlpha(alpha: number) {
|
||||
ctx.globalAlpha = alpha;
|
||||
},
|
||||
setLineJoin(lineJoin: 'bevel' | 'round' | 'miter') {
|
||||
ctx.lineJoin = lineJoin;
|
||||
},
|
||||
setTextAlign(align: 'left' | 'center' | 'right') {
|
||||
ctx.textAlign = align;
|
||||
},
|
||||
setMiterLimit(miterLimit: number) {
|
||||
ctx.miterLimit = miterLimit;
|
||||
},
|
||||
setShadow(offsetX: number, offsetY: number, blur: number, color: string) {
|
||||
ctx.shadowOffsetX = offsetX;
|
||||
ctx.shadowOffsetY = offsetY;
|
||||
ctx.shadowBlur = blur;
|
||||
ctx.shadowColor = color;
|
||||
},
|
||||
setTextBaseline(textBaseline: 'top' | 'bottom' | 'middle') {
|
||||
ctx.textBaseline = textBaseline;
|
||||
},
|
||||
createCircularGradient() {},
|
||||
draw() {},
|
||||
addColorStop() {}
|
||||
}) as unknown as UniApp.CanvasContext;
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,805 @@
|
||||
// libs/util/config-provider.ts
|
||||
|
||||
// 全局配置类:管理 Theme 的初始化、切换、持久化
|
||||
|
||||
import { ref } from 'vue';
|
||||
import type { DarkMode, Theme, ThemeColor } from '../../types/global';
|
||||
import config from '../config/config';
|
||||
import { defaultThemes } from '../config/theme-tokens';
|
||||
import { color as reactiveColor } from '../config/color';
|
||||
import { getSystemDarkMode as getNativeSystemDarkMode } from './system-theme';
|
||||
import * as localePack from '../../locale';
|
||||
|
||||
declare const uni: any;
|
||||
|
||||
const THEME_STORAGE_KEY = 'uview-pro-theme';
|
||||
const DARK_MODE_STORAGE_KEY = 'uview-pro-dark-mode';
|
||||
const LOCALE_STORAGE_KEY = 'uview-pro-locale';
|
||||
const DEFAULT_LIGHT_TOKENS = (defaultThemes[0]?.color || {}) as Partial<ThemeColor>;
|
||||
const DEFAULT_DARK_TOKENS = (defaultThemes[0]?.darkColor || {}) as Partial<ThemeColor>;
|
||||
const STRUCTURAL_TOKENS = new Set([
|
||||
'bgColor',
|
||||
'bgWhite',
|
||||
'bgGrayLight',
|
||||
'bgGrayDark',
|
||||
'bgBlack',
|
||||
'borderColor',
|
||||
'lightColor',
|
||||
'mainColor',
|
||||
'contentColor',
|
||||
'tipsColor',
|
||||
'whiteColor',
|
||||
'blackColor',
|
||||
'dividerColor',
|
||||
'maskColor',
|
||||
'shadowColor'
|
||||
]);
|
||||
|
||||
export type DefaultThemeConfig = {
|
||||
/**
|
||||
* 默认主题
|
||||
*/
|
||||
defaultTheme?: string;
|
||||
/**
|
||||
* 默认暗黑模式
|
||||
*/
|
||||
defaultDarkMode?: DarkMode;
|
||||
};
|
||||
|
||||
/**
|
||||
* ConfigProvider: 管理全局主题
|
||||
* - init(themes, defaultName): 初始化主题系统
|
||||
* - setTheme(name): 切换主题并持久化
|
||||
* - getThemes/getCurrentTheme: 读取当前数据
|
||||
* - setDarkMode/getDarkMode: 管理暗黑模式
|
||||
*/
|
||||
export class ConfigProvider {
|
||||
// 响应式状态,供外部直接引用
|
||||
public themesRef = ref<Theme[]>([]);
|
||||
public currentThemeRef = ref<Theme | null>(null);
|
||||
public darkModeRef = ref<DarkMode>(config.defaultDarkMode);
|
||||
public cssVarsRef = ref<Record<string, string>>({});
|
||||
// 国际化 i18n 状态
|
||||
public localesRef = ref<any[]>([]);
|
||||
public currentLocaleRef = ref<any | null>(null);
|
||||
private baseColorTokens: Partial<ThemeColor> = DEFAULT_LIGHT_TOKENS;
|
||||
private baseDarkColorTokens: Partial<ThemeColor> = DEFAULT_DARK_TOKENS;
|
||||
private debug: boolean = false;
|
||||
private systemDarkModeMediaQuery: MediaQueryList | null = null;
|
||||
private lastAppliedCssKeys: string[] = [];
|
||||
private interval: ReturnType<typeof setInterval> | number = 0;
|
||||
|
||||
constructor() {
|
||||
// 默认不自动初始化,调用 init 以传入主题列表
|
||||
this.initSystemDarkModeListener();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化系统暗黑模式监听器
|
||||
* 支持 H5、App、小程序等平台
|
||||
*/
|
||||
private initSystemDarkModeListener() {
|
||||
// H5 平台:使用 matchMedia API
|
||||
try {
|
||||
if (typeof window !== 'undefined' && window.matchMedia) {
|
||||
this.systemDarkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
const listener = () => {
|
||||
if (this.darkModeRef.value === 'auto') {
|
||||
this.applyTheme(this.currentThemeRef.value);
|
||||
}
|
||||
};
|
||||
if (this.systemDarkModeMediaQuery.addEventListener) {
|
||||
this.systemDarkModeMediaQuery.addEventListener('change', listener);
|
||||
} else if (this.systemDarkModeMediaQuery.addListener) {
|
||||
this.systemDarkModeMediaQuery.addListener(listener);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (this.debug) console.warn('[ConfigProvider] H5 system dark mode listener failed', e);
|
||||
}
|
||||
|
||||
// uni-app 平台:使用 uni.onThemeChange API
|
||||
try {
|
||||
if (typeof uni !== 'undefined' && typeof uni.onThemeChange === 'function') {
|
||||
uni.onThemeChange((res: { theme: string }) => {
|
||||
console.log('[ConfigProvider] system theme changed', res);
|
||||
if (this.darkModeRef.value === 'auto') {
|
||||
// 系统主题变化时,重新应用主题
|
||||
this.applyTheme(this.currentThemeRef.value);
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
if (this.debug) console.warn('[ConfigProvider] uni-app system dark mode listener failed', e);
|
||||
}
|
||||
this.initAppEvent();
|
||||
}
|
||||
|
||||
/**
|
||||
* App 平台事件监听
|
||||
* 经测试 uni.onThemeChange 在 App 平台目前没生效,暂时只能通过定时检查
|
||||
*/
|
||||
private initAppEvent(): void {
|
||||
// #ifdef APP
|
||||
try {
|
||||
if (this.interval) clearInterval(this.interval);
|
||||
|
||||
this.interval = setInterval(() => {
|
||||
if (this.darkModeRef.value === 'auto') {
|
||||
// 系统主题变化时,重新应用主题
|
||||
this.applyTheme(this.currentThemeRef.value);
|
||||
}
|
||||
}, 5000);
|
||||
} catch (e) {
|
||||
if (this.debug) console.warn('[ConfigProvider] setInterval failed', e);
|
||||
}
|
||||
// #endif
|
||||
}
|
||||
/**
|
||||
* 检测当前是否应该使用暗黑模式
|
||||
*/
|
||||
private isSystemDarkMode(): boolean {
|
||||
try {
|
||||
if (this.systemDarkModeMediaQuery) {
|
||||
return this.systemDarkModeMediaQuery.matches;
|
||||
}
|
||||
} catch (e) {
|
||||
if (this.debug) console.warn('[ConfigProvider] matchMedia check failed', e);
|
||||
}
|
||||
try {
|
||||
return getNativeSystemDarkMode() === 'dark';
|
||||
} catch (e) {
|
||||
if (this.debug) console.warn('[ConfigProvider] native system theme check failed', e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化主题系统
|
||||
* @param themes 可用主题数组
|
||||
* @param defaultTheme 可选默认主题名
|
||||
*/
|
||||
initTheme(themes?: Theme[], defaultConfig?: string | DefaultThemeConfig, isForce?: boolean) {
|
||||
const normalizedThemes = this.normalizeThemes(themes);
|
||||
if (!normalizedThemes.length) {
|
||||
console.warn('[ConfigProvider] init called with empty themes');
|
||||
return;
|
||||
}
|
||||
|
||||
// 配置默认主题
|
||||
if (defaultConfig) {
|
||||
if (typeof defaultConfig === 'string') {
|
||||
config.defaultTheme = defaultConfig || config.defaultTheme;
|
||||
} else if (typeof defaultConfig === 'object') {
|
||||
const { defaultTheme, defaultDarkMode } = defaultConfig;
|
||||
config.defaultTheme = defaultTheme || config.defaultTheme;
|
||||
config.defaultDarkMode = defaultDarkMode || config.defaultDarkMode;
|
||||
}
|
||||
}
|
||||
|
||||
// 设置主题列表,响应式
|
||||
this.themesRef.value = normalizedThemes.slice();
|
||||
|
||||
// 先尝试从 Storage 读取已保存主题名
|
||||
const saved = this.readStorage<string>(THEME_STORAGE_KEY);
|
||||
let initialName = saved || config.defaultTheme || this.themesRef.value[0].name;
|
||||
if (isForce && config.defaultTheme) initialName = config.defaultTheme;
|
||||
let found = this.themesRef.value.find(t => t.name === initialName);
|
||||
if (!found) found = this.themesRef.value.find(t => t.name === config.defaultTheme);
|
||||
if (!found) found = this.themesRef.value[0];
|
||||
|
||||
// 设置当前主题,响应式
|
||||
this.currentThemeRef.value = found;
|
||||
|
||||
// 初始化暗黑模式设置
|
||||
this.initDarkMode(config.defaultDarkMode, isForce);
|
||||
|
||||
// 应用主题
|
||||
this.applyTheme(found);
|
||||
|
||||
if (this.debug)
|
||||
console.log('[ConfigProvider] initialized, theme=', found.name, 'darkMode=', this.darkModeRef.value);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化暗黑模式设置
|
||||
* @param darkMode
|
||||
*/
|
||||
initDarkMode(darkMode?: DarkMode, isForce?: boolean) {
|
||||
// 尝试从 Storage 读取暗黑模式设置
|
||||
const savedDarkMode = this.readStorage<DarkMode>(DARK_MODE_STORAGE_KEY);
|
||||
let darkModeValue = savedDarkMode || darkMode || config.defaultDarkMode;
|
||||
if (isForce && darkMode) darkModeValue = darkMode;
|
||||
this.darkModeRef.value = darkModeValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化国际化数据
|
||||
* @param locales 可选的 locale 列表(对象数组,包含 name 字段)
|
||||
* @param defaultLocaleName 可选默认 locale 名称
|
||||
*/
|
||||
initLocales(locales?: any[], defaultLocaleName?: string, isForce?: boolean) {
|
||||
const normalized = this.normalizeLocales(locales);
|
||||
if (!normalized.length) {
|
||||
if (this.debug) console.warn('[ConfigProvider] initLocales called with empty locales');
|
||||
return;
|
||||
}
|
||||
|
||||
this.localesRef.value = normalized.slice();
|
||||
|
||||
// 尝试从 Storage 读取已保存 locale 名称
|
||||
const saved = this.readStorage<string>(LOCALE_STORAGE_KEY);
|
||||
|
||||
// 根据传入的 defaultLocaleName 或 saved 或 config.defaultLocale 查找 locale
|
||||
let initialName = saved || defaultLocaleName || config.defaultLocale;
|
||||
if (isForce && defaultLocaleName) initialName = defaultLocaleName;
|
||||
let found = this.localesRef.value.find((l: any) => l.name === initialName);
|
||||
if (!found) found = this.localesRef.value.find(l => l.name === config.defaultLocale);
|
||||
if (!found) found = this.localesRef.value[0];
|
||||
|
||||
this.currentLocaleRef.value = found;
|
||||
|
||||
if (this.debug) console.log('[ConfigProvider] locales initialized, locale=', found?.name);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 归一化 locale 配置,保证始终至少有一个默认 locale
|
||||
*/
|
||||
private normalizeLocales(locales?: any[]) {
|
||||
// 获取内置语言包(可能包含 zh-CN / en-US 等)
|
||||
let builtinList: any[] = [];
|
||||
try {
|
||||
builtinList = Object.values(localePack || {}).filter(v => v && typeof v === 'object');
|
||||
} catch (e) {
|
||||
if (this.debug) console.warn('[ConfigProvider] normalizeLocales read builtin failed', e);
|
||||
}
|
||||
|
||||
// 如果没有传入自定义 locales,直接返回内置列表
|
||||
if (!Array.isArray(locales) || !locales.length) {
|
||||
return builtinList.slice();
|
||||
}
|
||||
|
||||
// 将 builtin 按 name 映射,便于合并
|
||||
const map = new Map<string, any>();
|
||||
builtinList.forEach(item => {
|
||||
if (item && item.name) {
|
||||
map.set(item.name, { ...(item || {}) });
|
||||
}
|
||||
});
|
||||
|
||||
// 合并用户传入的 locales:若 name 相同则对对象字段进行浅合并(嵌套对象尝试合并)
|
||||
locales.forEach(loc => {
|
||||
if (!loc || !loc.name) return;
|
||||
const existing = map.get(loc.name);
|
||||
if (!existing) {
|
||||
// 新增语言直接加入
|
||||
map.set(loc.name, { ...(loc || {}) });
|
||||
return;
|
||||
}
|
||||
// 合并:对每个 key,如果是对象且现有为对象,则进行浅合并,否则覆盖
|
||||
const merged: any = { ...existing };
|
||||
Object.keys(loc).forEach(k => {
|
||||
const v = (loc as any)[k];
|
||||
if (v != null && typeof v === 'object' && !Array.isArray(v) && typeof merged[k] === 'object') {
|
||||
merged[k] = { ...(merged[k] || {}), ...(v || {}) };
|
||||
} else {
|
||||
merged[k] = v;
|
||||
}
|
||||
});
|
||||
map.set(loc.name, merged);
|
||||
});
|
||||
|
||||
return Array.from(map.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有可用 locale
|
||||
*/
|
||||
getLocales() {
|
||||
return this.localesRef.value.slice();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前 locale 对象
|
||||
*/
|
||||
getCurrentLocale() {
|
||||
return this.currentLocaleRef.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换 locale 并持久化
|
||||
*/
|
||||
setLocale(localeName: string) {
|
||||
if (!this.localesRef.value || this.localesRef.value.length === 0) {
|
||||
console.warn('[ConfigProvider] setLocale called but locales list empty');
|
||||
return;
|
||||
}
|
||||
const locale = this.localesRef.value.find(l => l.name === localeName);
|
||||
if (!locale) {
|
||||
console.warn('[ConfigProvider] locale not found:', localeName);
|
||||
return;
|
||||
}
|
||||
this.currentLocaleRef.value = locale;
|
||||
this.writeStorage(LOCALE_STORAGE_KEY, localeName);
|
||||
if (this.debug) console.log('[ConfigProvider] setLocale ->', localeName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 翻译函数
|
||||
* 支持 key 路径,例如 'calendar.placeholder'
|
||||
* replacements 支持数组或对象替换占位符 {0} 或 {name}
|
||||
*/
|
||||
t(key: string, replacements?: any, localeName?: string): string {
|
||||
// 如果 locales 尚未初始化,尝试懒初始化,使用内置语言包作为回退
|
||||
try {
|
||||
if (!Array.isArray(this.localesRef.value) || this.localesRef.value.length === 0) {
|
||||
this.initLocales();
|
||||
}
|
||||
} catch (e) {
|
||||
if (this.debug) console.warn('[ConfigProvider] lazy initLocales failed', e);
|
||||
}
|
||||
|
||||
const localeObj =
|
||||
(localeName && this.localesRef.value.find(l => l.name === localeName)) || this.currentLocaleRef.value;
|
||||
if (!localeObj) return key;
|
||||
const parts = key.split('.');
|
||||
let cur: any = localeObj;
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
if (cur == null) break;
|
||||
cur = cur[parts[i]];
|
||||
}
|
||||
let text = typeof cur === 'string' ? cur : key;
|
||||
if (replacements != null) {
|
||||
if (Array.isArray(replacements)) {
|
||||
replacements.forEach((val, idx) => {
|
||||
text = text.split(`{${idx}}`).join(String(val));
|
||||
});
|
||||
} else if (typeof replacements === 'object') {
|
||||
Object.keys(replacements).forEach(k => {
|
||||
text = text.split(`{${k}}`).join(String(replacements[k]));
|
||||
});
|
||||
}
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有可用主题
|
||||
*/
|
||||
getThemes(): Theme[] {
|
||||
return this.themesRef.value.slice();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前主题
|
||||
*/
|
||||
getCurrentTheme(): Theme | null {
|
||||
return this.currentThemeRef.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换主题并持久化
|
||||
*/
|
||||
setTheme(themeName: string) {
|
||||
if (!this.themesRef.value || this.themesRef.value.length === 0) {
|
||||
console.warn('[ConfigProvider] setTheme called but themes list empty');
|
||||
return;
|
||||
}
|
||||
|
||||
const theme = this.themesRef.value.find(t => t.name === themeName);
|
||||
if (!theme) {
|
||||
console.warn('[ConfigProvider] theme not found:', themeName);
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentThemeRef.value = theme;
|
||||
|
||||
// 应用
|
||||
this.applyTheme(theme);
|
||||
|
||||
// 持久化
|
||||
this.writeStorage(THEME_STORAGE_KEY, themeName);
|
||||
|
||||
if (this.debug) console.log('[ConfigProvider] setTheme ->', themeName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 运行时更新当前主题颜色并应用(不持久化)
|
||||
* @param colors 主题颜色键值,支持部分更新
|
||||
*/
|
||||
public setThemeColor(colors: Partial<ThemeColor>) {
|
||||
if (!colors || Object.keys(colors).length === 0) return;
|
||||
if (!this.currentThemeRef.value) {
|
||||
console.warn('[ConfigProvider] setThemeColor called but no current theme');
|
||||
return;
|
||||
}
|
||||
|
||||
const mode = this.getActiveMode();
|
||||
|
||||
if (mode === 'dark') {
|
||||
const existing = this.currentThemeRef.value.darkColor || {};
|
||||
this.currentThemeRef.value.darkColor = {
|
||||
...existing,
|
||||
...colors
|
||||
};
|
||||
} else {
|
||||
const existing = this.currentThemeRef.value.color || {};
|
||||
this.currentThemeRef.value.color = {
|
||||
...existing,
|
||||
...colors
|
||||
};
|
||||
}
|
||||
|
||||
// 重新应用当前主题以同步运行时 color、CSS 变量等
|
||||
this.applyTheme(this.currentThemeRef.value);
|
||||
|
||||
if (this.debug) console.log('[ConfigProvider] setThemeColor ->', colors);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前暗黑模式设置
|
||||
*/
|
||||
getDarkMode(): DarkMode {
|
||||
return this.darkModeRef.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置暗黑模式
|
||||
* @param mode 'auto' (跟随系统) | 'light' (强制亮色) | 'dark' (强制暗黑)
|
||||
*/
|
||||
setDarkMode(mode: DarkMode) {
|
||||
this.darkModeRef.value = mode;
|
||||
// 持久化
|
||||
this.writeStorage(DARK_MODE_STORAGE_KEY, mode);
|
||||
|
||||
// 重新应用主题
|
||||
this.applyTheme(this.currentThemeRef.value);
|
||||
|
||||
if (this.debug) console.log('[ConfigProvider] setDarkMode ->', mode);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查当前是否处于暗黑模式
|
||||
*/
|
||||
isInDarkMode(): boolean {
|
||||
const mode = this.darkModeRef.value;
|
||||
if (mode === 'dark') return true;
|
||||
if (mode === 'light') return false;
|
||||
// auto 模式下检查系统设置
|
||||
return this.isSystemDarkMode();
|
||||
}
|
||||
|
||||
/**
|
||||
* 归一化主题配置,保证始终至少有一个默认主题
|
||||
*/
|
||||
private normalizeThemes(themes?: Theme[]): Theme[] {
|
||||
if (Array.isArray(themes) && themes.length) {
|
||||
return this.mergeThemes(defaultThemes, themes);
|
||||
}
|
||||
return defaultThemes.slice();
|
||||
}
|
||||
|
||||
private mergeThemes(...lists: Array<Theme[] | undefined>): Theme[] {
|
||||
const map = new Map<string, Theme>();
|
||||
lists
|
||||
.filter((list): list is Theme[] => Array.isArray(list) && list.length > 0)
|
||||
.forEach(list => {
|
||||
list.forEach(theme => {
|
||||
const normalized = this.ensureDarkVariant({
|
||||
...theme,
|
||||
color: this.applyColorFallbacks(theme.color),
|
||||
darkColor: theme.darkColor ? { ...theme.darkColor } : undefined,
|
||||
css: theme.css ? { ...theme.css } : undefined,
|
||||
darkCss: theme.darkCss ? { ...theme.darkCss } : undefined
|
||||
});
|
||||
map.set(normalized.name, normalized);
|
||||
});
|
||||
});
|
||||
return Array.from(map.values());
|
||||
}
|
||||
|
||||
private ensureDarkVariant(theme: Theme): Theme {
|
||||
const finalDark = this.buildDarkPalette(theme);
|
||||
return {
|
||||
...theme,
|
||||
darkColor: this.applyDarkFallbacks(finalDark)
|
||||
};
|
||||
}
|
||||
|
||||
private buildDarkPalette(theme: Theme): Partial<ThemeColor> {
|
||||
const provided = theme.darkColor || {};
|
||||
const generated = this.generateDarkFromLight(theme.color || {}, provided);
|
||||
return {
|
||||
...generated,
|
||||
...provided
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用亮色主题
|
||||
*/
|
||||
private applyColorFallbacks(color?: Partial<ThemeColor>): Partial<ThemeColor> {
|
||||
return {
|
||||
...(this.baseColorTokens || {}),
|
||||
...(color || {})
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用暗黑主题
|
||||
*/
|
||||
private applyDarkFallbacks(color?: Partial<ThemeColor>): Partial<ThemeColor> {
|
||||
return {
|
||||
...(this.baseDarkColorTokens || {}),
|
||||
...(color || {})
|
||||
};
|
||||
}
|
||||
|
||||
private filterNonStructuralTokens(palette: Partial<ThemeColor>): Partial<ThemeColor> {
|
||||
const result: Partial<ThemeColor> = {};
|
||||
Object.entries(palette || {}).forEach(([key, value]) => {
|
||||
if (!this.isStructuralToken(key)) {
|
||||
(result as any)[key] = value;
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
private generateDarkFromLight(palette: Partial<ThemeColor>, provided: Partial<ThemeColor>): Partial<ThemeColor> {
|
||||
const result: Partial<ThemeColor> = {};
|
||||
const nonStructural = this.filterNonStructuralTokens(palette);
|
||||
Object.entries(nonStructural).forEach(([key, value]) => {
|
||||
if (typeof value !== 'string') return;
|
||||
if (provided && Object.prototype.hasOwnProperty.call(provided, key)) {
|
||||
return;
|
||||
}
|
||||
const fallback = (this.baseDarkColorTokens as any)?.[key];
|
||||
(result as any)[key] = this.createDarkVariantFromLight(value, fallback);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
private createDarkVariantFromLight(color: string, fallback?: string): string {
|
||||
const normalized = this.normalizeHex(color);
|
||||
const fallbackHex = fallback ? this.normalizeHex(fallback) : null;
|
||||
if (normalized && fallbackHex) {
|
||||
return this.mixHex(normalized, fallbackHex, 0.6);
|
||||
}
|
||||
if (fallbackHex) return fallbackHex;
|
||||
return normalized || color;
|
||||
}
|
||||
|
||||
private normalizeHex(color: string): string | null {
|
||||
if (!color) return null;
|
||||
const hex = color.trim();
|
||||
if (/^#([0-9a-fA-F]{6})$/.test(hex)) return hex.toLowerCase();
|
||||
return null;
|
||||
}
|
||||
|
||||
private mixHex(fromHex: string, toHex: string, ratio: number): string {
|
||||
const from = this.hexToRgb(fromHex);
|
||||
const to = this.hexToRgb(toHex);
|
||||
if (!from || !to) return toHex;
|
||||
const clamp = (val: number) => Math.min(255, Math.max(0, Math.round(val)));
|
||||
const r = clamp(from.r * (1 - ratio) + to.r * ratio);
|
||||
const g = clamp(from.g * (1 - ratio) + to.g * ratio);
|
||||
const b = clamp(from.b * (1 - ratio) + to.b * ratio);
|
||||
return this.rgbToHex(r, g, b);
|
||||
}
|
||||
|
||||
private hexToRgb(hex: string): { r: number; g: number; b: number } | null {
|
||||
const match = /^#([0-9a-fA-F]{6})$/.exec(hex);
|
||||
if (!match) return null;
|
||||
return {
|
||||
r: parseInt(match[1].slice(0, 2), 16),
|
||||
g: parseInt(match[1].slice(2, 4), 16),
|
||||
b: parseInt(match[1].slice(4, 6), 16)
|
||||
};
|
||||
}
|
||||
|
||||
private rgbToHex(r: number, g: number, b: number): string {
|
||||
const toHex = (val: number) => val.toString(16).padStart(2, '0');
|
||||
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
|
||||
}
|
||||
|
||||
private isStructuralToken(token: string): boolean {
|
||||
return STRUCTURAL_TOKENS.has(token);
|
||||
}
|
||||
|
||||
/**
|
||||
* 运行时同步主题颜色($u.color)
|
||||
* 更新响应式 color 对象,确保所有使用 $u.color 的地方都能响应式更新
|
||||
*/
|
||||
private syncRuntimeTheme(palette: Partial<ThemeColor>) {
|
||||
try {
|
||||
// 合并默认值,确保所有颜色都有值
|
||||
const defaultPalette = this.getActiveMode() === 'dark' ? this.baseDarkColorTokens : this.baseColorTokens;
|
||||
|
||||
const mergedPalette = {
|
||||
...defaultPalette,
|
||||
...palette
|
||||
};
|
||||
|
||||
// 更新响应式 color 对象
|
||||
Object.keys(mergedPalette).forEach(key => {
|
||||
const value = (mergedPalette as any)[key];
|
||||
if (value != null) {
|
||||
(reactiveColor as any)[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
// 同步到 uni.$u.color(如果存在)
|
||||
if (typeof uni !== 'undefined' && uni?.$u?.color) {
|
||||
uni.$u.color = reactiveColor;
|
||||
}
|
||||
} catch (e) {
|
||||
if (this.debug) console.warn('[ConfigProvider] sync runtime theme failed', e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前激活的模式
|
||||
*/
|
||||
private getActiveMode(): 'light' | 'dark' {
|
||||
return this.isInDarkMode() ? 'dark' : 'light';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前主题的配色方案
|
||||
*/
|
||||
private getPaletteForMode(theme: Theme, mode: 'light' | 'dark') {
|
||||
if (mode === 'dark') {
|
||||
return theme.darkColor && Object.keys(theme.darkColor).length ? theme.darkColor : theme.color || {};
|
||||
}
|
||||
return theme.color || {};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前主题的CSS变量覆盖
|
||||
*/
|
||||
private getCssOverrides(theme: Theme, mode: 'light' | 'dark') {
|
||||
if (mode === 'dark') {
|
||||
return (theme.darkCss && Object.keys(theme.darkCss).length ? theme.darkCss : theme.css) || {};
|
||||
}
|
||||
return theme.css || {};
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取Storage key
|
||||
*/
|
||||
private readStorage<T>(key: string): T | null {
|
||||
try {
|
||||
if (typeof uni === 'undefined' || typeof uni.getStorageSync !== 'function') return null;
|
||||
const value = uni.getStorageSync(key);
|
||||
return (value ?? null) as T | null;
|
||||
} catch (e) {
|
||||
if (this.debug) console.warn('[ConfigProvider] failed to read storage', e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入Storage key value
|
||||
*/
|
||||
private writeStorage(key: string, value: any) {
|
||||
try {
|
||||
if (typeof uni === 'undefined' || typeof uni.setStorageSync !== 'function') return;
|
||||
uni.setStorageSync(key, value);
|
||||
} catch (e) {
|
||||
if (this.debug) console.warn('[ConfigProvider] failed to write storage', e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新文档主题模式 H5
|
||||
*/
|
||||
private updateDocumentMode(mode: 'light' | 'dark') {
|
||||
if (typeof document === 'undefined' || !document.documentElement) return;
|
||||
const root = document.documentElement;
|
||||
root.dataset.uThemeMode = mode;
|
||||
root.classList.remove('u-theme-light', 'u-theme-dark');
|
||||
root.classList.add(`u-theme-${mode}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为 CSS 变量名称
|
||||
*/
|
||||
private toCssVarName(key: string, prefix: string = '--u'): string {
|
||||
const types = config.type;
|
||||
if (types.some(type => key.startsWith(type))) {
|
||||
prefix += '-type';
|
||||
}
|
||||
const kebab = key
|
||||
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
||||
.replace(/[\s_]+/g, '-')
|
||||
.toLowerCase();
|
||||
return `${prefix}-${kebab}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加 RGB 值
|
||||
*/
|
||||
private attachRgbVar(target: Record<string, string>, varName: string, value: string) {
|
||||
if (typeof value !== 'string') return;
|
||||
const hex = value.startsWith('#') ? value.slice(1) : '';
|
||||
if (!/^[0-9a-fA-F]{6}$/.test(hex)) return;
|
||||
const r = parseInt(hex.slice(0, 2), 16);
|
||||
const g = parseInt(hex.slice(2, 4), 16);
|
||||
const b = parseInt(hex.slice(4, 6), 16);
|
||||
target[`${varName}-rgb`] = `${r}, ${g}, ${b}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建 CSS 变量映射表
|
||||
* 生成格式:
|
||||
*/
|
||||
private buildCssVarMap(theme: Theme, mode: 'light' | 'dark'): Record<string, string> {
|
||||
const map: Record<string, string> = {
|
||||
'--u-theme-mode': mode
|
||||
};
|
||||
const palette = this.getPaletteForMode(theme, mode);
|
||||
const cssOverrides = this.getCssOverrides(theme, mode);
|
||||
|
||||
const applyEntry = (key: string, value: string | number | undefined | null) => {
|
||||
if (value == null) return;
|
||||
const strValue = String(value);
|
||||
if (key.startsWith('--')) {
|
||||
map[key] = strValue;
|
||||
this.attachRgbVar(map, key, strValue);
|
||||
return;
|
||||
}
|
||||
const cssVarName = this.toCssVarName(key);
|
||||
// const typeVarName = this.toCssVarName(key, '--u-type');
|
||||
map[cssVarName] = strValue;
|
||||
// map[typeVarName] = strValue;
|
||||
this.attachRgbVar(map, cssVarName, strValue);
|
||||
// this.attachRgbVar(map, typeVarName, strValue);
|
||||
};
|
||||
|
||||
Object.entries(palette || {}).forEach(([key, value]) => applyEntry(key, value as any));
|
||||
Object.entries(cssOverrides || {}).forEach(([key, value]) => applyEntry(key, value as any));
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新 CSS 变量 H5
|
||||
*/
|
||||
private flushCssVars(vars: Record<string, string>) {
|
||||
if (typeof document === 'undefined' || !document.documentElement) return;
|
||||
const root = document.documentElement;
|
||||
this.lastAppliedCssKeys.forEach(key => {
|
||||
root.style.removeProperty(key);
|
||||
});
|
||||
Object.entries(vars).forEach(([key, value]) => {
|
||||
root.style.setProperty(key, value);
|
||||
});
|
||||
this.lastAppliedCssKeys = Object.keys(vars);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将主题应用到运行时:
|
||||
* - 1) 调用 uni.$u.setColor(theme.color) 如果存在
|
||||
* - 2) 在 H5 环境中,将 css map 注入到 document.documentElement 的 CSS 变量中
|
||||
* - 3) 支持暗黑模式:根据 darkColor 或 color 应用相应的颜色
|
||||
*/
|
||||
private applyTheme(theme: Theme | null) {
|
||||
if (!theme) return;
|
||||
const mode = this.getActiveMode();
|
||||
const palette = this.getPaletteForMode(theme, mode);
|
||||
this.syncRuntimeTheme(palette);
|
||||
|
||||
const cssVarMap = this.buildCssVarMap(theme, mode);
|
||||
this.cssVarsRef.value = cssVarMap;
|
||||
this.flushCssVars(cssVarMap);
|
||||
this.updateDocumentMode(mode);
|
||||
}
|
||||
}
|
||||
|
||||
// 单例导出
|
||||
export const configProvider = new ConfigProvider();
|
||||
export default configProvider;
|
||||
@@ -0,0 +1,112 @@
|
||||
import { getCurrentInstance, type ComponentInternalInstance } from 'vue';
|
||||
|
||||
/**
|
||||
* 适用于 uni-app Vue3 的事件派发/广播工具
|
||||
* 用法:import { dispatch, broadcast } from './emitter'
|
||||
*/
|
||||
|
||||
/**
|
||||
* 向上查找父组件并派发事件
|
||||
* @param instance 当前组件实例(setup中可用getCurrentInstance())
|
||||
* @param componentName 目标组件名
|
||||
* @param eventName 事件名
|
||||
* @param params 参数
|
||||
*/
|
||||
|
||||
// 将事件名转换为驼峰格式
|
||||
// 例如:on-form-change -> onFormChange
|
||||
function formatToCamelCase(str: string): string {
|
||||
return str.replace(/-([a-z])/g, function (g) {
|
||||
return g[1].toUpperCase();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 向上查找父组件
|
||||
* @param instance 当前组件实例(setup中可用getCurrentInstance())
|
||||
* @param componentName 目标组件名
|
||||
* @returns 父组件实例
|
||||
*/
|
||||
function parent(instance: ComponentInternalInstance | null | undefined = undefined, componentName: string = '') {
|
||||
if (!instance) {
|
||||
instance = getCurrentInstance();
|
||||
}
|
||||
let parent = instance && (instance.parent as ComponentInternalInstance | null | undefined);
|
||||
|
||||
if (!componentName) return parent;
|
||||
while (parent) {
|
||||
const name = (parent.type as any)?.name as string | undefined;
|
||||
if (name === componentName) {
|
||||
return parent;
|
||||
}
|
||||
parent = parent.parent;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** * 向上查找父组件并派发事件
|
||||
* @param instance 当前组件实例(setup中可用getCurrentInstance())
|
||||
* @param componentName 目标组件名
|
||||
* @param eventName 事件名
|
||||
* @param params 参数
|
||||
*/
|
||||
function dispatch(
|
||||
instance: ComponentInternalInstance | null | undefined,
|
||||
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(
|
||||
instance: ComponentInternalInstance | null | undefined,
|
||||
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(child, componentName, eventName, ...params);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export { dispatch, broadcast, parent };
|
||||
@@ -0,0 +1,364 @@
|
||||
// utils/logger.ts
|
||||
|
||||
// 定义原始控制台方法的类型
|
||||
interface ConsoleMethods {
|
||||
log: typeof console.log;
|
||||
info: typeof console.info;
|
||||
warn: typeof console.warn;
|
||||
error: typeof console.error;
|
||||
debug?: typeof console.debug;
|
||||
trace?: typeof console.trace;
|
||||
table?: typeof console.table;
|
||||
time?: typeof console.time;
|
||||
timeEnd?: typeof console.timeEnd;
|
||||
group?: typeof console.group;
|
||||
groupEnd?: typeof console.groupEnd;
|
||||
groupCollapsed?: typeof console.groupCollapsed;
|
||||
assert?: typeof console.assert;
|
||||
clear?: typeof console.clear;
|
||||
count?: typeof console.count;
|
||||
countReset?: typeof console.countReset;
|
||||
}
|
||||
|
||||
// 安全地获取控制台方法
|
||||
const originalConsole: ConsoleMethods = {
|
||||
log: console.log,
|
||||
info: console.info,
|
||||
warn: console.warn,
|
||||
error: console.error,
|
||||
debug: console.debug,
|
||||
trace: console.trace,
|
||||
table: console.table,
|
||||
time: console.time,
|
||||
timeEnd: console.timeEnd,
|
||||
group: console.group,
|
||||
groupEnd: console.groupEnd,
|
||||
groupCollapsed: console.groupCollapsed,
|
||||
assert: console.assert,
|
||||
clear: console.clear,
|
||||
count: console.count,
|
||||
countReset: console.countReset
|
||||
};
|
||||
|
||||
// 检查并确保所有方法都存在,不存在的方法用空函数替代
|
||||
Object.keys(originalConsole).forEach(key => {
|
||||
const methodKey = key as keyof ConsoleMethods;
|
||||
if (!originalConsole[methodKey]) {
|
||||
(originalConsole[methodKey] as any) = () => {};
|
||||
}
|
||||
});
|
||||
|
||||
class Logger {
|
||||
private debugMode: boolean = false;
|
||||
private prefix: string = '[uViewPro]';
|
||||
private showCallerInfo: boolean = true;
|
||||
|
||||
/**
|
||||
* 设置调试模式
|
||||
* @param enabled 是否启用调试模式
|
||||
*/
|
||||
setDebugMode(enabled: boolean) {
|
||||
this.debugMode = !!enabled;
|
||||
|
||||
if (this.debugMode) {
|
||||
console.log('[uViewPro] Debug mode enabled');
|
||||
} else {
|
||||
console.log('[uViewPro] Debug mode disabled');
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置是否显示调用者信息(文件名和行号)
|
||||
* @param show 是否显示调用者信息
|
||||
*/
|
||||
setShowCallerInfo(show: boolean) {
|
||||
this.showCallerInfo = !!show;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置日志前缀
|
||||
* @param prefix 日志前缀
|
||||
*/
|
||||
setPrefix(prefix: string) {
|
||||
if (prefix) this.prefix = prefix;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前调试模式状态
|
||||
* @returns 当前调试模式状态
|
||||
*/
|
||||
getDebugMode(): boolean {
|
||||
return this.debugMode;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从文件路径中提取纯净的文件名(去除查询参数和路径)
|
||||
* @param filePath 文件路径
|
||||
* @returns 纯净的文件名
|
||||
*/
|
||||
private extractFileName(filePath: string): string {
|
||||
if (!filePath) return '';
|
||||
|
||||
// 去除查询参数(?后面的内容)
|
||||
const withoutQuery = filePath.split('?')[0];
|
||||
|
||||
// 使用正斜杠和反斜杠分割路径,取最后一部分
|
||||
const parts = withoutQuery.split(/[/\\]/);
|
||||
const fileNameWithExt = parts.pop() || '';
|
||||
|
||||
return fileNameWithExt;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取调用者信息(文件名和行号)
|
||||
* @returns 调用者信息字符串
|
||||
*/
|
||||
private getCallerInfo(): string {
|
||||
if (!this.showCallerInfo) return '';
|
||||
|
||||
try {
|
||||
// 创建一个 Error 对象来获取堆栈跟踪
|
||||
const error = new Error();
|
||||
const stack = error.stack;
|
||||
|
||||
if (!stack) return '';
|
||||
|
||||
// 解析堆栈跟踪,找到调用 logger 的文件和行号
|
||||
const stackLines = stack.split('\n');
|
||||
|
||||
// 找到第一个不是 logger.ts 的堆栈行
|
||||
for (let i = 3; i < stackLines.length; i++) {
|
||||
const line = stackLines[i].trim();
|
||||
if (line && !line.includes('logger.ts') && !line.includes('Logger.') && !line.includes('at Object.')) {
|
||||
// 提取文件名和行号
|
||||
const match = line.match(/\(?(.*):(\d+):(\d+)\)?/);
|
||||
if (match) {
|
||||
const filePath = match[1];
|
||||
const lineNumber = match[2];
|
||||
const fileName = this.extractFileName(filePath);
|
||||
return `[${fileName}:${lineNumber}]`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// 如果获取调用者信息失败,静默处理
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用日志输出方法
|
||||
* @param level 日志级别
|
||||
* @param args 日志参数
|
||||
*/
|
||||
private output(level: keyof ConsoleMethods, ...args: any[]): void {
|
||||
if (!this.debugMode || !originalConsole[level]) return;
|
||||
|
||||
const method = originalConsole[level] as Function;
|
||||
const callerInfo = this.getCallerInfo();
|
||||
|
||||
if (this.prefix) {
|
||||
if (callerInfo) {
|
||||
method(`${this.prefix}${callerInfo}`, ...args);
|
||||
} else {
|
||||
method(this.prefix, ...args);
|
||||
}
|
||||
} else {
|
||||
if (callerInfo) {
|
||||
method(callerInfo, ...args);
|
||||
} else {
|
||||
method(...args);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 普通日志
|
||||
* @param args 日志参数
|
||||
*/
|
||||
log(...args: any[]): void {
|
||||
this.output('log', ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* 信息日志
|
||||
* @param args 日志参数
|
||||
*/
|
||||
info(...args: any[]): void {
|
||||
this.output('info', ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* 警告日志
|
||||
* @param args 日志参数
|
||||
*/
|
||||
warn(...args: any[]): void {
|
||||
this.output('warn', ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* 错误日志
|
||||
* @param args 日志参数
|
||||
*/
|
||||
error(...args: any[]): void {
|
||||
this.output('error', ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* 调试日志
|
||||
* @param args 日志参数
|
||||
*/
|
||||
debug(...args: any[]): void {
|
||||
if (!originalConsole.debug) return;
|
||||
this.output('debug', ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* 堆栈跟踪
|
||||
* @param args 日志参数
|
||||
*/
|
||||
trace(...args: any[]): void {
|
||||
if (!originalConsole.trace) return;
|
||||
this.output('trace', ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* 表格输出
|
||||
* @param data 表格数据
|
||||
* @param columns 列名(可选)
|
||||
*/
|
||||
table(data: any, columns?: string[]): void {
|
||||
if (!this.debugMode || !originalConsole.table) return;
|
||||
|
||||
if (this.prefix) {
|
||||
originalConsole.log!(this.prefix);
|
||||
}
|
||||
originalConsole.table!(data, columns);
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始计时
|
||||
* @param label 计时器标签
|
||||
*/
|
||||
time(label: string): void {
|
||||
if (!this.debugMode || !originalConsole.time) return;
|
||||
|
||||
const fullLabel = this.prefix ? `${this.prefix} ${label}` : label;
|
||||
originalConsole.time!(fullLabel);
|
||||
}
|
||||
|
||||
/**
|
||||
* 结束计时
|
||||
* @param label 计时器标签
|
||||
*/
|
||||
timeEnd(label: string): void {
|
||||
if (!this.debugMode || !originalConsole.timeEnd) return;
|
||||
|
||||
const fullLabel = this.prefix ? `${this.prefix} ${label}` : label;
|
||||
originalConsole.timeEnd!(fullLabel);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分组日志
|
||||
* @param label 分组标签
|
||||
*/
|
||||
group(label: string): void {
|
||||
if (!this.debugMode || !originalConsole.group) return;
|
||||
|
||||
const fullLabel = this.prefix ? `${this.prefix} ${label}` : label;
|
||||
originalConsole.group!(fullLabel);
|
||||
}
|
||||
|
||||
/**
|
||||
* 结束分组
|
||||
*/
|
||||
groupEnd(): void {
|
||||
if (!this.debugMode || !originalConsole.groupEnd) return;
|
||||
originalConsole.groupEnd!();
|
||||
}
|
||||
|
||||
/**
|
||||
* 分组日志(默认折叠)
|
||||
* @param label 分组标签
|
||||
*/
|
||||
groupCollapsed(label: string): void {
|
||||
if (!this.debugMode || !originalConsole.groupCollapsed) return;
|
||||
|
||||
const fullLabel = this.prefix ? `${this.prefix} ${label}` : label;
|
||||
originalConsole.groupCollapsed!(fullLabel);
|
||||
}
|
||||
|
||||
/**
|
||||
* 断言
|
||||
* @param condition 条件
|
||||
* @param message 错误消息
|
||||
*/
|
||||
assert(condition: boolean, ...message: any[]): void {
|
||||
if (!this.debugMode || !originalConsole.assert) return;
|
||||
|
||||
if (this.prefix) {
|
||||
originalConsole.assert!(condition, this.prefix, ...message);
|
||||
} else {
|
||||
originalConsole.assert!(condition, ...message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空控制台
|
||||
*/
|
||||
clear(): void {
|
||||
if (!this.debugMode || !originalConsole.clear) return;
|
||||
originalConsole.clear!();
|
||||
}
|
||||
|
||||
/**
|
||||
* 计数器
|
||||
* @param label 计数器标签
|
||||
*/
|
||||
count(label?: string): void {
|
||||
if (!this.debugMode || !originalConsole.count) return;
|
||||
|
||||
const fullLabel = this.prefix && label ? `${this.prefix} ${label}` : label || this.prefix;
|
||||
originalConsole.count!(fullLabel);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置计数器
|
||||
* @param label 计数器标签
|
||||
*/
|
||||
countReset(label?: string): void {
|
||||
if (!this.debugMode || !originalConsole.countReset) return;
|
||||
|
||||
const fullLabel = this.prefix && label ? `${this.prefix} ${label}` : label || this.prefix;
|
||||
originalConsole.countReset!(fullLabel);
|
||||
}
|
||||
|
||||
/**
|
||||
* 带样式的日志
|
||||
* @param style CSS样式
|
||||
* @param message 消息内容
|
||||
*/
|
||||
styled(style: string, message: string): void {
|
||||
if (!this.debugMode) return;
|
||||
|
||||
const callerInfo = this.getCallerInfo();
|
||||
const fullMessage = callerInfo ? `${message} ${callerInfo}` : message;
|
||||
|
||||
if (this.prefix) {
|
||||
console.log(`%c${this.prefix} ${fullMessage}`, style);
|
||||
} else {
|
||||
console.log(`%c${fullMessage}`, style);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 创建全局单例
|
||||
const logger = new Logger();
|
||||
|
||||
// 导出单例和类
|
||||
export { logger, Logger };
|
||||
@@ -0,0 +1,118 @@
|
||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
/**
|
||||
* copy to https://github.com/developit/mitt
|
||||
* Expand clear method
|
||||
*/
|
||||
export type EventType = string | symbol;
|
||||
|
||||
// An event handler can take an optional event argument
|
||||
// and should not return a value
|
||||
export type Handler<T = unknown> = (event: T) => void;
|
||||
export type WildcardHandler<T = Record<string, unknown>> = (type: keyof T, event: T[keyof T]) => void;
|
||||
|
||||
// An array of all currently registered event handlers for a type
|
||||
export type EventHandlerList<T = unknown> = Array<Handler<T>>;
|
||||
export type WildCardEventHandlerList<T = Record<string, unknown>> = Array<WildcardHandler<T>>;
|
||||
|
||||
// A map of event types and their corresponding event handlers.
|
||||
export type EventHandlerMap<Events extends Record<EventType, unknown>> = Map<
|
||||
'*' | keyof Events,
|
||||
EventHandlerList<Events[keyof Events]> | WildCardEventHandlerList<Events>
|
||||
>;
|
||||
|
||||
export interface Emitter<Events extends Record<EventType, unknown>> {
|
||||
all: EventHandlerMap<Events>;
|
||||
|
||||
on<Key extends keyof Events>(type: Key, handler: Handler<Events[Key]>): void;
|
||||
on(type: '*', handler: WildcardHandler<Events>): void;
|
||||
|
||||
off<Key extends keyof Events>(type: Key, handler?: Handler<Events[Key]>): void;
|
||||
off(type: '*', handler: WildcardHandler<Events>): void;
|
||||
|
||||
emit<Key extends keyof Events>(type: Key, event: Events[Key]): void;
|
||||
emit<Key extends keyof Events>(type: undefined extends Events[Key] ? Key : never): void;
|
||||
clear(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mitt: Tiny (~200b) functional event emitter / pubsub.
|
||||
* @name mitt
|
||||
* @returns {Mitt}
|
||||
*/
|
||||
export function mitt<Events extends Record<EventType, unknown>>(all?: EventHandlerMap<Events>): Emitter<Events> {
|
||||
type GenericEventHandler = Handler<Events[keyof Events]> | WildcardHandler<Events>;
|
||||
all = all || new Map();
|
||||
|
||||
return {
|
||||
/**
|
||||
* A Map of event names to registered handler functions.
|
||||
*/
|
||||
all,
|
||||
|
||||
/**
|
||||
* Register an event handler for the given type.
|
||||
* @param {string|symbol} type Type of event to listen for, or `'*'` for all events
|
||||
* @param {Function} handler Function to call in response to given event
|
||||
* @memberOf mitt
|
||||
*/
|
||||
on<Key extends keyof Events>(type: Key, handler: GenericEventHandler) {
|
||||
const handlers: Array<GenericEventHandler> | undefined = all!.get(type);
|
||||
if (handlers) {
|
||||
handlers.push(handler);
|
||||
} else {
|
||||
all!.set(type, [handler] as EventHandlerList<Events[keyof Events]>);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove an event handler for the given type.
|
||||
* If `handler` is omitted, all handlers of the given type are removed.
|
||||
* @param {string|symbol} type Type of event to unregister `handler` from (`'*'` to remove a wildcard handler)
|
||||
* @param {Function} [handler] Handler function to remove
|
||||
* @memberOf mitt
|
||||
*/
|
||||
off<Key extends keyof Events>(type: Key, handler?: GenericEventHandler) {
|
||||
const handlers: Array<GenericEventHandler> | undefined = all!.get(type);
|
||||
if (handlers) {
|
||||
if (handler) {
|
||||
handlers.splice(handlers.indexOf(handler) >>> 0, 1);
|
||||
} else {
|
||||
all!.set(type, []);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Invoke all handlers for the given type.
|
||||
* If present, `'*'` handlers are invoked after type-matched handlers.
|
||||
*
|
||||
* Note: Manually firing '*' handlers is not supported.
|
||||
*
|
||||
* @param {string|symbol} type The event type to invoke
|
||||
* @param {Any} [evt] Any value (object is recommended and powerful), passed to each handler
|
||||
* @memberOf mitt
|
||||
*/
|
||||
emit<Key extends keyof Events>(type: Key, evt?: Events[Key]) {
|
||||
let handlers = all!.get(type);
|
||||
if (handlers) {
|
||||
[...(handlers as EventHandlerList<Events[keyof Events]>)].forEach(handler => {
|
||||
handler(evt as Events[Key]);
|
||||
});
|
||||
}
|
||||
|
||||
handlers = all!.get('*');
|
||||
if (handlers) {
|
||||
[...(handlers as WildCardEventHandlerList<Events>)].forEach(handler => {
|
||||
handler(type, evt as Events[Key]);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Clear all
|
||||
*/
|
||||
clear() {
|
||||
this.all.clear();
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export default [{label:"北京市",value:"11"},{label:"天津市",value:"12"},{label:"河北省",value:"13"},{label:"山西省",value:"14"},{label:"内蒙古自治区",value:"15"},{label:"辽宁省",value:"21"},{label:"吉林省",value:"22"},{label:"黑龙江省",value:"23"},{label:"上海市",value:"31"},{label:"江苏省",value:"32"},{label:"浙江省",value:"33"},{label:"安徽省",value:"34"},{label:"福建省",value:"35"},{label:"江西省",value:"36"},{label:"山东省",value:"37"},{label:"河南省",value:"41"},{label:"湖北省",value:"42"},{label:"湖南省",value:"43"},{label:"广东省",value:"44"},{label:"广西壮族自治区",value:"45"},{label:"海南省",value:"46"},{label:"重庆市",value:"50"},{label:"四川省",value:"51"},{label:"贵州省",value:"52"},{label:"云南省",value:"53"},{label:"西藏自治区",value:"54"},{label:"陕西省",value:"61"},{label:"甘肃省",value:"62"},{label:"青海省",value:"63"},{label:"宁夏回族自治区",value:"64"},{label:"新疆维吾尔自治区",value:"65"},{label:"台湾",value:"66"},{label:"香港",value:"67"},{label:"澳门",value:"68"}];
|
||||
@@ -0,0 +1,25 @@
|
||||
declare const uni: any;
|
||||
|
||||
type SystemTheme = 'light' | 'dark';
|
||||
|
||||
/**
|
||||
* 非 H5 平台获取当前系统主题
|
||||
*/
|
||||
export function getSystemDarkMode(): SystemTheme {
|
||||
try {
|
||||
if (typeof uni !== 'undefined' && typeof uni.getSystemInfoSync === 'function') {
|
||||
const systemInfo = uni.getSystemInfoSync();
|
||||
const theme = systemInfo.osTheme || systemInfo.theme || 'light';
|
||||
if (theme === 'dark') {
|
||||
return 'dark';
|
||||
}
|
||||
if (theme === 'light') {
|
||||
return 'light';
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// 获取失败时默认返回亮色
|
||||
console.warn('[system-theme] getSystemDarkMode failed', e);
|
||||
}
|
||||
return 'light';
|
||||
}
|
||||
Reference in New Issue
Block a user