@gadmin2n/schematics 0.0.96 → 0.0.97

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 (32) hide show
  1. package/dist/lib/application/files/gadmin2-game-angle-demo/DESIGN.md +348 -0
  2. package/dist/lib/application/files/gadmin2-game-angle-demo/PRODUCT.md +75 -0
  3. package/dist/lib/application/files/gadmin2-game-angle-demo/config/prisma/workflow.prisma +5 -0
  4. package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflow-node-types.ts +24 -1
  5. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-dsl-validate.spec.ts +220 -0
  6. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-dsl-validate.ts +129 -0
  7. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-export.dto.ts +1 -0
  8. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-export.service.ts +4 -0
  9. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.spec.ts +46 -0
  10. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.ts +27 -0
  11. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowNodeType/workflowNodeType.service.spec.ts +6 -0
  12. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/dsl/node-types.ts +43 -4
  13. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/dsl/validate.ts +109 -0
  14. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/tests/validate.test.ts +205 -0
  15. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/layout/header.tsx +55 -56
  16. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/layout/layout.tsx +7 -3
  17. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/layout/logo.tsx +7 -1
  18. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/layout/sider.tsx +179 -160
  19. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/layout/title.tsx +34 -31
  20. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/constants/layout.ts +24 -0
  21. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/en/common.json +6 -0
  22. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/zh_CN/common.json +6 -0
  23. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/CustomNode.tsx +66 -51
  24. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/EnhancedFlowRenderer.tsx +7 -1
  25. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/ExecutionStatusNode.tsx +66 -26
  26. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/FlowRenderer.tsx +7 -1
  27. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/editor.tsx +9 -2
  28. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/hooks/useWorkflowAgent.ts +30 -0
  29. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/show.tsx +9 -2
  30. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/types.ts +1 -0
  31. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/utils/resolveOutputs.ts +27 -0
  32. package/package.json +1 -1
@@ -3,6 +3,7 @@ import React, {
3
3
  useEffect,
4
4
  useRef,
5
5
  useCallback,
6
+ useState,
6
7
  type CSSProperties,
7
8
  } from 'react';
8
9
 
@@ -35,19 +36,25 @@ import {
35
36
  } from 'antd';
36
37
  import { Title } from './title';
37
38
  import { agentAttrs } from 'components/agentPanel/agentAttributes';
39
+ import {
40
+ HEADER_HEIGHT,
41
+ SIDER_WIDTH,
42
+ SIDER_COLLAPSED_WIDTH,
43
+ Z_INDEX,
44
+ } from 'constants/layout';
45
+
46
+ const { SubMenu: _SubMenu } = Menu; // eslint silencer; preserved for future use
47
+ void _SubMenu;
48
+ const { useToken } = theme;
38
49
 
39
- const drawerButtonStyles: CSSProperties = {
40
- borderTopLeftRadius: 0,
41
- borderBottomLeftRadius: 0,
42
- position: 'fixed',
43
- top: 64,
44
- zIndex: 1001,
45
- };
46
-
47
- let reRouteNum = 0;
50
+ /**
51
+ * Auto-redirect 安全阀: 一次 mount 周期内允许的最大重定向次数。
52
+ * 超过这个次数说明已经在 ping-pong, 应该停手避免死循环。
53
+ */
54
+ const MAX_AUTO_REDIRECTS = 5;
48
55
 
49
- const { SubMenu } = Menu;
50
- const { useToken } = theme;
56
+ /** Auto-redirect 触发延迟, selectedKey / items 在同一帧内尘埃落定 */
57
+ const AUTO_REDIRECT_DEBOUNCE_MS = 300;
51
58
 
52
59
  const getParentKeysFromMenuTree = (
53
60
  items: ITreeMenu[],
@@ -85,16 +92,41 @@ const getMenuLabel = (
85
92
  return meta?.label || name;
86
93
  };
87
94
 
95
+ /** 共用的标题栏(移动端 Drawer 与桌面 Sider 头部都用) */
96
+ const SiderTitleBar: React.FC<{ collapsed: boolean }> = ({ collapsed }) => {
97
+ const { token } = useToken();
98
+ return (
99
+ <div
100
+ {...agentAttrs({ type: 'app-title' })}
101
+ style={{
102
+ width: collapsed ? SIDER_COLLAPSED_WIDTH : SIDER_WIDTH,
103
+ padding: collapsed ? 0 : '0 16px',
104
+ display: 'flex',
105
+ justifyContent: collapsed ? 'center' : 'flex-start',
106
+ alignItems: 'center',
107
+ height: HEADER_HEIGHT,
108
+ backgroundColor: token.colorBgElevated,
109
+ borderBottom: `1px solid ${token.colorBorderSecondary}`,
110
+ }}
111
+ >
112
+ <Title collapsed={collapsed} />
113
+ </div>
114
+ );
115
+ };
116
+
88
117
  export const Sider: React.FC = () => {
89
118
  const { token } = useToken();
90
119
  const { menuItems, selectedKey, defaultOpenKeys } = useMenu();
91
120
  const { pathname: currentPathname } = useLocation();
92
- const { t, i18n } = useTranslation();
121
+ const { i18n } = useTranslation();
93
122
  const currentLocale = i18n.language;
94
123
 
95
- const [openKeys, setOpenKeys] = React.useState<string[]>(defaultOpenKeys);
124
+ const [openKeys, setOpenKeys] = useState<string[]>(defaultOpenKeys);
96
125
 
97
126
  const lastSelectedKeyRef = useRef<string | undefined>(undefined);
127
+ // 之前是 module-level 的 `let reRouteNum = 0`, 跨多个 mount 共享, HMR 后永远写死.
128
+ // 改成 useRef 让每次 mount 拿到独立计数器, 而仍然跨同一 mount 内的多次 effect 触发.
129
+ const redirectCountRef = useRef(0);
98
130
 
99
131
  useEffect(() => {
100
132
  if (
@@ -131,15 +163,20 @@ export const Sider: React.FC = () => {
131
163
  const renderTreeView = useCallback(
132
164
  (tree: ITreeMenu[], currentSelectedKey?: string): MenuProps['items'] => {
133
165
  return tree
134
- .map((item: ITreeMenu) => {
166
+ .map((item: ITreeMenu, index: number) => {
135
167
  const { key, name, children, meta = {}, list } = item;
136
168
  const { icon, parent: parentName } = meta;
137
169
 
138
170
  const route = typeof list === 'string' ? list : listUrl(name);
139
171
 
172
+ // 优先用 item.key, 退而求其次用 name (永远存在), 最后才用稳定的 path-index.
173
+ // 之前用 Math.random() 作为 fallback —— 那是 React 反模式, 每次渲染都换 key
174
+ // 会让整棵子树 unmount/mount, 破坏滚动位置和动画状态.
175
+ const stableKey = key || name || `fallback-${index}`;
176
+
140
177
  if (children.length > 0) {
141
178
  return {
142
- key: item.key || Math.random().toString(),
179
+ key: stableKey,
143
180
  label: (
144
181
  <span style={{ fontWeight: 600 }}>
145
182
  {getMenuLabel(name, meta, currentLocale)}
@@ -150,15 +187,14 @@ export const Sider: React.FC = () => {
150
187
  };
151
188
  }
152
189
 
153
- const menuItemKey = item.key || Math.random().toString();
154
- const isSelected = menuItemKey === currentSelectedKey;
190
+ const isSelected = stableKey === currentSelectedKey;
155
191
  const isRoute = !(
156
192
  pickNotDeprecated(meta?.parent, meta?.parent, parentName) !==
157
193
  undefined && children.length === 0
158
194
  );
159
195
 
160
196
  return {
161
- key: menuItemKey,
197
+ key: stableKey,
162
198
  label: (
163
199
  <>
164
200
  <Link to={route ?? ''}>
@@ -181,9 +217,10 @@ export const Sider: React.FC = () => {
181
217
  return renderTreeView(menuItems, selectedKey)?.filter(Boolean);
182
218
  }, [menuItems, selectedKey, renderTreeView]);
183
219
 
184
- // Auto-redirect to first available menu item if current selectedKey is not in the menu
220
+ // Auto-redirect to first available menu item if current selectedKey is not in the menu.
221
+ // 触发条件: items / selectedKey / pathname 变化.
185
222
  useEffect(() => {
186
- if (reRouteNum > 5) return;
223
+ if (redirectCountRef.current >= MAX_AUTO_REDIRECTS) return;
187
224
 
188
225
  const isKeyInMenuItems = (
189
226
  menuItems: MenuProps['items'],
@@ -201,12 +238,12 @@ export const Sider: React.FC = () => {
201
238
  return false;
202
239
  };
203
240
 
204
- // If selectedKey is in the menu, no need to redirect
241
+ // selectedKey 已经在菜单里, 不需要重定向.
205
242
  if (selectedKey && isKeyInMenuItems(items, selectedKey)) {
206
243
  return;
207
244
  }
208
245
 
209
- // Skip auto-redirect for CRUD detail pages (edit/show/create)
246
+ // Skip auto-redirect for CRUD detail pages (edit/show/create).
210
247
  // These pages are not in the menu but are valid navigation targets.
211
248
  // Also skip auto-generated /admin/data-mngt/* CRUD list pages —
212
249
  // some of them are intentionally hidden from the sidebar via
@@ -224,36 +261,41 @@ export const Sider: React.FC = () => {
224
261
  }
225
262
 
226
263
  const timer = setTimeout(() => {
227
- reRouteNum++;
264
+ redirectCountRef.current += 1;
228
265
 
229
266
  const firstKey = items?.[0]?.key;
230
- if (firstKey && firstKey !== '/dashboard') {
231
- setOpenKeys([String(firstKey)]);
232
- const firstItem = items?.find((item) => item?.key === firstKey) as any;
233
- if (firstItem?.children?.length > 0) {
234
- let childItem = firstItem?.children?.find(
235
- (item: any) => selectedKey.indexOf(item?.key) !== -1,
236
- );
237
- if (!childItem) {
238
- childItem = firstItem?.children?.[0];
239
- }
240
- const childKey = childItem?.key;
241
- if (childItem?.children?.length > 0) {
242
- setOpenKeys([String(firstKey), String(childKey)]);
243
- const grandsonItem = childItem?.children?.find(
244
- (item: any) => selectedKey.indexOf(item?.key) !== -1,
245
- );
246
- const grandsonKey =
247
- grandsonItem?.key || firstItem?.children?.[0]?.children?.[0]?.key;
248
- go({ to: grandsonKey, type: 'replace' });
249
- } else {
250
- go({ to: childKey, type: 'replace' });
251
- }
252
- } else {
253
- go({ to: String(firstKey), type: 'replace' });
267
+ if (!firstKey || firstKey === '/dashboard') return;
268
+
269
+ setOpenKeys([String(firstKey)]);
270
+ const firstItem = items?.find((item) => item?.key === firstKey) as any;
271
+ if (!firstItem?.children?.length) {
272
+ go({ to: String(firstKey), type: 'replace' });
273
+ return;
274
+ }
275
+
276
+ const safeSelectedKey = selectedKey ?? '';
277
+ let childItem = firstItem.children.find((child: any) =>
278
+ safeSelectedKey.includes(String(child?.key ?? '')),
279
+ );
280
+ if (!childItem) {
281
+ childItem = firstItem.children[0];
282
+ }
283
+ const childKey = childItem?.key;
284
+
285
+ if (childItem?.children?.length) {
286
+ setOpenKeys([String(firstKey), String(childKey)]);
287
+ const grandsonItem = childItem.children.find((g: any) =>
288
+ safeSelectedKey.includes(String(g?.key ?? '')),
289
+ );
290
+ const grandsonKey =
291
+ grandsonItem?.key ?? firstItem.children[0]?.children?.[0]?.key;
292
+ if (grandsonKey) {
293
+ go({ to: String(grandsonKey), type: 'replace' });
254
294
  }
295
+ } else if (childKey) {
296
+ go({ to: String(childKey), type: 'replace' });
255
297
  }
256
- }, 300);
298
+ }, AUTO_REDIRECT_DEBOUNCE_MS);
257
299
 
258
300
  return () => clearTimeout(timer);
259
301
  }, [items, selectedKey, currentPathname, go]);
@@ -284,35 +326,51 @@ export const Sider: React.FC = () => {
284
326
  [menuItems, setMobileSiderOpen],
285
327
  );
286
328
 
287
- const renderMenu = useCallback(() => {
288
- return (
289
- <Menu
290
- selectedKeys={selectedKey ? [selectedKey] : []}
291
- defaultOpenKeys={defaultOpenKeys}
292
- openKeys={openKeys}
293
- onOpenChange={handleOpenChange}
294
- mode="inline"
295
- items={items}
296
- style={{
297
- paddingTop: '8px',
298
- border: 'none',
299
- overflow: 'auto',
300
- height: 'calc(100% - 72px)',
301
- background: 'transparent',
302
- }}
303
- onClick={handleMenuClick}
304
- ></Menu>
305
- );
306
- }, [
307
- selectedKey,
308
- defaultOpenKeys,
309
- openKeys,
310
- handleOpenChange,
311
- items,
312
- handleMenuClick,
313
- ]);
314
-
315
- const renderDrawerSider = () => {
329
+ const renderMenu = useCallback(
330
+ (collapsed: boolean) => {
331
+ return (
332
+ <Menu
333
+ selectedKeys={selectedKey ? [selectedKey] : []}
334
+ defaultOpenKeys={defaultOpenKeys}
335
+ openKeys={openKeys}
336
+ onOpenChange={handleOpenChange}
337
+ mode="inline"
338
+ // Layout.Sider 的 collapsed 只压宽度, 不会自动让 Menu 切到 collapsed 模式.
339
+ // 必须显式 inlineCollapsed, 否则 SubMenu 的 label 仍渲染, 折叠时溢出 80px 容器
340
+ // (常见症状: 中文 label 被截成 "系统管", active 项还是蓝字)
341
+ inlineCollapsed={collapsed}
342
+ items={items}
343
+ style={{
344
+ paddingTop: 8,
345
+ border: 'none',
346
+ overflow: 'auto',
347
+ // 减去标题栏 (HEADER_HEIGHT) + 桌面 trigger 默认 ~48px 的占位.
348
+ height: `calc(100% - ${HEADER_HEIGHT + 8}px)`,
349
+ background: 'transparent',
350
+ }}
351
+ onClick={handleMenuClick}
352
+ />
353
+ );
354
+ },
355
+ [
356
+ selectedKey,
357
+ defaultOpenKeys,
358
+ openKeys,
359
+ handleOpenChange,
360
+ items,
361
+ handleMenuClick,
362
+ ],
363
+ );
364
+
365
+ const drawerButtonStyles: CSSProperties = {
366
+ borderTopLeftRadius: 0,
367
+ borderBottomLeftRadius: 0,
368
+ position: 'fixed',
369
+ top: HEADER_HEIGHT,
370
+ zIndex: Z_INDEX.FAB,
371
+ };
372
+
373
+ if (isMobile) {
316
374
  return (
317
375
  <>
318
376
  <Drawer
@@ -320,39 +378,22 @@ export const Sider: React.FC = () => {
320
378
  onClose={() => setMobileSiderOpen(false)}
321
379
  placement="left"
322
380
  closable={false}
323
- width={256}
324
- styles={{
325
- body: {
326
- padding: 0,
327
- },
328
- }}
329
- maskClosable={true}
381
+ width={SIDER_WIDTH}
382
+ styles={{ body: { padding: 0 } }}
383
+ maskClosable
330
384
  >
331
- <Layout>
385
+ <Layout style={{ height: '100vh' }}>
332
386
  <Layout.Sider
333
- width={500}
387
+ width={SIDER_WIDTH}
334
388
  style={{
335
389
  height: '100vh',
336
390
  backgroundColor: token.colorBgContainer,
337
391
  borderRight: `1px solid ${token.colorBorderBg}`,
338
392
  }}
339
393
  >
340
- <div
341
- {...agentAttrs({ type: 'app-title' })}
342
- style={{
343
- width: '256px',
344
- padding: '0 16px',
345
- display: 'flex',
346
- justifyContent: 'flex-start',
347
- alignItems: 'center',
348
- height: '64px',
349
- backgroundColor: token.colorBgElevated,
350
- borderBottom: 'none',
351
- }}
352
- >
353
- <Title collapsed={false} />
354
- </div>
355
- {renderMenu()}
394
+ <SiderTitleBar collapsed={false} />
395
+ {/* 移动端 Drawer 打开时给的是全宽容器, Menu 永远展开. */}
396
+ {renderMenu(false)}
356
397
  </Layout.Sider>
357
398
  </Layout>
358
399
  </Drawer>
@@ -361,13 +402,10 @@ export const Sider: React.FC = () => {
361
402
  size="large"
362
403
  onClick={() => setMobileSiderOpen(true)}
363
404
  icon={<BarsOutlined />}
405
+ aria-label="Open navigation menu"
364
406
  />
365
407
  </>
366
408
  );
367
- };
368
-
369
- if (isMobile) {
370
- return renderDrawerSider();
371
409
  }
372
410
 
373
411
  const siderStyles: React.CSSProperties = {
@@ -377,67 +415,48 @@ export const Sider: React.FC = () => {
377
415
  top: 0,
378
416
  left: 0,
379
417
  height: '100vh',
380
- zIndex: 999,
418
+ zIndex: Z_INDEX.STICKY,
381
419
  };
382
420
 
383
421
  return (
384
- <>
385
- <Layout.Sider
386
- style={siderStyles}
387
- width={256}
388
- collapsible
389
- collapsed={siderCollapsed}
390
- onCollapse={(collapsed, type) => {
391
- if (type === 'clickTrigger') {
392
- setSiderCollapsed(collapsed);
393
- }
394
- }}
395
- collapsedWidth={80}
396
- breakpoint="lg"
397
- trigger={
398
- <Button
399
- type="text"
400
- style={{
401
- borderRadius: 0,
402
- height: '100%',
403
- width: '100%',
404
- backgroundColor: token.colorBgElevated,
405
- borderRight: `1px solid ${token.colorBorderBg}`,
406
- }}
407
- >
408
- {siderCollapsed ? (
409
- <RightOutlined
410
- style={{
411
- color: token.colorPrimary,
412
- }}
413
- />
414
- ) : (
415
- <LeftOutlined
416
- style={{
417
- color: token.colorPrimary,
418
- }}
419
- />
420
- )}
421
- </Button>
422
+ <Layout.Sider
423
+ style={siderStyles}
424
+ width={SIDER_WIDTH}
425
+ collapsible
426
+ collapsed={siderCollapsed}
427
+ onCollapse={(collapsed, type) => {
428
+ if (type === 'clickTrigger') {
429
+ setSiderCollapsed(collapsed);
422
430
  }
423
- >
424
- <div
425
- {...agentAttrs({ type: 'app-title' })}
431
+ }}
432
+ collapsedWidth={SIDER_COLLAPSED_WIDTH}
433
+ breakpoint="lg"
434
+ trigger={
435
+ <Button
436
+ type="text"
437
+ aria-label={
438
+ siderCollapsed
439
+ ? 'Expand navigation menu'
440
+ : 'Collapse navigation menu'
441
+ }
442
+ aria-expanded={!siderCollapsed}
426
443
  style={{
427
- width: siderCollapsed ? '80px' : '256px',
428
- padding: siderCollapsed ? '0' : '0 16px',
429
- display: 'flex',
430
- justifyContent: siderCollapsed ? 'center' : 'flex-start',
431
- alignItems: 'center',
432
- height: '64px',
444
+ borderRadius: 0,
445
+ height: '100%',
446
+ width: '100%',
433
447
  backgroundColor: token.colorBgElevated,
434
- fontSize: '14px',
448
+ borderRight: `1px solid ${token.colorBorderBg}`,
449
+ // 之前用 colorPrimary 给 chrome 控件上色, 与 One Voice Rule 冲突.
450
+ // chrome 收纳器不需要抢眼, 用次级文本色即可.
451
+ color: token.colorTextSecondary,
435
452
  }}
436
453
  >
437
- <Title collapsed={siderCollapsed} />
438
- </div>
439
- {renderMenu()}
440
- </Layout.Sider>
441
- </>
454
+ {siderCollapsed ? <RightOutlined /> : <LeftOutlined />}
455
+ </Button>
456
+ }
457
+ >
458
+ <SiderTitleBar collapsed={siderCollapsed} />
459
+ {renderMenu(siderCollapsed)}
460
+ </Layout.Sider>
442
461
  );
443
462
  };
@@ -2,60 +2,63 @@ import React from 'react';
2
2
 
3
3
  import type { RefineLayoutThemedTitleProps } from '@refinedev/antd';
4
4
  import { useLink } from '@refinedev/core';
5
+ import { useTranslation } from 'react-i18next';
5
6
 
6
- import { Space, theme, Typography } from 'antd';
7
+ import { theme } from 'antd';
7
8
 
8
9
  import { Logo } from './logo';
9
10
 
10
11
  const { useToken } = theme;
11
12
 
12
- const name = 'AI+ Ops Admin';
13
-
13
+ /**
14
+ * App 品牌入口(左上角 Logo + 名称)。
15
+ *
16
+ * 设计意图:
17
+ * - 不再使用 Typography.Title —— 它会在每个页面渲染一个重复的 <h1>,
18
+ * 与页面自身的 <h1> 冲突,破坏 a11y 层级。这里只是一个导航入口,
19
+ * 用 <span> + 显式样式即可。
20
+ * - 名称走 i18n,方便后续 white-label 替换。
21
+ * - Logo 用 currentColor,颜色由这里的 wrapper 决定(主色蓝)。
22
+ */
14
23
  export const Title: React.FC<RefineLayoutThemedTitleProps> = ({
15
24
  collapsed,
16
25
  wrapperStyles,
17
26
  }) => {
18
27
  const { token } = useToken();
28
+ const { t } = useTranslation();
19
29
  const Link = useLink();
20
30
 
31
+ const appName = t('app.name', 'AI+ Ops Admin');
32
+
21
33
  return (
22
34
  <Link
23
35
  to="/"
24
36
  style={{
25
- display: 'inline-block',
37
+ display: 'inline-flex',
38
+ alignItems: 'center',
39
+ gap: 8,
40
+ color: token.colorPrimary,
26
41
  textDecoration: 'none',
42
+ ...wrapperStyles,
27
43
  }}
44
+ aria-label={appName}
28
45
  >
29
- <Space
30
- style={{
31
- display: 'flex',
32
- alignItems: 'center',
33
- fontSize: 'inherit',
34
- ...wrapperStyles,
35
- }}
36
- >
37
- <div
46
+ <Logo width={24} height={24} />
47
+
48
+ {!collapsed && (
49
+ <span
38
50
  style={{
39
- height: '24px',
40
- width: '24px',
41
- color: token.colorPrimary,
51
+ fontSize: 16,
52
+ fontWeight: 600,
53
+ lineHeight: 1.4,
54
+ color: token.colorTextHeading,
55
+ letterSpacing: 0,
56
+ whiteSpace: 'nowrap',
42
57
  }}
43
58
  >
44
- <Logo />
45
- </div>
46
-
47
- {!collapsed && (
48
- <Typography.Title
49
- style={{
50
- fontSize: 'inherit',
51
- marginBottom: 0,
52
- fontWeight: 700,
53
- }}
54
- >
55
- {name}
56
- </Typography.Title>
57
- )}
58
- </Space>
59
+ {appName}
60
+ </span>
61
+ )}
59
62
  </Link>
60
63
  );
61
64
  };
@@ -1,3 +1,27 @@
1
+ /**
2
+ * App chrome 共享尺寸。layout / sider / header / floating triggers
3
+ * 都从这里取,避免在多个文件里散落同一个魔法数字。
4
+ */
5
+ export const HEADER_HEIGHT = 64;
6
+ export const SIDER_WIDTH = 256;
7
+ export const SIDER_COLLAPSED_WIDTH = 80;
8
+
9
+ /**
10
+ * 语义化的 z-index 层级表。
11
+ *
12
+ * Ant Design v5 的浮层 (Modal / Drawer / Dropdown / Select) 默认从 1000 起;
13
+ * 所以这里的 STICKY / FAB 都安排在 1000 以下,确保 chrome 在 popup 打开时
14
+ * 被正确遮挡。永远不要在业务代码里写裸 zIndex 数字 —— 加一个语义键回到这里。
15
+ */
16
+ export const Z_INDEX = {
17
+ /** AppHeader、Sider 等 sticky chrome */
18
+ STICKY: 100,
19
+ /** 悬浮按钮:移动端的 Drawer 触发器 */
20
+ FAB: 110,
21
+ /** 自定义遮罩,低于 Ant popups */
22
+ OVERLAY: 800,
23
+ } as const;
24
+
1
25
  /** List / Create 页面顶层组件的内容区样式 */
2
26
  export const LIST_CONTENT_PROPS = {
3
27
  style: { marginTop: '1rem' },
@@ -1,4 +1,10 @@
1
1
  {
2
+ "app": {
3
+ "name": "AI+ Ops Admin"
4
+ },
5
+ "header": {
6
+ "changeLanguage": "Change language"
7
+ },
2
8
  "pages": {
3
9
  "login": {
4
10
  "title": "Sign in to your account",
@@ -1,4 +1,10 @@
1
1
  {
2
+ "app": {
3
+ "name": "AI+ Ops Admin"
4
+ },
5
+ "header": {
6
+ "changeLanguage": "切换语言"
7
+ },
2
8
  "pages": {
3
9
  "login": {
4
10
  "title": "Sign in to your account",