theme.js 17 KB

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