@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
@@ -0,0 +1,109 @@
1
+ /**
2
+ * DSL handle validator. Confirms each edge's sourceHandle matches the
3
+ * source node's declared outputs (or, for dynamic nodes like switch,
4
+ * matches a value derived from config).
5
+ *
6
+ * Used at workflow publish time (server-side via a duplicated copy)
7
+ * and optionally during DSL parsing on the worker side.
8
+ */
9
+
10
+ import type { WorkflowDSL, DslNode } from './types';
11
+ import { NODE_TYPE_META } from './node-types';
12
+
13
+ export type DslValidationCode =
14
+ | 'EXTRA_HANDLE' // node has [] outputs, but edge set sourceHandle
15
+ | 'MISSING_HANDLE' // node has named outputs, but edge has no sourceHandle (or empty string)
16
+ | 'UNKNOWN_HANDLE'; // edge sourceHandle is set but ∉ declared outputs
17
+
18
+ export interface DslValidationError {
19
+ edgeId: string;
20
+ source: string;
21
+ target: string;
22
+ code: DslValidationCode;
23
+ reason: string;
24
+ }
25
+
26
+ /**
27
+ * Validate every edge's sourceHandle against the source node's outputs metadata.
28
+ *
29
+ * Rules per source node's outputs:
30
+ * - outputs === null: dynamic — handles must come from deriveDynamicOutputs(node).
31
+ * Edge sourceHandle MUST be present and ∈ derived list, OR if the
32
+ * derived list is empty (config not yet filled), no constraint.
33
+ * - outputs === []: edge sourceHandle MUST be undefined (no named handles allowed).
34
+ * - outputs.length>0: edge sourceHandle MUST be present and ∈ outputs.
35
+ *
36
+ * Returns an array of errors (empty if DSL is valid).
37
+ */
38
+ export function validateDslHandles(dsl: WorkflowDSL): DslValidationError[] {
39
+ const errors: DslValidationError[] = [];
40
+ const nodeMap = new Map<string, DslNode>(dsl.nodes.map((n) => [n.id, n]));
41
+
42
+ for (const edge of dsl.edges) {
43
+ const src = nodeMap.get(edge.source);
44
+ if (!src) continue; // separate concern (orphan edge); not this validator's job
45
+
46
+ const meta = NODE_TYPE_META[src.type];
47
+ if (!meta) continue; // guards against retired/unknown node types from older DSLs
48
+
49
+ const handles = meta.outputs === null
50
+ ? deriveDynamicOutputs(src)
51
+ : meta.outputs;
52
+
53
+ if (meta.outputs !== null && handles.length === 0) {
54
+ // Single anonymous output: sourceHandle must be absent
55
+ if (edge.sourceHandle != null && edge.sourceHandle !== '') {
56
+ errors.push({
57
+ edgeId: edge.id,
58
+ source: edge.source,
59
+ target: edge.target,
60
+ code: 'EXTRA_HANDLE',
61
+ reason: `Node "${src.type}" has no named output handles; edge must not set sourceHandle (got "${edge.sourceHandle}").`,
62
+ });
63
+ }
64
+ continue;
65
+ }
66
+
67
+ // Enumerated or dynamic non-empty: sourceHandle required and must match
68
+ if (handles.length === 0) {
69
+ // Dynamic + config not yet filled: skip rather than block edits in progress
70
+ continue;
71
+ }
72
+ if (edge.sourceHandle == null || edge.sourceHandle === '') {
73
+ errors.push({
74
+ edgeId: edge.id,
75
+ source: edge.source,
76
+ target: edge.target,
77
+ code: 'MISSING_HANDLE',
78
+ reason: `Node "${src.type}" requires a sourceHandle; choose one of: ${handles.join(', ')}.`,
79
+ });
80
+ continue;
81
+ }
82
+ if (!handles.includes(edge.sourceHandle)) {
83
+ errors.push({
84
+ edgeId: edge.id,
85
+ source: edge.source,
86
+ target: edge.target,
87
+ code: 'UNKNOWN_HANDLE',
88
+ reason: `Node "${src.type}" sourceHandle "${edge.sourceHandle}" is not in declared outputs: ${handles.join(', ')}.`,
89
+ });
90
+ }
91
+ }
92
+
93
+ return errors;
94
+ }
95
+
96
+ /**
97
+ * For dynamic-output nodes (currently just `switch`), derive the list of
98
+ * handle ids from the node's config.
99
+ */
100
+ function deriveDynamicOutputs(node: DslNode): string[] {
101
+ if (node.type === 'switch') {
102
+ const cases = (node.config as any)?.cases as Array<{ value?: string }> | undefined;
103
+ if (!Array.isArray(cases)) return [];
104
+ return cases
105
+ .map((c) => c?.value)
106
+ .filter((v): v is string => typeof v === 'string' && v.length > 0);
107
+ }
108
+ return [];
109
+ }
@@ -0,0 +1,205 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { validateDslHandles } from '../dsl/validate';
3
+ import type { WorkflowDSL } from '../dsl/types';
4
+
5
+ // ─── helpers ─────────────────────────────────────────────────────────────────
6
+
7
+ function makeNode(
8
+ id: string,
9
+ type: string,
10
+ config: Record<string, any> = {},
11
+ ): WorkflowDSL['nodes'][number] {
12
+ return {
13
+ id,
14
+ type: type as any,
15
+ label: id,
16
+ position: { x: 0, y: 0 },
17
+ config,
18
+ };
19
+ }
20
+
21
+ function makeEdge(
22
+ id: string,
23
+ source: string,
24
+ target: string,
25
+ sourceHandle?: string,
26
+ ): WorkflowDSL['edges'][number] {
27
+ return sourceHandle === undefined
28
+ ? { id, source, target }
29
+ : { id, source, target, sourceHandle };
30
+ }
31
+
32
+ // ─── if_else (enumerated outputs) ────────────────────────────────────────────
33
+
34
+ describe('validateDslHandles — if_else', () => {
35
+ it('accepts sourceHandle "true"', () => {
36
+ const dsl: WorkflowDSL = {
37
+ nodes: [makeNode('a', 'if_else'), makeNode('b', 'http_request')],
38
+ edges: [makeEdge('e1', 'a', 'b', 'true')],
39
+ };
40
+ expect(validateDslHandles(dsl)).toEqual([]);
41
+ });
42
+
43
+ it('accepts sourceHandle "false"', () => {
44
+ const dsl: WorkflowDSL = {
45
+ nodes: [makeNode('a', 'if_else'), makeNode('b', 'http_request')],
46
+ edges: [makeEdge('e1', 'a', 'b', 'false')],
47
+ };
48
+ expect(validateDslHandles(dsl)).toEqual([]);
49
+ });
50
+
51
+ it('rejects unknown sourceHandle "maybe"', () => {
52
+ const dsl: WorkflowDSL = {
53
+ nodes: [makeNode('a', 'if_else'), makeNode('b', 'http_request')],
54
+ edges: [makeEdge('e1', 'a', 'b', 'maybe')],
55
+ };
56
+ const errors = validateDslHandles(dsl);
57
+ expect(errors).toHaveLength(1);
58
+ expect(errors[0].edgeId).toBe('e1');
59
+ expect(errors[0].code).toBe('UNKNOWN_HANDLE');
60
+ expect(errors[0].reason).toMatch(/not in declared outputs/);
61
+ });
62
+
63
+ it('rejects missing sourceHandle', () => {
64
+ const dsl: WorkflowDSL = {
65
+ nodes: [makeNode('a', 'if_else'), makeNode('b', 'http_request')],
66
+ edges: [makeEdge('e1', 'a', 'b')],
67
+ };
68
+ const errors = validateDslHandles(dsl);
69
+ expect(errors).toHaveLength(1);
70
+ expect(errors[0].edgeId).toBe('e1');
71
+ expect(errors[0].code).toBe('MISSING_HANDLE');
72
+ expect(errors[0].reason).toMatch(/requires a sourceHandle/);
73
+ });
74
+
75
+ it('rejects empty-string sourceHandle as missing', () => {
76
+ const dsl: WorkflowDSL = {
77
+ nodes: [makeNode('a', 'if_else'), makeNode('b', 'http_request')],
78
+ edges: [makeEdge('e1', 'a', 'b', '')],
79
+ };
80
+ const errors = validateDslHandles(dsl);
81
+ expect(errors).toHaveLength(1);
82
+ expect(errors[0].edgeId).toBe('e1');
83
+ expect(errors[0].code).toBe('MISSING_HANDLE');
84
+ });
85
+ });
86
+
87
+ // ─── cron_trigger (no named outputs) ─────────────────────────────────────────
88
+
89
+ describe('validateDslHandles — cron_trigger (no named outputs)', () => {
90
+ it('rejects sourceHandle "foo" on a node with no named handles', () => {
91
+ const dsl: WorkflowDSL = {
92
+ nodes: [makeNode('t', 'cron_trigger'), makeNode('a', 'http_request')],
93
+ edges: [makeEdge('e1', 't', 'a', 'foo')],
94
+ };
95
+ const errors = validateDslHandles(dsl);
96
+ expect(errors).toHaveLength(1);
97
+ expect(errors[0].edgeId).toBe('e1');
98
+ expect(errors[0].code).toBe('EXTRA_HANDLE');
99
+ expect(errors[0].reason).toMatch(/no named output handles/);
100
+ });
101
+
102
+ it('accepts no sourceHandle', () => {
103
+ const dsl: WorkflowDSL = {
104
+ nodes: [makeNode('t', 'cron_trigger'), makeNode('a', 'http_request')],
105
+ edges: [makeEdge('e1', 't', 'a')],
106
+ };
107
+ expect(validateDslHandles(dsl)).toEqual([]);
108
+ });
109
+
110
+ it('accepts empty-string sourceHandle (treated as absent for [] nodes)', () => {
111
+ const dsl: WorkflowDSL = {
112
+ nodes: [makeNode('t', 'cron_trigger'), makeNode('a', 'http_request')],
113
+ edges: [makeEdge('e1', 't', 'a', '')],
114
+ };
115
+ expect(validateDslHandles(dsl)).toEqual([]);
116
+ });
117
+ });
118
+
119
+ // ─── switch (dynamic outputs) ────────────────────────────────────────────────
120
+
121
+ describe('validateDslHandles — switch (dynamic outputs)', () => {
122
+ it('accepts sourceHandle matching a case value', () => {
123
+ const dsl: WorkflowDSL = {
124
+ nodes: [
125
+ makeNode('s', 'switch', { cases: [{ value: 'a' }, { value: 'b' }] }),
126
+ makeNode('x', 'http_request'),
127
+ ],
128
+ edges: [makeEdge('e1', 's', 'x', 'a')],
129
+ };
130
+ expect(validateDslHandles(dsl)).toEqual([]);
131
+ });
132
+
133
+ it('rejects sourceHandle not in case values', () => {
134
+ const dsl: WorkflowDSL = {
135
+ nodes: [
136
+ makeNode('s', 'switch', { cases: [{ value: 'a' }] }),
137
+ makeNode('x', 'http_request'),
138
+ ],
139
+ edges: [makeEdge('e1', 's', 'x', 'c')],
140
+ };
141
+ const errors = validateDslHandles(dsl);
142
+ expect(errors).toHaveLength(1);
143
+ expect(errors[0].edgeId).toBe('e1');
144
+ expect(errors[0].code).toBe('UNKNOWN_HANDLE');
145
+ });
146
+
147
+ it('skips validation when switch config has no cases (in-progress editor)', () => {
148
+ const dsl: WorkflowDSL = {
149
+ nodes: [makeNode('s', 'switch'), makeNode('x', 'http_request')],
150
+ edges: [makeEdge('e1', 's', 'x', 'anything')],
151
+ };
152
+ expect(validateDslHandles(dsl)).toEqual([]);
153
+ });
154
+ });
155
+
156
+ // ─── out-of-scope (orphan / unknown) ────────────────────────────────────────
157
+
158
+ describe('validateDslHandles — out-of-scope', () => {
159
+ it('ignores edge referencing missing source node', () => {
160
+ const dsl: WorkflowDSL = {
161
+ nodes: [makeNode('b', 'http_request')],
162
+ edges: [makeEdge('e1', 'ghost', 'b', 'true')],
163
+ };
164
+ expect(validateDslHandles(dsl)).toEqual([]);
165
+ });
166
+
167
+ it('ignores edges from unknown node types', () => {
168
+ const dsl: WorkflowDSL = {
169
+ nodes: [makeNode('u', 'wholly_made_up_type'), makeNode('b', 'http_request')],
170
+ edges: [makeEdge('e1', 'u', 'b', 'whatever')],
171
+ };
172
+ expect(validateDslHandles(dsl)).toEqual([]);
173
+ });
174
+ });
175
+
176
+ // ─── multiple errors collected ──────────────────────────────────────────────
177
+
178
+ describe('validateDslHandles — multiple errors', () => {
179
+ it('collects every error in one pass', () => {
180
+ const dsl: WorkflowDSL = {
181
+ nodes: [
182
+ makeNode('if', 'if_else'),
183
+ makeNode('cron', 'cron_trigger'),
184
+ makeNode('sw', 'switch', { cases: [{ value: 'a' }] }),
185
+ makeNode('x', 'http_request'),
186
+ ],
187
+ edges: [
188
+ makeEdge('e1', 'if', 'x', 'maybe'), // bad enumerated
189
+ makeEdge('e2', 'if', 'x'), // missing required
190
+ makeEdge('e3', 'cron', 'x', 'foo'), // forbidden handle
191
+ makeEdge('e4', 'sw', 'x', 'c'), // bad dynamic
192
+ ],
193
+ };
194
+ const errors = validateDslHandles(dsl);
195
+ expect(errors).toHaveLength(4);
196
+ expect(errors.map((e) => e.edgeId).sort()).toEqual(['e1', 'e2', 'e3', 'e4']);
197
+ const byEdge = Object.fromEntries(errors.map((e) => [e.edgeId, e.code]));
198
+ expect(byEdge).toEqual({
199
+ e1: 'UNKNOWN_HANDLE',
200
+ e2: 'MISSING_HANDLE',
201
+ e3: 'EXTRA_HANDLE',
202
+ e4: 'UNKNOWN_HANDLE',
203
+ });
204
+ });
205
+ });
@@ -1,3 +1,4 @@
1
+ import React, { useMemo } from 'react';
1
2
  import { useGetLocale, useSetLocale, useGetIdentity } from '@refinedev/core';
2
3
  import {
3
4
  Layout,
@@ -7,14 +8,12 @@ import {
7
8
  Avatar,
8
9
  Typography,
9
10
  theme,
10
- Select,
11
11
  } from 'antd';
12
12
  import { DownOutlined } from '@ant-design/icons';
13
13
  import { useTranslation } from 'react-i18next';
14
- import { useContext } from 'react';
15
- import { BusinessContext } from 'components/contexts/business';
16
14
  import type { MenuProps } from 'antd';
17
15
  import { agentAttrs } from 'components/agentPanel/agentAttributes';
16
+ import { HEADER_HEIGHT, Z_INDEX } from 'constants/layout';
18
17
 
19
18
  type IUser = {
20
19
  id: number;
@@ -26,81 +25,81 @@ const { Text } = Typography;
26
25
  const { useToken } = theme;
27
26
 
28
27
  export const Header: React.FC = () => {
29
- const { i18n } = useTranslation();
28
+ const { i18n, t } = useTranslation();
30
29
  const locale = useGetLocale();
31
30
  const changeLanguage = useSetLocale();
32
31
  const { token } = useToken();
33
32
  const { data: user } = useGetIdentity<IUser>();
34
- const { business, setBusiness, businessList } = useContext(BusinessContext);
35
33
 
36
34
  const currentLocale = locale();
37
35
 
38
- const langMenu: MenuProps = {
39
- selectedKeys: currentLocale ? [currentLocale] : [],
40
- items: [...(i18n.languages || [])].sort().map((lang: string) => ({
41
- key: lang,
42
- icon: (
43
- <span style={{ marginRight: 8 }}>
44
- <Avatar size={16} src={`/images/flags/${lang}.svg`} />
45
- </span>
46
- ),
47
- label: lang === 'en' ? 'English' : '中文',
48
- })),
49
- onClick: ({ key: lang }) => changeLanguage(lang),
50
- };
36
+ // Sort once per language list change, not on every render.
37
+ const langMenu = useMemo<MenuProps>(
38
+ () => ({
39
+ selectedKeys: currentLocale ? [currentLocale] : [],
40
+ items: [...(i18n.languages || [])].sort().map((lang: string) => ({
41
+ key: lang,
42
+ icon: (
43
+ <span style={{ marginRight: 8, display: 'inline-flex' }}>
44
+ <Avatar size={16} src={`/images/flags/${lang}.svg`} alt="" />
45
+ </span>
46
+ ),
47
+ label: lang === 'en' ? 'English' : '中文',
48
+ })),
49
+ onClick: ({ key: lang }) => changeLanguage(lang),
50
+ }),
51
+ [i18n.languages, currentLocale, changeLanguage],
52
+ );
53
+
54
+ const currentLanguageLabel = currentLocale === 'en' ? 'English' : '中文';
51
55
 
52
56
  const headerStyles: React.CSSProperties = {
53
57
  backgroundColor: token.colorBgElevated,
54
58
  display: 'flex',
55
59
  justifyContent: 'flex-end',
56
60
  alignItems: 'center',
57
- padding: '0px 24px',
58
- height: '64px',
61
+ padding: '0 24px',
62
+ height: HEADER_HEIGHT,
59
63
  position: 'sticky',
60
64
  top: 0,
61
- zIndex: 999,
65
+ zIndex: Z_INDEX.STICKY,
66
+ // 与下方内容区切分。1px 软分隔线胜过纯白对纯白边界。
67
+ borderBottom: `1px solid ${token.colorBorderSecondary}`,
62
68
  };
63
69
 
64
70
  return (
65
71
  <Layout.Header style={headerStyles} {...agentAttrs({ type: 'app-header' })}>
66
- {/* <Select
67
- showSearch
68
- defaultValue={business.gameId}
69
- style={{ width: 160 }}
70
- variant="borderless"
71
- options={[
72
- ...businessList.map((item) => ({
73
- label: item.gameName,
74
- value: item.gameId,
75
- })),
76
- {
77
- label: '管理员专用',
78
- options: [{ value: '-1', label: 'All games' }],
79
- },
80
- ]}
81
- optionFilterProp="label"
82
- onChange={(gameId) => {
83
- const gameInfo =
84
- gameId === '-1'
85
- ? { gameId: '-1', gameName: 'All games' }
86
- : businessList.find((item) => item.gameId === gameId)!;
87
- setBusiness(gameInfo);
88
- window.location.reload();
89
- }}
90
- /> */}
91
- <Dropdown menu={langMenu}>
92
- <Button type="link">
93
- <Space>
94
- <Avatar size={16} src={`/images/flags/${currentLocale}.svg`} />
95
- {currentLocale === 'en' ? 'English' : '中文'}
96
- <DownOutlined />
72
+ <Dropdown
73
+ menu={langMenu}
74
+ placement="bottomRight"
75
+ trigger={['hover', 'click']}
76
+ >
77
+ <Button
78
+ type="text"
79
+ aria-label={t('header.changeLanguage', 'Change language')}
80
+ >
81
+ <Space size={6}>
82
+ <Avatar
83
+ size={16}
84
+ src={`/images/flags/${currentLocale}.svg`}
85
+ alt=""
86
+ />
87
+ <span>{currentLanguageLabel}</span>
88
+ <DownOutlined
89
+ style={{ fontSize: 10, color: token.colorTextTertiary }}
90
+ />
97
91
  </Space>
98
92
  </Button>
99
93
  </Dropdown>
100
- <Space style={{ marginLeft: '8px' }} size="middle">
101
- {user?.name && <Text strong>{user.name}</Text>}
102
- {user?.avatar && <Avatar src={user?.avatar} alt={user?.name} />}
103
- </Space>
94
+
95
+ {(user?.name || user?.avatar) && (
96
+ <Space size="middle" style={{ marginLeft: 16 }}>
97
+ {user?.name && <Text strong>{user.name}</Text>}
98
+ {user?.avatar && (
99
+ <Avatar src={user.avatar} alt={user?.name ?? 'User avatar'} />
100
+ )}
101
+ </Space>
102
+ )}
104
103
  </Layout.Header>
105
104
  );
106
105
  };
@@ -4,10 +4,14 @@ import { Grid, Layout as AntdLayout } from 'antd';
4
4
  import { Header } from './header';
5
5
  import { Sider } from './sider';
6
6
  import { OffLayoutArea } from 'components/offLayoutArea';
7
+ import { HEADER_HEIGHT } from 'constants/layout';
7
8
 
8
9
  export const Layout: React.FC<React.PropsWithChildren> = ({ children }) => {
9
10
  const breakpoint = Grid.useBreakpoint();
10
- const isSmall = typeof breakpoint.sm === 'undefined' ? true : breakpoint.sm;
11
+ // Ant Design 的命名里, breakpoint.sm = true 表示 viewport >= 576px
12
+ // (即 *不是* 极窄手机). 用 isAtLeastSm 命名比反语义的 isSmall 清楚得多.
13
+ // useBreakpoint 首次渲染可能返回空对象, 默认按桌面处理避免抖动.
14
+ const isAtLeastSm = breakpoint.sm ?? true;
11
15
 
12
16
  return (
13
17
  <ThemedLayoutContextProvider>
@@ -17,8 +21,8 @@ export const Layout: React.FC<React.PropsWithChildren> = ({ children }) => {
17
21
  <Header />
18
22
  <AntdLayout.Content
19
23
  style={{
20
- padding: isSmall ? 32 : 16,
21
- height: 'calc(100vh - 64px)',
24
+ padding: isAtLeastSm ? 32 : 16,
25
+ height: `calc(100vh - ${HEADER_HEIGHT}px)`,
22
26
  overflowY: 'auto',
23
27
  }}
24
28
  >
@@ -1,5 +1,9 @@
1
1
  import React from 'react';
2
2
 
3
+ /**
4
+ * App 品牌图标。fill 使用 currentColor,让父级通过 `color` 控制颜色,
5
+ * 因此可被深色 / 浅色主题、selected / disabled 等不同上下文复用。
6
+ */
3
7
  export const Logo = (props: React.SVGProps<SVGSVGElement>) => (
4
8
  <svg
5
9
  xmlns="http://www.w3.org/2000/svg"
@@ -7,10 +11,12 @@ export const Logo = (props: React.SVGProps<SVGSVGElement>) => (
7
11
  height={24}
8
12
  viewBox="0 0 24 24"
9
13
  fill="none"
14
+ role="img"
15
+ aria-hidden="true"
10
16
  {...props}
11
17
  >
12
18
  <path
13
- fill="#1677FF"
19
+ fill="currentColor"
14
20
  fillRule="evenodd"
15
21
  d="M12 24c6.627 0 12-5.373 12-12S18.627 0 12 0 0 5.373 0 12s5.373 12 12 12Zm3.744-18.41c.182-.647-.446-1.03-1.02-.621l-8.008 5.705c-.622.443-.524 1.326.147 1.326h2.109v-.016h4.11l-3.35 1.181-1.476 5.245c-.182.647.446 1.03 1.02.621l8.008-5.705c.622-.443.524-1.326-.147-1.326H13.94l1.805-6.41Z"
16
22
  clipRule="evenodd"