theme.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629
  1. /**
  2. * 主题状态管理
  3. */
  4. import {
  5. changeColor,
  6. screenWidth,
  7. screenHeight,
  8. contentWidth,
  9. contentHeight,
  10. WEAK_CLASS,
  11. BODY_LIMIT_CLASS,
  12. DISABLES_CLASS
  13. } from 'ele-admin';
  14. import {
  15. TAB_KEEP_ALIVE,
  16. KEEP_ALIVE_EXCLUDES,
  17. THEME_STORE_NAME
  18. } from '@/config/setting';
  19. /**
  20. * state 默认值
  21. */
  22. const DEFAULT_STATE = Object.freeze({
  23. // 页签数据
  24. tabs: [],
  25. // 是否折叠侧栏
  26. collapse: false,
  27. // 是否折叠一级侧栏
  28. sideNavCollapse: false,
  29. // 内容区域是否全屏
  30. bodyFullscreen: false,
  31. // 是否开启页签栏
  32. showTabs: true,
  33. // 是否开启页脚
  34. showFooter: true,
  35. // 顶栏风格: light(亮色), dark(暗色), primary(主色)
  36. headStyle: 'light',
  37. // 侧栏风格: light(亮色), dark(暗色)
  38. sideStyle: 'dark',
  39. // 布局风格: side(默认), top(顶栏导航), mix(混合导航)
  40. layoutStyle: 'side',
  41. // 侧栏菜单风格: default(默认), mix(双排侧栏)
  42. sideMenuStyle: 'default',
  43. // 页签风格: default(默认), dot(圆点), card(卡片)
  44. tabStyle: 'default',
  45. // 路由切换动画
  46. transitionName: 'slide-right',
  47. // 是否固定顶栏
  48. fixedHeader: false,
  49. // 是否固定侧栏
  50. fixedSidebar: true,
  51. // 是否固定主体
  52. fixedBody: true,
  53. // 内容区域宽度铺满
  54. bodyFull: true,
  55. // logo 是否自适应宽度
  56. logoAutoSize: false,
  57. // 侧栏是否彩色图标
  58. colorfulIcon: false,
  59. // 侧栏是否只保持一个子菜单展开
  60. sideUniqueOpen: true,
  61. // 是否是色弱模式
  62. weakMode: false,
  63. // 是否是暗黑模式
  64. darkMode: false,
  65. // 主题色
  66. color: null,
  67. // 主页的组件名称
  68. homeComponents: [],
  69. // 刷新路由时的参数
  70. routeReload: null,
  71. // 屏幕宽度
  72. screenWidth: screenWidth(),
  73. // 屏幕高度
  74. screenHeight: screenHeight(),
  75. // 内容区域宽度
  76. contentWidth: contentWidth(),
  77. // 内容区域高度
  78. contentHeight: contentHeight(),
  79. // 是否开启响应式
  80. styleResponsive: true
  81. });
  82. // 延时操作定时器
  83. let disableTransitionTimer, updateContentSizeTimer;
  84. /**
  85. * 读取缓存配置
  86. */
  87. function getCacheSetting () {
  88. try {
  89. const value = localStorage.getItem(THEME_STORE_NAME);
  90. if (value) {
  91. const cache = JSON.parse(value);
  92. if (typeof cache === 'object' && cache !== null) {
  93. return cache;
  94. }
  95. }
  96. } catch (e) {
  97. console.error(e);
  98. }
  99. return {};
  100. }
  101. /**
  102. * 缓存配置
  103. */
  104. function cacheSetting (key, value) {
  105. const cache = getCacheSetting();
  106. if (cache[key] !== value) {
  107. cache[key] = value;
  108. localStorage.setItem(THEME_STORE_NAME, JSON.stringify(cache));
  109. }
  110. }
  111. /**
  112. * 开关响应式布局
  113. */
  114. function changeStyleResponsive (styleResponsive) {
  115. if (styleResponsive) {
  116. document.body.classList.remove(BODY_LIMIT_CLASS);
  117. } else {
  118. document.body.classList.add(BODY_LIMIT_CLASS);
  119. }
  120. }
  121. /**
  122. * 切换色弱模式
  123. */
  124. function changeWeakMode (weakMode) {
  125. if (weakMode) {
  126. document.body.classList.add(WEAK_CLASS);
  127. } else {
  128. document.body.classList.remove(WEAK_CLASS);
  129. }
  130. }
  131. /**
  132. * 切换主题
  133. */
  134. function changeTheme (value, dark) {
  135. return new Promise((resolve, reject) => {
  136. try {
  137. changeColor(value, dark);
  138. resolve();
  139. } catch (e) {
  140. reject(e);
  141. }
  142. });
  143. }
  144. /**
  145. * 切换布局时禁用过渡动画
  146. */
  147. function disableTransition () {
  148. disableTransitionTimer && clearTimeout(disableTransitionTimer);
  149. document.body.classList.add(DISABLES_CLASS);
  150. disableTransitionTimer = setTimeout(() => {
  151. document.body.classList.remove(DISABLES_CLASS);
  152. }, 100);
  153. }
  154. export default {
  155. namespaced: true,
  156. state: (() => {
  157. const state = { ...DEFAULT_STATE };
  158. const cache = getCacheSetting();
  159. Object.keys(state).forEach((key) => {
  160. if (typeof cache[key] !== 'undefined') {
  161. state[key] = cache[key];
  162. }
  163. });
  164. return state;
  165. })(),
  166. getters: {
  167. // 需要 keep-alive 的组件
  168. keepAliveInclude (state) {
  169. if (!TAB_KEEP_ALIVE || !state.showTabs) {
  170. return [];
  171. }
  172. const components = new Set();
  173. const { reloadPath, reloadHome } = state.routeReload || {};
  174. state.tabs?.forEach((t) => {
  175. const isAlive = t.meta?.keepAlive !== false;
  176. const isExclude = KEEP_ALIVE_EXCLUDES.includes(t.path);
  177. const isReload = reloadPath && reloadPath === t.fullPath;
  178. if (isAlive && !isExclude && !isReload && t.components) {
  179. t.components.forEach((c) => {
  180. if (typeof c === 'string' && c) {
  181. components.add(c);
  182. }
  183. });
  184. }
  185. });
  186. if (!reloadHome) {
  187. state.homeComponents?.forEach((c) => {
  188. if (typeof c === 'string' && c) {
  189. components.add(c);
  190. }
  191. });
  192. }
  193. return Array.from(components);
  194. }
  195. },
  196. mutations: {
  197. SET (state, { key, value }) {
  198. state[key] = value;
  199. }
  200. },
  201. actions: {
  202. setTabs ({ commit }, value) {
  203. commit('SET', { key: 'tabs', value });
  204. //cacheSetting('tabs', value);
  205. },
  206. setCollapse ({ commit, dispatch }, value) {
  207. commit('SET', { key: 'collapse', value });
  208. dispatch('delayUpdateContentSize', 800);
  209. },
  210. setSideNavCollapse ({ commit, dispatch }, value) {
  211. commit('SET', { key: 'sideNavCollapse', value });
  212. dispatch('delayUpdateContentSize', 800);
  213. },
  214. setBodyFullscreen ({ commit, dispatch }, value) {
  215. disableTransition();
  216. commit('SET', { key: 'bodyFullscreen', value });
  217. dispatch('delayUpdateContentSize', 800);
  218. },
  219. setShowTabs ({ commit, dispatch }, value) {
  220. commit('SET', { key: 'showTabs', value });
  221. cacheSetting('showTabs', value);
  222. dispatch('delayUpdateContentSize');
  223. },
  224. setShowFooter ({ commit, dispatch }, value) {
  225. commit('SET', { key: 'showFooter', value });
  226. cacheSetting('showFooter', value);
  227. dispatch('delayUpdateContentSize');
  228. },
  229. setHeadStyle ({ commit }, value) {
  230. commit('SET', { key: 'headStyle', value });
  231. cacheSetting('headStyle', value);
  232. },
  233. setSideStyle ({ commit }, value) {
  234. commit('SET', { key: 'sideStyle', value });
  235. cacheSetting('sideStyle', value);
  236. },
  237. setLayoutStyle ({ commit, dispatch }, value) {
  238. disableTransition();
  239. commit('SET', { key: 'layoutStyle', value });
  240. cacheSetting('layoutStyle', value);
  241. dispatch('delayUpdateContentSize');
  242. },
  243. setSideMenuStyle ({ commit, dispatch }, value) {
  244. disableTransition();
  245. commit('SET', { key: 'sideMenuStyle', value });
  246. cacheSetting('sideMenuStyle', value);
  247. dispatch('delayUpdateContentSize');
  248. },
  249. setTabStyle ({ commit }, value) {
  250. commit('SET', { key: 'tabStyle', value });
  251. cacheSetting('tabStyle', value);
  252. },
  253. setTransitionName ({ commit }, value) {
  254. commit('SET', { key: 'transitionName', value });
  255. cacheSetting('transitionName', value);
  256. },
  257. setFixedHeader ({ commit }, value) {
  258. disableTransition();
  259. commit('SET', { key: 'fixedHeader', value });
  260. cacheSetting('fixedHeader', value);
  261. },
  262. setFixedSidebar ({ commit }, value) {
  263. disableTransition();
  264. commit('SET', { key: 'fixedSidebar', value });
  265. cacheSetting('fixedSidebar', value);
  266. },
  267. setFixedBody ({ commit }, value) {
  268. disableTransition();
  269. commit('SET', { key: 'fixedBody', value });
  270. cacheSetting('fixedBody', value);
  271. },
  272. setBodyFull ({ commit, dispatch }, value) {
  273. commit('SET', { key: 'bodyFull', value });
  274. cacheSetting('bodyFull', value);
  275. dispatch('delayUpdateContentSize');
  276. },
  277. setLogoAutoSize ({ commit }, value) {
  278. disableTransition();
  279. commit('SET', { key: 'logoAutoSize', value });
  280. cacheSetting('logoAutoSize', value);
  281. },
  282. setColorfulIcon ({ commit }, value) {
  283. commit('SET', { key: 'colorfulIcon', value });
  284. cacheSetting('colorfulIcon', value);
  285. },
  286. setSideUniqueOpen ({ commit }, value) {
  287. commit('SET', { key: 'sideUniqueOpen', value });
  288. cacheSetting('sideUniqueOpen', value);
  289. },
  290. setStyleResponsive ({ commit }, value) {
  291. changeStyleResponsive(value);
  292. commit('SET', { key: 'styleResponsive', value });
  293. cacheSetting('styleResponsive', value);
  294. },
  295. /**
  296. * 切换色弱模式
  297. * @param value 是否是色弱模式
  298. */
  299. setWeakMode ({ commit }, value) {
  300. return new Promise((resolve) => {
  301. changeWeakMode(value);
  302. commit('SET', { key: 'weakMode', value });
  303. cacheSetting('weakMode', value);
  304. resolve();
  305. });
  306. },
  307. /**
  308. * 切换暗黑模式
  309. * @param value 是否是暗黑模式
  310. */
  311. setDarkMode ({ commit, state }, value) {
  312. return new Promise((resolve, reject) => {
  313. changeTheme(state.color, value)
  314. .then(() => {
  315. commit('SET', { key: 'darkMode', value });
  316. cacheSetting('darkMode', value);
  317. resolve();
  318. })
  319. .catch((e) => {
  320. reject(e);
  321. });
  322. });
  323. },
  324. /**
  325. * 切换主题色
  326. * @param value 主题色
  327. */
  328. setColor ({ commit, state }, value) {
  329. return new Promise((resolve, reject) => {
  330. changeTheme(value, state.darkMode)
  331. .then(() => {
  332. commit('SET', { key: 'color', value });
  333. cacheSetting('color', value);
  334. resolve();
  335. })
  336. .catch((e) => {
  337. reject(e);
  338. });
  339. });
  340. },
  341. /**
  342. * 设置主页路由对应的组件名称
  343. * @param components 组件名称
  344. */
  345. setHomeComponents ({ commit }, value) {
  346. commit('SET', { key: 'homeComponents', value });
  347. },
  348. /**
  349. * 设置刷新路由信息
  350. * @param option 路由刷新参数
  351. */
  352. setRouteReload ({ commit }, value) {
  353. commit('SET', { key: 'routeReload', value });
  354. },
  355. /**
  356. * 更新屏幕尺寸
  357. */
  358. updateScreenSize ({ commit, dispatch }) {
  359. commit('SET', { key: 'screenWidth', value: screenWidth() });
  360. commit('SET', { key: 'screenHeight', value: screenHeight() });
  361. dispatch('updateContentSize');
  362. },
  363. /**
  364. * 更新内容区域尺寸
  365. */
  366. updateContentSize ({ commit }) {
  367. commit('SET', { key: 'contentWidth', value: contentWidth() });
  368. commit('SET', { key: 'contentHeight', value: contentHeight() });
  369. },
  370. /**
  371. * 延时更新内容区域尺寸
  372. * @param delay 延迟时间
  373. */
  374. delayUpdateContentSize ({ dispatch }, delay) {
  375. updateContentSizeTimer && clearTimeout(updateContentSizeTimer);
  376. updateContentSizeTimer = setTimeout(() => {
  377. dispatch('updateContentSize');
  378. }, delay ?? 100);
  379. },
  380. /**
  381. * 重置设置
  382. */
  383. resetSetting ({ commit, state }) {
  384. return new Promise((resolve, reject) => {
  385. disableTransition();
  386. [
  387. 'showTabs',
  388. 'showFooter',
  389. 'headStyle',
  390. 'sideStyle',
  391. 'layoutStyle',
  392. 'sideMenuStyle',
  393. 'tabStyle',
  394. 'transitionName',
  395. 'fixedHeader',
  396. 'fixedSidebar',
  397. 'fixedBody',
  398. 'bodyFull',
  399. 'logoAutoSize',
  400. 'colorfulIcon',
  401. 'sideUniqueOpen',
  402. 'styleResponsive',
  403. 'weakMode',
  404. 'darkMode',
  405. 'color'
  406. ].forEach((key) => {
  407. commit('SET', { key, value: DEFAULT_STATE[key] });
  408. });
  409. localStorage.removeItem(THEME_STORE_NAME);
  410. Promise.all([
  411. changeStyleResponsive(state.styleResponsive),
  412. changeWeakMode(state.weakMode),
  413. changeTheme(state.color, state.darkMode)
  414. ])
  415. .then(() => {
  416. resolve();
  417. })
  418. .catch((e) => {
  419. reject(e);
  420. });
  421. });
  422. },
  423. /**
  424. * 恢复主题
  425. */
  426. recoverTheme ({ state }) {
  427. // 关闭响应式布局
  428. if (!state.styleResponsive) {
  429. changeStyleResponsive(false);
  430. }
  431. // 恢复色弱模式
  432. if (state.weakMode) {
  433. changeWeakMode(true);
  434. }
  435. // 恢复主题色
  436. if (state.color || state.darkMode) {
  437. changeTheme(state.color, state.darkMode).catch((e) => {
  438. console.error(e);
  439. });
  440. }
  441. },
  442. /**
  443. * 添加页签或更新相同 key 的页签数据
  444. * @param data 页签数据
  445. */
  446. tabAdd ({ dispatch, state }, data) {
  447. if (Array.isArray(data)) {
  448. data.forEach((d) => {
  449. dispatch('tabAdd', d);
  450. });
  451. return;
  452. }
  453. const i = state.tabs.findIndex((d) => d.key === data.key);
  454. if (i === -1) {
  455. dispatch('setTabs', state.tabs.concat([data]));
  456. } else if (data.fullPath !== state.tabs[i].fullPath) {
  457. dispatch(
  458. 'setTabs',
  459. state.tabs
  460. .slice(0, i)
  461. .concat([data])
  462. .concat(state.tabs.slice(i + 1))
  463. );
  464. }
  465. },
  466. /**
  467. * 关闭页签
  468. * @param key 页签 key
  469. */
  470. async tabRemove ({ dispatch, state }, { key, active }) {
  471. const i = state.tabs.findIndex(
  472. (t) => t.key === key || t.fullPath === key
  473. );
  474. if (i === -1) {
  475. return {};
  476. }
  477. const t = state.tabs[i];
  478. if (!t.closable) {
  479. return Promise.reject();
  480. }
  481. const path = state.tabs[i - 1]?.fullPath;
  482. dispatch(
  483. 'setTabs',
  484. state.tabs.filter((_d, j) => j !== i)
  485. );
  486. return t.key === active ? { path, home: !path } : {};
  487. },
  488. /**
  489. * 关闭左侧页签
  490. */
  491. async tabRemoveLeft ({ dispatch, state }, { key, active }) {
  492. let index = -1; // 选中页签的 index
  493. for (let i = 0; i < state.tabs.length; i++) {
  494. if (state.tabs[i].key === active) {
  495. index = i;
  496. }
  497. if (state.tabs[i].key === key) {
  498. if (i === 0) {
  499. break;
  500. }
  501. const temp = state.tabs.filter((d, j) => !d.closable && j < i);
  502. if (temp.length === i + 1) {
  503. break;
  504. }
  505. const path = index === -1 ? void 0 : state.tabs[i].fullPath;
  506. dispatch('setTabs', temp.concat(state.tabs.slice(i)));
  507. return { path };
  508. }
  509. }
  510. return Promise.reject();
  511. },
  512. /**
  513. * 关闭右侧页签
  514. */
  515. async tabRemoveRight ({ dispatch, state }, { key, active }) {
  516. if (state.tabs.length) {
  517. let index = -1; // 选中页签的 index
  518. for (let i = 0; i < state.tabs.length; i++) {
  519. if (state.tabs[i].key === active) {
  520. index = i;
  521. }
  522. if (state.tabs[i].key === key) {
  523. if (i === state.tabs.length - 1) {
  524. return Promise.reject();
  525. }
  526. const temp = state.tabs.filter((d, j) => !d.closable && j > i);
  527. if (temp.length === state.tabs.length - i - 1) {
  528. return Promise.reject();
  529. }
  530. const path = index === -1 ? state.tabs[i].fullPath : void 0;
  531. dispatch(
  532. 'setTabs',
  533. state.tabs
  534. .slice(0, i + 1)
  535. .concat(state.tabs.filter((d, j) => !d.closable && j > i))
  536. );
  537. return { path };
  538. }
  539. }
  540. // 主页时关闭全部
  541. const temp = state.tabs.filter((d) => !d.closable);
  542. if (temp.length !== state.tabs.length) {
  543. dispatch('setTabs', temp);
  544. return { home: index !== -1 };
  545. }
  546. }
  547. return Promise.reject();
  548. },
  549. /**
  550. * 关闭其它页签
  551. */
  552. async tabRemoveOther ({ dispatch, state }, { key, active }) {
  553. let index = -1; // 选中页签的 index
  554. let path; // 关闭后跳转的 path
  555. const temp = state.tabs.filter((d, i) => {
  556. if (d.key === active) {
  557. index = i;
  558. }
  559. if (d.key === key) {
  560. path = d.fullPath;
  561. }
  562. return !d.closable || d.key === key;
  563. });
  564. if (temp.length === state.tabs.length) {
  565. return Promise.reject();
  566. }
  567. dispatch('setTabs', temp);
  568. if (index === -1) {
  569. return {};
  570. }
  571. return key === active ? {} : { path, home: !path };
  572. },
  573. /**
  574. * 关闭全部页签
  575. * @param active 选中页签的 key
  576. */
  577. async tabRemoveAll ({ dispatch, state }, active) {
  578. const t = state.tabs.find((d) => d.key === active);
  579. const home = typeof t !== 'undefined' && t.closable === true; // 是否跳转主页
  580. const temp = state.tabs.filter((d) => !d.closable);
  581. if (temp.length === state.tabs.length) {
  582. return Promise.reject();
  583. }
  584. dispatch('setTabs', temp);
  585. return { home };
  586. },
  587. /**
  588. * 修改页签
  589. * @param data 页签数据
  590. */
  591. tabSetItem ({ dispatch, state }, data) {
  592. let i = -1;
  593. if (data.key) {
  594. i = state.tabs.findIndex((d) => d.key === data.key);
  595. } else if (data.fullPath) {
  596. i = state.tabs.findIndex((d) => d.fullPath === data.fullPath);
  597. } else if (data.path) {
  598. i = state.tabs.findIndex((d) => d.path === data.path);
  599. }
  600. if (i !== -1) {
  601. const item = { ...state.tabs[i] };
  602. if (data.title) {
  603. item.title = data.title;
  604. }
  605. if (typeof data.closable === 'boolean') {
  606. item.closable = data.closable;
  607. }
  608. if (data.components) {
  609. item.components = data.components;
  610. }
  611. dispatch(
  612. 'setTabs',
  613. state.tabs
  614. .slice(0, i)
  615. .concat([item])
  616. .concat(state.tabs.slice(i + 1))
  617. );
  618. }
  619. }
  620. }
  621. };