preferences.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367
  1. import type { DeepPartial } from '@vben-core/typings';
  2. import type { Preferences } from './types';
  3. import { markRaw, reactive, readonly, watch } from 'vue';
  4. import { StorageManager } from '@vben-core/cache';
  5. import { generatorColorVariables } from '@vben-core/colorful';
  6. import { merge, updateCSSVariables } from '@vben-core/toolkit';
  7. import {
  8. breakpointsTailwind,
  9. useBreakpoints,
  10. useDebounceFn,
  11. } from '@vueuse/core';
  12. import { defaultPreferences } from './config';
  13. import { BUILT_IN_THEME_PRESETS } from './constants';
  14. const STORAGE_KEY = 'preferences';
  15. const STORAGE_KEY_LOCALE = `${STORAGE_KEY}-locale`;
  16. const STORAGE_KEY_THEME = `${STORAGE_KEY}-theme`;
  17. interface initialOptions {
  18. namespace: string;
  19. overrides?: DeepPartial<Preferences>;
  20. }
  21. function isDarkTheme(theme: string) {
  22. let dark = theme === 'dark';
  23. if (theme === 'auto') {
  24. dark = window.matchMedia('(prefers-color-scheme: dark)').matches;
  25. }
  26. return dark;
  27. }
  28. class PreferenceManager {
  29. private cache: StorageManager | null = null;
  30. // private flattenedState: Flatten<Preferences>;
  31. private initialPreferences: Preferences = defaultPreferences;
  32. private isInitialized: boolean = false;
  33. private savePreferences: (preference: Preferences) => void;
  34. private state: Preferences = reactive<Preferences>({
  35. ...this.loadPreferences(),
  36. });
  37. constructor() {
  38. this.cache = new StorageManager();
  39. // this.flattenedState = reactive(flattenObject(this.state));
  40. this.savePreferences = useDebounceFn(
  41. (preference: Preferences) => this._savePreferences(preference),
  42. 100,
  43. );
  44. }
  45. /**
  46. * 保存偏好设置
  47. * @param {Preferences} preference - 需要保存的偏好设置
  48. */
  49. private _savePreferences(preference: Preferences) {
  50. this.cache?.setItem(STORAGE_KEY, preference);
  51. this.cache?.setItem(STORAGE_KEY_LOCALE, preference.app.locale);
  52. this.cache?.setItem(STORAGE_KEY_THEME, preference.theme.mode);
  53. }
  54. /**
  55. * 处理更新的键值
  56. * 根据更新的键值执行相应的操作。
  57. *
  58. * @param {DeepPartial<Preferences>} updates - 部分更新的偏好设置
  59. */
  60. private handleUpdates(updates: DeepPartial<Preferences>) {
  61. const themeUpdates = updates.theme || {};
  62. const appUpdates = updates.app || {};
  63. if (themeUpdates && Object.keys(themeUpdates).length > 0) {
  64. this.updateTheme(this.state);
  65. }
  66. if (appUpdates.colorGrayMode || appUpdates.colorWeakMode) {
  67. this.updateColorMode(this.state);
  68. }
  69. }
  70. /**
  71. * 从缓存中加载偏好设置。如果缓存中没有找到对应的偏好设置,则返回默认偏好设置。
  72. */
  73. private loadCachedPreferences() {
  74. return this.cache?.getItem<Preferences>(STORAGE_KEY);
  75. }
  76. /**
  77. * 加载偏好设置
  78. * @returns {Preferences} 加载的偏好设置
  79. */
  80. private loadPreferences(): Preferences {
  81. return this.loadCachedPreferences() || { ...defaultPreferences };
  82. }
  83. /**
  84. * 监听状态和系统偏好设置的变化。
  85. */
  86. private setupWatcher() {
  87. if (this.isInitialized) {
  88. return;
  89. }
  90. // const debounceWaterState = useDebounceFn(() => {
  91. // const newFlattenedState = flattenObject(this.state);
  92. // for (const k in newFlattenedState) {
  93. // const key = k as FlattenObjectKeys<Preferences>;
  94. // this.flattenedState[key] = newFlattenedState[key];
  95. // }
  96. // this.savePreferences(this.state);
  97. // }, 16);
  98. // const debounceWaterFlattenedState = useDebounceFn(
  99. // (val: Flatten<Preferences>) => {
  100. // this.updateState(val);
  101. // this.savePreferences(this.state);
  102. // },
  103. // 16,
  104. // );
  105. // 监听 state 的变化
  106. // watch(this.state, debounceWaterState, { deep: true });
  107. // 监听 flattenedState 的变化并触发 set 方法
  108. // watch(this.flattenedState, debounceWaterFlattenedState, { deep: true });
  109. // 监听断点,判断是否移动端
  110. const breakpoints = useBreakpoints(breakpointsTailwind);
  111. const isMobile = breakpoints.smaller('md');
  112. watch(
  113. () => isMobile.value,
  114. (val) => {
  115. this.updatePreferences({
  116. app: { isMobile: val },
  117. });
  118. },
  119. { immediate: true },
  120. );
  121. // 监听系统主题偏好设置变化
  122. window
  123. .matchMedia('(prefers-color-scheme: dark)')
  124. .addEventListener('change', ({ matches: isDark }) => {
  125. this.updatePreferences({
  126. theme: { mode: isDark ? 'dark' : 'light' },
  127. });
  128. this.updateTheme(this.state);
  129. });
  130. }
  131. /**
  132. * 更新页面颜色模式(灰色、色弱)
  133. * @param preference
  134. */
  135. private updateColorMode(preference: Preferences) {
  136. if (preference.app) {
  137. const { colorGrayMode, colorWeakMode } = preference.app;
  138. const body = document.body;
  139. const COLOR_WEAK = 'invert-mode';
  140. const COLOR_GRAY = 'grayscale-mode';
  141. colorWeakMode
  142. ? body.classList.add(COLOR_WEAK)
  143. : body.classList.remove(COLOR_WEAK);
  144. colorGrayMode
  145. ? body.classList.add(COLOR_GRAY)
  146. : body.classList.remove(COLOR_GRAY);
  147. }
  148. }
  149. /**
  150. * 更新 CSS 变量
  151. * @param preference - 当前偏好设置对象,它的颜色值将被转换成 HSL 格式并设置为 CSS 变量。
  152. */
  153. private updateMainColors(preference: Preferences) {
  154. if (!preference.theme) {
  155. return;
  156. }
  157. const { colorDestructive, colorPrimary, colorSuccess, colorWarning } =
  158. preference.theme;
  159. const colorVariables = generatorColorVariables([
  160. { color: colorPrimary, name: 'primary' },
  161. { alias: 'warning', color: colorWarning, name: 'yellow' },
  162. { alias: 'success', color: colorSuccess, name: 'green' },
  163. { alias: 'destructive', color: colorDestructive, name: 'red' },
  164. ]);
  165. if (colorPrimary) {
  166. document.documentElement.style.setProperty(
  167. '--primary',
  168. colorVariables['--primary-600'],
  169. );
  170. }
  171. if (colorVariables['--green-600']) {
  172. colorVariables['--success'] = colorVariables['--green-600'];
  173. }
  174. if (colorVariables['--yellow-600']) {
  175. colorVariables['--warning'] = colorVariables['--yellow-600'];
  176. }
  177. if (colorVariables['--red-600']) {
  178. colorVariables['--destructive'] = colorVariables['--red-600'];
  179. }
  180. updateCSSVariables(colorVariables);
  181. }
  182. /**
  183. * 更新状态
  184. * 将新的扁平对象转换为嵌套对象,并与当前状态合并。
  185. * @param {FlattenObject<Preferences>} newValue - 新的扁平对象
  186. */
  187. // private updateState(newValue: Flatten<Preferences>) {
  188. // const nestObj = nestedObject(newValue, 2);
  189. // Object.assign(this.state, merge(nestObj, this.state));
  190. // }
  191. /**
  192. * 更新主题
  193. * @param preferences - 当前偏好设置对象,它的主题值将被用来设置文档的主题。
  194. */
  195. private updateTheme(preferences: Preferences) {
  196. // 当修改到颜色变量时,更新 css 变量
  197. const root = document.documentElement;
  198. if (!root) {
  199. return;
  200. }
  201. const {
  202. builtinType,
  203. colorDestructive,
  204. colorPrimary,
  205. colorSuccess,
  206. colorWarning,
  207. mode,
  208. radius,
  209. } = preferences?.theme ?? {};
  210. if (mode) {
  211. const dark = isDarkTheme(mode);
  212. root.classList.toggle('dark', dark);
  213. }
  214. if (builtinType) {
  215. const rootTheme = root.dataset.theme;
  216. if (rootTheme !== builtinType) {
  217. root.dataset.theme = builtinType;
  218. }
  219. }
  220. const currentBuiltType = BUILT_IN_THEME_PRESETS.find(
  221. (item) => item.type === builtinType,
  222. );
  223. let builtinTypeColorPrimary: string | undefined = '';
  224. if (currentBuiltType) {
  225. const isDark = isDarkTheme(this.state.theme.mode);
  226. const color = isDark
  227. ? currentBuiltType.darkPrimaryColor || currentBuiltType.primaryColor
  228. : currentBuiltType.primaryColor;
  229. builtinTypeColorPrimary = color || currentBuiltType.color;
  230. }
  231. if (
  232. builtinTypeColorPrimary ||
  233. colorPrimary ||
  234. colorDestructive ||
  235. colorSuccess ||
  236. colorWarning
  237. ) {
  238. preferences.theme.colorPrimary = builtinTypeColorPrimary || colorPrimary;
  239. this.updateMainColors(preferences);
  240. }
  241. if (radius) {
  242. document.documentElement.style.setProperty('--radius', `${radius}rem`);
  243. }
  244. }
  245. // public getFlatPreferences() {
  246. // return this.flattenedState;
  247. // }
  248. public getInitialPreferences() {
  249. return this.initialPreferences;
  250. }
  251. public getPreferences() {
  252. return readonly(this.state);
  253. }
  254. /**
  255. * 覆盖偏好设置
  256. * overrides 要覆盖的偏好设置
  257. * namespace 命名空间
  258. */
  259. public async initPreferences({ namespace, overrides }: initialOptions) {
  260. // 是否初始化过
  261. if (this.isInitialized) {
  262. return;
  263. }
  264. // 初始化存储管理器
  265. this.cache = new StorageManager({ prefix: namespace });
  266. // 合并初始偏好设置
  267. this.initialPreferences = merge({}, overrides, defaultPreferences);
  268. // 加载并合并当前存储的偏好设置
  269. const mergedPreference = merge(
  270. {},
  271. this.loadCachedPreferences(),
  272. this.initialPreferences,
  273. );
  274. // 更新偏好设置
  275. this.updatePreferences(mergedPreference);
  276. this.setupWatcher();
  277. // 标记为已初始化
  278. this.isInitialized = true;
  279. }
  280. /**
  281. * 重置偏好设置
  282. * 偏好设置将被重置为初始值,并从 localStorage 中移除。
  283. *
  284. * @example
  285. * 假设 initialPreferences 为 { theme: 'light', language: 'en' }
  286. * 当前 state 为 { theme: 'dark', language: 'fr' }
  287. * this.resetPreferences();
  288. * 调用后,state 将被重置为 { theme: 'light', language: 'en' }
  289. * 并且 localStorage 中的对应项将被移除
  290. */
  291. resetPreferences() {
  292. // 将状态重置为初始偏好设置
  293. Object.assign(this.state, this.initialPreferences);
  294. // 保存重置后的偏好设置
  295. this.savePreferences(this.state);
  296. // 从存储中移除偏好设置项
  297. [STORAGE_KEY, STORAGE_KEY_THEME, STORAGE_KEY_LOCALE].forEach((key) => {
  298. this.cache?.removeItem(key);
  299. });
  300. }
  301. /**
  302. * 更新偏好设置
  303. * @param updates - 要更新的偏好设置
  304. */
  305. public updatePreferences(updates: DeepPartial<Preferences>) {
  306. const mergedState = merge({}, updates, markRaw(this.state));
  307. Object.assign(this.state, mergedState);
  308. // Object.assign(this.flattenedState, flattenObject(this.state));
  309. // 根据更新的键值执行相应的操作
  310. this.handleUpdates(updates);
  311. this.savePreferences(this.state);
  312. }
  313. }
  314. const preferencesManager = new PreferenceManager();
  315. export { PreferenceManager, isDarkTheme, preferencesManager };