uview-pro 0.3.16 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. package/changelog.md +66 -0
  2. package/components/u-action-sheet-item/u-action-sheet-item.vue +1 -1
  3. package/components/u-alert-tips/u-alert-tips.vue +2 -2
  4. package/components/u-avatar/u-avatar.vue +5 -5
  5. package/components/u-avatar-cropper/u-avatar-cropper.vue +5 -5
  6. package/components/u-avatar-cropper/weCropper.js +1 -1
  7. package/components/u-avatar-cropper/weCropper.ts +1 -1
  8. package/components/u-back-top/types.ts +1 -1
  9. package/components/u-back-top/u-back-top.vue +1 -1
  10. package/components/u-badge/u-badge.vue +1 -31
  11. package/components/u-button/types.ts +1 -1
  12. package/components/u-button/u-button.vue +45 -37
  13. package/components/u-calendar/types.ts +4 -4
  14. package/components/u-calendar/u-calendar.vue +8 -8
  15. package/components/u-car-keyboard/u-car-keyboard.vue +5 -5
  16. package/components/u-card/types.ts +2 -2
  17. package/components/u-card/u-card.vue +3 -3
  18. package/components/u-cell-group/u-cell-group.vue +1 -1
  19. package/components/u-cell-item/u-cell-item.vue +2 -2
  20. package/components/u-checkbox/u-checkbox.vue +7 -7
  21. package/components/u-circle-progress/types.ts +4 -3
  22. package/components/u-circle-progress/u-circle-progress.vue +3 -3
  23. package/components/u-city-select/u-city-select.vue +1 -1
  24. package/components/u-collapse/types.ts +2 -2
  25. package/components/u-collapse/u-collapse.vue +1 -1
  26. package/components/u-collapse-item/u-collapse-item.vue +1 -1
  27. package/components/u-column-notice/u-column-notice.vue +2 -2
  28. package/components/u-config-provider/types.ts +34 -0
  29. package/components/u-config-provider/u-config-provider.vue +141 -0
  30. package/components/u-count-down/types.ts +4 -4
  31. package/components/u-count-down/u-count-down.vue +4 -4
  32. package/components/u-count-to/types.ts +1 -1
  33. package/components/u-count-to/u-count-to.vue +1 -1
  34. package/components/u-divider/types.ts +3 -3
  35. package/components/u-divider/u-divider.vue +4 -4
  36. package/components/u-dropdown/u-dropdown.vue +3 -3
  37. package/components/u-empty/types.ts +2 -2
  38. package/components/u-empty/u-empty.vue +1 -1
  39. package/components/u-field/types.ts +3 -3
  40. package/components/u-field/u-field.vue +6 -6
  41. package/components/u-form-item/u-form-item.vue +1 -1
  42. package/components/u-full-screen/u-full-screen.vue +1 -1
  43. package/components/u-gap/u-gap.vue +2 -2
  44. package/components/u-grid-item/types.ts +1 -1
  45. package/components/u-grid-item/u-grid-item.vue +3 -3
  46. package/components/u-icon/types.ts +2 -2
  47. package/components/u-icon/u-icon.vue +2 -2
  48. package/components/u-image/types.ts +4 -2
  49. package/components/u-image/u-image.vue +7 -2
  50. package/components/u-index-anchor/u-index-anchor.vue +3 -3
  51. package/components/u-index-list/u-index-list.vue +1 -1
  52. package/components/u-input/types.ts +4 -4
  53. package/components/u-input/u-input.vue +7 -7
  54. package/components/u-keyboard/u-keyboard.vue +3 -3
  55. package/components/u-lazy-load/u-lazy-load.vue +1 -1
  56. package/components/u-line/types.ts +1 -1
  57. package/components/u-line/u-line.vue +1 -1
  58. package/components/u-line-progress/types.ts +2 -2
  59. package/components/u-line-progress/u-line-progress.vue +3 -3
  60. package/components/u-link/u-link.vue +1 -1
  61. package/components/u-loading/types.ts +1 -1
  62. package/components/u-loading/u-loading.vue +3 -3
  63. package/components/u-loading-popup/types.ts +1 -1
  64. package/components/u-loading-popup/u-loading-popup.vue +2 -2
  65. package/components/u-loadmore/types.ts +2 -2
  66. package/components/u-loadmore/u-loadmore.vue +6 -6
  67. package/components/u-message-input/u-message-input.vue +5 -5
  68. package/components/u-modal/u-modal.vue +2 -2
  69. package/components/u-navbar/types.ts +4 -4
  70. package/components/u-navbar/u-navbar.vue +27 -20
  71. package/components/u-no-network/u-no-network.vue +2 -2
  72. package/components/u-number-box/types.ts +4 -4
  73. package/components/u-number-box/u-number-box.vue +6 -6
  74. package/components/u-number-keyboard/u-number-keyboard.vue +2 -2
  75. package/components/u-picker/u-picker.vue +4 -4
  76. package/components/u-popup/types.ts +1 -1
  77. package/components/u-popup/u-popup.vue +6 -6
  78. package/components/u-radio/u-radio.vue +7 -7
  79. package/components/u-rate/types.ts +2 -2
  80. package/components/u-rate/u-rate.vue +2 -2
  81. package/components/u-read-more/types.ts +1 -1
  82. package/components/u-row-notice/u-row-notice.vue +2 -2
  83. package/components/u-search/types.ts +4 -4
  84. package/components/u-search/u-search.vue +4 -4
  85. package/components/u-section/types.ts +2 -2
  86. package/components/u-section/u-section.vue +2 -2
  87. package/components/u-select/u-select.vue +6 -6
  88. package/components/u-skeleton/types.ts +2 -2
  89. package/components/u-skeleton/u-skeleton.vue +2 -2
  90. package/components/u-slider/types.ts +1 -1
  91. package/components/u-slider/u-slider.vue +4 -4
  92. package/components/u-step/u-step.vue +4 -4
  93. package/components/u-steps/u-steps.vue +3 -3
  94. package/components/u-sticky/types.ts +1 -1
  95. package/components/u-sticky/u-sticky.vue +1 -1
  96. package/components/u-subsection/types.ts +4 -4
  97. package/components/u-subsection/u-subsection.vue +7 -7
  98. package/components/u-swipe-action/types.ts +1 -1
  99. package/components/u-swipe-action/u-swipe-action.vue +2 -2
  100. package/components/u-swiper/types.ts +1 -1
  101. package/components/u-swiper/u-swiper.vue +1 -1
  102. package/components/u-switch/types.ts +1 -1
  103. package/components/u-switch/u-switch.vue +5 -5
  104. package/components/u-tabbar/types.ts +5 -4
  105. package/components/u-tabbar/u-tabbar.vue +5 -5
  106. package/components/u-table/types.ts +3 -3
  107. package/components/u-table/u-table.vue +3 -3
  108. package/components/u-tabs/types.ts +1 -1
  109. package/components/u-tabs/u-tabs.vue +2 -2
  110. package/components/u-tabs-swiper/types.ts +1 -1
  111. package/components/u-tabs-swiper/u-tabs-swiper.vue +2 -2
  112. package/components/u-tag/u-tag.vue +12 -12
  113. package/components/u-text/types.ts +1 -1
  114. package/components/u-text/u-text.vue +1 -1
  115. package/components/u-textarea/types.ts +1 -1
  116. package/components/u-textarea/u-textarea.vue +6 -6
  117. package/components/u-time-line/u-time-line.vue +1 -1
  118. package/components/u-time-line-item/types.ts +1 -1
  119. package/components/u-time-line-item/u-time-line-item.vue +2 -3
  120. package/components/u-toast/u-toast.vue +8 -8
  121. package/components/u-top-tips/u-top-tips.vue +1 -1
  122. package/components/u-upload/types.ts +2 -2
  123. package/components/u-upload/u-upload.vue +1 -1
  124. package/index.scss +1 -0
  125. package/index.ts +35 -10
  126. package/libs/config/theme-tokens.ts +101 -0
  127. package/libs/css/style.theme.scss +31 -0
  128. package/libs/css/style.vue.scss +1 -1
  129. package/libs/function/clipboard.ts +6 -11
  130. package/libs/function/color.ts +53 -22
  131. package/libs/hooks/index.ts +2 -0
  132. package/libs/hooks/useColor.ts +61 -0
  133. package/libs/hooks/useTheme.ts +162 -0
  134. package/libs/index.ts +4 -3
  135. package/libs/util/config-provider.ts +558 -0
  136. package/libs/util/system-theme.ts +25 -0
  137. package/package.json +107 -107
  138. package/theme.scss +50 -34
  139. package/types/components.d.ts +1 -0
  140. package/types/global.d.ts +47 -2
@@ -0,0 +1,558 @@
1
+ // libs/util/config-provider.ts
2
+
3
+ // 全局配置类:管理 Theme 的初始化、切换、持久化
4
+
5
+ import { ref } from 'vue';
6
+ import type { DarkMode, Theme, ThemeColor } from '../../types/global';
7
+ import { config, setColor } from '..';
8
+ import { defaultThemes } from '../config/theme-tokens';
9
+ import { color as reactiveColor } from '../function/color';
10
+ import { getSystemDarkMode as getNativeSystemDarkMode } from './system-theme';
11
+
12
+ declare const uni: any;
13
+
14
+ const THEME_STORAGE_KEY = 'uview-pro-theme';
15
+ const DARK_MODE_STORAGE_KEY = 'uview-pro-dark-mode';
16
+ const DEFAULT_LIGHT_TOKENS = (defaultThemes[0]?.color || {}) as Partial<ThemeColor>;
17
+ const DEFAULT_DARK_TOKENS = (defaultThemes[0]?.darkColor || {}) as Partial<ThemeColor>;
18
+ const STRUCTURAL_TOKENS = new Set([
19
+ 'bgColor',
20
+ 'bgWhite',
21
+ 'bgGrayLight',
22
+ 'bgGrayDark',
23
+ 'bgBlack',
24
+ 'borderColor',
25
+ 'lightColor',
26
+ 'mainColor',
27
+ 'contentColor',
28
+ 'tipsColor',
29
+ 'whiteColor',
30
+ 'blackColor',
31
+ 'dividerColor',
32
+ 'maskColor',
33
+ 'shadowColor'
34
+ ]);
35
+
36
+ /**
37
+ * ConfigProvider: 管理全局主题
38
+ * - init(themes, defaultName): 初始化主题系统
39
+ * - setTheme(name): 切换主题并持久化
40
+ * - getThemes/getCurrentTheme: 读取当前数据
41
+ * - setDarkMode/getDarkMode: 管理暗黑模式
42
+ */
43
+ export class ConfigProvider {
44
+ // 响应式状态,供外部直接引用
45
+ public themesRef = ref<Theme[]>([]);
46
+ public currentThemeRef = ref<Theme | null>(null);
47
+ public darkModeRef = ref<DarkMode>('auto');
48
+ public cssVarsRef = ref<Record<string, string>>({});
49
+ private baseColorTokens: Partial<ThemeColor> = DEFAULT_LIGHT_TOKENS;
50
+ private baseDarkColorTokens: Partial<ThemeColor> = DEFAULT_DARK_TOKENS;
51
+ private debug: boolean = false;
52
+ private systemDarkModeMediaQuery: MediaQueryList | null = null;
53
+ private lastAppliedCssKeys: string[] = [];
54
+
55
+ constructor() {
56
+ // 默认不自动初始化,调用 init 以传入主题列表
57
+ this.initSystemDarkModeListener();
58
+ }
59
+
60
+ /**
61
+ * 初始化系统暗黑模式监听器
62
+ * 支持 H5、App、小程序等平台
63
+ */
64
+ private initSystemDarkModeListener() {
65
+ // H5 平台:使用 matchMedia API
66
+ try {
67
+ if (typeof window !== 'undefined' && window.matchMedia) {
68
+ this.systemDarkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
69
+ const listener = () => {
70
+ if (this.darkModeRef.value === 'auto') {
71
+ this.applyTheme(this.currentThemeRef.value);
72
+ }
73
+ };
74
+ if (this.systemDarkModeMediaQuery.addEventListener) {
75
+ this.systemDarkModeMediaQuery.addEventListener('change', listener);
76
+ } else if (this.systemDarkModeMediaQuery.addListener) {
77
+ this.systemDarkModeMediaQuery.addListener(listener);
78
+ }
79
+ }
80
+ } catch (e) {
81
+ if (this.debug) console.warn('[ConfigProvider] H5 system dark mode listener failed', e);
82
+ }
83
+
84
+ // uni-app 平台:使用 uni.onThemeChange API
85
+ try {
86
+ if (typeof uni !== 'undefined' && typeof uni.onThemeChange === 'function') {
87
+ uni.onThemeChange((res: { theme: string }) => {
88
+ console.log('[ConfigProvider] system theme changed', res);
89
+ if (this.darkModeRef.value === 'auto') {
90
+ // 系统主题变化时,重新应用主题
91
+ this.applyTheme(this.currentThemeRef.value);
92
+ }
93
+ });
94
+ }
95
+ } catch (e) {
96
+ if (this.debug) console.warn('[ConfigProvider] uni-app system dark mode listener failed', e);
97
+ }
98
+ }
99
+
100
+ /**
101
+ * 检测当前是否应该使用暗黑模式
102
+ */
103
+ private isSystemDarkMode(): boolean {
104
+ try {
105
+ if (this.systemDarkModeMediaQuery) {
106
+ return this.systemDarkModeMediaQuery.matches;
107
+ }
108
+ } catch (e) {
109
+ if (this.debug) console.warn('[ConfigProvider] matchMedia check failed', e);
110
+ }
111
+ try {
112
+ return getNativeSystemDarkMode() === 'dark';
113
+ } catch (e) {
114
+ if (this.debug) console.warn('[ConfigProvider] native system theme check failed', e);
115
+ return false;
116
+ }
117
+ }
118
+
119
+ /**
120
+ * 初始化主题系统
121
+ * @param themes 可用主题数组
122
+ * @param defaultThemeName 可选默认主题名
123
+ */
124
+ init(themes?: Theme[], defaultThemeName?: string) {
125
+ const normalizedThemes = this.normalizeThemes(themes);
126
+ if (!normalizedThemes.length) {
127
+ console.warn('[ConfigProvider] init called with empty themes');
128
+ return;
129
+ }
130
+
131
+ this.themesRef.value = normalizedThemes.slice();
132
+
133
+ // 先尝试从 Storage 读取已保存主题名
134
+ const saved = this.readStorage<string>(THEME_STORAGE_KEY);
135
+
136
+ let initialName = saved || defaultThemeName || this.themesRef.value[0].name;
137
+ const found = this.themesRef.value.find(t => t.name === initialName) || this.themesRef.value[0];
138
+
139
+ this.currentThemeRef.value = found;
140
+
141
+ // 尝试从 Storage 读取暗黑模式设置
142
+ const savedDarkMode = this.readStorage<DarkMode>(DARK_MODE_STORAGE_KEY);
143
+ this.darkModeRef.value = savedDarkMode || 'auto';
144
+
145
+ // 应用主题
146
+ this.applyTheme(found);
147
+
148
+ if (this.debug)
149
+ console.log('[ConfigProvider] initialized, theme=', found.name, 'darkMode=', this.darkModeRef.value);
150
+
151
+ return this;
152
+ }
153
+
154
+ /**
155
+ * 获取所有可用主题
156
+ */
157
+ getThemes(): Theme[] {
158
+ return this.themesRef.value.slice();
159
+ }
160
+
161
+ /**
162
+ * 获取当前主题
163
+ */
164
+ getCurrentTheme(): Theme | null {
165
+ return this.currentThemeRef.value;
166
+ }
167
+
168
+ /**
169
+ * 切换主题并持久化
170
+ */
171
+ setTheme(themeName: string) {
172
+ if (!this.themesRef.value || this.themesRef.value.length === 0) {
173
+ console.warn('[ConfigProvider] setTheme called but themes list empty');
174
+ return;
175
+ }
176
+
177
+ const theme = this.themesRef.value.find(t => t.name === themeName);
178
+ if (!theme) {
179
+ console.warn('[ConfigProvider] theme not found:', themeName);
180
+ return;
181
+ }
182
+
183
+ this.currentThemeRef.value = theme;
184
+
185
+ // 应用
186
+ this.applyTheme(theme);
187
+
188
+ // 持久化
189
+ this.writeStorage(THEME_STORAGE_KEY, themeName);
190
+
191
+ if (this.debug) console.log('[ConfigProvider] setTheme ->', themeName);
192
+ }
193
+
194
+ /**
195
+ * 获取当前暗黑模式设置
196
+ */
197
+ getDarkMode(): DarkMode {
198
+ return this.darkModeRef.value;
199
+ }
200
+
201
+ /**
202
+ * 设置暗黑模式
203
+ * @param mode 'auto' (跟随系统) | 'light' (强制亮色) | 'dark' (强制暗黑)
204
+ */
205
+ setDarkMode(mode: DarkMode) {
206
+ this.darkModeRef.value = mode;
207
+ // 持久化
208
+ this.writeStorage(DARK_MODE_STORAGE_KEY, mode);
209
+
210
+ // 重新应用主题
211
+ this.applyTheme(this.currentThemeRef.value);
212
+
213
+ if (this.debug) console.log('[ConfigProvider] setDarkMode ->', mode);
214
+ }
215
+
216
+ /**
217
+ * 检查当前是否处于暗黑模式
218
+ */
219
+ isInDarkMode(): boolean {
220
+ const mode = this.darkModeRef.value;
221
+ if (mode === 'dark') return true;
222
+ if (mode === 'light') return false;
223
+ // auto 模式下检查系统设置
224
+ return this.isSystemDarkMode();
225
+ }
226
+
227
+ /**
228
+ * 归一化主题配置,保证始终至少有一个默认主题
229
+ */
230
+ private normalizeThemes(themes?: Theme[]): Theme[] {
231
+ if (Array.isArray(themes) && themes.length) {
232
+ return this.mergeThemes(defaultThemes, themes);
233
+ }
234
+ return defaultThemes.slice();
235
+ }
236
+
237
+ private mergeThemes(...lists: Array<Theme[] | undefined>): Theme[] {
238
+ const map = new Map<string, Theme>();
239
+ lists
240
+ .filter((list): list is Theme[] => Array.isArray(list) && list.length > 0)
241
+ .forEach(list => {
242
+ list.forEach(theme => {
243
+ const normalized = this.ensureDarkVariant({
244
+ ...theme,
245
+ color: this.applyColorFallbacks(theme.color),
246
+ darkColor: theme.darkColor ? { ...theme.darkColor } : undefined,
247
+ css: theme.css ? { ...theme.css } : undefined,
248
+ darkCss: theme.darkCss ? { ...theme.darkCss } : undefined
249
+ });
250
+ map.set(normalized.name, normalized);
251
+ });
252
+ });
253
+ return Array.from(map.values());
254
+ }
255
+
256
+ private ensureDarkVariant(theme: Theme): Theme {
257
+ const finalDark = this.buildDarkPalette(theme);
258
+ return {
259
+ ...theme,
260
+ darkColor: this.applyDarkFallbacks(finalDark)
261
+ };
262
+ }
263
+
264
+ private buildDarkPalette(theme: Theme): Partial<ThemeColor> {
265
+ const provided = theme.darkColor || {};
266
+ const generated = this.generateDarkFromLight(theme.color || {}, provided);
267
+ return {
268
+ ...generated,
269
+ ...provided
270
+ };
271
+ }
272
+
273
+ /**
274
+ * 应用亮色主题
275
+ */
276
+ private applyColorFallbacks(color?: Partial<ThemeColor>): Partial<ThemeColor> {
277
+ return {
278
+ ...(this.baseColorTokens || {}),
279
+ ...(color || {})
280
+ };
281
+ }
282
+
283
+ /**
284
+ * 应用暗黑主题
285
+ */
286
+ private applyDarkFallbacks(color?: Partial<ThemeColor>): Partial<ThemeColor> {
287
+ return {
288
+ ...(this.baseDarkColorTokens || {}),
289
+ ...(color || {})
290
+ };
291
+ }
292
+
293
+ private filterNonStructuralTokens(palette: Partial<ThemeColor>): Partial<ThemeColor> {
294
+ const result: Partial<ThemeColor> = {};
295
+ Object.entries(palette || {}).forEach(([key, value]) => {
296
+ if (!this.isStructuralToken(key)) {
297
+ (result as any)[key] = value;
298
+ }
299
+ });
300
+ return result;
301
+ }
302
+
303
+ private generateDarkFromLight(palette: Partial<ThemeColor>, provided: Partial<ThemeColor>): Partial<ThemeColor> {
304
+ const result: Partial<ThemeColor> = {};
305
+ const nonStructural = this.filterNonStructuralTokens(palette);
306
+ Object.entries(nonStructural).forEach(([key, value]) => {
307
+ if (typeof value !== 'string') return;
308
+ if (provided && Object.prototype.hasOwnProperty.call(provided, key)) {
309
+ return;
310
+ }
311
+ const fallback = (this.baseDarkColorTokens as any)?.[key];
312
+ (result as any)[key] = this.createDarkVariantFromLight(value, fallback);
313
+ });
314
+ return result;
315
+ }
316
+
317
+ private createDarkVariantFromLight(color: string, fallback?: string): string {
318
+ const normalized = this.normalizeHex(color);
319
+ const fallbackHex = fallback ? this.normalizeHex(fallback) : null;
320
+ if (normalized && fallbackHex) {
321
+ return this.mixHex(normalized, fallbackHex, 0.6);
322
+ }
323
+ if (fallbackHex) return fallbackHex;
324
+ return normalized || color;
325
+ }
326
+
327
+ private normalizeHex(color: string): string | null {
328
+ if (!color) return null;
329
+ const hex = color.trim();
330
+ if (/^#([0-9a-fA-F]{6})$/.test(hex)) return hex.toLowerCase();
331
+ return null;
332
+ }
333
+
334
+ private mixHex(fromHex: string, toHex: string, ratio: number): string {
335
+ const from = this.hexToRgb(fromHex);
336
+ const to = this.hexToRgb(toHex);
337
+ if (!from || !to) return toHex;
338
+ const clamp = (val: number) => Math.min(255, Math.max(0, Math.round(val)));
339
+ const r = clamp(from.r * (1 - ratio) + to.r * ratio);
340
+ const g = clamp(from.g * (1 - ratio) + to.g * ratio);
341
+ const b = clamp(from.b * (1 - ratio) + to.b * ratio);
342
+ return this.rgbToHex(r, g, b);
343
+ }
344
+
345
+ private hexToRgb(hex: string): { r: number; g: number; b: number } | null {
346
+ const match = /^#([0-9a-fA-F]{6})$/.exec(hex);
347
+ if (!match) return null;
348
+ return {
349
+ r: parseInt(match[1].slice(0, 2), 16),
350
+ g: parseInt(match[1].slice(2, 4), 16),
351
+ b: parseInt(match[1].slice(4, 6), 16)
352
+ };
353
+ }
354
+
355
+ private rgbToHex(r: number, g: number, b: number): string {
356
+ const toHex = (val: number) => val.toString(16).padStart(2, '0');
357
+ return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
358
+ }
359
+
360
+ private isStructuralToken(token: string): boolean {
361
+ return STRUCTURAL_TOKENS.has(token);
362
+ }
363
+
364
+ /**
365
+ * 运行时同步主题颜色($u.color)
366
+ * 更新响应式 color 对象,确保所有使用 $u.color 的地方都能响应式更新
367
+ */
368
+ private syncRuntimeTheme(palette: Partial<ThemeColor>) {
369
+ try {
370
+ // 合并默认值,确保所有颜色都有值
371
+ const defaultPalette = this.getActiveMode() === 'dark' ? this.baseDarkColorTokens : this.baseColorTokens;
372
+
373
+ const mergedPalette = {
374
+ ...defaultPalette,
375
+ ...palette
376
+ };
377
+
378
+ // 更新响应式 color 对象
379
+ Object.keys(mergedPalette).forEach(key => {
380
+ const value = (mergedPalette as any)[key];
381
+ if (value != null) {
382
+ (reactiveColor as any)[key] = value;
383
+ }
384
+ });
385
+
386
+ // 同步到 uni.$u.color(如果存在)
387
+ if (typeof uni !== 'undefined' && uni?.$u?.setColor) {
388
+ uni.$u.setColor(mergedPalette);
389
+ } else {
390
+ setColor(mergedPalette);
391
+ }
392
+ } catch (e) {
393
+ if (this.debug) console.warn('[ConfigProvider] sync runtime theme failed', e);
394
+ }
395
+ }
396
+
397
+ /**
398
+ * 获取当前激活的模式
399
+ */
400
+ private getActiveMode(): 'light' | 'dark' {
401
+ return this.isInDarkMode() ? 'dark' : 'light';
402
+ }
403
+
404
+ /**
405
+ * 获取当前主题的配色方案
406
+ */
407
+ private getPaletteForMode(theme: Theme, mode: 'light' | 'dark') {
408
+ if (mode === 'dark') {
409
+ return theme.darkColor && Object.keys(theme.darkColor).length ? theme.darkColor : theme.color || {};
410
+ }
411
+ return theme.color || {};
412
+ }
413
+
414
+ /**
415
+ * 获取当前主题的CSS变量覆盖
416
+ */
417
+ private getCssOverrides(theme: Theme, mode: 'light' | 'dark') {
418
+ if (mode === 'dark') {
419
+ return (theme.darkCss && Object.keys(theme.darkCss).length ? theme.darkCss : theme.css) || {};
420
+ }
421
+ return theme.css || {};
422
+ }
423
+
424
+ /**
425
+ * 读取Storage key
426
+ */
427
+ private readStorage<T>(key: string): T | null {
428
+ try {
429
+ if (typeof uni === 'undefined' || typeof uni.getStorageSync !== 'function') return null;
430
+ const value = uni.getStorageSync(key);
431
+ return (value ?? null) as T | null;
432
+ } catch (e) {
433
+ if (this.debug) console.warn('[ConfigProvider] failed to read storage', e);
434
+ return null;
435
+ }
436
+ }
437
+
438
+ /**
439
+ * 写入Storage key value
440
+ */
441
+ private writeStorage(key: string, value: any) {
442
+ try {
443
+ if (typeof uni === 'undefined' || typeof uni.setStorageSync !== 'function') return;
444
+ uni.setStorageSync(key, value);
445
+ } catch (e) {
446
+ if (this.debug) console.warn('[ConfigProvider] failed to write storage', e);
447
+ }
448
+ }
449
+
450
+ /**
451
+ * 更新文档主题模式 H5
452
+ */
453
+ private updateDocumentMode(mode: 'light' | 'dark') {
454
+ if (typeof document === 'undefined' || !document.documentElement) return;
455
+ const root = document.documentElement;
456
+ root.dataset.uThemeMode = mode;
457
+ root.classList.remove('u-theme-light', 'u-theme-dark');
458
+ root.classList.add(`u-theme-${mode}`);
459
+ }
460
+
461
+ /**
462
+ * 转换为 CSS 变量名称
463
+ */
464
+ private toCssVarName(key: string, prefix: string = '--u'): string {
465
+ const types = config.type;
466
+ if (types.some(type => key.startsWith(type))) {
467
+ prefix += '-type';
468
+ }
469
+ const kebab = key
470
+ .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
471
+ .replace(/[\s_]+/g, '-')
472
+ .toLowerCase();
473
+ return `${prefix}-${kebab}`;
474
+ }
475
+
476
+ /**
477
+ * 添加 RGB 值
478
+ */
479
+ private attachRgbVar(target: Record<string, string>, varName: string, value: string) {
480
+ if (typeof value !== 'string') return;
481
+ const hex = value.startsWith('#') ? value.slice(1) : '';
482
+ if (!/^[0-9a-fA-F]{6}$/.test(hex)) return;
483
+ const r = parseInt(hex.slice(0, 2), 16);
484
+ const g = parseInt(hex.slice(2, 4), 16);
485
+ const b = parseInt(hex.slice(4, 6), 16);
486
+ target[`${varName}-rgb`] = `${r}, ${g}, ${b}`;
487
+ }
488
+
489
+ /**
490
+ * 构建 CSS 变量映射表
491
+ * 生成格式:
492
+ */
493
+ private buildCssVarMap(theme: Theme, mode: 'light' | 'dark'): Record<string, string> {
494
+ const map: Record<string, string> = {
495
+ '--u-theme-mode': mode
496
+ };
497
+ const palette = this.getPaletteForMode(theme, mode);
498
+ const cssOverrides = this.getCssOverrides(theme, mode);
499
+
500
+ const applyEntry = (key: string, value: string | number | undefined | null) => {
501
+ if (value == null) return;
502
+ const strValue = String(value);
503
+ if (key.startsWith('--')) {
504
+ map[key] = strValue;
505
+ this.attachRgbVar(map, key, strValue);
506
+ return;
507
+ }
508
+ const cssVarName = this.toCssVarName(key);
509
+ // const typeVarName = this.toCssVarName(key, '--u-type');
510
+ map[cssVarName] = strValue;
511
+ // map[typeVarName] = strValue;
512
+ this.attachRgbVar(map, cssVarName, strValue);
513
+ // this.attachRgbVar(map, typeVarName, strValue);
514
+ };
515
+
516
+ Object.entries(palette || {}).forEach(([key, value]) => applyEntry(key, value as any));
517
+ Object.entries(cssOverrides || {}).forEach(([key, value]) => applyEntry(key, value as any));
518
+
519
+ return map;
520
+ }
521
+
522
+ /**
523
+ * 刷新 CSS 变量 H5
524
+ */
525
+ private flushCssVars(vars: Record<string, string>) {
526
+ if (typeof document === 'undefined' || !document.documentElement) return;
527
+ const root = document.documentElement;
528
+ this.lastAppliedCssKeys.forEach(key => {
529
+ root.style.removeProperty(key);
530
+ });
531
+ Object.entries(vars).forEach(([key, value]) => {
532
+ root.style.setProperty(key, value);
533
+ });
534
+ this.lastAppliedCssKeys = Object.keys(vars);
535
+ }
536
+
537
+ /**
538
+ * 将主题应用到运行时:
539
+ * - 1) 调用 uni.$u.setColor(theme.color) 如果存在
540
+ * - 2) 在 H5 环境中,将 css map 注入到 document.documentElement 的 CSS 变量中
541
+ * - 3) 支持暗黑模式:根据 darkColor 或 color 应用相应的颜色
542
+ */
543
+ private applyTheme(theme: Theme | null) {
544
+ if (!theme) return;
545
+ const mode = this.getActiveMode();
546
+ const palette = this.getPaletteForMode(theme, mode);
547
+ this.syncRuntimeTheme(palette);
548
+
549
+ const cssVarMap = this.buildCssVarMap(theme, mode);
550
+ this.cssVarsRef.value = cssVarMap;
551
+ this.flushCssVars(cssVarMap);
552
+ this.updateDocumentMode(mode);
553
+ }
554
+ }
555
+
556
+ // 单例导出
557
+ export const configProvider = new ConfigProvider();
558
+ export default configProvider;
@@ -0,0 +1,25 @@
1
+ declare const uni: any;
2
+
3
+ type SystemTheme = 'light' | 'dark';
4
+
5
+ /**
6
+ * 非 H5 平台获取当前系统主题
7
+ */
8
+ export function getSystemDarkMode(): SystemTheme {
9
+ try {
10
+ if (typeof uni !== 'undefined' && typeof uni.getSystemInfoSync === 'function') {
11
+ const systemInfo = uni.getSystemInfoSync();
12
+ const theme = systemInfo.osTheme || systemInfo.theme || 'light';
13
+ if (theme === 'dark') {
14
+ return 'dark';
15
+ }
16
+ if (theme === 'light') {
17
+ return 'light';
18
+ }
19
+ }
20
+ } catch (e) {
21
+ // 获取失败时默认返回亮色
22
+ console.warn('[system-theme] getSystemDarkMode failed', e);
23
+ }
24
+ return 'light';
25
+ }