preferences.ts 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. import type { DeepPartial } from '@vben-core/typings';
  2. import type { InitialOptions, Preferences } from './types';
  3. import { markRaw, reactive, readonly, watch } from 'vue';
  4. import { StorageManager } from '@vben-core/shared/cache';
  5. import { isMacOs, merge } from '@vben-core/shared/utils';
  6. import {
  7. breakpointsTailwind,
  8. useBreakpoints,
  9. useDebounceFn,
  10. } from '@vueuse/core';
  11. import { defaultPreferences } from './config';
  12. import { updateCSSVariables } from './update-css-variables';
  13. const STORAGE_KEYS = {
  14. MAIN: 'preferences',
  15. LOCALE: 'preferences-locale',
  16. THEME: 'preferences-theme',
  17. } as const;
  18. class PreferenceManager {
  19. private cache: StorageManager;
  20. private debouncedSave: (preference: Preferences) => void;
  21. private initialPreferences: Preferences = defaultPreferences;
  22. private isInitialized = false;
  23. private state: Preferences;
  24. constructor() {
  25. this.cache = new StorageManager();
  26. this.state = reactive<Preferences>(
  27. this.loadFromCache() || { ...defaultPreferences },
  28. );
  29. this.debouncedSave = useDebounceFn(
  30. (preference) => this.saveToCache(preference),
  31. 150,
  32. );
  33. }
  34. /**
  35. * 清除所有缓存的偏好设置
  36. */
  37. clearCache = () => {
  38. Object.values(STORAGE_KEYS).forEach((key) => this.cache.removeItem(key));
  39. };
  40. /**
  41. * 获取初始化偏好设置
  42. */
  43. getInitialPreferences = () => {
  44. return this.initialPreferences;
  45. };
  46. /**
  47. * 获取当前偏好设置(只读)
  48. */
  49. getPreferences = () => {
  50. return readonly(this.state);
  51. };
  52. /**
  53. * 初始化偏好设置
  54. * @param options - 初始化配置项
  55. * @param options.namespace - 命名空间,用于隔离不同应用的配置
  56. * @param options.overrides - 要覆盖的偏好设置
  57. */
  58. initPreferences = async ({ namespace, overrides }: InitialOptions) => {
  59. // 防止重复初始化
  60. if (this.isInitialized) {
  61. return;
  62. }
  63. // 使用命名空间初始化存储管理器
  64. this.cache = new StorageManager({ prefix: namespace });
  65. // 合并初始偏好设置
  66. this.initialPreferences = merge({}, overrides, defaultPreferences);
  67. // 加载缓存的偏好设置并与初始配置合并
  68. const cachedPreferences = this.loadFromCache() || {};
  69. const mergedPreference = merge(
  70. {},
  71. cachedPreferences,
  72. this.initialPreferences,
  73. );
  74. // 更新偏好设置
  75. this.updatePreferences(mergedPreference);
  76. // 设置监听器
  77. this.setupWatcher();
  78. // 初始化平台标识
  79. this.initPlatform();
  80. this.isInitialized = true;
  81. };
  82. /**
  83. * 重置偏好设置到初始状态
  84. */
  85. resetPreferences = () => {
  86. // 将状态重置为初始偏好设置
  87. Object.assign(this.state, this.initialPreferences);
  88. // 保存偏好设置至缓存
  89. this.saveToCache(this.state);
  90. // 直接触发 UI 更新
  91. this.handleUpdates(this.state);
  92. };
  93. /**
  94. * 更新偏好设置
  95. * @param updates - 要更新的偏好设置
  96. */
  97. updatePreferences = (updates: DeepPartial<Preferences>) => {
  98. // 深度合并更新内容和当前状态
  99. const mergedState = merge({}, updates, markRaw(this.state));
  100. Object.assign(this.state, mergedState);
  101. // 根据更新的值执行更新
  102. this.handleUpdates(updates);
  103. // 保存到缓存
  104. this.debouncedSave(this.state);
  105. };
  106. /**
  107. * 处理更新
  108. * @param updates - 更新的偏好设置
  109. */
  110. private handleUpdates(updates: DeepPartial<Preferences>) {
  111. const { theme, app } = updates;
  112. if (
  113. theme &&
  114. (Object.keys(theme).length > 0 || Reflect.has(theme, 'fontSize'))
  115. ) {
  116. updateCSSVariables(this.state);
  117. }
  118. if (
  119. app &&
  120. (Reflect.has(app, 'colorGrayMode') || Reflect.has(app, 'colorWeakMode'))
  121. ) {
  122. this.updateColorMode(this.state);
  123. }
  124. }
  125. /**
  126. * 初始化平台标识
  127. */
  128. private initPlatform() {
  129. document.documentElement.dataset.platform = isMacOs() ? 'macOs' : 'window';
  130. }
  131. /**
  132. * 从缓存加载偏好设置
  133. * @returns 缓存的偏好设置,如果不存在则返回 null
  134. */
  135. private loadFromCache(): null | Preferences {
  136. return this.cache.getItem<Preferences>(STORAGE_KEYS.MAIN);
  137. }
  138. /**
  139. * 保存偏好设置到缓存
  140. * @param preference - 要保存的偏好设置
  141. */
  142. private saveToCache(preference: Preferences) {
  143. this.cache.setItem(STORAGE_KEYS.MAIN, preference);
  144. this.cache.setItem(STORAGE_KEYS.LOCALE, preference.app.locale);
  145. this.cache.setItem(STORAGE_KEYS.THEME, preference.theme.mode);
  146. }
  147. /**
  148. * 监听状态和系统偏好设置的变化
  149. */
  150. private setupWatcher() {
  151. if (this.isInitialized) {
  152. return;
  153. }
  154. // 监听断点,判断是否移动端
  155. const breakpoints = useBreakpoints(breakpointsTailwind);
  156. const isMobile = breakpoints.smaller('md');
  157. watch(
  158. () => isMobile.value,
  159. (val) => {
  160. this.updatePreferences({
  161. app: { isMobile: val },
  162. });
  163. },
  164. { immediate: true },
  165. );
  166. // 监听系统主题偏好设置变化
  167. window
  168. .matchMedia('(prefers-color-scheme: dark)')
  169. .addEventListener('change', ({ matches: isDark }) => {
  170. // 仅在自动模式下跟随系统主题
  171. if (this.state.theme.mode === 'auto') {
  172. // 先应用实际的主题
  173. this.updatePreferences({
  174. theme: { mode: isDark ? 'dark' : 'light' },
  175. });
  176. // 再恢复为 auto 模式,保持跟随系统的状态
  177. this.updatePreferences({
  178. theme: { mode: 'auto' },
  179. });
  180. }
  181. });
  182. }
  183. /**
  184. * 更新页面颜色模式(灰色、色弱)
  185. * @param preference - 偏好设置
  186. */
  187. private updateColorMode(preference: Preferences) {
  188. const { colorGrayMode, colorWeakMode } = preference.app;
  189. const dom = document.documentElement;
  190. dom.classList.toggle('invert-mode', colorWeakMode);
  191. dom.classList.toggle('grayscale-mode', colorGrayMode);
  192. }
  193. }
  194. const preferencesManager = new PreferenceManager();
  195. export { PreferenceManager, preferencesManager };