@melt-js/libs-vben 0.0.0-stage → 2026.11.8-2

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 (174) hide show
  1. package/LICENSE +10 -0
  2. package/README.md +86 -2
  3. package/package.json +98 -3
  4. package/src/access/access-control.vue +47 -0
  5. package/src/access/accessible.ts +236 -0
  6. package/src/access/directive.ts +42 -0
  7. package/src/access/index.ts +4 -0
  8. package/src/access/use-access.ts +53 -0
  9. package/src/composables/index.ts +14 -0
  10. package/src/composables/use-is-mobile.ts +7 -0
  11. package/src/composables/use-layout-style.ts +51 -0
  12. package/src/composables/use-layout-viewport-height.ts +71 -0
  13. package/src/composables/use-namespace.ts +106 -0
  14. package/src/composables/use-priority-value.ts +96 -0
  15. package/src/composables/use-scroll-lock.ts +151 -0
  16. package/src/composables/use-simple-locale/README.md +3 -0
  17. package/src/composables/use-simple-locale/index.ts +27 -0
  18. package/src/composables/use-simple-locale/messages.ts +31 -0
  19. package/src/composables/use-sortable.ts +32 -0
  20. package/src/constants/core.ts +76 -0
  21. package/src/constants/index.ts +2 -0
  22. package/src/core-icons/create-icon.ts +14 -0
  23. package/src/core-icons/index.ts +11 -0
  24. package/src/core-icons/lucide.ts +95 -0
  25. package/src/core-preferences/config.ts +172 -0
  26. package/src/core-preferences/constants.ts +116 -0
  27. package/src/core-preferences/index.ts +24 -0
  28. package/src/core-preferences/preferences.ts +503 -0
  29. package/src/core-preferences/types.ts +471 -0
  30. package/src/core-preferences/update-css-variables.ts +129 -0
  31. package/src/core-preferences/use-preferences.ts +282 -0
  32. package/src/design/css/global.css +1 -0
  33. package/src/design/css/nprogress.css +61 -0
  34. package/src/design/css/transition.css +238 -0
  35. package/src/design/css/ui.css +101 -0
  36. package/src/design/design-tokens/dark.css +446 -0
  37. package/src/design/design-tokens/default.css +491 -0
  38. package/src/design/design-tokens/index.ts +2 -0
  39. package/src/design/index.ts +6 -0
  40. package/src/design/scss-bem/bem.scss +34 -0
  41. package/src/design/scss-bem/constants.scss +5 -0
  42. package/src/hooks/index.ts +9 -0
  43. package/src/hooks/use-app-config.ts +39 -0
  44. package/src/hooks/use-content-maximize.ts +24 -0
  45. package/src/hooks/use-design-tokens.ts +421 -0
  46. package/src/hooks/use-hover-toggle.ts +163 -0
  47. package/src/hooks/use-pagination.ts +72 -0
  48. package/src/hooks/use-refresh.ts +16 -0
  49. package/src/hooks/use-tabs.ts +143 -0
  50. package/src/hooks/use-watermark.ts +84 -0
  51. package/src/icons/iconify/index.ts +5 -0
  52. package/src/icons/icons/empty-icon.vue +27 -0
  53. package/src/icons/index.ts +3 -0
  54. package/src/icons/svg/icons/antdv-logo.svg +29 -0
  55. package/src/icons/svg/icons/antdv-next-logo.svg +1 -0
  56. package/src/icons/svg/icons/avatar-1.svg +1 -0
  57. package/src/icons/svg/icons/avatar-2.svg +1 -0
  58. package/src/icons/svg/icons/avatar-3.svg +1 -0
  59. package/src/icons/svg/icons/avatar-4.svg +1 -0
  60. package/src/icons/svg/icons/bell.svg +1 -0
  61. package/src/icons/svg/icons/cake.svg +1 -0
  62. package/src/icons/svg/icons/card.svg +1 -0
  63. package/src/icons/svg/icons/dingding.svg +1 -0
  64. package/src/icons/svg/icons/download.svg +1 -0
  65. package/src/icons/svg/icons/github.svg +1 -0
  66. package/src/icons/svg/icons/google.svg +1 -0
  67. package/src/icons/svg/icons/qqchat.svg +1 -0
  68. package/src/icons/svg/icons/tdesign-logo.svg +39 -0
  69. package/src/icons/svg/icons/wechat.svg +1 -0
  70. package/src/icons/svg/index.ts +39 -0
  71. package/src/icons/svg/load.ts +81 -0
  72. package/src/locales/i18n.ts +147 -0
  73. package/src/locales/index.ts +30 -0
  74. package/src/locales/langs/en-US/authentication.json +65 -0
  75. package/src/locales/langs/en-US/common.json +27 -0
  76. package/src/locales/langs/en-US/preferences.json +245 -0
  77. package/src/locales/langs/en-US/profile.json +4 -0
  78. package/src/locales/langs/en-US/ui.json +165 -0
  79. package/src/locales/langs/zh-CN/authentication.json +65 -0
  80. package/src/locales/langs/zh-CN/common.json +27 -0
  81. package/src/locales/langs/zh-CN/preferences.json +245 -0
  82. package/src/locales/langs/zh-CN/profile.json +4 -0
  83. package/src/locales/langs/zh-CN/ui.json +165 -0
  84. package/src/locales/langs/zh-TW/authentication.json +65 -0
  85. package/src/locales/langs/zh-TW/common.json +27 -0
  86. package/src/locales/langs/zh-TW/preferences.json +245 -0
  87. package/src/locales/langs/zh-TW/profile.json +4 -0
  88. package/src/locales/langs/zh-TW/ui.json +165 -0
  89. package/src/locales/typing.ts +27 -0
  90. package/src/preferences/index.ts +42 -0
  91. package/src/request/index.ts +2 -0
  92. package/src/request/request-client/index.ts +3 -0
  93. package/src/request/request-client/modules/downloader.ts +60 -0
  94. package/src/request/request-client/modules/interceptor.ts +40 -0
  95. package/src/request/request-client/modules/sse.ts +134 -0
  96. package/src/request/request-client/modules/uploader.ts +79 -0
  97. package/src/request/request-client/preset-interceptors.ts +178 -0
  98. package/src/request/request-client/request-client.ts +163 -0
  99. package/src/request/request-client/types.ts +90 -0
  100. package/src/shared/cache/README.md +418 -0
  101. package/src/shared/cache/index.ts +5 -0
  102. package/src/shared/cache/indexeddb-driver.ts +137 -0
  103. package/src/shared/cache/local-storage-driver.ts +70 -0
  104. package/src/shared/cache/memory-storage-driver.ts +32 -0
  105. package/src/shared/cache/storage-manager.ts +146 -0
  106. package/src/shared/cache/types.ts +39 -0
  107. package/src/shared/color/color.ts +9 -0
  108. package/src/shared/color/convert.ts +62 -0
  109. package/src/shared/color/generator.ts +45 -0
  110. package/src/shared/color/index.ts +3 -0
  111. package/src/shared/constants/globals.ts +20 -0
  112. package/src/shared/constants/index.ts +2 -0
  113. package/src/shared/constants/vben.ts +30 -0
  114. package/src/shared/global-state.ts +45 -0
  115. package/src/shared/store.ts +1 -0
  116. package/src/shared/utils/cn.ts +10 -0
  117. package/src/shared/utils/date.ts +80 -0
  118. package/src/shared/utils/diff.ts +114 -0
  119. package/src/shared/utils/dom.ts +126 -0
  120. package/src/shared/utils/download.ts +186 -0
  121. package/src/shared/utils/error.ts +19 -0
  122. package/src/shared/utils/index.ts +22 -0
  123. package/src/shared/utils/inference.ts +164 -0
  124. package/src/shared/utils/letter.ts +47 -0
  125. package/src/shared/utils/merge.ts +11 -0
  126. package/src/shared/utils/nprogress.ts +52 -0
  127. package/src/shared/utils/resources.ts +21 -0
  128. package/src/shared/utils/stack.ts +103 -0
  129. package/src/shared/utils/state-handler.ts +61 -0
  130. package/src/shared/utils/to.ts +21 -0
  131. package/src/shared/utils/tree.ts +159 -0
  132. package/src/shared/utils/unique.ts +15 -0
  133. package/src/shared/utils/update-css-variables.ts +47 -0
  134. package/src/shared/utils/util.ts +50 -0
  135. package/src/shared/utils/window.ts +37 -0
  136. package/src/stores/index.ts +3 -0
  137. package/src/stores/modules/access.ts +130 -0
  138. package/src/stores/modules/dict.ts +244 -0
  139. package/src/stores/modules/index.ts +5 -0
  140. package/src/stores/modules/tabbar.ts +797 -0
  141. package/src/stores/modules/timezone.ts +132 -0
  142. package/src/stores/modules/user.ts +42 -0
  143. package/src/stores/setup.ts +79 -0
  144. package/src/stores/shim-pinia.d.ts +11 -0
  145. package/src/styles/antd/index.css +78 -0
  146. package/src/styles/antdv-next/index.css +90 -0
  147. package/src/styles/ele/index.css +44 -0
  148. package/src/styles/global/index.scss +1 -0
  149. package/src/styles/index.ts +1 -0
  150. package/src/styles/naive/index.css +26 -0
  151. package/src/styles/style-exports.d.ts +3 -0
  152. package/src/theme.css +592 -0
  153. package/src/types/global.d.ts +28 -0
  154. package/src/types/index.ts +2 -0
  155. package/src/types/shims.d.ts +1 -0
  156. package/src/types/user.ts +20 -0
  157. package/src/typings/app.d.ts +128 -0
  158. package/src/typings/basic.d.ts +42 -0
  159. package/src/typings/helper.d.ts +147 -0
  160. package/src/typings/index.ts +16 -0
  161. package/src/typings/languages.ts +28 -0
  162. package/src/typings/menu-record.ts +82 -0
  163. package/src/typings/tabs.ts +8 -0
  164. package/src/typings/vue-router.d.ts +169 -0
  165. package/src/utils/helpers/find-menu-by-path.ts +37 -0
  166. package/src/utils/helpers/generate-menus.ts +89 -0
  167. package/src/utils/helpers/generate-routes-backend.ts +108 -0
  168. package/src/utils/helpers/generate-routes-frontend.ts +58 -0
  169. package/src/utils/helpers/get-popup-container.ts +10 -0
  170. package/src/utils/helpers/index.ts +8 -0
  171. package/src/utils/helpers/merge-route-modules.ts +28 -0
  172. package/src/utils/helpers/reset-routes.ts +30 -0
  173. package/src/utils/helpers/unmount-global-loading.ts +31 -0
  174. package/src/utils/index.ts +4 -0
@@ -0,0 +1,503 @@
1
+ import type { DeepPartial } from '@melt-js/libs-vben/typings';
2
+
3
+ import type {
4
+ CustomPreferencesField,
5
+ CustomPreferencesRecord,
6
+ InitialOptions,
7
+ Preferences,
8
+ PreferencesExtension,
9
+ } from './types';
10
+
11
+ import { markRaw, reactive, readonly, watch } from 'vue';
12
+
13
+ import { MemoryStorageDriver, StorageManager } from '@melt-js/libs-vben/shared/cache';
14
+ import {
15
+ isMacOs,
16
+ merge,
17
+ mergeWithArrayOverride,
18
+ } from '@melt-js/libs-vben/shared/utils';
19
+
20
+ import {
21
+ breakpointsTailwind,
22
+ useBreakpoints,
23
+ useDebounceFn,
24
+ } from '@vueuse/core';
25
+
26
+ import { defaultPreferences } from './config';
27
+ import { updateCSSVariables } from './update-css-variables';
28
+
29
+ const STORAGE_KEYS = {
30
+ CUSTOM: 'preferences-custom',
31
+ MAIN: 'preferences',
32
+ LOCALE: 'preferences-locale',
33
+ THEME: 'preferences-theme',
34
+ } as const;
35
+
36
+ class PreferenceManager {
37
+ private cache: StorageManager;
38
+ private customPreferencesExtension: null | PreferencesExtension<any> = null;
39
+ private customState = reactive<CustomPreferencesRecord>({});
40
+ private debouncedSave: () => void;
41
+ private initialCustomPreferences: CustomPreferencesRecord = {};
42
+ private initialPreferences: Preferences = defaultPreferences;
43
+ private isInitialized = false;
44
+ private state: Preferences;
45
+
46
+ constructor() {
47
+ this.cache = new StorageManager({ driver: new MemoryStorageDriver() });
48
+ // 构造函数不再同步读取缓存,使用默认值初始化
49
+ // 真正的缓存加载在 initPreferences 中完成(已经是 async)
50
+ this.state = reactive<Preferences>({ ...defaultPreferences });
51
+ this.debouncedSave = useDebounceFn(() => this.saveToCache(), 150);
52
+ }
53
+
54
+ /**
55
+ * 清除所有缓存的偏好设置
56
+ */
57
+ clearCache = async () => {
58
+ await Promise.all(
59
+ Object.values(STORAGE_KEYS).map((key) => this.cache.removeItem(key)),
60
+ );
61
+ };
62
+
63
+ /**
64
+ * 获取扩展偏好设置
65
+ */
66
+ getCustomPreferences = <
67
+ TCustomPreferences extends object = CustomPreferencesRecord,
68
+ >() => {
69
+ return readonly(this.customState) as Readonly<TCustomPreferences>;
70
+ };
71
+
72
+ /**
73
+ * 获取初始化扩展偏好设置
74
+ */
75
+ getInitialCustomPreferences = <
76
+ TCustomPreferences extends object = CustomPreferencesRecord,
77
+ >() => {
78
+ return this.cloneValue(
79
+ this.initialCustomPreferences,
80
+ ) as Readonly<TCustomPreferences>;
81
+ };
82
+
83
+ /**
84
+ * 获取初始化偏好设置
85
+ */
86
+ getInitialPreferences = () => {
87
+ return this.initialPreferences;
88
+ };
89
+
90
+ /**
91
+ * 获取当前偏好设置(只读)
92
+ */
93
+ getPreferences = () => {
94
+ return readonly(this.state);
95
+ };
96
+
97
+ /**
98
+ * 获取扩展偏好设置配置
99
+ */
100
+ getPreferencesExtension = <
101
+ TCustomPreferences extends object = CustomPreferencesRecord,
102
+ >() => {
103
+ return this.customPreferencesExtension
104
+ ? (this.cloneValue(this.customPreferencesExtension) as Readonly<
105
+ PreferencesExtension<TCustomPreferences>
106
+ >)
107
+ : null;
108
+ };
109
+
110
+ /**
111
+ * 初始化偏好设置
112
+ * @param options - 初始化配置项
113
+ * @param options.namespace - 命名空间,用于隔离不同应用的配置
114
+ * @param options.overrides - 要覆盖的偏好设置
115
+ */
116
+ initPreferences = async <
117
+ TCustomPreferences extends object = CustomPreferencesRecord,
118
+ >({
119
+ namespace,
120
+ overrides,
121
+ extension,
122
+ }: InitialOptions<TCustomPreferences>) => {
123
+ // 防止重复初始化
124
+ if (this.isInitialized) {
125
+ return;
126
+ }
127
+
128
+ // 使用命名空间初始化存储管理器
129
+ this.cache = new StorageManager({ prefix: namespace });
130
+
131
+ // 合并初始偏好设置:前面的对象优先,后面的对象仅补齐缺失字段
132
+ this.initialPreferences = merge({}, overrides, defaultPreferences);
133
+ this.customPreferencesExtension = extension ?? null;
134
+ this.initialCustomPreferences = this.resolveCustomPreferencesDefaults(
135
+ this.customPreferencesExtension,
136
+ );
137
+
138
+ // 加载缓存的偏好设置,并仅用缓存补齐初始化配置中未显式设置的字段
139
+ const cachedPreferences = (await this.loadFromCache()) || {};
140
+ // 修复历史脏数据:早期版本用 defu(merge) 合并数组,导致 widget.order
141
+ // 在每次 updatePreferences 时被 concat 追加,膨胀到几十/上百条重复 key。
142
+ // 这里在加载缓存后做一次去重,保留首次出现的 key。
143
+ this.sanitizeCachedArray(cachedPreferences, 'widget', 'order');
144
+ const mergedPreference = mergeWithArrayOverride(
145
+ {},
146
+ cachedPreferences, // 用户缓存的设置优先
147
+ this.initialPreferences, // 初始设置仅补齐缺失字段
148
+ );
149
+
150
+ // 更新偏好设置
151
+ this.updatePreferences(mergedPreference);
152
+
153
+ const cachedCustom = (await this.loadCustomFromCache()) || {};
154
+ this.replaceCustomPreferences(
155
+ mergeWithArrayOverride(
156
+ {},
157
+ this.sanitizeCustomPreferences(cachedCustom),
158
+ this.initialCustomPreferences,
159
+ ),
160
+ );
161
+ await this.saveToCache();
162
+
163
+ // 设置监听器
164
+ this.setupWatcher();
165
+
166
+ // 初始化平台标识
167
+ this.initPlatform();
168
+
169
+ this.isInitialized = true;
170
+ };
171
+
172
+ /**
173
+ * 重置偏好设置到初始状态
174
+ */
175
+ resetPreferences = async () => {
176
+ // 将状态重置为初始偏好设置
177
+ Object.assign(this.state, this.initialPreferences);
178
+ this.replaceCustomPreferences(this.initialCustomPreferences);
179
+
180
+ // 直接触发 UI 更新
181
+ this.handleUpdates(this.state);
182
+
183
+ // 保存偏好设置至缓存
184
+ await this.saveToCache();
185
+ };
186
+
187
+ /**
188
+ * 更新扩展偏好设置
189
+ * @param updates - 要更新的扩展偏好设置
190
+ */
191
+ updateCustomPreferences = <
192
+ TCustomPreferences extends object = CustomPreferencesRecord,
193
+ >(
194
+ updates: DeepPartial<TCustomPreferences>,
195
+ ) => {
196
+ if (!this.customPreferencesExtension) {
197
+ return;
198
+ }
199
+
200
+ const sanitizedUpdates = this.sanitizeCustomPreferences(updates);
201
+
202
+ if (Object.keys(sanitizedUpdates).length === 0) {
203
+ return;
204
+ }
205
+
206
+ this.replaceCustomPreferences(
207
+ mergeWithArrayOverride({}, sanitizedUpdates, markRaw(this.customState)),
208
+ );
209
+ this.debouncedSave();
210
+ };
211
+
212
+ /**
213
+ * 更新偏好设置
214
+ * @param updates - 要更新的偏好设置
215
+ */
216
+ updatePreferences = (updates: DeepPartial<Preferences>) => {
217
+ // 深度合并更新内容和当前状态
218
+ // 注意:必须用 mergeWithArrayOverride 而不是 merge。
219
+ // defu(merge) 对数组的语义是 concat 追加,会导致 widget.order 等
220
+ // 数组字段在每次 updatePreferences 时被重复追加,指数级膨胀。
221
+ const mergedState = mergeWithArrayOverride(
222
+ {},
223
+ updates,
224
+ markRaw(this.state),
225
+ );
226
+ Object.assign(this.state, mergedState);
227
+
228
+ // 根据更新的值执行更新
229
+ this.handleUpdates(updates);
230
+
231
+ // 保存到缓存(fire-and-forget,通过 debounce 控制频率)
232
+ this.debouncedSave();
233
+ };
234
+
235
+ private cloneValue<T>(value: T): T {
236
+ if (Array.isArray(value)) {
237
+ return value.map((item) => this.cloneValue(item)) as T;
238
+ }
239
+
240
+ if (value && typeof value === 'object') {
241
+ return Object.fromEntries(
242
+ Object.entries(value as Record<string, unknown>).map(
243
+ ([key, nestedValue]) => [key, this.cloneValue(nestedValue)],
244
+ ),
245
+ ) as T;
246
+ }
247
+
248
+ return value;
249
+ }
250
+
251
+ /**
252
+ * 处理更新
253
+ * @param updates - 更新的偏好设置
254
+ */
255
+ private handleUpdates(updates: DeepPartial<Preferences>) {
256
+ const { theme, app } = updates;
257
+
258
+ if (
259
+ theme &&
260
+ (Object.keys(theme).length > 0 || Reflect.has(theme, 'fontSize'))
261
+ ) {
262
+ updateCSSVariables(this.state);
263
+ }
264
+
265
+ if (
266
+ app &&
267
+ (Reflect.has(app, 'colorGrayMode') || Reflect.has(app, 'colorWeakMode'))
268
+ ) {
269
+ this.updateColorMode(this.state);
270
+ }
271
+ }
272
+
273
+ /**
274
+ * 初始化平台标识
275
+ */
276
+ private initPlatform() {
277
+ document.documentElement.dataset.platform = isMacOs() ? 'macOs' : 'window';
278
+ }
279
+
280
+ private isAlmostInteger(value: number, epsilon = Number.EPSILON * 10) {
281
+ return Math.abs(value - Math.round(value)) < epsilon;
282
+ }
283
+
284
+ private isValidCustomPreferenceValue(
285
+ field: CustomPreferencesField,
286
+ value: unknown,
287
+ ) {
288
+ switch (field.component) {
289
+ case 'number': {
290
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
291
+ return false;
292
+ }
293
+
294
+ const max = this.resolveNumericConstraint(field.componentProps?.max);
295
+ const min = this.resolveNumericConstraint(field.componentProps?.min);
296
+ const step = this.resolveNumericConstraint(field.componentProps?.step);
297
+
298
+ if (min !== undefined && value < min) {
299
+ return false;
300
+ }
301
+
302
+ if (max !== undefined && value > max) {
303
+ return false;
304
+ }
305
+
306
+ if (step !== undefined) {
307
+ if (step <= 0) {
308
+ return false;
309
+ }
310
+
311
+ const stepBase = min ?? 0;
312
+ const stepCount = (value - stepBase) / step;
313
+
314
+ if (!this.isAlmostInteger(stepCount)) {
315
+ return false;
316
+ }
317
+ }
318
+
319
+ return true;
320
+ }
321
+ case 'select': {
322
+ return (
323
+ typeof value === 'string' &&
324
+ field.options.some((option) => option.value === value)
325
+ );
326
+ }
327
+ case 'switch': {
328
+ return typeof value === 'boolean';
329
+ }
330
+ default: {
331
+ return typeof value === 'string';
332
+ }
333
+ }
334
+ }
335
+
336
+ /**
337
+ * 从缓存加载扩展偏好设置
338
+ * @returns 缓存的扩展偏好设置,如果不存在则返回 null
339
+ */
340
+ private async loadCustomFromCache(): Promise<CustomPreferencesRecord | null> {
341
+ return this.cache.getItem<CustomPreferencesRecord>(STORAGE_KEYS.CUSTOM);
342
+ }
343
+
344
+ /**
345
+ * 从缓存加载偏好设置
346
+ * @returns 缓存的偏好设置,如果不存在则返回 null
347
+ */
348
+ private async loadFromCache(): Promise<null | Preferences> {
349
+ return this.cache.getItem<Preferences>(STORAGE_KEYS.MAIN);
350
+ }
351
+
352
+ private replaceCustomPreferences(preferences: CustomPreferencesRecord) {
353
+ Object.keys(this.customState).forEach((key) => {
354
+ Reflect.deleteProperty(this.customState, key);
355
+ });
356
+ Object.assign(this.customState, preferences);
357
+ }
358
+
359
+ private resolveCustomPreferencesDefaults(
360
+ extension: null | PreferencesExtension<any>,
361
+ ) {
362
+ if (!extension) {
363
+ return {};
364
+ }
365
+
366
+ const result: CustomPreferencesRecord = {};
367
+
368
+ for (const field of extension.fields) {
369
+ result[field.key] = field.defaultValue;
370
+ }
371
+
372
+ return result;
373
+ }
374
+
375
+ private resolveNumericConstraint(value: unknown) {
376
+ return typeof value === 'number' && Number.isFinite(value)
377
+ ? value
378
+ : undefined;
379
+ }
380
+
381
+ /**
382
+ * 清理缓存中膨胀的数组字段(保留首次出现的元素)
383
+ * 用于修复早期版本 defu 合并数组时 concat 追加导致的脏数据
384
+ */
385
+ private sanitizeCachedArray(
386
+ cached: Record<string, any>,
387
+ group: string,
388
+ field: string,
389
+ ) {
390
+ const node = cached?.[group]?.[field];
391
+ if (!Array.isArray(node) || node.length <= 1) return;
392
+ const seen = new Set<unknown>();
393
+ const deduped = node.filter((item) => {
394
+ if (seen.has(item)) return false;
395
+ seen.add(item);
396
+ return true;
397
+ });
398
+ if (deduped.length !== node.length) {
399
+ cached[group][field] = deduped;
400
+ }
401
+ }
402
+
403
+ private sanitizeCustomPreferences(
404
+ updates: DeepPartial<CustomPreferencesRecord>,
405
+ ) {
406
+ if (!this.customPreferencesExtension) {
407
+ return {};
408
+ }
409
+
410
+ const result: CustomPreferencesRecord = {};
411
+
412
+ for (const field of this.customPreferencesExtension.fields) {
413
+ const value = updates[field.key];
414
+
415
+ if (
416
+ value !== undefined &&
417
+ this.isValidCustomPreferenceValue(field, value)
418
+ ) {
419
+ result[field.key] = value;
420
+ }
421
+ }
422
+
423
+ return result;
424
+ }
425
+
426
+ /**
427
+ * 保存偏好设置到缓存
428
+ */
429
+ private async saveToCache() {
430
+ try {
431
+ await this.cache.setItem(STORAGE_KEYS.MAIN, this.state);
432
+ await this.cache.setItem(STORAGE_KEYS.LOCALE, this.state.app.locale);
433
+ await this.cache.setItem(STORAGE_KEYS.THEME, this.state.theme.mode);
434
+
435
+ if (this.customPreferencesExtension) {
436
+ await this.cache.setItem(STORAGE_KEYS.CUSTOM, {
437
+ ...this.customState,
438
+ });
439
+ return;
440
+ }
441
+
442
+ await this.cache.removeItem(STORAGE_KEYS.CUSTOM);
443
+ } catch (error) {
444
+ console.error('Failed to save preferences to cache:', error);
445
+ }
446
+ }
447
+
448
+ /**
449
+ * 监听状态和系统偏好设置的变化
450
+ */
451
+ private setupWatcher() {
452
+ if (this.isInitialized) {
453
+ return;
454
+ }
455
+
456
+ // 监听断点,判断是否移动端
457
+ const breakpoints = useBreakpoints(breakpointsTailwind);
458
+ const isMobile = breakpoints.smaller('md');
459
+
460
+ watch(
461
+ () => isMobile.value,
462
+ (val) => {
463
+ this.updatePreferences({
464
+ app: { isMobile: val },
465
+ });
466
+ },
467
+ { immediate: true },
468
+ );
469
+
470
+ // 监听系统主题偏好设置变化
471
+ window
472
+ .matchMedia('(prefers-color-scheme: dark)')
473
+ .addEventListener('change', ({ matches: isDark }) => {
474
+ // 仅在自动模式下跟随系统主题
475
+ if (this.state.theme.mode === 'auto') {
476
+ // 先应用实际的主题
477
+ this.updatePreferences({
478
+ theme: { mode: isDark ? 'dark' : 'light' },
479
+ });
480
+ // 再恢复为 auto 模式,保持跟随系统的状态
481
+ this.updatePreferences({
482
+ theme: { mode: 'auto' },
483
+ });
484
+ }
485
+ });
486
+ }
487
+
488
+ /**
489
+ * 更新页面颜色模式(灰色、色弱)
490
+ * @param preference - 偏好设置
491
+ */
492
+ private updateColorMode(preference: Preferences) {
493
+ const { colorGrayMode, colorWeakMode } = preference.app;
494
+ const dom = document.documentElement;
495
+
496
+ dom.classList.toggle('invert-mode', colorWeakMode);
497
+ dom.classList.toggle('grayscale-mode', colorGrayMode);
498
+ }
499
+ }
500
+
501
+ const preferencesManager = new PreferenceManager();
502
+
503
+ export { PreferenceManager, preferencesManager };