generator-mico-cli 0.2.28 → 0.2.30

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 (112) hide show
  1. package/README.md +7 -20
  2. package/bin/mico.js +27 -62
  3. package/generators/micro-react/index.js +25 -1
  4. package/generators/micro-react/templates/.cursor/rules/always-read-docs.mdc +3 -0
  5. package/generators/micro-react/templates/.cursor/rules/project-overview.mdc +1 -0
  6. package/generators/micro-react/templates/CICD/start_dev.sh +11 -0
  7. package/generators/micro-react/templates/CICD/start_local.sh +9 -0
  8. package/generators/micro-react/templates/CICD/start_prod.sh +13 -0
  9. package/generators/micro-react/templates/CICD/start_test.sh +11 -0
  10. package/generators/micro-react/templates/CLAUDE.md +1 -0
  11. package/generators/micro-react/templates/README.md +1 -1
  12. package/generators/micro-react/templates/apps/layout/config/config.dev.ts +13 -5
  13. package/generators/micro-react/templates/apps/layout/config/config.prod.development.ts +12 -0
  14. package/generators/micro-react/templates/apps/layout/config/config.prod.testing.ts +12 -0
  15. package/generators/micro-react/templates/apps/layout/config/config.prod.ts +14 -0
  16. package/generators/micro-react/templates/apps/layout/docs/feature-PermissionFilter/346/214/211/351/222/256/346/235/203/351/231/220.md +116 -0
  17. package/generators/micro-react/templates/apps/layout/docs/feature-/345/233/275/351/231/205/345/214/226.md +121 -0
  18. package/generators/micro-react/templates/apps/layout/docs/feature-/345/276/256/345/211/215/347/253/257/346/250/241/345/274/217.md +8 -0
  19. package/generators/micro-react/templates/apps/layout/docs/feature-/350/217/234/345/215/225/346/235/203/351/231/220/346/216/247/345/210/266.md +83 -77
  20. package/generators/micro-react/templates/apps/layout/docs/feature-/350/267/257/347/224/261/344/270/216/350/217/234/345/215/225/350/247/243/350/200/246.md +50 -35
  21. package/generators/micro-react/templates/apps/layout/docs/feature-/350/267/257/347/224/261/346/235/203/351/231/220/346/227/245/345/277/227.md +162 -0
  22. package/generators/micro-react/templates/apps/layout/mock/api.mock.ts +23 -31
  23. package/generators/micro-react/templates/apps/layout/mock/menus.ts +14 -0
  24. package/generators/micro-react/templates/apps/layout/mock/pages.ts +27 -8
  25. package/generators/micro-react/templates/apps/layout/package.json +2 -0
  26. package/generators/micro-react/templates/apps/layout/src/app.tsx +85 -4
  27. package/generators/micro-react/templates/apps/layout/src/common/auth/index.ts +3 -0
  28. package/generators/micro-react/templates/apps/layout/src/common/auth/tenant.ts +25 -0
  29. package/generators/micro-react/templates/apps/layout/src/common/auth/type.ts +41 -27
  30. package/generators/micro-react/templates/apps/layout/src/common/intl/formatLayoutMessage.ts +30 -0
  31. package/generators/micro-react/templates/apps/layout/src/common/intl/index.ts +6 -0
  32. package/generators/micro-react/templates/apps/layout/src/common/intl/intlRuntime.ts +14 -0
  33. package/generators/micro-react/templates/apps/layout/src/common/intl/localeMapping.ts +30 -0
  34. package/generators/micro-react/templates/apps/layout/src/common/intl/types.ts +14 -0
  35. package/generators/micro-react/templates/apps/layout/src/common/intl/useLayoutIntl.ts +40 -0
  36. package/generators/micro-react/templates/apps/layout/src/common/logger.ts +3 -4
  37. package/generators/micro-react/templates/apps/layout/src/common/menu/parser.ts +148 -85
  38. package/generators/micro-react/templates/apps/layout/src/common/menu/types.ts +29 -6
  39. package/generators/micro-react/templates/apps/layout/src/common/micro/types.ts +23 -0
  40. package/generators/micro-react/templates/apps/layout/src/common/portal-data.ts +46 -2
  41. package/generators/micro-react/templates/apps/layout/src/common/request/sso.ts +74 -15
  42. package/generators/micro-react/templates/apps/layout/src/common/request/token-refresh.ts +2 -0
  43. package/generators/micro-react/templates/apps/layout/src/components/MicroAppLoader/index.tsx +32 -6
  44. package/generators/micro-react/templates/apps/layout/src/components/PermissionFilter/index.tsx +51 -0
  45. package/generators/micro-react/templates/apps/layout/src/components/RightContent/AvatarDropdown.tsx +10 -1
  46. package/generators/micro-react/templates/apps/layout/src/components/RightContent/TenantDropdown.tsx +76 -0
  47. package/generators/micro-react/templates/apps/layout/src/components/RightContent/index.ts +1 -0
  48. package/generators/micro-react/templates/apps/layout/src/components/RightContent/tenant-dropdown.less +48 -0
  49. package/generators/micro-react/templates/apps/layout/src/constants/index.ts +1 -0
  50. package/generators/micro-react/templates/apps/layout/src/hooks/index.ts +1 -0
  51. package/generators/micro-react/templates/apps/layout/src/hooks/useMenuState.ts +18 -0
  52. package/generators/micro-react/templates/apps/layout/src/hooks/useTenant.ts +41 -0
  53. package/generators/micro-react/templates/apps/layout/src/layouts/components/header/index.tsx +4 -1
  54. package/generators/micro-react/templates/apps/layout/src/layouts/components/menu/index.tsx +21 -9
  55. package/generators/micro-react/templates/apps/layout/src/layouts/index.tsx +105 -60
  56. package/generators/micro-react/templates/apps/layout/src/locales/en-US.ts +28 -0
  57. package/generators/micro-react/templates/apps/layout/src/locales/zh-CN.ts +26 -0
  58. package/generators/micro-react/templates/apps/layout/src/pages/404/index.tsx +7 -3
  59. package/generators/micro-react/templates/apps/layout/src/pages/Home/index.less +32 -0
  60. package/generators/micro-react/templates/apps/layout/src/pages/Home/index.tsx +148 -4
  61. package/generators/micro-react/templates/apps/layout/src/requestErrorConfig.ts +2 -1
  62. package/generators/micro-react/templates/apps/layout/src/services/user.ts +79 -21
  63. package/generators/micro-react/templates/apps/layout/typings.d.ts +16 -0
  64. package/generators/micro-react/templates/docs/package-shared.md +189 -0
  65. package/generators/micro-react/templates/package.json +1 -1
  66. package/generators/micro-react/templates/packages/common-intl/README.md +78 -368
  67. package/generators/micro-react/templates/packages/common-intl/package.json +3 -13
  68. package/generators/micro-react/templates/packages/common-intl/src/index.ts +5 -6
  69. package/generators/micro-react/templates/packages/common-intl/src/intl.ts +115 -28
  70. package/generators/micro-react/templates/packages/common-intl/src/umiLocaleBridge.ts +101 -0
  71. package/generators/micro-react/templates/packages/common-intl/tsconfig.json +2 -4
  72. package/generators/micro-react/templates/packages/shared/README.md +120 -0
  73. package/generators/micro-react/templates/packages/shared/package.json +26 -0
  74. package/generators/micro-react/templates/packages/shared/services/common/index.ts +43 -0
  75. package/generators/micro-react/templates/packages/shared/services/index.ts +21 -0
  76. package/generators/micro-react/templates/packages/shared/services/request.ts +43 -0
  77. package/generators/micro-react/templates/packages/shared/timezone/index.ts +228 -0
  78. package/generators/micro-react/templates/packages/shared/tsconfig.json +20 -0
  79. package/generators/micro-react/templates/scripts/apply-sentry-plugin.ts +6 -1
  80. package/generators/micro-react/templates/turbo.json +9 -1
  81. package/generators/subapp-react/index.js +28 -22
  82. package/generators/subapp-react/templates/homepage/README.md +1 -0
  83. package/generators/subapp-react/templates/homepage/config/config.prod.development.ts +1 -0
  84. package/generators/subapp-react/templates/homepage/config/config.prod.testing.ts +1 -0
  85. package/generators/subapp-react/templates/homepage/config/config.prod.ts +1 -0
  86. package/generators/subapp-react/templates/homepage/config/config.ts +10 -0
  87. package/generators/subapp-react/templates/homepage/docs/feature-PermissionFilter/346/214/211/351/222/256/346/235/203/351/231/220.md +35 -0
  88. package/generators/subapp-react/templates/homepage/docs/feature-/345/233/275/351/231/205/345/214/226.md +124 -0
  89. package/generators/subapp-react/templates/homepage/package.json +3 -1
  90. package/generators/subapp-react/templates/homepage/src/app.tsx +104 -2
  91. package/generators/subapp-react/templates/homepage/src/common/intl/index.ts +15 -0
  92. package/generators/subapp-react/templates/homepage/src/common/intl/intlRuntime.ts +14 -0
  93. package/generators/subapp-react/templates/homepage/src/common/intl/localeMapping.ts +24 -0
  94. package/generators/subapp-react/templates/homepage/src/common/intl/subappIntlConfig.ts +28 -0
  95. package/generators/subapp-react/templates/homepage/src/common/intl/subappLocale.ts +18 -0
  96. package/generators/subapp-react/templates/homepage/src/common/intl/subappOwnIntl.ts +63 -0
  97. package/generators/subapp-react/templates/homepage/src/common/intl/types.ts +14 -0
  98. package/generators/subapp-react/templates/homepage/src/common/intl/useSubappIntl.ts +61 -0
  99. package/generators/subapp-react/templates/homepage/src/common/locale.ts +80 -0
  100. package/generators/subapp-react/templates/homepage/src/common/mainApp.ts +41 -2
  101. package/generators/subapp-react/templates/homepage/src/components/PermissionFilter/index.tsx +48 -0
  102. package/generators/subapp-react/templates/homepage/src/locales/en-US.ts +6 -0
  103. package/generators/subapp-react/templates/homepage/src/locales/zh-CN.ts +6 -0
  104. package/generators/subapp-react/templates/homepage/src/pages/index.less +10 -0
  105. package/generators/subapp-react/templates/homepage/src/pages/index.tsx +86 -1
  106. package/generators/subapp-react/templates/homepage/typings.d.ts +12 -0
  107. package/lib/utils.js +0 -1
  108. package/package.json +2 -2
  109. package/generators/micro-react/templates/apps/layout/docs/common-intl.md +0 -372
  110. package/generators/micro-react/templates/packages/common-intl/src/indexedDBUtils.ts +0 -51
  111. package/generators/micro-react/templates/packages/common-intl/src/utils.ts +0 -482
  112. package/generators/micro-react/templates/packages/common-intl/vite.config.ts +0 -25
@@ -0,0 +1,116 @@
1
+ # PermissionFilter 按钮权限
2
+
3
+ > 创建时间:2025-03-27
4
+
5
+ ## 功能概述
6
+
7
+ `PermissionFilter` 用于按**按钮级权限**控制 UI 是否渲染:传入权限标识 `permissionKey`,仅当当前用户具备该权限(或为超级用户)时渲染 `children`,否则渲染可选的 `fallback`。
8
+
9
+ - **主应用(layout)**:权限数据来自 `fetchUserInfo` 返回的用户信息字段 `button_perms`、`is_superuser`(与 `initialState.currentUser` 一致)。
10
+ - **子应用**:不直接请求用户接口,权限列表由主应用通过 qiankun 注入到子应用 props,与主应用同一套 `button_perms` 语义。
11
+
12
+ 与侧栏菜单使用的 `menu_perms` / `routeKey` 不同,`button_perms` 面向页面内按钮、操作入口等细粒度控制。详见 [菜单权限控制](./feature-菜单权限控制.md) 中对 `button_perms` 的说明。
13
+
14
+ ## 技术方案
15
+
16
+ ### 技术栈
17
+
18
+ - React 18 + TypeScript
19
+ - 主应用:`useModel('@@initialState')` 读取 `currentUser`
20
+ - 子应用:`@/common/mainApp` 的 `useMainAppProps()`(内部 `useSyncExternalStore`,随 qiankun `update` 刷新)
21
+
22
+ ### 主应用数据流
23
+
24
+ 1. `GET /user/info/`(`services/user.ts` → `fetchUserInfo`)返回 `button_perms: string[]`、`is_superuser`。
25
+ 2. `getInitialState`(`app.tsx`)在有 **token** 时拉取用户信息;免认证页同样需要 `currentUser` 时,逻辑为「有 token 即请求」,避免免认证页面无 `currentUser` 导致按钮权限恒为无权限。
26
+ 3. `MicroAppLoader` 将 `button_perms`、`is_superuser` 放入传给子应用的 qiankun props,子应用 `setMainAppProps` 后可供 `PermissionFilter` 使用。
27
+
28
+ ### 子应用数据流
29
+
30
+ 1. 主应用 `MicroAppLoader` 构建 props 时写入 `button_perms`、`is_superuser`。
31
+ 2. 子应用 `app.tsx` 中 qiankun `mount` / `update` 调用 `setMainAppProps(props)`。
32
+ 3. `PermissionFilter` 通过 `useMainAppProps()` 订阅 props 变化;独立运行子应用时无主应用 props,默认视为无对应按钮权限(走 `fallback`)。
33
+
34
+ ### 权限判定规则
35
+
36
+ 1. `permissionKey` 为空或仅空白:视为**无权限**。
37
+ 2. `is_superuser` 为真(含与现有 parser 一致的 `1`):视为**有全部按钮权限**(与 `isSuperuserUser` 行为一致)。
38
+ 3. 否则:`button_perms.includes(permissionKey)` 为真则有权限。
39
+
40
+ ## 文件清单
41
+
42
+ | 文件路径 | 说明 |
43
+ | --- | --- |
44
+ | `src/components/PermissionFilter/index.tsx` | 主应用组件 |
45
+ | `src/components/MicroAppLoader/index.tsx` | 向子应用注入 `button_perms`、`is_superuser` |
46
+ | `src/services/user.ts` | `fetchUserInfo` 规范化 `button_perms` |
47
+ | `src/app.tsx` | `getInitialState` 拉取 `currentUser` |
48
+
49
+ 子应用:
50
+
51
+ | 文件路径 | 说明 |
52
+ | --- | --- |
53
+ | `src/components/PermissionFilter/index.tsx` | 子应用组件(基于 `useMainAppProps`) |
54
+ | `src/common/mainApp.ts` | qiankun props、`useMainAppProps`、`subscribeMainAppProps` |
55
+
56
+ ## API / 组件接口
57
+
58
+ ### Props(主应用与子应用一致)
59
+
60
+ ```typescript
61
+ interface IPermissionFilterProps {
62
+ /** 按钮/操作权限标识,需在用户 button_perms 中命中 */
63
+ permissionKey: string;
64
+ children?: React.ReactNode;
65
+ /** 无权限时渲染;默认 null */
66
+ fallback?: React.ReactNode;
67
+ }
68
+ ```
69
+
70
+ ## 使用示例
71
+
72
+ ### 主应用
73
+
74
+ ```tsx
75
+ import PermissionFilter from '@/components/PermissionFilter';
76
+
77
+ <PermissionFilter
78
+ permissionKey="cs_web_btn_export"
79
+ fallback={<span>无权限</span>}
80
+ >
81
+ <Button type="primary">导出</Button>
82
+ </PermissionFilter>
83
+ ```
84
+
85
+ 首页示例见 `src/pages/Home/index.tsx`;本地 Mock 可在 `mock/api.mock.ts` 的 `GET /user/info/` 响应 `data.button_perms` 中加入对应字符串。
86
+
87
+ ### 子应用
88
+
89
+ ```tsx
90
+ import PermissionFilter from '@/components/PermissionFilter';
91
+
92
+ <PermissionFilter
93
+ permissionKey="cs_web_btn_subapp_demo"
94
+ fallback={<Alert type="warning" content="无权限" />}
95
+ >
96
+ <Button type="primary">操作</Button>
97
+ </PermissionFilter>
98
+ ```
99
+
100
+ 示例见子应用 `src/pages/index.tsx`。嵌入主应用时,主应用用户信息中的 `button_perms` 需包含该 key;**独立运行**子应用时通常始终为无权限。
101
+
102
+ ## Mock / 联调说明
103
+
104
+ - 主应用 Mock:`mock/api.mock.ts` 中 `button_perms` 数组需包含你在页面中使用的 `permissionKey`。
105
+ - 若页面在配置中为**免认证**(`accessControlEnabled: false`),仍依赖「有 token 时拉取用户信息」才能在主应用侧得到 `button_perms`;请确认 `app.tsx` 中 `getInitialState` 与当前模板一致。
106
+
107
+ ## 注意事项
108
+
109
+ - 权限标识由后端/配置约定,需与 `button_perms` 中字符串完全一致(区分大小写)。
110
+ - 子应用请勿自行缓存过期的 `button_perms`;以 qiankun 传入的最新 props 为准(已用 `useMainAppProps` 订阅 `update`)。
111
+
112
+ ## 相关文档
113
+
114
+ - [微前端模式](./feature-微前端模式.md)(MicroAppLoader、子应用加载)
115
+ - [菜单权限控制](./feature-菜单权限控制.md)(`menu_perms` 与 `button_perms` 分工)
116
+ - [路由与菜单解耦](./feature-路由与菜单解耦.md)
@@ -0,0 +1,121 @@
1
+ # 国际化(common-intl 与 Umi 兜底)
2
+
3
+ > 创建时间:2026-04-07
4
+
5
+ ## 功能概述
6
+
7
+ layout 主应用通过 **`<%= packageScope %>/common-intl`**(`packages/common-intl` 中转包)绑定中台 **`tag`**,在 **`app.tsx`** 中 **`configureLocale`** 与 Umi **`umi_locale`** 对齐;当 **`window.__MICO_CONFIG__.intl` 或 `commonIntl`** 含有效 **`tag`、`app_name`** 时视为启用中台,首屏在 **`render`** 中 **`fetchMultilingualData`**。业务侧统一使用 **`useLayoutIntl()`**:**`i18n` 优先**,与 `defaultMessage` 比较后未命中则 **Umi `formatMessage`**(`src/locales/*`)。未启用中台时 **仅 Umi**。
8
+
9
+ ## 技术方案
10
+
11
+ ### 技术栈
12
+
13
+ - Umi 4 `@umijs/max`:`locale` 运行时、`export const locale`、`useIntl`
14
+ - `<%= packageScope %>/common-intl`:`configureLocale`、`fetchMultilingualData`、`i18n`、`intl`(包内 `initIntl`)
15
+ - 门控与类型:`src/common/intl/intlRuntime.ts`、`types.ts`
16
+
17
+ ### 核心实现
18
+
19
+ 1. **`app.tsx`**:最早 **`configureLocale({ getLocale, setLocale })`**,与 **`getCurrentLocale`** / **`setLocaleToStorage`**(`src/common/locale.ts`)及 **`localeMapping`** 一致;**`export const locale`** 供 Umi 插件;**`render`** 仅在 **`isCommonIntlEnabled()`** 为真时调用 **`fetchMultilingualData`**(`request`、`Message`、`lang`、`LOCALE_REQUEST_URL`)。
20
+ 2. **`useLayoutIntl.ts`**:读取 **`isCommonIntlEnabled()`**;启用时 **`i18n({ key, defaultMessage })`**,否则直接 **`useIntl().formatMessage`**;兜底规则与 **`defaultMessage`**、Umi 结果比较(同子应用 **`useSubappIntl`** 策略)。
21
+ 3. **全局暴露**:**`window.CommonIntl`** 指向包模块,供子应用 **externals** 复用。
22
+ 4. **包内实现**:**`packages/common-intl/src/intl.ts`** 中 **`initIntl`** 合并 **`__MICO_CONFIG__`** 与默认 tag,详见包 README。
23
+
24
+ ## 文件清单(相对 `apps/layout`)
25
+
26
+ | 文件路径 | 说明 |
27
+ | --- | --- |
28
+ | `src/common/intl/useLayoutIntl.ts` | 页面统一 Hook |
29
+ | `src/common/intl/intlRuntime.ts` | `isCommonIntlEnabled`、`getCommonIntlConfig` |
30
+ | `src/common/intl/localeMapping.ts` | Umi locale ↔ `ILang` |
31
+ | `src/common/intl/types.ts` | `IMicoConfigCommonIntl` |
32
+ | `src/common/intl/index.ts` | 导出 `useLayoutIntl`、`intl`、`isCommonIntlEnabled` 等 |
33
+ | `src/common/locale.ts` | URL / storage / 浏览器语言(与 Umi 一致) |
34
+ | `src/app.tsx` | `configureLocale`、`locale`、`render`、**`window.CommonIntl`** |
35
+ | `config/config.ts` | `locale` 插件(`default`、`antd: false`、`baseNavigator: false`) |
36
+ | `src/locales/zh-CN.ts`、`en-US.ts` | Umi 兜底文案 |
37
+ | `src/pages/Home/index.tsx` | 国际化用法示例(可选参考) |
38
+
39
+ ## API / 组件接口
40
+
41
+ ### `useLayoutIntl`
42
+
43
+ ```typescript
44
+ const {
45
+ formatMessage, // 与 useIntl 相同 descriptor / values 签名
46
+ commonIntlEnabled, // 是否启用中台(有效 tag + app_name)
47
+ locale, // 当前 Umi 风格 locale(getCurrentLocale)
48
+ intl, // 包内便捷对象,仅 commonIntlEnabled 为 true 时非 undefined
49
+ } = useLayoutIntl();
50
+ ```
51
+
52
+ ### `window.__MICO_CONFIG__.intl` / `commonIntl`
53
+
54
+ 二者取其一(见 `intlRuntime`),字段含义一致:
55
+
56
+ | 字段 | 说明 |
57
+ | --- | --- |
58
+ | `tag` | 多语言中台 tag(必填) |
59
+ | `app_name` | 中台应用名(必填) |
60
+ | `indexedDBParams` | 可选本地缓存参数 |
61
+
62
+ ## 使用示例
63
+
64
+ **运行时注入(示例):**
65
+
66
+ ```javascript
67
+ window.__MICO_CONFIG__ = {
68
+ commonIntl: {
69
+ tag: 'cs_fe_pc',
70
+ app_name: 'middle',
71
+ indexedDBParams: { dbName: 'mico_cs_web_i18n_db' },
72
+ },
73
+ };
74
+ ```
75
+
76
+ **页面:**
77
+
78
+ ```tsx
79
+ import { useLayoutIntl } from '@/common/intl';
80
+
81
+ const { formatMessage, commonIntlEnabled, locale } = useLayoutIntl();
82
+
83
+ return (
84
+ <span>
85
+ {formatMessage({ id: 'page.home.title', defaultMessage: '首页' })}
86
+ </span>
87
+ );
88
+ ```
89
+
90
+ **包内预置文案(中台 key 亦可走 `i18n`):**
91
+
92
+ ```tsx
93
+ import { intl } from '@/common/intl';
94
+
95
+ intl.common_hello();
96
+ ```
97
+
98
+ ## 设计决策
99
+
100
+ | 决策点 | 选择 | 理由 |
101
+ | --- | --- | --- |
102
+ | 门控 | `intl` 与 `commonIntl` 字段兼容 | 与注入脚本、历史配置一致 |
103
+ | 首屏拉取 | 仅 `isCommonIntlEnabled()` 时 `render` 内 fetch | 未配置中台时不阻塞首屏 |
104
+ | 合并策略 | `i18n` 与 Umi 二选一兜底 | 与微前端子应用 `useSubappIntl` 对齐,便于维护 |
105
+
106
+ ## 已知限制与待改进
107
+
108
+ - **`LOCALE_REQUEST_URL`** 未配置时,行为依赖上游 **`fetchMultilingualData`** 实现。
109
+ - 中台文案与 **`src/locales`** 的 key 需自行约定,避免重复 id 语义冲突。
110
+
111
+ ## 注意事项
112
+
113
+ - 修改语言时请走与 **`getCurrentLocale`** / **`setLocaleToStorage`** 一致的入口,避免与 **`configureLocale`** 脱节。
114
+ - 子应用不复制上述 **`render`** 逻辑时,嵌入主应用后通常仍依赖主应用已加载的 **`CommonIntl`**;独立运行子应用需自行 **`render` + fetch**(见子应用模板文档)。
115
+
116
+ ## 相关文档
117
+
118
+ - [packages/common-intl README](../../../../packages/common-intl/README.md)
119
+ - [Layout 国际化(CLI 仓库总览)](../../../../../../docs/feat-layout-国际化翻译.md)
120
+ - [子应用国际化(模板内说明)](../../../../../subapp-react/templates/homepage/docs/feature-国际化.md)
121
+ - [微前端模式](./feature-微前端模式.md)(主应用暴露 `CommonIntl`)
@@ -136,6 +136,14 @@ interface MicroAppProps {
136
136
 
137
137
  **说明**:子应用在 `mount` 生命周期中通过 `props` 接收这些数据。
138
138
 
139
+ ### 国际化(common-intl)
140
+
141
+ - **共享文案(默认)**:主应用在 `render` 中拉取中台后,子应用 `import { intl } from '<%= packageScope %>/common-intl'` 即可(与主应用同一默认 tag),详见 `packages/common-intl/README.md`。
142
+ - **不同 tag**:上游支持多次 `initIntl`,子应用可对同一依赖再绑定一套 tag 并在子应用 `render` 中拉取;或直连 `@common-web/common-intl`,见 README「不同 tag」。
143
+ - **可选模板层**:若子应用需 `formatMessage` 式 API 与 Umi 兜底,见仓库 `docs/feat-subapp-国际化翻译.md`(生成项目根目录文档以实际为准)。
144
+
145
+ 主应用 **不向** 子应用 props 传递 `commonIntl`;子应用 **locale** 以 props 与主应用对齐(见上表 `locale`)。
146
+
139
147
  ### 子应用 Props 更新机制
140
148
 
141
149
  主应用通过 qiankun 的 `update()` 方法实时通知子应用状态变化,无需重新加载:
@@ -1,10 +1,10 @@
1
1
  # 菜单权限控制
2
2
 
3
- > 创建时间:2026-01-24 更新时间:2026-02-08
3
+ > 创建时间:2026-01-24 更新时间:2026-03-27(同步 `getMenuPage`:pageId 优先,path 兜底)
4
4
 
5
5
  ## 功能概述
6
6
 
7
- 基于用户信息中的 `side_menus` 字段实现菜单和路由的白名单权限控制。非超级用户只能看到和访问 `side_menus` 中配置的菜单项,访问无权限路由时显示 403 页面。
7
+ 基于用户信息中的 **`menu_perms`**(菜单权限 code 数组,与 OpenAPI 8 `GET /user/info/` 一致)实现菜单和路由权限。非超级用户仅当 **`menu_perms` 包含对应 code** 时可见菜单项、可访问受控动态路由(`accessControlEnabled && routeKey` 时要求 `routeKey menu_perms`)。访问无权限路由时显示 403 页面。
8
8
 
9
9
  **v2 更新**:支持认证(Authentication)与授权(Authorization)分离配置,可独立控制"跳过 SSO 登录"和"跳过菜单权限校验"。
10
10
 
@@ -54,22 +54,21 @@
54
54
  │ 2. 权限校验 (layouts/index.tsx) │
55
55
  │ isNoPermissionRoute(pathname)? │
56
56
  │ ├── 是 → 跳过权限校验 │
57
- │ └── 否 → 双层权限判断
58
- Tier 1: 页面在菜单中?
59
- ├── 沿用 sideMenus
60
- │ │ 白名单逻辑 │
61
- │ └── 否 → Tier 2: 页面级权限 │
62
- │ adminOnly / routeKey│
57
+ │ └── 否 → PAGES 页面:adminOnly /
58
+ accessControlEnabled+routeKey
59
+ menu_perms;无 page
60
+ 按菜单过滤结果兜底
63
61
  │ 详见:路由与菜单解耦文档 │
64
62
  └──────────────────────────────────────────┘
65
63
 
66
64
 
67
65
  ┌──────────────────────────────────────────┐
68
66
  │ 3. 菜单渲染 (menu/index.tsx) │
69
- │ 按 side_menus 过滤
67
+ │ 按 menu_perms 过滤(菜单项 code
68
+ │ 与关联页 routeKey / nameKey 一致) │
70
69
  │ 每个菜单项独立检查 isNoPermissionRoute│
71
70
  │ ├── 匹配 → 该项始终显示 │
72
- │ └── 不匹配 → sideMenus 白名单过滤
71
+ │ └── 不匹配 → code menu_perms
73
72
  └──────────────────────────────────────────┘
74
73
 
75
74
 
@@ -111,23 +110,20 @@ page.accessControlEnabled === false?
111
110
  ├── 是 → 允许访问所有页面
112
111
 
113
112
 
114
- Tier 1: 页面在菜单中?
115
- ├── 是 → 沿用菜单权限逻辑:
116
- 菜单项 adminOnly === true?
117
- ├── → 禁止访问
118
- 检查 side_menus 白名单
119
- │ ├── 匹配允许访问
120
- │ └── 不匹配显示 403
113
+ 能关联到 PAGES 页面配置?
114
+ ├── 是 → adminOnly === true?
115
+ ├── 403
116
+ accessControlEnabled === true?
117
+ ├── → routeKey ∈ menu_perms?
118
+ ├── 允许
119
+ └── → 403
120
+ │ └── 否 → 允许
121
121
 
122
122
 
123
- Tier 2: 隐藏页面(不在菜单中)
124
- adminOnly === true?
125
- ├── 是 → 显示 403
126
- accessControlEnabled === true?
127
- ├── 是 → 检查 routeKey ∈ side_menus
128
- │ ├── 匹配 → 允许访问
129
- │ └── 不匹配 → 显示 403
130
- └── 否 → 允许访问
123
+ 路径仅在菜单路由中、无 page 元数据
124
+ └── filterMenuItems 后的路由集中?
125
+ ├── 是 → 允许
126
+ └── → 403
131
127
  ```
132
128
 
133
129
  ## 文件清单
@@ -144,7 +140,8 @@ Tier 2: 隐藏页面(不在菜单中)
144
140
  | --- | --- |
145
141
  | `src/common/menu/types.ts` | 新增 `noPermissionRouteList` 类型定义 |
146
142
  | `src/constants/index.ts` | 新增 `isNoPermissionRoute`、`getNoPermissionRouteList` 函数 |
147
- | `src/common/menu/parser.ts` | 新增 `filterMenuItems`、`isMenuAllowed` 等权限过滤函数 |
143
+ | `src/common/portal-data.ts` | `getMenuPage`:pageId 优先,无匹配再用 path 与页面 route |
144
+ | `src/common/menu/parser.ts` | `filterMenuItems`、`getMenuItemPermCode` 等,消费 `getMenuPage` |
148
145
  | `src/layouts/index.tsx` | 新增 `isForbidden` 判断,集成 `isNoPermissionRoute` |
149
146
  | `src/layouts/components/menu/index.tsx` | 集成菜单过滤,支持免权限校验路由 |
150
147
  | `src/components/MicroAppLoader/index.tsx` | 集成 `isNoPermissionRoute`,免权限路由直接加载 |
@@ -194,33 +191,43 @@ function isAuthDisabled(): boolean;
194
191
  interface MenuFilterOptions {
195
192
  /** 是否是超级用户 */
196
193
  isSuperuser?: boolean | number;
197
- /** 允许访问的菜单路径列表(白名单) */
198
- sideMenus?: string[];
194
+ /** 用户拥有的菜单权限 code(与页面 routeKey、菜单项一致) */
195
+ menuPerms?: string[];
199
196
  }
200
197
  ```
201
198
 
199
+ ### 菜单项如何关联到页面(与 `routeKey` / `menu_perms`)
200
+
201
+ `page` 类型菜单项的权限 code 来自关联页的 **`routeKey`**(当该页 `accessControlEnabled && routeKey` 时)。关联页由 **`getMenuPage(item)`**(`src/common/portal-data.ts`)解析:
202
+
203
+ 1. **`pageId`**:在 `__MICO_PAGES__` 中按 `id` 查找,**命中则使用该页面**(与菜单 `path` 是否一致无关)。
204
+ 2. **`path` + 页面 `route`**:上一步无结果时,用 `findPageByPath` 将菜单 `path` 与页面 `route` 匹配(精确 + `/*` 通配)。
205
+
206
+ 详见 [路由与菜单解耦](./feature-路由与菜单解耦.md) 文档内「菜单项关联页面(getMenuPage)」小节。
207
+
202
208
  ### filterMenuItems
203
209
 
204
210
  ```typescript
205
211
  /**
206
- * 根据权限过滤菜单项(白名单逻辑)
212
+ * 根据 menu_perms 过滤菜单项
213
+ * - page:关联页由 getMenuPage → routeKey(需校验时);link:nameKey
207
214
  */
208
- function filterMenuItems(
209
- items: MenuItem[],
210
- options?: MenuFilterOptions,
211
- parentPath?: string,
212
- ): MenuItem[];
215
+ function filterMenuItems(items: MenuItem[], options?: MenuFilterOptions): MenuItem[];
213
216
  ```
214
217
 
215
218
  ### 用户信息相关字段
216
219
 
217
220
  ```typescript
218
221
  interface IUserInfo {
219
- is_superuser: boolean | number;
220
- /** 允许访问的菜单路径列表 */
221
- side_menus: string[];
222
- /** 缺失的操作权限(用于按钮级别控制,非菜单) */
223
- miss_permissions: string[];
222
+ is_superuser: boolean;
223
+ menu_perms: string[];
224
+ button_perms: string[];
225
+ app_perms: string[];
226
+ region_perms: string[];
227
+ name: string;
228
+ user_name: string;
229
+ username: string;
230
+ // …见 src/common/auth/type.ts IUserInfo / IUserInfoApiData
224
231
  }
225
232
  ```
226
233
 
@@ -264,43 +271,41 @@ export const NO_AUTH_ROUTE_LIST: string[] = [
264
271
  export const NO_PERMISSION_ROUTE_LIST: string[] = ['/403', '/404'];
265
272
  ```
266
273
 
267
- ### 菜单过滤结果
274
+ ### 菜单过滤结果(示意)
268
275
 
269
276
  ```
270
- 原始菜单:
271
- ├── 工作台 精确匹配 "工作台"
272
- ├── 列队管理 ✅ 前缀匹配 "列队管理.配置队列"
273
- │ └── 配置队列 精确匹配 "列队管理.配置队列"
274
- ├── 质量管理 不在白名单
275
- │ └── 抽样检查 ❌ 不在白名单
276
- └── 权限管理 ❌ adminOnly=true,非超级用户不可见
277
+ menu_perms 含 code "a"、"b.c"
278
+ ├── 页面菜单(routeKey=a)
279
+ ├── 页面菜单(routeKey 不在列表)
280
+ ├── 分组(仅容器,有可见子项) 作为父级保留
281
+ └── adminOnly 菜单 非超管
277
282
  ```
278
283
 
279
284
  ### Layout 中的权限判断
280
285
 
281
286
  ```tsx
282
- // layouts/index.tsx — 双层权限判断
287
+ // layouts/index.tsx — 以 PAGES + menu_perms 为主,无 page 时菜单路由兜底
283
288
  const isForbidden = useMemo(() => {
284
289
  if (isAuthDisabled()) return false;
285
290
  if (isNoPermissionRoute(location.pathname)) return false;
286
- if (!currentRoute) return false; // 非动态路由
291
+ if (!currentRoute) return false;
287
292
  if (isSuperuserUser(currentUser?.is_superuser)) return false;
288
293
 
289
- // Tier 1: 菜单权限交叉引用
294
+ const page =
295
+ currentRoute.pageConfig ??
296
+ (hasPages() ? findPageByPath(getPages(), location.pathname) : undefined);
297
+
298
+ if (page) {
299
+ if (page.adminOnly) return true;
300
+ if (!page.accessControlEnabled) return false;
301
+ const menuPerms = currentUser?.menu_perms || [];
302
+ return !page.routeKey || !menuPerms.includes(page.routeKey);
303
+ }
304
+
290
305
  const inAllMenu = findRouteByPath(allMenuRoutes, location.pathname);
291
306
  if (inAllMenu) {
292
307
  return !findRouteByPath(allowedMenuRoutes, location.pathname);
293
308
  }
294
-
295
- // Tier 2: 隐藏页面级权限
296
- if (!hasWindowPages()) return false;
297
- const page = findPageByPath(getWindowPages(), location.pathname);
298
- if (!page) return false;
299
- if (page.adminOnly) return true;
300
- if (page.accessControlEnabled) {
301
- const sideMenus = (currentUser?.side_menus || []) as string[];
302
- return !page.routeKey || !sideMenus.includes(page.routeKey);
303
- }
304
309
  return false;
305
310
  }, [...]);
306
311
  ```
@@ -323,6 +328,7 @@ if (!isAuthReady) {
323
328
  ```
324
329
 
325
330
  判断优先级:
331
+
326
332
  1. `isAuthDisabled()` — 全局关闭权限,直接放行
327
333
  2. `isPageAuthFree` — **PAGES 数据驱动**,页面 `accessControlEnabled === false` 时跳过认证和权限校验
328
334
  3. `isNoAuthRoute()` — 静态配置兜底,免认证路由(PAGES 未注入时的降级保护)
@@ -341,9 +347,9 @@ if (!isAuthReady) {
341
347
  | --- | --- | --- |
342
348
  | accessControlEnabled 统一控制 | `false` 同时跳过认证和授权 | PAGES 数据驱动,一个字段即可标记公开页面,无需重复配置 noAuthRouteList |
343
349
  | 认证与授权分离 | 两个独立配置项 | 不同场景需要不同组合,如"需要登录但不需要权限"的个人设置页 |
344
- | 权限模型 | 白名单 (`side_menus`) | 后端返回的 `side_menus` 是允许列表,比黑名单更安全 |
350
+ | 权限模型 | 白名单 (`menu_perms`) | 后端返回的 code 列表为允许集合;与 `routeKey` / 菜单项一一对应 |
345
351
  | 403 处理 | 原地渲染组件 | 保持 URL 不变,用户体验更好,便于分享链接 |
346
- | 父级菜单显示 | 前缀匹配 | 子菜单有权限时,父级菜单需要作为容器显示 |
352
+ | 父级菜单显示 | 子项驱动 | `group` 无独立 code,有可见子项时保留为容器 |
347
353
  | 超级用户 | 跳过所有检查 | 管理员需要完整访问权限 |
348
354
  | 免权限路由的菜单 | 按菜单项独立判断 | 每个菜单项根据自身路由是否匹配 noPermissionRouteList 独立决定显示,避免菜单随当前页面变化 |
349
355
  | 免认证路由的子应用 | 直接加载 | 不等待 currentUser,免认证路由本身不需要登录 |
@@ -370,9 +376,9 @@ if (!isAuthReady) {
370
376
  2. 检查 isNoPermissionRoute() → 在免权限列表中则允许
371
377
  3. 检查 currentRoute(PAGES 中的路由)→ 不存在则非动态路由,交给 Umi
372
378
  4. 检查 isSuperuserUser() → 超级用户放行
373
- 5. Tier 1: 在菜单中检查 allowedMenuRoutes
374
- 6. Tier 2: 不在菜单(隐藏页面)→ 检查 adminOnly + accessControlEnabled + routeKey
375
- 7. 都不匹配 → 放行
379
+ 5. PAGES 页面元数据adminOnly / accessControlEnabled + routeKey ∈ menu_perms
380
+ 6. 无页面元数据但在菜单路由中 检查 allowedMenuRoutes(已按 menu_perms 过滤)
381
+ 7. 其他 → 放行
376
382
  ```
377
383
 
378
384
  ### 设计理由
@@ -384,15 +390,15 @@ if (!isAuthReady) {
384
390
 
385
391
  ### 影响
386
392
 
387
- 当用户 `is_superuser=false` 且 `side_menus=[]` 时:
393
+ 当用户 `is_superuser=false` 且 `menu_perms=[]` 时:
388
394
 
389
- | 路由 | 结果 | 原因 |
390
- | --- | --- | --- |
391
- | `/` (Home) | ✅ 正常显示 | 静态路由,不受权限控制 |
392
- | `/403` | ✅ 正常显示 | 在 `noPermissionRouteList` 中 |
393
- | `/404` | ✅ 正常显示 | 在 `noPermissionRouteList` 中 |
394
- | `/user/login` | ✅ 正常显示 | 静态路由 + 在 `noAuthRouteList` 中 |
395
- | `/queue-management` | ❌ 显示 403 | 动态路由,无权限 |
395
+ | 路由 | 结果 | 原因 |
396
+ | ------------------- | ----------- | ---------------------------------- |
397
+ | `/` (Home) | ✅ 正常显示 | 静态路由,不受权限控制 |
398
+ | `/403` | ✅ 正常显示 | 在 `noPermissionRouteList` 中 |
399
+ | `/404` | ✅ 正常显示 | 在 `noPermissionRouteList` 中 |
400
+ | `/user/login` | ✅ 正常显示 | 静态路由 + 在 `noAuthRouteList` 中 |
401
+ | `/queue-management` | ❌ 显示 403 | 动态路由,无权限 |
396
402
 
397
403
  ### 如需控制静态路由
398
404
 
@@ -406,11 +412,11 @@ if (!isAuthReady) {
406
412
 
407
413
  - `noAuthRouteList` 和 `noPermissionRouteList` 是独立的,需要根据场景分别配置
408
414
  - 两个列表都支持 `/*` 后缀进行前缀匹配
409
- - `side_menus` 为空时,非超级用户没有任何菜单权限
410
- - `side_menus` 格式为菜单路径,如 `"列队管理.配置队列"`
411
- - `miss_permissions` 用于按钮级别权限控制,不影响菜单显示
415
+ - `menu_perms` 为空时,非超级用户没有任何菜单权限(`page`/`link` 项)
416
+ - 菜单项权限 code 与页面 `routeKey` 一致;`page` 类型先通过 **`getMenuPage`** 得到关联页再取 `routeKey`,否则回退 `nameKey`(link)
417
+ - `button_perms` 可用于按钮级权限(layout 侧栏不直接消费)
412
418
  - 403 页面在 Layout 内渲染,不会触发路由跳转
413
- - 调试时可在控制台搜索 `isForbidden (menu check)` `isForbidden (hidden page` 查看权限判断日志
419
+ - 调试时可在控制台过滤 `routePermission` 查看结构化权限日志(详见 [路由权限日志](./feature-路由权限日志.md))
414
420
  - **常见问题**:配置了 `noAuthRouteList` 但页面仍显示 403,需同时配置 `noPermissionRouteList`
415
421
 
416
422
  ## 相关文档