瀏覽代碼

验证路由

ZGC4846 5 天之前
父節點
當前提交
e3b58bb01e

+ 23 - 12
qiankun_config/store.js

@@ -1,25 +1,36 @@
 import store from '../src/store';
 
-/** 同步门户下发的主题与用户信息 */
+/**
+ * 同步门户下发的主题与用户信息(对齐 EOM qiankun_config/store.js)
+ * @param {object} state 可为 vuex store.state,或 onGlobalStateChange 下发的 state
+ */
 export default function initParentStore(state) {
   if (!state) return;
 
-  if (state.theme) {
-    for (const key in state.theme) {
-      store.commit('theme/SET', { key, value: state.theme[key] });
+  // 兼容传入整个 vuex store
+  const data = state.state && (state.theme || state.user) ? state.state : state;
+
+  if (data.theme) {
+    for (const key in data.theme) {
+      store.commit('theme/SET', { key, value: data.theme[key] });
     }
-    if (state.theme.color != null) {
-      store.dispatch('theme/setColor', state.theme.color);
+    if (data.theme.color != null) {
+      store.dispatch('theme/setColor', data.theme.color);
     }
-    if (state.theme.weakMode != null) {
-      store.dispatch('theme/setWeakMode', state.theme.weakMode);
+    if (data.theme.weakMode != null) {
+      store.dispatch('theme/setWeakMode', data.theme.weakMode);
     }
-    if (state.theme.styleResponsive != null) {
-      store.dispatch('theme/setStyleResponsive', state.theme.styleResponsive);
+    if (data.theme.styleResponsive != null) {
+      store.dispatch('theme/setStyleResponsive', data.theme.styleResponsive);
     }
   }
 
-  if (state.user?.info) {
-    store.commit('user/setUserInfo', state.user.info);
+  if (data.user?.info) {
+    store.commit('user/setUserInfo', data.user.info);
+  }
+
+  // 与 EOM 一致:同步按钮权限
+  if (data.user?.authorities) {
+    store.commit('user/setAuthorities', data.user.authorities);
   }
 }

+ 20 - 8
src/config/setting.js

@@ -93,11 +93,23 @@ export const I18N_CACHE_NAME = SYSTEM_NAME + '-i18n-lang';
 /** Token 传递的 header 名称 */
 export const TOKEN_HEADER_NAME = 'Authorization';
 
-/** 独立运行隔离缓存,嵌入门户时复用门户缓存 */
-export const TOKEN_STORE_NAME = window.__POWERED_BY_QIANKUN__
-  ? 'access_token'
-  : SYSTEM_NAME + '-access_token';
-
-export const THEME_STORE_NAME = window.__POWERED_BY_QIANKUN__
-  ? 'theme'
-  : SYSTEM_NAME + '-theme';
+/**
+ * Token / 主题缓存名:运行时判断乾坤环境(对齐 EOM)。
+ * 不用模块加载瞬间的常量,避免 __POWERED_BY_QIANKUN__ 尚未注入时误用独立缓存名,
+ * 导致读不到门户 token、跳登录并把侧栏打回第一个系统。
+ */
+export function getTokenStoreName() {
+  return window.__POWERED_BY_QIANKUN__
+    ? 'access_token'
+    : `${SYSTEM_NAME}-access_token`;
+}
+
+export function getThemeStoreName() {
+  return window.__POWERED_BY_QIANKUN__ ? 'theme' : `${SYSTEM_NAME}-theme`;
+}
+
+/** @deprecated 请用 getTokenStoreName();保留导出兼容旧引用 */
+export const TOKEN_STORE_NAME = getTokenStoreName();
+
+/** @deprecated 请用 getThemeStoreName() */
+export const THEME_STORE_NAME = getThemeStoreName();

+ 2 - 2
src/layout/index.vue

@@ -27,7 +27,7 @@
     :hide-sidebars="HIDE_SIDEBARS"
     :repeatable-tabs="REPEATABLE_TABS"
     :home-title="HOME_TITLE || '首页'"
-    :home-path="HOME_PATH || '/home'"
+    :home-path="HOME_PATH || '/home/index'"
     :layout-path="LAYOUT_PATH"
     :redirect-path="REDIRECT_PATH"
     :tab-sortable="true"
@@ -115,7 +115,7 @@ export default {
       this.$store.dispatch('theme/setBodyFullscreen', value);
     },
     onLogoClick(isHome) {
-      if (!isHome) this.$router.push(HOME_PATH || '/home');
+      if (!isHome) this.$router.push(HOME_PATH || '/home/index');
     },
     onTabContextMenu(option) {
       const { key, tabKey, item, active } = option || {};

+ 24 - 3
src/main.js

@@ -3,7 +3,7 @@ import Vue from 'vue';
 import EleAdmin from 'ele-admin';
 import VueClipboard from 'vue-clipboard2';
 import App from './App.vue';
-import router from './router';
+import router, { resetRouter, getAppRouterBase } from './router';
 import store from './store';
 import permission from './utils/permission';
 import initParentStore from '../qiankun_config/store';
@@ -103,16 +103,37 @@ export async function bootstrap() {
 }
 
 export async function mount(props) {
+  console.log('[page-hr] props from main framework', props);
   Vue.prototype.$portalStore = props.store;
-  initParentStore(props.store);
 
+  // 与 EOM 一致:同步门户主题/用户;不立即 fire,避免用首个系统状态覆盖
+  initParentStore(props.store?.state || props.store);
   if (typeof props.onGlobalStateChange === 'function') {
     props.onGlobalStateChange((state) => {
       initParentStore(state);
-    }, true);
+    });
+  }
+
+  // 确保路由 base 为 /page-hr/(防止模块加载时乾坤标记未就绪)
+  if (
+    getAppRouterBase().includes('page-hr') &&
+    !String(router.options?.base || '').includes('page-hr')
+  ) {
+    resetRouter();
   }
 
   render(props);
+
+  // 接口菜单拉取成功后会 sync 到门户;这里再补几次,挡住主应用异步回写
+  const pushMenus = () => {
+    if (!String(window.location.pathname || '').includes('/page-hr')) return;
+    const menus = store.state.user.menus;
+    if (menus?.length) {
+      Vue.prototype.$portalStore?.commit('user/setMenus', menus);
+    }
+  };
+  setTimeout(pushMenus, 300);
+  setTimeout(pushMenus, 1000);
 }
 
 export async function unmount() {

+ 91 - 61
src/router/index.js

@@ -1,4 +1,7 @@
-import Vue from 'vue';
+/**
+ * 路由配置(对齐 EOM)
+ */
+import Vue from 'vue';
 import NProgress from 'nprogress';
 import VueRouter from 'vue-router';
 import {
@@ -10,78 +13,102 @@ import {
   isHomeAliasPath,
 } from '@/config/setting';
 import store from '@/store';
-import { getToken, setToken, getCurrentUser, removeToken } from '@/utils/token-util';
+import { getToken, setToken, getCurrentUser } from '@/utils/token-util';
 import { routes, getMenuRoutes } from './routes';
 import { changeRole } from '@/api/layout';
 import { rewriteLegacyHrMenus } from '@/utils/menu-rewrite';
 
 Vue.use(VueRouter);
 
-// 与 EOM 一致:
-// - WT 内浏览器路径:/page-hr/xxxx(同 page-eos/home/index)
-// - 独立运行:/hr/xxxx(同 /eos/,对应 publicPath)
-const routerOptions = {
-  base: window.__POWERED_BY_QIANKUN__
-    ? `/page-${SYSTEM_NAME}/`
-    : `/${SYSTEM_NAME}/`,
-  routes,
-  mode: 'history',
-  scrollBehavior() {
-    return { y: 0 };
-  },
-};
+/** 运行时计算 base,避免模块加载时 __POWERED_BY_QIANKUN__ 尚未注入 */
+export function getAppRouterBase() {
+  const pageBase = `/page-${SYSTEM_NAME}/`;
+  const shortBase = `/${SYSTEM_NAME}/`;
+  if (window.__POWERED_BY_QIANKUN__) {
+    return pageBase;
+  }
+  try {
+    const path = window.location?.pathname || '';
+    if (path === `/page-${SYSTEM_NAME}` || path.startsWith(pageBase)) {
+      return pageBase;
+    }
+  } catch (e) {
+    /* ignore */
+  }
+  return shortBase;
+}
 
 function createRouter() {
-  return new VueRouter(routerOptions);
+  return new VueRouter({
+    base: getAppRouterBase(),
+    routes,
+    mode: 'history',
+    scrollBehavior() {
+      return { y: 0 };
+    },
+  });
 }
 
 const router = createRouter();
 
-/** 动态路由是否已挂到当前 matcher(用标记,避免 getRoutes 长度比较死循环) */
+/** 动态路由是否已挂到当前 matcher */
 let dynamicRoutesReady = false;
 
-/** 重新登录 / 切角色前清空动态路由,避免重复注册导致白屏 */
+/** 重新登录 / 切角色前清空动态路由 */
 export function resetRouter() {
   const newRouter = createRouter();
   router.matcher = newRouter.matcher;
+  // 同步 options.base,避免后续跳转仍用旧 base
+  router.options.base = newRouter.options.base;
   dynamicRoutesReady = false;
 }
 
+/** 乾坤下把本系统菜单写回门户,防止被主应用默认/首个系统菜单覆盖 */
+function syncMenusToPortal(menus) {
+  if (!window.__POWERED_BY_QIANKUN__ || !menus?.length) return;
+  try {
+    Vue.prototype.$portalStore?.commit('user/setMenus', menus);
+  } catch (e) {
+    console.warn('[page-hr] syncMenusToPortal failed', e);
+  }
+}
+
 function registerMenuRoutes(menus, homePath, authoritiesRouter = []) {
-  resetRouter();
   const nextMenus = rewriteLegacyHrMenus(menus || []);
   const nextAuth = rewriteLegacyHrMenus(authoritiesRouter || []);
   store.commit('user/setMenus', nextMenus);
   store.commit('user/setAuthoritiesRouter', nextAuth);
-  router.addRoute(
-    getMenuRoutes(
-      [...nextMenus, ...nextAuth],
-      homePath || HOME_PATH || '/home/index',
-    ),
-  );
+  // 接口拉到的菜单注册时同步写回门户
+  syncMenusToPortal(nextMenus);
+  // 与 EOM 一致:有菜单才注册
+  if (nextMenus.length || nextAuth.length) {
+    router.addRoute(
+      getMenuRoutes(
+        [...nextMenus, ...nextAuth],
+        homePath || HOME_PATH || '/home/index',
+      ),
+    );
+  }
   dynamicRoutesReady = true;
 }
 
 /** 供登录页等外部主动注册动态路由 */
 export function setupDynamicRoutes(menus, homePath, authoritiesRouter = []) {
+  resetRouter();
   registerMenuRoutes(menus, homePath, authoritiesRouter);
 }
 
+/**
+ * 路由守卫(对齐 EOM:fetch 后 next({ ...to }),不强制改写地址栏)
+ */
 router.beforeEach((to, from, next) => {
   if (!from.path.includes(REDIRECT_PATH)) {
     NProgress.start();
   }
 
-  const homePath = HOME_PATH || '/home/index';
-
   if (getToken()) {
-    // 已登录访问登录页:直接进首页,避免停在空白态
-    if (to.path === '/login') {
-      next({ path: homePath, replace: true });
-      return;
-    }
-
-    // 旧首页别名统一跳到 /home/index,避免再开「首页」可关闭页签
+    // 旧首页别名统一到 HOME_PATH
+    const homePath = HOME_PATH || '/home/index';
     if (isHomeAliasPath(to.path) && to.path !== homePath) {
       next({
         path: homePath,
@@ -92,49 +119,51 @@ router.beforeEach((to, from, next) => {
       return;
     }
 
+    // 还未注册动态路由则先获取(与 EOM:menus == null)
     if (store.state.user.menus == null) {
       store
         .dispatch('user/fetchUserInfo')
         .then(({ menus, homePath: menuHome, authoritiesRouter }) => {
-          registerMenuRoutes(
-            menus || [],
-            menuHome || homePath,
-            authoritiesRouter || [],
-          );
-          // 根路径必须落到首页,否则只有布局、子路由空白
-          const targetPath =
-            to.path === LAYOUT_PATH || to.path === '/'
-              ? homePath
-              : to.fullPath;
-          next({ path: targetPath, replace: true });
+          // 与 EOM:有菜单才 addRoute;无菜单也 next,避免卡死
+          if (menus?.length || authoritiesRouter?.length) {
+            registerMenuRoutes(
+              menus || [],
+              menuHome || homePath,
+              authoritiesRouter || [],
+            );
+          } else {
+            // 乾坤下接口暂无 /page-hr:沿用门户侧栏做路由,但不回写,
+            // 避免把主应用已错误覆盖的「第一个系统菜单」再次固化
+            const portalMenus =
+              Vue.prototype.$portalStore?.state?.user?.menus;
+            if (window.__POWERED_BY_QIANKUN__ && portalMenus?.length) {
+              store.commit(
+                'user/setMenus',
+                rewriteLegacyHrMenus(portalMenus),
+              );
+              dynamicRoutesReady = true;
+            } else {
+              store.commit('user/setMenus', []);
+              dynamicRoutesReady = true;
+            }
+          }
+          next({ ...to, replace: true });
         })
         .catch((e) => {
           console.error(e);
-          removeToken();
-          store.commit('user/setMenus', null);
-          store.commit('user/setAuthoritiesRouter', []);
-          resetRouter();
-          next({ path: '/login', replace: true });
+          next();
         });
       return;
     }
 
-    // 有菜单但动态路由未就绪时补注册(热更新 / reset 后)
-    if (!dynamicRoutesReady) {
+    // 有菜单但动态路由未就绪时补注册
+    if (!dynamicRoutesReady && store.state.user.menus?.length) {
       registerMenuRoutes(
         store.state.user.menus,
         homePath,
         store.state.user.authoritiesRouter || [],
       );
-      const targetPath =
-        to.path === LAYOUT_PATH || to.path === '/' ? homePath : to.fullPath;
-      next({ path: targetPath, replace: true });
-      return;
-    }
-
-    // 动态路由已就绪时,访问 / 仍强制进首页
-    if (to.path === LAYOUT_PATH || to.path === '/') {
-      next({ path: homePath, replace: true });
+      next({ ...to, replace: true });
       return;
     }
 
@@ -164,6 +193,7 @@ router.afterEach((to) => {
 router.roleChange = async ({ menus, homePath, authoritiesRouter }) => {
   const currentUser = getCurrentUser();
   if (menus?.length) {
+    resetRouter();
     registerMenuRoutes(menus, homePath, authoritiesRouter);
     const target = HOME_PATH || menus[0].redirect || menus[0].path;
     if (router.currentRoute.path !== target) {

+ 11 - 3
src/store/modules/theme.js

@@ -10,11 +10,12 @@ import {
 import {
   TAB_KEEP_ALIVE,
   KEEP_ALIVE_EXCLUDES,
-  THEME_STORE_NAME,
+  getThemeStoreName,
   HOME_PATH,
   LAYOUT_PATH,
   isHomeAliasPath,
 } from '@/config/setting';
+import Vue from 'vue';
 
 const HOME_ROUTE = HOME_PATH || '/home/index';
 
@@ -69,7 +70,7 @@ const DEFAULT_STATE = Object.freeze({
 
 function getCacheSetting() {
   try {
-    const value = localStorage.getItem(THEME_STORE_NAME);
+    const value = localStorage.getItem(getThemeStoreName());
     if (value) {
       const cache = JSON.parse(value);
       if (typeof cache === 'object' && cache !== null) return cache;
@@ -84,7 +85,7 @@ function cacheSetting(key, value) {
   const cache = getCacheSetting();
   if (cache[key] !== value) {
     cache[key] = value;
-    localStorage.setItem(THEME_STORE_NAME, JSON.stringify(cache));
+    localStorage.setItem(getThemeStoreName(), JSON.stringify(cache));
   }
 }
 
@@ -167,6 +168,13 @@ export default {
       }
     },
     tabRemove({ state, commit }, { key, active }) {
+      // 与 EOM 一致:乾坤下页签由门户管理
+      if (window.__POWERED_BY_QIANKUN__) {
+        return Vue.prototype.$portalStore?.dispatch('theme/tabRemove', {
+          key,
+          active,
+        });
+      }
       if (isHomeTabKey(key)) return Promise.reject();
       const tabs = withoutHomeTabs(state.tabs || []);
       const index = tabs.findIndex((t) => t.key === key);

+ 40 - 14
src/store/modules/user.js

@@ -37,6 +37,16 @@ function buildLocalMenus() {
   return formatMenus(JSON.parse(JSON.stringify(DEFAULT_MENUS)));
 }
 
+/** 乾坤下同步侧栏菜单到门户 store(侧栏由主应用渲染) */
+function syncMenusToPortal(menus) {
+  if (!window.__POWERED_BY_QIANKUN__ || !menus?.length) return;
+  try {
+    Vue.prototype.$portalStore?.commit('user/setMenus', menus);
+  } catch (e) {
+    console.warn('[page-hr] syncMenusToPortal failed', e);
+  }
+}
+
 export default {
   namespaced: true,
   state: {
@@ -54,7 +64,10 @@ export default {
       state.menus = menus == null ? menus : rewriteLegacyHrMenus(menus);
     },
     setAuthorities(state, authorities) {
-      state.authorities = (authorities || []).map((item) => item.permissionCode);
+      // 乾坤下门户可能下发 permissionCode 字符串数组,与 EOM 本地对象数组兼容
+      state.authorities = (authorities || []).map((item) =>
+        typeof item === 'string' ? item : item.permissionCode,
+      );
     },
     setAuthoritiesRouter(state, authoritiesRouter) {
       state.authoritiesRouter =
@@ -124,6 +137,8 @@ export default {
         const nextAuth = rewriteLegacyHrMenus(authoritiesRouter || []);
         commit('setMenus', nextMenus);
         commit('setAuthoritiesRouter', nextAuth);
+        // 关键写门户侧栏,避免闪一下又变回第一个系统
+        syncMenusToPortal(nextMenus);
         return {
           menus: nextMenus,
           homePath: HOME_PATH || homePath || '/home/index',
@@ -131,21 +146,32 @@ export default {
         };
       }
 
-      // 独立运行且接口无 /page-hr 时本地兜底;乾坤下侧栏由主应用菜单树提供
-      if (!window.__POWERED_BY_QIANKUN__) {
-        const local = buildLocalMenus();
-        const nextMenus = rewriteLegacyHrMenus(local.menus || []);
-        commit('setMenus', nextMenus);
-        commit('setAuthorities', []);
-        commit('setAuthoritiesRouter', []);
-        return {
-          menus: nextMenus,
-          homePath: HOME_PATH || local.homePath || '/home/index',
-          authoritiesRouter: [],
-        };
+      // 乾坤下:接口暂无节点时沿用门户当前侧栏做本地路由,不回写门户
+      if (window.__POWERED_BY_QIANKUN__) {
+        const portalMenus = Vue.prototype.$portalStore?.state?.user?.menus;
+        if (portalMenus?.length) {
+          const nextMenus = rewriteLegacyHrMenus(portalMenus);
+          commit('setMenus', nextMenus);
+          return {
+            menus: nextMenus,
+            homePath: HOME_PATH || '/home/index',
+            authoritiesRouter: [],
+          };
+        }
+        return {};
       }
 
-      return {};
+      // 独立运行且接口无 /page-hr 时本地兜底
+      const local = buildLocalMenus();
+      const nextMenus = rewriteLegacyHrMenus(local.menus || []);
+      commit('setMenus', nextMenus);
+      commit('setAuthorities', []);
+      commit('setAuthoritiesRouter', []);
+      return {
+        menus: nextMenus,
+        homePath: HOME_PATH || local.homePath || '/home/index',
+        authoritiesRouter: [],
+      };
     },
     setInfo({ commit }, value) {
       commit('setUserInfo', value);

+ 2 - 2
src/styles/App.css

@@ -7,11 +7,11 @@ html.hr-standalone #app {
   margin: 0;
 }
 
-#app {
+html.hr-standalone #app {
   height: 100%;
   margin: 0;
 }
 
-* {
+html.hr-standalone * {
   box-sizing: border-box;
 }

+ 4 - 1
src/utils/page-tab-util.js

@@ -11,6 +11,7 @@ import {
   LAYOUT_PATH,
   REDIRECT_PATH,
   REPEATABLE_TABS,
+  SYSTEM_NAME,
   isHomeAliasPath,
 } from '@/config/setting';
 
@@ -208,11 +209,13 @@ export function setPageTabTitle(title) {
 
 /**
  * 获取当前路由对应的页签 key
+ * 乾坤下带 /page-hr 前缀,与 EOM 的 /page-eos 一致,便于门户识别当前系统
  */
 export function getRouteTabKey() {
   const { path, fullPath, meta } = router.currentRoute;
   const isUnique = meta.tabUnique === false || REPEATABLE_TABS.includes(path);
-  return isUnique ? fullPath : path;
+  const key = isUnique ? fullPath : path;
+  return `${window.__POWERED_BY_QIANKUN__ ? `/page-${SYSTEM_NAME}` : ''}${key}`;
 }
 
 /**

+ 9 - 9
src/utils/token-util.js

@@ -1,11 +1,9 @@
-import { TOKEN_STORE_NAME } from '@/config/setting';
+import { getTokenStoreName } from '@/config/setting';
 
 /** 获取本应用或门户共用的 Token */
 export function getToken() {
-  return (
-    localStorage.getItem(TOKEN_STORE_NAME) ||
-    sessionStorage.getItem(TOKEN_STORE_NAME)
-  );
+  const key = getTokenStoreName();
+  return localStorage.getItem(key) || sessionStorage.getItem(key);
 }
 
 /** 根据“记住我”设置选择持久或会话级缓存 */
@@ -13,18 +11,20 @@ export function setToken(token, remember = false) {
   removeToken();
   if (!token) return;
   const storage = remember ? localStorage : sessionStorage;
-  storage.setItem(TOKEN_STORE_NAME, token);
+  storage.setItem(getTokenStoreName(), token);
 }
 
 /** 更新服务端续期后的 Token,并保持原来的存储方式 */
 export function refreshToken(token) {
-  const remember = localStorage.getItem(TOKEN_STORE_NAME) !== null;
+  const key = getTokenStoreName();
+  const remember = localStorage.getItem(key) !== null;
   setToken(token, remember);
 }
 
 export function removeToken() {
-  localStorage.removeItem(TOKEN_STORE_NAME);
-  sessionStorage.removeItem(TOKEN_STORE_NAME);
+  const key = getTokenStoreName();
+  localStorage.removeItem(key);
+  sessionStorage.removeItem(key);
 }
 
 /** 获取当前登录组织/角色 */

+ 32 - 1
src/views/hiring/components/OfferLaunchDialog.vue

@@ -118,6 +118,8 @@
                   </el-select>
                 </el-form-item>
               </el-col>
+            </el-row>
+            <el-row :gutter="16">
               <el-col :xs="24" :sm="12">
                 <el-form-item label="接收邮箱" prop="email">
                   <el-input
@@ -139,6 +141,8 @@
                   </el-select>
                 </el-form-item>
               </el-col>
+            </el-row>
+            <el-row :gutter="16">
               <el-col :xs="24" :sm="12">
                 <el-form-item label="签署方式" prop="signingMethod">
                   <el-select
@@ -676,9 +680,36 @@ export default {
       this.form.id = offerId;
       return offerId;
     },
+    scrollToFirstError() {
+      this.$nextTick(() => {
+        const formEl = this.$refs.form && this.$refs.form.$el;
+        if (!formEl) return;
+        const errorItem = formEl.querySelector(".el-form-item.is-error");
+        if (!errorItem) return;
+        const scroller =
+          (this.$el && this.$el.querySelector(".drawer-body")) || null;
+        if (scroller) {
+          const scrollerRect = scroller.getBoundingClientRect();
+          const itemRect = errorItem.getBoundingClientRect();
+          scroller.scrollTop += itemRect.top - scrollerRect.top - 24;
+        } else {
+          errorItem.scrollIntoView({ behavior: "smooth", block: "center" });
+        }
+        const focusable = errorItem.querySelector(
+          "input, textarea, select, .el-input__inner, .el-textarea__inner",
+        );
+        if (focusable && typeof focusable.focus === "function") {
+          focusable.focus();
+        }
+      });
+    },
     async save() {
       const valid = await this.$refs.form.validate().catch(() => false);
-      if (!valid || this.saving) return;
+      if (!valid) {
+        this.scrollToFirstError();
+        return;
+      }
+      if (this.saving) return;
       if (!this.hasOfferCandidate()) {
         this.$message.warning("录用审批未带出候选人姓名,请重新选择审批单");
         return;