generator-mico-cli 0.2.32 → 0.2.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/README.md +6 -0
  2. package/generators/micro-react/README.md +1 -1
  3. package/generators/micro-react/index.js +9 -18
  4. package/generators/micro-react/templates/apps/layout/config/config.dev.ts +2 -0
  5. package/generators/micro-react/templates/apps/layout/docs/feature-/345/244/264/351/203/250/344/270/200/347/272/247/345/257/274/350/210/252/345/270/203/345/261/200.md +166 -0
  6. package/generators/micro-react/templates/apps/layout/src/app.tsx +20 -2
  7. package/generators/micro-react/templates/apps/layout/src/common/logger.ts +1 -1
  8. package/generators/micro-react/templates/apps/layout/src/common/menu/parser.ts +36 -0
  9. package/generators/micro-react/templates/apps/layout/src/common/menu/types.ts +6 -0
  10. package/generators/micro-react/templates/apps/layout/src/common/request/index.ts +8 -2
  11. package/generators/micro-react/templates/apps/layout/src/common/request/sso.ts +128 -11
  12. package/generators/micro-react/templates/apps/layout/src/components/MicroAppLoader/index.tsx +6 -2
  13. package/generators/micro-react/templates/apps/layout/src/constants/index.ts +22 -0
  14. package/generators/micro-react/templates/apps/layout/src/hooks/useFilteredMenuItems.ts +29 -0
  15. package/generators/micro-react/templates/apps/layout/src/layouts/components/header/index.less +73 -0
  16. package/generators/micro-react/templates/apps/layout/src/layouts/components/header/index.tsx +51 -4
  17. package/generators/micro-react/templates/apps/layout/src/layouts/components/menu/index.tsx +25 -18
  18. package/generators/micro-react/templates/apps/layout/src/locales/en-US.ts +4 -0
  19. package/generators/micro-react/templates/apps/layout/src/locales/zh-CN.ts +4 -0
  20. package/generators/micro-react/templates/apps/layout/src/services/config/index.ts +1 -0
  21. package/generators/micro-react/templates/packages/common-intl/src/intl.ts +20 -0
  22. package/generators/subapp-react/index.js +10 -19
  23. package/generators/subapp-react/templates/homepage/src/common/logger.ts +1 -1
  24. package/generators/subapp-umd/index.js +6 -0
  25. package/generators/subapp-umd/templates/package.json +1 -1
  26. package/lib/utils.js +222 -0
  27. package/package.json +5 -1
@@ -0,0 +1,29 @@
1
+ import type { ParsedMenuItem } from '@/common/menu';
2
+ import { filterMenuItems, parseMenuItems } from '@/common/menu';
3
+ import { getMenus } from '@/common/portal-data';
4
+ import { isAuthDisabled } from '@/constants';
5
+ import { useModel } from '@umijs/max';
6
+ import { useMemo } from 'react';
7
+
8
+ /**
9
+ * 权限过滤 + 解析后的菜单树
10
+ * 供侧边栏与头部一级导航共用,避免重复解析
11
+ */
12
+ export const useFilteredMenuItems = (): ParsedMenuItem[] => {
13
+ const { initialState } = useModel('@@initialState');
14
+ const currentUser = initialState?.currentUser;
15
+
16
+ return useMemo(() => {
17
+ const menus = getMenus();
18
+ if (isAuthDisabled()) {
19
+ return parseMenuItems(menus);
20
+ }
21
+ const filteredMenus = filterMenuItems(menus, {
22
+ isSuperuser: currentUser?.is_superuser,
23
+ menuPerms: currentUser?.menu_perms || [],
24
+ });
25
+ return parseMenuItems(filteredMenus);
26
+ }, [currentUser?.is_superuser, currentUser?.menu_perms]);
27
+ };
28
+
29
+ export default useFilteredMenuItems;
@@ -40,6 +40,7 @@
40
40
  .layout-header-logo {
41
41
  display: flex;
42
42
  align-items: center;
43
+ // side 布局保持原行为:Logo 居左,右侧内容靠右
43
44
  margin-right: auto;
44
45
 
45
46
  .logo-text {
@@ -47,12 +48,84 @@
47
48
  font-weight: @font-weight-bold;
48
49
  color: @Brand1-6;
49
50
  letter-spacing: 1px;
51
+ overflow: hidden;
52
+ white-space: nowrap;
53
+ text-overflow: ellipsis;
54
+ }
55
+ }
56
+
57
+ // mix 布局:Logo 列对齐侧边栏宽度,使一级导航起始 x 与内容区左边界对齐
58
+ // 左侧 padding(@spacing-lg) + 此宽度 = 侧边栏右边界(@sidebar-width)
59
+ .layout-header--mix .layout-header-logo {
60
+ width: calc(@sidebar-width - @spacing-lg);
61
+ margin-right: 0;
62
+ flex-shrink: 0;
63
+ }
64
+
65
+ // 头部一级目录导航(mix 布局)
66
+ // 样式对齐 Figma 中台组件「水平 Tab」:14/22 PingFang SC,
67
+ // 激活态品牌紫 + Medium + 底部下划线,未激活 text/2 + Regular
68
+ .layout-header-nav {
69
+ flex: 1;
70
+ display: flex;
71
+ align-items: center;
72
+ overflow: hidden;
73
+ margin-left: @spacing-xl;
74
+
75
+ .layout-header-tabs.arco-tabs {
76
+ width: 100%;
77
+
78
+ // 隐藏整条底部分隔线,仅保留激活态下划线,避免与头部阴影叠加
79
+ .arco-tabs-header-nav::before {
80
+ display: none;
81
+ }
82
+
83
+ .arco-tabs-header-title {
84
+ // 字号 14 / 行高 22,标题为「胶囊」形态,hover 显示浅灰圆角底
85
+ margin: 0 8px;
86
+ padding: 5px 12px;
87
+ font-size: 14px;
88
+ line-height: 22px;
89
+ font-weight: 400;
90
+ color: @color-text-2;
91
+ border-radius: @border-radius-md;
92
+ transition: background-color @animation-duration-fast,
93
+ color @animation-duration-fast;
94
+
95
+ &:first-child {
96
+ margin-left: 0;
97
+ }
98
+
99
+ // 未选中 hover:仅加浅灰底(fill/2),文字保持灰色不变紫
100
+ &:hover {
101
+ color: @color-text-2;
102
+ background-color: @color-fill-2;
103
+ }
104
+
105
+ // 选中态:品牌紫 + Medium
106
+ &-active {
107
+ color: @Brand1-6;
108
+ font-weight: 500;
109
+ }
110
+
111
+ // 选中 + hover:品牌紫文字 + 浅灰圆角底
112
+ &-active:hover {
113
+ color: @Brand1-6;
114
+ background-color: @color-fill-2;
115
+ }
116
+ }
117
+
118
+ // 激活下划线(ink bar):品牌紫
119
+ .arco-tabs-header-ink {
120
+ background-color: @Brand1-6;
121
+ }
50
122
  }
51
123
  }
52
124
 
53
125
  .layout-header-right {
54
126
  display: flex;
55
127
  align-items: center;
128
+ margin-left: auto;
56
129
  }
57
130
 
58
131
  .layout-header-action {
@@ -1,14 +1,45 @@
1
+ import { findFirstLeaf, getActiveTopKey } from '@/common/menu';
1
2
  import { AvatarDropdown, TenantDropdown } from '@/components/RightContent';
2
- import { DEFAULT_NAME } from '@/constants';
3
- import { Layout, Space } from '@mico-platform/ui';
4
- import React from 'react';
3
+ import { DEFAULT_NAME, isMixLayout } from '@/constants';
4
+ import { useFilteredMenuItems } from '@/hooks/useFilteredMenuItems';
5
+ import { Layout, Space, Tabs } from '@mico-platform/ui';
6
+ import { history, useLocation } from '@umijs/max';
7
+ import React, { useMemo } from 'react';
5
8
  import './index.less';
6
9
 
7
10
  const Header = Layout.Header;
11
+ const TabPane = Tabs.TabPane;
8
12
 
9
13
  const LayoutHeader: React.FC = () => {
14
+ const location = useLocation();
15
+ const menuItems = useFilteredMenuItems();
16
+ const mix = isMixLayout();
17
+
18
+ // 头部一级导航数据(仅 mix 布局使用顶层菜单项)
19
+ const topMenuItems = mix ? menuItems : [];
20
+
21
+ const activeTopKey = useMemo(() => {
22
+ if (!mix) return undefined;
23
+ return getActiveTopKey(topMenuItems, location.pathname);
24
+ }, [mix, topMenuItems, location.pathname]);
25
+
26
+ // 点击一级 Tab:跳转到该一级目录下第一个可跳转叶子
27
+ // - 外链(link):新窗口打开,避免外部 URL 被推进路由
28
+ // - 内部页面(page):路由跳转
29
+ const handleTopTabChange = (key: string) => {
30
+ const target = topMenuItems.find((item) => item.key === key);
31
+ if (!target) return;
32
+ const leaf = findFirstLeaf(target);
33
+ if (!leaf?.path) return;
34
+ if (leaf.type === 'link') {
35
+ window.open(leaf.path, '_blank');
36
+ return;
37
+ }
38
+ history.push(leaf.path);
39
+ };
40
+
10
41
  return (
11
- <Header className="layout-header">
42
+ <Header className={`layout-header${mix ? ' layout-header--mix' : ''}`}>
12
43
  {/* Logo */}
13
44
  <div className="layout-header-logo">
14
45
  <span className="logo-text">
@@ -16,6 +47,22 @@ const LayoutHeader: React.FC = () => {
16
47
  </span>
17
48
  </div>
18
49
 
50
+ {/* 一级目录导航(mix 布局) */}
51
+ {mix && topMenuItems.length > 0 && (
52
+ <div className="layout-header-nav">
53
+ <Tabs
54
+ className="layout-header-tabs"
55
+ type="line"
56
+ activeTab={activeTopKey}
57
+ onChange={handleTopTabChange}
58
+ >
59
+ {topMenuItems.map((item) => (
60
+ <TabPane key={item.key} title={item.label} />
61
+ ))}
62
+ </Tabs>
63
+ </div>
64
+ )}
65
+
19
66
  {/* Right section */}
20
67
  <div className="layout-header-right">
21
68
  <Space size="medium">
@@ -1,13 +1,13 @@
1
1
  import type { ParsedMenuItem } from '@/common/menu';
2
- import { filterMenuItems, parseMenuItems } from '@/common/menu';
3
- import { getMenus } from '@/common/portal-data';
2
+ import { getActiveTopKey } from '@/common/menu';
4
3
  import IconFont from '@/components/IconFont';
5
- import { isAuthDisabled } from '@/constants';
4
+ import { isMixLayout } from '@/constants';
5
+ import { useFilteredMenuItems } from '@/hooks/useFilteredMenuItems';
6
6
  import { useMenuState } from '@/hooks/useMenuState';
7
7
  import { useTheme } from '@/hooks/useTheme';
8
8
  import { Layout, Menu } from '@mico-platform/ui';
9
9
  import * as Icons from '@mico-platform/ui/icon';
10
- import { useModel } from '@umijs/max';
10
+ import { useLocation } from '@umijs/max';
11
11
  import React, { useEffect, useMemo, useRef } from 'react';
12
12
  import './index.less';
13
13
 
@@ -115,23 +115,25 @@ interface LayoutMenuProps {
115
115
  const LayoutMenu: React.FC<LayoutMenuProps> = () => {
116
116
  const siderRef = useRef<HTMLDivElement>(null);
117
117
  const { isDark } = useTheme();
118
- const { initialState } = useModel('@@initialState');
119
- const currentUser = initialState?.currentUser;
118
+ const location = useLocation();
120
119
 
121
- // Parse menu data
120
+ // 权限过滤 + 解析后的完整菜单树(与头部一级导航共用)
122
121
  // getMenuItemFilterOutcome 内部已对每个菜单项单独检查 isNoPermissionRoute,
123
122
  // 无需在此按当前页面路径全局跳过过滤,避免菜单可见性随页面变化
123
+ const allMenuItems = useFilteredMenuItems();
124
+
125
+ // mix 布局:侧边栏只渲染「当前激活一级目录」下的子树;side 布局:渲染完整树
124
126
  const menuItems = useMemo(() => {
125
- const menus = getMenus();
126
- if (isAuthDisabled()) {
127
- return parseMenuItems(menus);
127
+ if (!isMixLayout()) {
128
+ return allMenuItems;
128
129
  }
129
- const filteredMenus = filterMenuItems(menus, {
130
- isSuperuser: currentUser?.is_superuser,
131
- menuPerms: currentUser?.menu_perms || [],
132
- });
133
- return parseMenuItems(filteredMenus);
134
- }, [currentUser?.is_superuser, currentUser?.menu_perms]);
130
+ const activeTopKey = getActiveTopKey(allMenuItems, location.pathname);
131
+ const activeTop = allMenuItems.find((item) => item.key === activeTopKey);
132
+ return activeTop?.children ?? [];
133
+ }, [allMenuItems, location.pathname]);
134
+
135
+ // mix 布局下,当前一级目录没有子目录时隐藏侧边栏
136
+ const hideSider = isMixLayout() && menuItems.length === 0;
135
137
 
136
138
  // 使用菜单状态 Hook
137
139
  const {
@@ -150,10 +152,10 @@ const LayoutMenu: React.FC<LayoutMenuProps> = () => {
150
152
  if (layoutContent) {
151
153
  (layoutContent as HTMLElement).style.setProperty(
152
154
  '--sider-width',
153
- collapsed ? '48px' : '200px',
155
+ hideSider ? '0px' : collapsed ? '48px' : '200px',
154
156
  );
155
157
  }
156
- }, [collapsed]);
158
+ }, [collapsed, hideSider]);
157
159
 
158
160
  useEffect(() => {
159
161
  const sider = siderRef.current;
@@ -166,6 +168,11 @@ const LayoutMenu: React.FC<LayoutMenuProps> = () => {
166
168
  };
167
169
  }, [handleMouseEnter, handleMouseLeave]);
168
170
 
171
+ // 一级目录无子目录:不渲染侧边栏(内容区占满,--sider-width 已置 0)
172
+ if (hideSider) {
173
+ return null;
174
+ }
175
+
169
176
  // 点击触发按钮图标
170
177
  const clickTriggerBtnIcon = collapsed
171
178
  ? 'webcs-outline_fold'
@@ -48,6 +48,10 @@ export default {
48
48
  sso_auth_failure_modal_content: 'Auto login failed. Would you like to try logging in again?',
49
49
  sso_auth_failure_modal_ok: 'Re-login',
50
50
  sso_auth_failure_modal_cancel: 'Cancel',
51
+ sso_auth_failure_network_primary: 'Network error. Unable to complete auto login.',
52
+ sso_auth_failure_network_hint_title: 'Please check:',
53
+ sso_auth_failure_network_hint_ioa: '1. Whether IOA is connected',
54
+ sso_auth_failure_network_hint_network: '2. Whether your network can access the service',
51
55
 
52
56
  // AvatarDropdown
53
57
  'avatar.language': 'Language',
@@ -45,6 +45,10 @@ export default {
45
45
  sso_auth_failure_modal_content: '自动登录失败,是否重新尝试登录?',
46
46
  sso_auth_failure_modal_ok: '重新登录',
47
47
  sso_auth_failure_modal_cancel: '取消',
48
+ sso_auth_failure_network_primary: '网络异常,无法完成自动登录',
49
+ sso_auth_failure_network_hint_title: '请检查以下情况:',
50
+ sso_auth_failure_network_hint_ioa: '1. IOA 是否已连接',
51
+ sso_auth_failure_network_hint_network: '2. 当前网络是否可正常访问',
48
52
 
49
53
  // AvatarDropdown
50
54
  'avatar.language': '语言',
@@ -16,6 +16,7 @@ const STATIC_TIMEZONE_LIST: ITimezone[] = [
16
16
  { region: 'DE', utc_offset: 1 },
17
17
  { region: 'FJ', utc_offset: 15 },
18
18
  { region: 'FR', utc_offset: 1 },
19
+ { region: 'GB', utc_offset: 0 },
19
20
  { region: 'Global', utc_offset: 8 },
20
21
  { region: 'ID', utc_offset: 7 },
21
22
  { region: 'IN', utc_offset: 5 },
@@ -132,6 +132,26 @@ const intl = addIntl({
132
132
  key: 'sso_auth_failure_modal_cancel',
133
133
  defaultMessage: '取消',
134
134
  }),
135
+ sso_auth_failure_network_primary: () =>
136
+ i18n({
137
+ key: 'sso_auth_failure_network_primary',
138
+ defaultMessage: '网络异常,无法完成自动登录',
139
+ }),
140
+ sso_auth_failure_network_hint_title: () =>
141
+ i18n({
142
+ key: 'sso_auth_failure_network_hint_title',
143
+ defaultMessage: '请检查以下情况:',
144
+ }),
145
+ sso_auth_failure_network_hint_ioa: () =>
146
+ i18n({
147
+ key: 'sso_auth_failure_network_hint_ioa',
148
+ defaultMessage: '1. IOA 是否已连接',
149
+ }),
150
+ sso_auth_failure_network_hint_network: () =>
151
+ i18n({
152
+ key: 'sso_auth_failure_network_hint_network',
153
+ defaultMessage: '2. 当前网络是否可正常访问',
154
+ }),
135
155
  }, i18n);
136
156
 
137
157
  export default intl;
@@ -9,7 +9,7 @@ const {
9
9
  collectFiles,
10
10
  transformDestPath,
11
11
  isTemplateFile,
12
- getPackageVersionsParallel,
12
+ getMicoTemplateVersions,
13
13
  detectPackageScope,
14
14
  setupErrorHandlers,
15
15
  createLogger,
@@ -147,23 +147,14 @@ module.exports = class extends Generator {
147
147
 
148
148
  // 在 mico_cli 根目录执行 npm view,以使用该目录 .npmrc 中的 Nexus 认证
149
149
  const cliRoot = path.resolve(__dirname, '../..');
150
- this.logger.verbose('Fetching latest package versions (parallel)...');
151
-
152
- // 并行获取版本
153
- const versions = await getPackageVersionsParallel(
154
- [
155
- { name: '@mico-platform/ui', fallback: '1.0.0' },
156
- { name: '@mico-platform/theme', fallback: '1.0.0' },
157
- ],
158
- 8000,
159
- cliRoot,
160
- );
150
+ this.logger.verbose('Fetching mico package versions from manifest...');
161
151
 
162
- const micoUiVer = versions['@mico-platform/ui'];
163
- const themeVer = versions['@mico-platform/theme'];
152
+ const micoVersions = await getMicoTemplateVersions(cliRoot, 8000);
153
+ const ui = micoVersions['@mico-platform/ui'];
154
+ const theme = micoVersions['@mico-platform/theme'];
164
155
 
165
- this.logger.verbose('@mico-platform/ui version:', micoUiVer);
166
- this.logger.verbose('@mico-platform/theme version:', themeVer);
156
+ this.logger.verbose('@mico-platform/ui range:', ui.range, 'resolved:', ui.resolved);
157
+ this.logger.verbose('@mico-platform/theme range:', theme.range, 'resolved:', theme.resolved);
167
158
 
168
159
  const templateData = {
169
160
  appName: this.appName,
@@ -172,9 +163,9 @@ module.exports = class extends Generator {
172
163
  appNameSnake: this.appName.replace(/-/g, '_'),
173
164
  packageScope: this.packageScope,
174
165
  devPort: this.devPort,
175
- micoUiVersion: `^${micoUiVer}`,
176
- themeVersion: `^${themeVer}`,
177
- micoUiVersionExact: micoUiVer
166
+ micoUiVersion: `^${ui.resolved}`,
167
+ themeVersion: `^${theme.resolved}`,
168
+ micoUiVersionExact: ui.resolved,
178
169
  };
179
170
 
180
171
  this.logger.verbose('Template data:', JSON.stringify(templateData, null, 2));
@@ -23,7 +23,7 @@ function readVerboseDebugQueryOnce(): boolean {
23
23
  }
24
24
  try {
25
25
  const v = new URLSearchParams(window.location.search).get(DEBUG_LOGS_QUERY_KEY);
26
- if (v == null || v === '') {
26
+ if (v === null || v === '') {
27
27
  return false;
28
28
  }
29
29
  const normalized = v.toLowerCase();
@@ -14,6 +14,7 @@ const {
14
14
  setupErrorHandlers,
15
15
  createLogger,
16
16
  loadMicorc,
17
+ getMicoTemplateVersions,
17
18
  } = require('../../lib/utils');
18
19
 
19
20
  const IGNORE_LIST = require('./ignore-list.json');
@@ -145,6 +146,10 @@ module.exports = class extends Generator {
145
146
  this.logger.verbose('Template directory:', this.templateDir);
146
147
  this.logger.verbose('Destination directory:', this.destDir);
147
148
 
149
+ const cliRoot = path.resolve(__dirname, '../..');
150
+ const micoVersions = await getMicoTemplateVersions(cliRoot, 8000);
151
+ const ui = micoVersions['@mico-platform/ui'];
152
+
148
153
  const templateData = {
149
154
  appName: this.appName,
150
155
  AppName: this.appNamePascal,
@@ -152,6 +157,7 @@ module.exports = class extends Generator {
152
157
  umdGlobalName: this.umdGlobalName,
153
158
  packageScope: this.packageScope,
154
159
  devPort: this.devPort,
160
+ micoUiPeerRange: `^${ui.resolved}`,
155
161
  };
156
162
 
157
163
  this.logger.verbose('Template data:', JSON.stringify(templateData, null, 2));
@@ -15,7 +15,7 @@
15
15
  "peerDependencies": {
16
16
  "react": "^18.0.0",
17
17
  "react-dom": "^18.0.0",
18
- "@mico-platform/ui": ">=0.0.13"
18
+ "@mico-platform/ui": "<%= micoUiPeerRange %>"
19
19
  },
20
20
  "devDependencies": {
21
21
  "@types/react": "^18.3.26",