Ver código fonte

[Feat 0000] 首页选择功能开发

houzekong 2 semanas atrás
pai
commit
61187bcdee

+ 8 - 0
src/api/sys/menu.ts

@@ -3,9 +3,17 @@ import { getMenuListResultModel } from './model/menuModel';
 
 enum Api {
   GetMenuList = '/sys/permissionNew/getUserPermissionByToken',
+  List = '/sys/permissionNew/list',
   SwitchVue3Menu = '/sys/switchVue3Menu',
 }
 
+/**
+ * @description: 菜单/权限列表(首页风格选项由此接口动态获取)
+ */
+export const getPermissionList = (params?) => {
+  return defHttp.get({ url: Api.List, params });
+};
+
 /**
  * @description: Get user menu based on id
  */

+ 2 - 1
src/components/Application/src/AppLogo.vue

@@ -18,6 +18,7 @@
   import { useDesign } from '/@/hooks/web/useDesign';
   import { PageEnum } from '/@/enums/pageEnum';
   import { useUserStore } from '/@/store/modules/user';
+  import { getSavedHomeRoute } from '/@/utils/homeStyle';
 
   const props = defineProps({
     /**
@@ -51,7 +52,7 @@
 
   function goHome() {
     const glob = useGlobSetting();
-    go(glob.homePath || PageEnum.BASE_HOME);
+    go(getSavedHomeRoute() || glob.homePath || PageEnum.BASE_HOME);
   }
 </script>
 <style lang="less" scoped>

+ 3 - 0
src/enums/cacheEnum.ts

@@ -22,6 +22,9 @@ export const MULTIPLE_TABS_KEY = 'MULTIPLE_TABS__KEY__';
 
 export const APP_DARK_MODE_KEY_ = '__APP__DARK__MODE__';
 
+// 首页风格选择 key
+export const HOME_STYLE_KEY = '__HOME__STYLE__';
+
 // base global local key
 export const APP_LOCAL_CACHE_KEY = 'COMMON__LOCAL__KEY__';
 

+ 179 - 0
src/layouts/default/header/components/user-dropdown/HomeStyleSelect.vue

@@ -0,0 +1,179 @@
+<template>
+  <BasicModal
+    v-model:visible="visible"
+    title="首页风格"
+    :ok-text="'确认'"
+    :cancel-text="'取消'"
+    :mask-closable="false"
+    :can-fullscreen="false"
+    width="480px"
+    @ok="handleConfirm"
+  >
+    <div class="home-style-select">
+      <p class="home-style-tip">选择后系统将应用对应的首页风格并切换主题。</p>
+      <a-select v-model:value="selectedId" :options="selectOptions" placeholder="请选择首页风格" :loading="loading" style="width: 100%" />
+      <p v-if="currentOption" class="home-style-detail">首页路由:{{ currentOption.route }} 主题:{{ currentOption.theme }}</p>
+    </div>
+    <!-- 默认 footer 顺序:取消 → centerFooter(恢复默认) → 确认 -->
+    <template #centerFooter>
+      <a-button @click="handleRestore">恢复默认</a-button>
+    </template>
+  </BasicModal>
+</template>
+<script lang="ts" setup>
+  // 首页风格切换弹窗:下拉框选择首页风格,右下角 取消 / 恢复默认 / 确认。
+  // 选项动态获取自 /sys/permissionNew/list(getPermissionList),component 以 vent/home/ 开头视为已注册首页;
+  // 主题按标题关键词推导(绿→GREEN、蓝→DEEPBLUE、5.5→DEEPBLUE+theme5_5,其余→VENT1);本地存储见 /@/utils/homeStyle。
+  import { computed, ref } from 'vue';
+
+  import { BasicModal } from '/@/components/Modal';
+  import { ThemeEnum } from '/@/enums/appEnum';
+  import { PageEnum } from '/@/enums/pageEnum';
+  import { useGlobSetting } from '/@/hooks/setting';
+  import { useRootSetting } from '/@/hooks/setting/useRootSetting';
+  import { useMessage } from '/@/hooks/web/useMessage';
+  import { useGo } from '/@/hooks/web/usePage';
+  import { ThemeModel } from '/@/layouts/default/layout.data';
+  import { updateDarkTheme } from '/@/logics/theme/dark';
+  import { updateHeaderBgColor, updateSidebarBgColor } from '/@/logics/theme/updateBackground';
+
+  import { getPermissionList } from '/@/api/sys/menu';
+  import { getSavedHomeStyle, saveHomeStyle } from '/@/utils/homeStyle';
+  import type { HomeStyleOption } from '/@/utils/homeStyle';
+
+  const visible = ref(false);
+  const selectedId = ref<string>('');
+  const loading = ref(false);
+  const options = ref<HomeStyleOption[]>([]);
+
+  const { createMessage } = useMessage();
+  const go = useGo();
+  const glob = useGlobSetting();
+  const { setDarkMode } = useRootSetting();
+
+  /** 递归展平菜单树(兼容 children 嵌套) */
+  function flattenMenu(nodes: any[], list: any[] = []): any[] {
+    nodes?.forEach((node) => {
+      list.push(node);
+      if (Array.isArray(node.children)) flattenMenu(node.children, list);
+    });
+    return list;
+  }
+
+  /** 是否已注册首页:component 以 vent/home/ 开头且配置了路由 */
+  function isRegisteredHome(item: any): boolean {
+    return !!item && !!item.component && String(item.component).startsWith('vent/home/') && !!item.url;
+  }
+
+  /** 由菜单节点生成首页风格选项,主题按标题关键词推导 */
+  function toHomeStyleOption(item: any): HomeStyleOption {
+    const route: string = item.url;
+    const name: string = item.title || item.name || route;
+    const is55 = name.includes('5.5') || route.includes('modelchannel/model3D');
+    let theme = ThemeEnum.VENT1;
+    if (name.includes('绿')) theme = ThemeEnum.GREEN;
+    else if (name.includes('蓝') || is55) theme = ThemeEnum.DEEPBLUE;
+    return { id: String(item.id), name, route, theme, cssVars: is55 ? ThemeModel.theme5_5 : undefined };
+  }
+
+  // Promise 记忆化缓存:同一会话只请求一次,失败清空可重试
+  let homeStyleOptionsPromise: Promise<HomeStyleOption[]> | null = null;
+
+  /** 动态获取已注册首页选项 */
+  function getHomeStyleOptions(): Promise<HomeStyleOption[]> {
+    if (!homeStyleOptionsPromise) {
+      homeStyleOptionsPromise = getPermissionList()
+        .then((res: any) => {
+          const optionMap = new Map<string, HomeStyleOption>();
+          flattenMenu(Array.isArray(res) ? res : [])
+            .filter(isRegisteredHome)
+            .forEach((item) => {
+              const option = toHomeStyleOption(item);
+              // 2D/3D 去重:按归一化路由去重,同键优先 3D 项(2D 由路由守卫自动改写)
+              const key = `${item.component}|${option.route.replace('micro-vent-2dModal', 'micro-vent-3dModal')}`;
+              const existed = optionMap.get(key);
+              if (!existed || (!existed.route.includes('micro-vent-3dModal') && option.route.includes('micro-vent-3dModal'))) {
+                optionMap.set(key, option);
+              }
+            });
+          return [...optionMap.values()];
+        })
+        .catch((e) => {
+          console.error('获取首页风格列表失败:', e);
+          homeStyleOptionsPromise = null;
+          return [];
+        });
+    }
+    return homeStyleOptionsPromise;
+  }
+
+  const selectOptions = computed(() => options.value.map((o) => ({ value: o.id, label: o.name })));
+  const currentOption = computed(() => options.value.find((o) => o.id === selectedId.value) || null);
+
+  /** 打开弹窗:获取选项后按 已保存 → 系统默认首页 → 第一项 预选 */
+  async function show() {
+    visible.value = true;
+    loading.value = true;
+    options.value = await getHomeStyleOptions();
+    loading.value = false;
+    // 本地选择已失效时清理
+    const saved = getSavedHomeStyle();
+    const savedValid = !!saved && options.value.some((o) => o.id === saved.id);
+    if (saved && !savedValid) saveHomeStyle(null);
+    const savedId = saved && savedValid ? saved.id : '';
+    selectedId.value = savedId || options.value.find((o) => o.route === glob.homePath)?.id || options.value[0]?.id || '';
+  }
+
+  /** 仅应用主题(模式 + 样式变量),不处理持久化 */
+  function applyTheme({ theme, cssVars }: Pick<HomeStyleOption, 'theme' | 'cssVars'>) {
+    setDarkMode(theme);
+    updateDarkTheme(theme);
+    updateHeaderBgColor();
+    updateSidebarBgColor();
+    if (cssVars) {
+      Object.entries(cssVars).forEach(([key, value]) => document.body.style.setProperty(`--${key}`, value));
+    }
+  }
+
+  /** 应用首页风格:主题 + 持久化 + 提示 + 跳转 */
+  function applyStyle(option: HomeStyleOption) {
+    applyTheme(option);
+    saveHomeStyle(option);
+    createMessage.success(`已切换首页风格为「${option.name}」,正在应用新首页…`);
+    visible.value = false;
+    go(option.route);
+  }
+
+  function handleConfirm() {
+    if (currentOption.value) applyStyle(currentOption.value);
+  }
+
+  /** 恢复默认:本地存储首页置为 null,应用系统默认首页主题并跳转 */
+  function handleRestore() {
+    const defaultOption = options.value.find((o) => o.route === glob.homePath);
+    saveHomeStyle(null);
+    selectedId.value = defaultOption?.id || '';
+    applyTheme(defaultOption ?? { theme: ThemeEnum.VENT1 });
+    createMessage.success('已恢复默认首页风格,正在应用系统默认首页…');
+    visible.value = false;
+    go(glob.homePath || PageEnum.BASE_HOME);
+  }
+
+  defineExpose({ show });
+</script>
+<style lang="less" scoped>
+  .home-style-select {
+    .home-style-tip {
+      margin-bottom: 12px;
+      color: #999;
+      font-size: 12px;
+    }
+
+    .home-style-detail {
+      margin-top: 12px;
+      color: #666;
+      font-size: 12px;
+      line-height: 1.6;
+    }
+  }
+</style>

+ 15 - 1
src/layouts/default/header/components/user-dropdown/index.vue

@@ -25,6 +25,7 @@
             :text="t('layout.header.tooltipLock')"
             icon="ion:lock-closed-outline"
         />-->
+        <MenuItem key="homeStyle" :text="t('layout.header.dropdownItemHomeStyle')" icon="ion:home-outline" />
         <MenuItem key="about" :text="t('layout.header.dropdownItemAbout')" icon="ion:information-outline" />
         <MenuItem key="logout" :text="t('layout.header.dropdownItemLoginOut')" icon="ion:power-outline" />
       </Menu>
@@ -33,6 +34,7 @@
   <LockAction v-if="lockActionVisible" ref="lockActionRef" @register="register" />
   <DepartSelect ref="loginSelectRef" />
   <ThemeSelect ref="themeSelectRef" />
+  <HomeStyleSelect ref="homeStyleSelectRef" />
   <UpdatePassword v-if="passwordVisible" ref="updatePasswordRef" />
 </template>
 <script lang="ts">
@@ -63,7 +65,7 @@
   import { getRefPromise } from '/@/utils/index';
   import { get } from 'lodash-es';
 
-  type MenuEvent = 'logout' | 'doc' | 'lock' | 'cache' | 'depart' | 'modalCache' | 'switchTheme' | 'info';
+  type MenuEvent = 'logout' | 'doc' | 'lock' | 'cache' | 'depart' | 'modalCache' | 'switchTheme' | 'info' | 'homeStyle';
   const { createMessage } = useMessage();
   export default defineComponent({
     name: 'UserDropdown',
@@ -75,6 +77,7 @@
       LockAction: createAsyncComponent(() => import('../lock/LockModal.vue')),
       DepartSelect: createAsyncComponent(() => import('./DepartSelect.vue')),
       ThemeSelect: createAsyncComponent(() => import('./ThemeSelect.vue')),
+      HomeStyleSelect: createAsyncComponent(() => import('./HomeStyleSelect.vue')),
       UpdatePassword: createAsyncComponent(() => import('./UpdatePassword.vue')),
     },
     props: {
@@ -175,6 +178,9 @@
       // update-end--author:liaozhiyang---date:20230901---for:【QQYUN-6333】空路由问题—首次访问资源太大
       function handleMenuClick(e: { key: MenuEvent }) {
         switch (e.key) {
+          case 'homeStyle':
+            updateHomeStyle();
+            break;
           case 'about':
             message.info(`当前版本:${get(window, '__LAST_PRODUCTION_TAG__.version', '开发模式')}`);
             break;
@@ -218,6 +224,12 @@
         themeSelectRef.value.show();
       }
 
+      // 首页风格弹窗
+      const homeStyleSelectRef = ref();
+      function updateHomeStyle() {
+        homeStyleSelectRef.value.show();
+      }
+
       return {
         prefixCls,
         t,
@@ -229,6 +241,8 @@
         getUseLockPage,
         loginSelectRef,
         themeSelectRef,
+        homeStyleSelectRef,
+        updateHomeStyle,
         updatePasswordRef,
         passwordVisible,
         lockActionVisible,

+ 5 - 2
src/layouts/default/sider/bottomSideder.vue

@@ -52,7 +52,7 @@
     </div>
   </div>
   <div v-else-if="isShowMenu == 0" class="menu-show-icon" :style="iconStyle" @mousedown="onDragStart">
-  <!-- <div v-else-if="isShowMenu == 0" class="menu-show-icon"> -->
+    <!-- <div v-else-if="isShowMenu == 0" class="menu-show-icon"> -->
     <div class="icon" :class="themeIcon == 'styleTwo' ? 'icon-2' : 'icon-1'" @click="openMenu"></div>
   </div>
 </template>
@@ -72,6 +72,7 @@
   import { useUserStoreWithOut } from '/@/store/modules/user';
   import { useAppStore } from '/@/store/modules/app';
   import { router } from '/@/router';
+  import { getSavedHomeRoute } from '/@/utils/homeStyle';
 
   export default defineComponent({
     name: 'BottomSider',
@@ -208,7 +209,9 @@
       }
 
       function geHome() {
-        if (userStore.getUserInfo.homePath) {
+        if (getSavedHomeRoute()) {
+          go(getSavedHomeRoute());
+        } else if (userStore.getUserInfo.homePath) {
           go(userStore.getUserInfo.homePath);
         } else if (currentRoute.value.path.startsWith('/micro-need-air')) {
           window.history.pushState({}, '', glob.homePath || PageEnum.BASE_HOME);

+ 4 - 1
src/layouts/default/sider/bottomSideder3.vue

@@ -67,6 +67,7 @@
   import { useAppStore } from '/@/store/modules/app';
   import { router } from '/@/router';
   import SiderBorderBg from '/@/components/vent/siderBorderBg.vue';
+  import { getSavedHomeRoute } from '/@/utils/homeStyle';
 
   export default defineComponent({
     name: 'BottomSider',
@@ -185,7 +186,9 @@
       }
 
       function geHome() {
-        if (userStore.getUserInfo.homePath) {
+        if (getSavedHomeRoute()) {
+          go(getSavedHomeRoute());
+        } else if (userStore.getUserInfo.homePath) {
           go(userStore.getUserInfo.homePath);
         } else if (currentRoute.value.path.startsWith('/micro-need-air')) {
           window.history.pushState({}, '', glob.homePath || PageEnum.BASE_HOME);

+ 1 - 0
src/locales/lang/en/layout.ts

@@ -4,6 +4,7 @@ export default {
     // user dropdown
     dropdownItemDoc: 'Document',
     dropdownItemAbout: 'About',
+    dropdownItemHomeStyle: 'Home Style',
     dropdownItemLoginOut: 'Login Out',
     dropdownItemSwitchPassword: 'Password Change',
     dropdownItemSwitchDepart: 'Switch Department',

+ 1 - 0
src/locales/lang/zh-CN/layout.ts

@@ -4,6 +4,7 @@ export default {
     // user dropdown
     // dropdownItemDoc: '官网',
     dropdownItemAbout: '关于',
+    dropdownItemHomeStyle: '首页风格',
     dropdownItemLoginOut: '退出系统',
     dropdownItemSwitchPassword: '密码修改',
     // dropdownItemSwitchHome: '切换风格',

+ 2 - 1
src/router/guard/index.ts

@@ -17,6 +17,7 @@ import { createParamMenuGuard } from './paramMenuGuard';
 import { RootRoute } from '/@/router/routes';
 import { useGlobSetting } from '/@/hooks/setting';
 import { PageEnum } from '/@/enums/pageEnum';
+import { getSavedHomeRoute } from '/@/utils/homeStyle';
 // import { unmountMicroApps } from '/@/qiankun';
 
 // Don't change the order of creation
@@ -33,7 +34,7 @@ export function setupRouterGuard(router: Router) {
 }
 const glob = useGlobSetting();
 
-RootRoute.redirect = glob.homePath || PageEnum.BASE_HOME;
+RootRoute.redirect = getSavedHomeRoute() || glob.homePath || PageEnum.BASE_HOME;
 /**
  * Hooks for handling page state
  */

+ 3 - 1
src/router/guard/permissionGuard.ts

@@ -14,6 +14,8 @@ import { OAUTH2_THIRD_LOGIN_TENANT_ID } from '/@/enums/cacheEnum';
 
 import { useGlobSetting } from '/@/hooks/setting';
 
+import { getSavedHomeRoute } from '/@/utils/homeStyle';
+
 import { isEmpty, assign } from 'lodash-es';
 import { MOCK_LOGIN_URL_QUERY, SKIP_SSO_URL_QUERY } from '../constant';
 import { useSso } from '/@/hooks/web/useSso';
@@ -44,7 +46,7 @@ export function createPermissionGuard(router: Router) {
   const permissionStore = usePermissionStoreWithOut();
   const { doAutoLogin, doTokenLogin, validateRoute, tokenValidateRoute } = useAutoLogin();
   router.beforeEach(async (to, from, next) => {
-    RootRoute.redirect = glob.homePath || PageEnum.BASE_HOME;
+    RootRoute.redirect = getSavedHomeRoute() || glob.homePath || PageEnum.BASE_HOME;
     if (to.query['isNoReverse'] != '1') {
       if (VENT_PARAM['is2DModel']) {
         for (const key in to) {

+ 3 - 2
src/store/modules/user.ts

@@ -19,6 +19,7 @@ import { JDragConfigEnum } from '/@/enums/jeecgEnum';
 import { RoleEnum } from '/@/enums/roleEnum';
 import { useSso } from '/@/hooks/web/useSso';
 import { getActions } from '/@/qiankun/state';
+import { getSavedHomeRoute } from '/@/utils/homeStyle';
 import { MOCK_LOGIN_PASSWORD, MOCK_LOGIN_UESRNAME } from '../constant';
 import { AesEncryption } from '/@/utils/cipher';
 import { loginCipher } from '/@/settings/encryptionSetting';
@@ -195,7 +196,7 @@ export const useUserStore = defineStore({
         //update-begin-author:liusq date:2022-5-5 for:登录成功后缓存拖拽模块的接口前缀
         localStorage.setItem(JDragConfigEnum.DRAG_BASE_URL, useGlobSetting().domainUrl);
         //update-end-author:liusq date:2022-5-5 for: 登录成功后缓存拖拽模块的接口前缀
-        goHome && (await router.replace((userInfo && userInfo.homePath) || glob.homePath || PageEnum.BASE_HOME));
+        goHome && (await router.replace(getSavedHomeRoute() || (userInfo && userInfo.homePath) || glob.homePath || PageEnum.BASE_HOME));
         // update-begin-author:sunjianlei date:20230306 for: 修复登录成功后,没有正确重定向的问题
         const redirect = router.currentRoute.value?.query?.redirect as string;
         // 判断是否有 redirect 重定向地址
@@ -208,7 +209,7 @@ export const useUserStore = defineStore({
         }
         // update-end-author:sunjianlei date:20230306 for: 修复登录成功后,没有正确重定向的问题
 
-        goHome && (await router.replace((userInfo && userInfo.homePath) || glob.homePath || PageEnum.BASE_HOME));
+        goHome && (await router.replace(getSavedHomeRoute() || (userInfo && userInfo.homePath) || glob.homePath || PageEnum.BASE_HOME));
       }
       if (useGlobSetting().openQianKun) {
         const actions = getActions();

+ 45 - 0
src/utils/homeStyle.ts

@@ -0,0 +1,45 @@
+import { ThemeEnum } from '/@/enums/appEnum';
+import { HOME_STYLE_KEY } from '/@/enums/cacheEnum';
+
+/**
+ * 首页风格选择:localStorage 的存放与读取工具。
+ * key 定义见 /@/enums/cacheEnum 的 HOME_STYLE_KEY;
+ * 选项数据本身由 HomeStyleSelect.vue 从 /sys/permissionNew/list 动态获取(component 以 vent/home/ 开头视为已注册首页)。
+ */
+
+export interface HomeStyleOption {
+  /** 唯一标识(菜单 id 字符串化,持久化用) */
+  id: string;
+  /** 首页风格名称(下拉框显示,对应菜单 title) */
+  name: string;
+  /** 首页路由(对应菜单 url) */
+  route: string;
+  /** 主题模式:ThemeEnum.VENT1 / DEEPBLUE / GREEN / LIGHT / DARK */
+  theme: ThemeEnum;
+  /** 页面样式变量,可选(如 ThemeModel.theme5_5) */
+  cssVars?: Record<string, string>;
+}
+
+/** 读取已保存的首页风格(无则返回 null,损坏数据安全兜底) */
+export function getSavedHomeStyle(): HomeStyleOption | null {
+  try {
+    const raw = localStorage.getItem(HOME_STYLE_KEY);
+    return raw ? (JSON.parse(raw) as HomeStyleOption) : null;
+  } catch {
+    return null;
+  }
+}
+
+/** 保存首页风格选择;传入 null 表示清除(恢复系统默认) */
+export function saveHomeStyle(option: HomeStyleOption | null): void {
+  if (option) {
+    localStorage.setItem(HOME_STYLE_KEY, JSON.stringify({ id: option.id, route: option.route }));
+  } else {
+    localStorage.removeItem(HOME_STYLE_KEY);
+  }
+}
+
+/** 供路由守卫 / 登录流程读取已保存的首页路由(无则返回 '') */
+export function getSavedHomeRoute(): string {
+  return getSavedHomeStyle()?.route || '';
+}