2 Ревизии c9e28d2fba ... 85ac990a23

Автор SHA1 Съобщение Дата
  ZGC4846 85ac990a23 页面路径变了的问题 преди 5 дни
  ZGC4846 488e3f98d8 修复在wt项目中左侧空白的问题 преди 5 дни

+ 4 - 1
README.md

@@ -18,6 +18,7 @@ npm run dev
 | `publicPath`(静态资源目录 / entry) | `/eos` | `/hr` |
 | 独立运行 router `base` | `/eos/` | `/hr/` |
 | 乾坤 router `base` / activeRule | `/page-eos/` | `/page-hr/` |
+| WT 内示例地址 | `/page-eos/home/index` | `/page-hr/home/index` |
 
 ## qiankun 注册信息
 
@@ -33,4 +34,6 @@ npm run dev
 
 开发联调时 `entry` 可改为 `http://localhost:8091/hr/`。
 
-**重要**:构建产物需部署到网关的 `/hr/` 目录,不要部署到 `/page-hr/`。
+**重要**:
+- 构建产物部署到网关 **`/hr/`**(不要放到 `/page-hr/`)
+- 门户顶栏「人力资源系统」打开后,地址栏应为 **`/page-hr/xxxx`**(例如 `/page-hr/home/index`)

+ 13 - 1
src/App.vue

@@ -1,5 +1,5 @@
 <template>
-  <div id="page-hr-app">
+  <div id="app">
     <router-view />
   </div>
 </template>
@@ -9,6 +9,18 @@ export default {
   name: 'App',
   created() {
     this.$store.dispatch('theme/recoverTheme');
+    // 独立运行打开根路径时清掉历史脏页签,避免白屏时还显示旧面包屑
+    if (!window.__POWERED_BY_QIANKUN__) {
+      const path = this.$route?.path;
+      if (
+        !path ||
+        path === '/' ||
+        path === '/home' ||
+        path === '/home/index'
+      ) {
+        this.$store.dispatch('theme/tabRemoveAll');
+      }
+    }
   },
 };
 </script>

+ 1 - 1
src/config/menus.js

@@ -4,7 +4,7 @@
  */
 export const DEFAULT_MENUS = [
   {
-    path: '/home',
+    path: '/home/index',
     component: 'homeView/index',
     meta: { title: '首页', icon: 'el-icon-house' },
   },

+ 10 - 8
src/config/setting.js

@@ -1,9 +1,11 @@
-import { DEFAULT_MENUS } from './menus';
-
 /** 接口基础地址 */
 export const API_BASE_URL = process.env.VUE_APP_API_BASE_URL;
 
-/** 应用及系统标识(与门户 activeRule `/page-hr`、资源目录 `/hr` 对齐,同 EOM 的 eos) */
+/**
+ * 应用及系统标识(对照 EOM):
+ * - 静态资源 / entry:`/hr`(同 EOM 的 `/eos`)
+ * - WT 内浏览器路径:`/page-hr/xxxx`(同 EOM 的 `/page-eos/home/index`)
+ */
 export const PROJECT_NAME = process.env.VUE_APP_NAME || 'HR';
 export const SYSTEM_NAME = 'hr';
 
@@ -29,14 +31,14 @@ export const WHITE_LIST = ['/login'];
 export const KEEP_ALIVE_EXCLUDES = [];
 
 /**
- * 菜单数据:优先接口 /page-hr;接口未配置时用本地 DEFAULT_MENUS 兜底
- * 门户菜单配置好后,可将此处改回 undefined,仅走接口
+ * 菜单数据:与 EOM 一致默认 undefined(走接口 /page-hr)
+ * 独立运行且接口无菜单时,由 store 回退 DEFAULT_MENUS
  */
-export const USER_MENUS = DEFAULT_MENUS;
+export const USER_MENUS = undefined;
 
-/** 首页名称 / 路径 */
+/** 首页名称 / 路径(与 EOM 菜单一致:WT 内为 /page-hr/home/index) */
 export const HOME_TITLE = '首页';
-export const HOME_PATH = '/home';
+export const HOME_PATH = '/home/index';
 
 /** 接口/旧菜单可能用的首页 path,统一归一到 HOME_PATH */
 export const HOME_PATH_ALIASES = [

+ 5 - 0
src/main.js

@@ -23,6 +23,11 @@ import 'bpmn-js/dist/assets/bpmn-font/css/bpmn-embedded.css';
 import 'nprogress/nprogress.css';
 import './styles/index.scss';
 
+// 独立运行标记:样式仅在该 class 下接管 html/body,避免乾坤冲掉主应用侧栏
+if (!window.__POWERED_BY_QIANKUN__) {
+  document.documentElement.classList.add('hr-standalone');
+}
+
 Vue.config.productionTip = false;
 Vue.directive('click-once', clickOnce);
 Vue.component('HeaderTitle', HeaderTitle);

+ 33 - 14
src/router/index.js

@@ -17,9 +17,9 @@ import { rewriteLegacyHrMenus } from '@/utils/menu-rewrite';
 
 Vue.use(VueRouter);
 
-// 与 EOM/WT 一致:
-// - 乾坤嵌入:base = /page-{SYSTEM_NAME}/(对应主应用 activeRule
-// - 独立运行:base = /{SYSTEM_NAME}/(对应 publicPath /hr
+// 与 EOM 一致:
+// - WT 内浏览器路径:/page-hr/xxxx(同 page-eos/home/index
+// - 独立运行:/hr/xxxx(同 /eos/,对应 publicPath
 const routerOptions = {
   base: window.__POWERED_BY_QIANKUN__
     ? `/page-${SYSTEM_NAME}/`
@@ -56,7 +56,7 @@ function registerMenuRoutes(menus, homePath, authoritiesRouter = []) {
   router.addRoute(
     getMenuRoutes(
       [...nextMenus, ...nextAuth],
-      homePath || HOME_PATH || '/home',
+      homePath || HOME_PATH || '/home/index',
     ),
   );
   dynamicRoutesReady = true;
@@ -72,17 +72,19 @@ router.beforeEach((to, from, next) => {
     NProgress.start();
   }
 
+  const homePath = HOME_PATH || '/home/index';
+
   if (getToken()) {
     // 已登录访问登录页:直接进首页,避免停在空白态
     if (to.path === '/login') {
-      next({ path: HOME_PATH || '/home', replace: true });
+      next({ path: homePath, replace: true });
       return;
     }
 
-    // 旧首页别名统一跳到 /home,避免再开「首页」可关闭页签
-    if (isHomeAliasPath(to.path) && to.path !== (HOME_PATH || '/home')) {
+    // 旧首页别名统一跳到 /home/index,避免再开「首页」可关闭页签
+    if (isHomeAliasPath(to.path) && to.path !== homePath) {
       next({
-        path: HOME_PATH || '/home',
+        path: homePath,
         query: to.query,
         hash: to.hash,
         replace: true,
@@ -90,12 +92,21 @@ router.beforeEach((to, from, next) => {
       return;
     }
 
-    if (!store.state.user.menus?.length) {
+    if (store.state.user.menus == null) {
       store
         .dispatch('user/fetchUserInfo')
-        .then(({ menus, homePath, authoritiesRouter }) => {
-          registerMenuRoutes(menus, homePath, authoritiesRouter);
-          next({ ...to, replace: true });
+        .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 });
         })
         .catch((e) => {
           console.error(e);
@@ -112,10 +123,18 @@ router.beforeEach((to, from, next) => {
     if (!dynamicRoutesReady) {
       registerMenuRoutes(
         store.state.user.menus,
-        HOME_PATH || '/home',
+        homePath,
         store.state.user.authoritiesRouter || [],
       );
-      next({ ...to, replace: true });
+      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 });
       return;
     }
 

+ 13 - 4
src/store/modules/theme.js

@@ -16,7 +16,7 @@ import {
   isHomeAliasPath,
 } from '@/config/setting';
 
-const HOME_ROUTE = HOME_PATH || '/home';
+const HOME_ROUTE = HOME_PATH || '/home/index';
 
 function isHomeTabKey(key) {
   if (!key) return false;
@@ -94,9 +94,13 @@ export default {
     const state = { ...DEFAULT_STATE };
     const cache = getCacheSetting();
     Object.keys(state).forEach((key) => {
+      // 不从本地恢复页签:刷新后容易残留旧路径面包屑(如资质证照/新增)
+      if (key === 'tabs' || key === 'homeComponents' || key === 'routeReload') {
+        return;
+      }
       if (typeof cache[key] !== 'undefined') state[key] = cache[key];
     });
-    state.tabs = withoutHomeTabs(state.tabs || []);
+    state.tabs = [];
     return state;
   })(),
   getters: {
@@ -221,6 +225,7 @@ export default {
     },
     tabRemoveAll({ commit }) {
       commit('SET', { key: 'tabs', value: [] });
+      cacheSetting('tabs', []);
       return Promise.resolve({ home: true });
     },
     tabSetItem({ state, commit }, data) {
@@ -257,8 +262,12 @@ export default {
       commit('SET', { key: 'styleResponsive', value });
       cacheSetting('styleResponsive', value);
     },
-    recoverTheme() {
-      // 已在 state 初始化时从缓存恢复
+    recoverTheme({ commit, state }) {
+      // 清理与当前地址无关的脏页签,避免面包屑残留「资质证照/新增」等旧路径
+      const cache = getCacheSetting();
+      const tabs = withoutHomeTabs(cache.tabs || state.tabs || []);
+      commit('SET', { key: 'tabs', value: tabs });
+      cacheSetting('tabs', tabs);
     },
   },
 };

+ 61 - 52
src/store/modules/user.js

@@ -1,5 +1,5 @@
 /**
- * 登录用户 / 菜单 / 权限
+ * 登录用户 / 菜单 / 权限(对齐 EOM)
  */
 import { formatMenus, toTreeData, formatTreeData } from 'ele-admin';
 import { USER_MENUS, SYSTEM_NAME, HOME_PATH } from '@/config/setting';
@@ -34,8 +34,7 @@ const formatRouter = (list = []) => {
 };
 
 function buildLocalMenus() {
-  const source = USER_MENUS || DEFAULT_MENUS;
-  return formatMenus(JSON.parse(JSON.stringify(source)));
+  return formatMenus(JSON.parse(JSON.stringify(DEFAULT_MENUS)));
 }
 
 export default {
@@ -66,7 +65,6 @@ export default {
     setRoles(state, roles) {
       state.roles = roles;
     },
-    /** 清理 session 里残留的旧菜单标题/路径 */
     sanitizeMenus(state) {
       if (state.menus?.length) {
         state.menus = rewriteLegacyHrMenus(state.menus);
@@ -79,64 +77,75 @@ export default {
     },
   },
   actions: {
+    /**
+     * 请求用户菜单:与 EOM 一致,按 `/page-{SYSTEM_NAME}` 过滤资源树
+     */
     async fetchUserInfo({ commit }) {
-      const currentUser = getCurrentUser();
-      let menus;
-      let homePath;
-      let authoritiesRouter = [];
-
+      const currentUser = getCurrentUser() || {};
+      let result = [];
       try {
-        if (currentUser?.currentGroupId && currentUser?.currentRoleId) {
-          const result = await getResourcesTree({
-            groupId: currentUser.currentGroupId,
-            roleId: currentUser.currentRoleId,
-          });
-          const list = result?.filter((item) => item.path === APP_MENU_PATH);
-          if (list?.length) {
-            const { menuList, authorities } = formatRouter(list[0].children || []);
-            commit('setAuthorities', authorities);
-            const formatted = formatMenus(
-              toTreeData({
-                data: menuList,
-                idField: 'id',
-                parentIdField: 'parentId',
-              }),
-            );
-            menus = formatted.menus;
-            homePath = formatted.homePath;
-            const authFormatted = formatMenus(
-              toTreeData({
-                data: authorities.filter((item) => item.path),
-                idField: 'id',
-                parentIdField: 'parentId',
-              }),
-            );
-            authoritiesRouter = authFormatted.menus || [];
-          }
+        if (currentUser.currentGroupId && currentUser.currentRoleId) {
+          result =
+            (await getResourcesTree({
+              groupId: currentUser.currentGroupId,
+              roleId: currentUser.currentRoleId,
+            })) || [];
         }
       } catch (e) {
-        console.warn('[hr] 菜单接口不可用,使用本地菜单', e);
+        console.warn('[hr] 菜单接口不可用', e);
+        result = [];
+      }
+
+      const list = (Array.isArray(result) ? result : []).filter(
+        (item) => item.path === APP_MENU_PATH,
+      );
+
+      // 与 EOM 一致:有 /page-hr 节点时走接口菜单
+      if (list?.length) {
+        const { menuList, authorities } = formatRouter(list[0].children || []);
+        commit('setAuthorities', authorities);
+        const { menus, homePath } = formatMenus(
+          USER_MENUS ??
+            toTreeData({
+              data: menuList,
+              idField: 'id',
+              parentIdField: 'parentId',
+            }),
+        );
+        const { menus: authoritiesRouter } = formatMenus(
+          USER_MENUS ??
+            toTreeData({
+              data: authorities.filter((item) => item.path),
+              idField: 'id',
+              parentIdField: 'parentId',
+            }),
+        );
+        const nextMenus = rewriteLegacyHrMenus(menus || []);
+        const nextAuth = rewriteLegacyHrMenus(authoritiesRouter || []);
+        commit('setMenus', nextMenus);
+        commit('setAuthoritiesRouter', nextAuth);
+        return {
+          menus: nextMenus,
+          homePath: HOME_PATH || homePath || '/home/index',
+          authoritiesRouter: nextAuth,
+        };
       }
 
-      // 接口无 /page-hr 或失败时,回退本地菜单(恢复改造前侧栏)
-      if (!menus?.length) {
+      // 独立运行且接口无 /page-hr 时本地兜底;乾坤下侧栏由主应用菜单树提供
+      if (!window.__POWERED_BY_QIANKUN__) {
         const local = buildLocalMenus();
-        menus = local.menus;
-        homePath = local.homePath || '/home';
-        authoritiesRouter = [];
+        const nextMenus = rewriteLegacyHrMenus(local.menus || []);
+        commit('setMenus', nextMenus);
         commit('setAuthorities', []);
+        commit('setAuthoritiesRouter', []);
+        return {
+          menus: nextMenus,
+          homePath: HOME_PATH || local.homePath || '/home/index',
+          authoritiesRouter: [],
+        };
       }
 
-      // 首页路径固定,避免接口 homePath 落到岗位序列等业务页
-      homePath = HOME_PATH || homePath || '/home';
-
-      commit('setMenus', menus);
-      commit('setAuthoritiesRouter', authoritiesRouter);
-      return {
-        menus: rewriteLegacyHrMenus(menus),
-        homePath,
-        authoritiesRouter: rewriteLegacyHrMenus(authoritiesRouter),
-      };
+      return {};
     },
     setInfo({ commit }, value) {
       commit('setUserInfo', value);

+ 9 - 4
src/styles/App.css

@@ -1,12 +1,17 @@
-html,
-body,
-#app,
-#page-hr-app {
+/* 仅独立运行时撑满视口,避免乾坤嵌入污染主应用布局(与 EOM 一致) */
+html.hr-standalone,
+html.hr-standalone body,
+html.hr-standalone #app {
   height: 100%;
   min-height: 100vh;
   margin: 0;
 }
 
+#app {
+  height: 100%;
+  margin: 0;
+}
+
 * {
   box-sizing: border-box;
 }

+ 8 - 14
src/styles/index.scss

@@ -3,28 +3,22 @@ $--ele-font-path: '~ele-admin/es/style/fonts';
 @import '~ele-admin/es/style/themes/dynamic.scss';
 @import '~ele-admin/es/style/index.scss';
 
-html,
-body {
+/*
+ * 仅独立运行时接管 html/body。
+ * 乾坤嵌入时若改主应用滚动根节点,会把 WT 左侧菜单冲掉(与 EOM 一致:嵌入不污染全局)。
+ */
+html.hr-standalone,
+html.hr-standalone body {
   width: 100%;
   height: 100%;
 }
 
-/*
- * EleAdmin 只给 body 设置 overflow-x: hidden。按照 CSS 规范,当另一轴为
- * visible 时,这会让 body 的 overflow-y 计算为 auto,最终由 body 承担页面
- * 滚动。Element UI 的 Popper 默认挂载到 body,并按照 document 的滚动坐标
- * 定位,因此页面滚动后会把 body.scrollTop 重复计算,导致下拉层整体错位。
- *
- * 统一让 html 作为页面滚动根节点;横向裁剪也放到 html 上,body 本身不再
- * 创建滚动容器。这样 Select、Dropdown、DatePicker 等所有 Popper 弹层都能
- * 使用同一坐标系。
- */
-html {
+html.hr-standalone {
   overflow-x: hidden;
   overflow-y: auto;
 }
 
-body {
+html.hr-standalone body {
   overflow-x: visible;
   overflow-y: visible;
 }

+ 64 - 51
src/styles/views/personnelStructureDashboard/index.scss

@@ -57,7 +57,7 @@
   height: calc(100vh - 72px);
   min-height: 0;
   padding: 14px 16px 16px;
-  overflow: hidden;
+  overflow: auto;
   color: var(--text);
   background:
     radial-gradient(ellipse 70% 42% at 50% -8%, rgba(0, 150, 220, 0.26), transparent 58%),
@@ -112,7 +112,7 @@ button {
 }
 
 .dashboard-header {
-  overflow: hidden;
+  overflow: visible;
   border: 1px solid rgba(0, 210, 255, 0.28);
   background: linear-gradient(135deg, rgba(0, 50, 85, 0.45), rgba(0, 16, 34, 0.78));
   box-shadow:
@@ -159,9 +159,10 @@ button {
 .header-toolbar {
   position: relative;
   justify-content: flex-end;
-  gap: 14px;
-  min-height: 54px;
-  padding: 8px 16px;
+  flex-wrap: wrap;
+  gap: 12px 14px;
+  min-height: 58px;
+  padding: 10px 16px;
   background: linear-gradient(90deg, rgba(0, 40, 70, 0.35), rgba(0, 20, 40, 0.15));
 }
 
@@ -175,13 +176,13 @@ button {
 .header-brand .board-tag,
 .board-heading .board-tag {
   color: rgba(120, 220, 255, 0.75);
-  font-size: 10px;
+  font-size: 11px;
   letter-spacing: 0.28em;
 }
 
 .header-brand strong {
   color: #eaf9ff;
-  font-size: 16px;
+  font-size: 20px;
   font-weight: 650;
   letter-spacing: 0.12em;
   text-shadow: 0 0 12px rgba(0, 200, 255, 0.35);
@@ -203,9 +204,9 @@ button {
 }
 
 .analysis-nav button {
-  min-width: 84px;
-  padding: 8px 15px;
-  font-size: 12px;
+  min-width: 92px;
+  padding: 8px 16px;
+  font-size: 14px;
 }
 
 .analysis-nav button:hover:not(.active) {
@@ -233,7 +234,7 @@ button {
 .filter-item {
   gap: 7px;
   color: #8ebfd8;
-  font-size: 11px;
+  font-size: 13px;
   white-space: nowrap;
 }
 
@@ -243,16 +244,17 @@ button {
 }
 
 .filter-item ::v-deep .el-select {
-  width: 112px;
+  width: 128px;
 }
 
 .organization-filter ::v-deep .el-select {
-  width: 150px;
+  width: 168px;
 }
 
 .filter-item ::v-deep .el-input__inner {
   height: 34px;
   color: #d5f1ff;
+  font-size: 13px;
   border-color: rgba(0, 210, 255, 0.35);
   border-radius: 0;
   background: linear-gradient(180deg, rgba(0, 40, 70, 0.9), rgba(0, 18, 36, 0.95));
@@ -293,8 +295,8 @@ button {
   position: relative;
   display: flex;
   align-items: flex-start;
-  min-height: 100px;
-  padding: 14px 15px;
+  min-height: 108px;
+  padding: 14px 15px 28px;
   overflow: hidden;
   color: var(--text);
   text-align: left;
@@ -385,7 +387,7 @@ button {
   display: block;
   margin: 1px 0 3px;
   color: #8fc4de;
-  font-size: 11px;
+  font-size: 13px;
 }
 
 .metric-value {
@@ -395,7 +397,7 @@ button {
 
 .metric-copy strong {
   color: #f5fcff;
-  font-size: 24px;
+  font-size: 28px;
   font-weight: 700;
   line-height: 1.1;
   text-shadow: 0 0 12px rgba(0, 220, 255, 0.4);
@@ -404,7 +406,7 @@ button {
 .metric-copy em {
   margin-left: 5px;
   color: #7eb2cd;
-  font-size: 10px;
+  font-size: 13px;
   font-style: normal;
 }
 
@@ -412,10 +414,10 @@ button {
   display: block;
   margin-top: 5px;
   color: #7aa8c2;
-  font-size: 11px;
+  font-size: 12px;
   font-weight: 500;
-  line-height: 16px;
-  white-space: nowrap;
+  line-height: 18px;
+  white-space: normal;
 }
 
 .metric-link {
@@ -423,7 +425,7 @@ button {
   right: 13px;
   bottom: 10px;
   color: var(--cyan);
-  font-size: 9px;
+  font-size: 12px;
   opacity: 0;
   transform: translateX(-3px);
   transition: 0.2s;
@@ -503,8 +505,8 @@ button {
   display: flex;
   justify-content: space-between;
   flex: 0 0 auto;
-  min-height: 42px;
-  padding-bottom: 8px;
+  min-height: 48px;
+  padding-bottom: 10px;
   border-bottom: 1px solid rgba(0, 180, 255, 0.14);
 }
 
@@ -512,7 +514,7 @@ button {
   position: relative;
   padding-left: 12px;
   color: #eaf9ff;
-  font-size: 14px;
+  font-size: 16px;
   font-weight: 650;
   letter-spacing: 0.06em;
   text-shadow: 0 0 10px rgba(0, 200, 255, 0.3);
@@ -522,7 +524,7 @@ button {
   content: "";
   position: absolute;
   left: 0;
-  top: 1px;
+  top: 3px;
   width: 3px;
   height: 16px;
   background: linear-gradient(180deg, #7af6ff, rgba(0, 180, 255, 0.15));
@@ -532,7 +534,7 @@ button {
 .panel-header p {
   margin: 5px 0 0 12px;
   color: #6f97b0;
-  font-size: 10px;
+  font-size: 12px;
 }
 
 .panel-unit {
@@ -542,7 +544,7 @@ button {
   gap: 5px;
   padding: 4px 8px;
   color: #8ebfd8;
-  font-size: 10px;
+  font-size: 12px;
   border: 1px solid rgba(0, 200, 255, 0.22);
   background: rgba(0, 30, 55, 0.55);
 }
@@ -577,7 +579,7 @@ button {
 .summary-grid button {
   position: relative;
   min-height: 0;
-  padding: 10px;
+  padding: 12px 12px 14px;
   overflow: hidden;
   color: var(--text);
   text-align: left;
@@ -601,11 +603,11 @@ button {
 .summary-grid .summary-icon {
   display: grid;
   place-items: center;
-  width: 26px;
-  height: 26px;
+  width: 28px;
+  height: 28px;
   margin-bottom: 8px;
   color: var(--cyan);
-  font-size: 13px;
+  font-size: 14px;
   border: 1px solid rgba(0, 220, 255, 0.35);
   background: rgba(0, 180, 255, 0.12);
 }
@@ -620,12 +622,13 @@ button {
   display: block;
   margin-bottom: 4px;
   color: #8ebfd8;
-  font-size: 10px;
+  font-size: 12px;
+  line-height: 1.4;
 }
 
 .summary-grid strong {
   color: #f5fcff;
-  font-size: 20px;
+  font-size: 22px;
   font-weight: 700;
   text-shadow: 0 0 10px rgba(0, 220, 255, 0.35);
 }
@@ -633,7 +636,7 @@ button {
 .summary-grid em {
   margin-left: 4px;
   color: #7ea9c2;
-  font-size: 9px;
+  font-size: 12px;
   font-style: normal;
 }
 
@@ -642,7 +645,7 @@ button {
   right: 9px;
   bottom: 11px;
   color: rgba(0, 220, 255, 0.45);
-  font-size: 10px;
+  font-size: 12px;
   opacity: 0;
   transition: 0.2s;
 }
@@ -655,10 +658,13 @@ button {
 
 .inline-header {
   justify-content: space-between;
+  align-items: flex-start;
+  gap: 10px;
 }
 
 .mini-tabs {
   display: flex;
+  flex-shrink: 0;
   align-items: flex-start;
   padding: 3px;
   border: 1px solid rgba(0, 180, 255, 0.2);
@@ -666,9 +672,9 @@ button {
 }
 
 .mini-tabs button {
-  min-width: 46px;
-  padding: 6px 9px;
-  font-size: 10px;
+  min-width: 56px;
+  padding: 6px 10px;
+  font-size: 12px;
 }
 
 .mini-tabs button:hover:not(.active) {
@@ -682,15 +688,21 @@ button {
   box-shadow: 0 0 12px rgba(0, 220, 255, 0.3);
 }
 
+.quality-tabs {
+  flex-wrap: wrap;
+  justify-content: flex-end;
+  max-width: 280px;
+}
+
 .quality-tabs button {
-  min-width: 0;
-  padding-left: 7px;
-  padding-right: 7px;
+  min-width: 72px;
+  padding-left: 10px;
+  padding-right: 10px;
 }
 
 .legend-dot {
   color: #8ebfd8;
-  font-size: 10px;
+  font-size: 12px;
 }
 
 .legend-dot i {
@@ -769,17 +781,18 @@ button {
   }
   .header-toolbar {
     gap: 10px;
-    min-height: 48px;
-    padding: 7px 12px;
+    min-height: 52px;
+    padding: 8px 12px;
   }
   .analysis-nav button {
-    min-width: 72px;
-    padding-right: 10px;
-    padding-left: 10px;
+    min-width: 80px;
+    padding-right: 12px;
+    padding-left: 12px;
+    font-size: 13px;
   }
   .metric-card {
-    min-height: 90px;
-    padding: 11px 10px;
+    min-height: 98px;
+    padding: 12px 12px 26px;
   }
   .metric-icon {
     flex-basis: 34px;
@@ -788,7 +801,7 @@ button {
     font-size: 16px;
   }
   .metric-copy strong {
-    font-size: 22px;
+    font-size: 24px;
   }
   .panel {
     padding: 11px 12px 6px;

+ 1 - 1
src/utils/menu-rewrite.js

@@ -68,7 +68,7 @@ function isHomeMenu(menu) {
 
 /**
  * 递归改写菜单树中的旧模块
- * - 首页 path 统一为 /home,侧栏仍显示「首页」
+ * - 首页 path 统一为 /home/index(WT 内完整地址 /page-hr/home/index),侧栏仍显示「首页」
  * - 顶部只走小房子,不另开可关闭页签
  */
 export function rewriteLegacyHrMenus(menus = []) {

+ 2 - 1
src/utils/request.js

@@ -44,7 +44,8 @@ function markParentHandledToast(notified) {
 
 /** 跳转登录页,独立运行和 qiankun 嵌入运行分别处理。 */
 function redirectToLogin(from) {
-  const isHome = !from || from === '/' || from === '/home';
+  const isHome =
+    !from || from === '/' || from === '/home' || from === '/home/index';
   removeToken();
   try {
     const store = require('@/store').default;

+ 88 - 56
src/views/hrAnalysis/personnelStructureDashboard/index.vue

@@ -421,12 +421,15 @@ export default {
         backgroundColor: "rgba(4, 18, 36, .94)",
         borderColor: "rgba(0, 210, 255, .35)",
         borderWidth: 1,
-        padding: [10, 12],
-        textStyle: { color: "#e8f7ff", fontSize: 12, lineHeight: 20 },
+        padding: [10, 14],
+        textStyle: { color: "#e8f7ff", fontSize: 13, lineHeight: 22 },
         extraCssText:
           "box-shadow: 0 0 18px rgba(0,180,255,.18); border-radius: 4px;",
       };
     },
+    axisLabel(extra = {}) {
+      return { color: TEXT, fontSize: 12, ...extra };
+    },
     renderTrendChart() {
       const chart = this.charts.trend;
       if (!chart) return;
@@ -446,26 +449,30 @@ export default {
                 items[0].value
               }${isEfficiency ? "%" : " 人"}</b>`,
           },
-          grid: { left: 44, right: 18, top: 30, bottom: 38 },
+          grid: {
+            left: 12,
+            right: 16,
+            top: 28,
+            bottom: 8,
+            containLabel: true,
+          },
           xAxis: {
             type: "category",
             boundaryGap: false,
             data: this.dashboard.trend.months,
             axisLine: { lineStyle: { color: AXIS } },
             axisTick: { show: false },
-            axisLabel: {
-              color: TEXT,
-              fontSize: 10,
-              rotate: 25,
+            axisLabel: this.axisLabel({
               margin: 12,
-            },
+              formatter: (value) => String(value).slice(2).replace("-", "/"),
+            }),
           },
           yAxis: {
             type: "value",
             min: isEfficiency ? 70 : null,
             axisLine: { show: false },
             axisTick: { show: false },
-            axisLabel: { color: TEXT, fontSize: 10 },
+            axisLabel: this.axisLabel(),
             splitLine: { lineStyle: { color: GRID, type: "dashed" } },
           },
           series: [
@@ -518,23 +525,27 @@ export default {
           },
           legend: {
             orient: "vertical",
-            right: 4,
-            top: "center",
+            right: 8,
+            top: "middle",
             icon: "circle",
-            itemWidth: 8,
-            itemHeight: 8,
+            itemWidth: 10,
+            itemHeight: 10,
             itemGap: 14,
-            textStyle: { color: TEXT, fontSize: 11 },
+            textStyle: { color: TEXT, fontSize: 13, lineHeight: 20 },
+            formatter: (name) => {
+              const hit = data.find((item) => item.name === name);
+              return hit ? `${name}  ${hit.value}人` : name;
+            },
           },
           series: [
             {
               type: "pie",
-              radius: ["50%", "69%"],
-              center: ["39%", "54%"],
+              radius: ["42%", "66%"],
+              center: ["34%", "52%"],
               avoidLabelOverlap: true,
-              minAngle: 5,
-              label: { color: LABEL, fontSize: 10, formatter: "{b}\n{c}" },
-              labelLine: { length: 8, length2: 5, lineStyle: { color: "rgba(0,200,255,.35)" } },
+              minAngle: 8,
+              label: { show: false },
+              labelLine: { show: false },
               itemStyle: {
                 borderColor: "#041222",
                 borderWidth: 3,
@@ -572,38 +583,47 @@ export default {
             formatter: (items) =>
               `${items[0].axisValue}<br/>实际人数:<b>${items[0].value} 人</b>`,
           },
-          grid: { left: 43, right: 14, top: 18, bottom: 62 },
-          xAxis: {
-            type: "category",
-            data: this.dashboard.departments.map((item) => item.name),
-            axisTick: { show: false },
-            axisLine: { lineStyle: { color: AXIS } },
-            axisLabel: {
-              color: TEXT,
-              fontSize: 9,
-              interval: 0,
-              rotate: 30,
-            },
+          grid: {
+            left: 8,
+            right: 40,
+            top: 8,
+            bottom: 8,
+            containLabel: true,
           },
-          yAxis: {
+          xAxis: {
             type: "value",
             axisLine: { show: false },
             axisTick: { show: false },
-            axisLabel: { color: TEXT, fontSize: 10 },
+            axisLabel: this.axisLabel(),
             splitLine: { lineStyle: { color: GRID } },
           },
+          yAxis: {
+            type: "category",
+            inverse: true,
+            data: this.dashboard.departments.map((item) => item.name),
+            axisTick: { show: false },
+            axisLine: { lineStyle: { color: AXIS } },
+            axisLabel: this.axisLabel({ interval: 0, margin: 10 }),
+          },
           series: [
             {
               type: "bar",
               data,
-              barMaxWidth: 24,
+              barMaxWidth: 16,
+              label: {
+                show: true,
+                position: "right",
+                color: LABEL,
+                fontSize: 12,
+                distance: 6,
+              },
               itemStyle: {
-                color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
-                  { offset: 0, color: "#7af6ff" },
-                  { offset: 0.45, color: "#1ec8ff" },
-                  { offset: 1, color: "#0a5f9e" },
+                color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
+                  { offset: 0, color: "#0a5f9e" },
+                  { offset: 0.55, color: "#1ec8ff" },
+                  { offset: 1, color: "#7af6ff" },
                 ]),
-                borderRadius: [3, 3, 0, 0],
+                borderRadius: [0, 3, 3, 0],
                 shadowBlur: 12,
                 shadowColor: "rgba(30,200,255,.4)",
               },
@@ -649,19 +669,25 @@ export default {
             formatter: (items) =>
               `${items[0].axisValue}<br/>人数:<b>${items[0].value} 人</b>`,
           },
-          grid: { left: 42, right: 80, top: 18, bottom: 42 },
+          grid: {
+            left: 8,
+            right: 12,
+            top: 36,
+            bottom: 8,
+            containLabel: true,
+          },
           xAxis: {
             type: "category",
             data: this.dashboard.talent.map((item) => item.name),
             axisTick: { show: false },
             axisLine: { lineStyle: { color: AXIS } },
-            axisLabel: { color: TEXT, fontSize: 10 },
+            axisLabel: this.axisLabel(),
           },
           yAxis: {
             type: "value",
             axisLine: { show: false },
             axisTick: { show: false },
-            axisLabel: { color: TEXT, fontSize: 10 },
+            axisLabel: this.axisLabel(),
             splitLine: { lineStyle: { color: GRID } },
           },
           series: [
@@ -674,7 +700,8 @@ export default {
                 show: true,
                 position: "top",
                 color: LABEL,
-                fontSize: 10,
+                fontSize: 12,
+                distance: 6,
                 textShadowColor: "rgba(0,180,255,.7)",
                 textShadowBlur: 6,
               },
@@ -708,24 +735,28 @@ export default {
           },
           legend: {
             orient: "vertical",
-            right: 2,
-            top: "center",
+            right: 8,
+            top: "middle",
             icon: "circle",
-            itemWidth: 8,
-            itemHeight: 8,
-            itemGap: 10,
-            textStyle: { color: TEXT, fontSize: 10 },
+            itemWidth: 10,
+            itemHeight: 10,
+            itemGap: 12,
+            textStyle: { color: TEXT, fontSize: 13, lineHeight: 20 },
+            formatter: (name) => {
+              const hit = data.find((item) => item.name === name);
+              return hit ? `${name}  ${hit.value}人` : name;
+            },
           },
           graphic: [
             {
               type: "text",
-              left: "36%",
+              left: "34%",
               top: "48%",
               style: {
                 text: `${data.reduce((sum, item) => sum + item.value, 0)} 人`,
                 textAlign: "center",
                 fill: "#b7e9ff",
-                font: "600 15px sans-serif",
+                font: "600 16px sans-serif",
                 textShadowColor: "rgba(0,200,255,.6)",
                 textShadowBlur: 8,
               },
@@ -734,9 +765,12 @@ export default {
           series: [
             {
               type: "pie",
-              radius: ["53%", "70%"],
-              center: ["39%", "55%"],
-              minAngle: 4,
+              radius: ["42%", "66%"],
+              center: ["34%", "50%"],
+              minAngle: 8,
+              avoidLabelOverlap: true,
+              label: { show: false },
+              labelLine: { show: false },
               itemStyle: {
                 borderColor: "#041222",
                 borderWidth: 3,
@@ -750,8 +784,6 @@ export default {
                   shadowColor: "rgba(0,220,255,.45)",
                 },
               },
-              label: { color: LABEL, fontSize: 10, formatter: "{b}\n{c}" },
-              labelLine: { length: 8, length2: 5, lineStyle: { color: "rgba(0,200,255,.35)" } },
               data,
             },
           ],

+ 5 - 2
src/views/login/index.vue

@@ -114,10 +114,13 @@
         ) {
           return from;
         }
-        return HOME_PATH || '/home';
+        return HOME_PATH || '/home/index';
       },
       goAfterLogin(path) {
-        const base = String(this.$router.options.base || '/hr/').replace(
+        const base = String(
+          this.$router.options.base ||
+            (window.__POWERED_BY_QIANKUN__ ? '/page-hr/' : '/hr/'),
+        ).replace(
           /\/$/,
           '',
         );