openxiangda-cli 2.0.0-alpha.86 → 2.0.0-alpha.90

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 (78) hide show
  1. package/README.md +2 -2
  2. package/bin/run.js +1 -0
  3. package/dist/commands/skill.d.ts +26 -0
  4. package/dist/commands/skill.d.ts.map +1 -0
  5. package/dist/commands/skill.js +43 -0
  6. package/dist/commands/skill.js.map +1 -0
  7. package/dist/create-workspace.d.ts.map +1 -1
  8. package/dist/create-workspace.js +3 -3
  9. package/dist/create-workspace.js.map +1 -1
  10. package/dist/toolchain-capsule.d.ts.map +1 -1
  11. package/dist/toolchain-capsule.js +4 -8
  12. package/dist/toolchain-capsule.js.map +1 -1
  13. package/package.json +6 -7
  14. package/template/AGENTS.md +2 -1
  15. package/template/README.md +5 -5
  16. package/template/apps/server/package.json +1 -1
  17. package/template/apps/server/src/app.module.ts +14 -1
  18. package/template/apps/server/src/main.ts +1 -1
  19. package/template/apps/server/test/smoke.test.ts +3 -1
  20. package/template/apps/web/e2e/client-fixture.ts +5 -0
  21. package/template/apps/web/e2e/data-api-live-fixture.tsx +19 -14
  22. package/template/apps/web/e2e/field-protocol-fixture.tsx +87 -20
  23. package/template/apps/web/e2e/field-protocol.spec.ts +86 -0
  24. package/template/apps/web/e2e/resources.spec.ts +20 -181
  25. package/template/apps/web/field-protocol.e2e.html +3 -0
  26. package/template/apps/web/package.json +2 -5
  27. package/template/apps/web/src/main.tsx +12 -60
  28. package/template/apps/web/test/contracts.test.ts +52 -104
  29. package/template/openxiangda.config.ts +2 -2
  30. package/template/package.json +1 -3
  31. package/template/packages/contracts/src/generated.ts +27 -0
  32. package/template/scripts/verify-template-budget.mjs +2 -2
  33. package/template/apps/web/src/AuthoritativeSelector.tsx +0 -661
  34. package/template/apps/web/src/FilePreviewPage.tsx +0 -324
  35. package/template/apps/web/src/RoleSubjectSelect.tsx +0 -128
  36. package/template/apps/web/src/Shell.tsx +0 -445
  37. package/template/apps/web/src/appearance.tsx +0 -147
  38. package/template/apps/web/src/components/platform-fields/AddressField.tsx +0 -338
  39. package/template/apps/web/src/components/platform-fields/AttachmentFileList.tsx +0 -245
  40. package/template/apps/web/src/components/platform-fields/CascadeField.tsx +0 -49
  41. package/template/apps/web/src/components/platform-fields/DateTimeField.tsx +0 -148
  42. package/template/apps/web/src/components/platform-fields/JsonField.tsx +0 -77
  43. package/template/apps/web/src/components/platform-fields/LocationField.tsx +0 -164
  44. package/template/apps/web/src/components/platform-fields/PlatformDirectoryPicker.tsx +0 -673
  45. package/template/apps/web/src/components/platform-fields/ResourceReferenceField.tsx +0 -67
  46. package/template/apps/web/src/components/platform-fields/RichTextField.tsx +0 -196
  47. package/template/apps/web/src/components/platform-fields/SignatureField.tsx +0 -315
  48. package/template/apps/web/src/components/platform-fields/SubtableField.tsx +0 -553
  49. package/template/apps/web/src/components/platform-fields/address-value.ts +0 -80
  50. package/template/apps/web/src/components/platform-fields/cascade-value.ts +0 -66
  51. package/template/apps/web/src/components/platform-fields/directory-value.ts +0 -53
  52. package/template/apps/web/src/components/platform-fields/field-form-codec.ts +0 -98
  53. package/template/apps/web/src/components/platform-fields/location-value.ts +0 -87
  54. package/template/apps/web/src/components/platform-fields/resource-query.ts +0 -131
  55. package/template/apps/web/src/components/platform-fields/rich-text-value.ts +0 -59
  56. package/template/apps/web/src/components/platform-fields/subtable-value.ts +0 -187
  57. package/template/apps/web/src/components/resource/GeneratedResourceCrud.tsx +0 -1184
  58. package/template/apps/web/src/components/resource/ResourceBatchActions.tsx +0 -326
  59. package/template/apps/web/src/components/resource/StandardResourcePages.tsx +0 -392
  60. package/template/apps/web/src/components/resource/SurfaceFields.tsx +0 -915
  61. package/template/apps/web/src/components/resource/generated-resource-definition.ts +0 -18
  62. package/template/apps/web/src/components/resource/resource-import.ts +0 -205
  63. package/template/apps/web/src/data-provider.ts +0 -72
  64. package/template/apps/web/src/platform-client.ts +0 -708
  65. package/template/apps/web/src/runtime-meta.ts +0 -75
  66. package/template/apps/web/src/runtime.tsx +0 -227
  67. package/template/apps/web/src/styles.css +0 -1941
  68. package/template/apps/web/test/address-field.test.ts +0 -66
  69. package/template/apps/web/test/cascade-value.test.ts +0 -68
  70. package/template/apps/web/test/directory-value.test.ts +0 -55
  71. package/template/apps/web/test/field-bounds.test.ts +0 -17
  72. package/template/apps/web/test/field-form-codec.test.ts +0 -85
  73. package/template/apps/web/test/location-field.test.ts +0 -83
  74. package/template/apps/web/test/resource-import.test.ts +0 -143
  75. package/template/apps/web/test/resource-query.test.ts +0 -144
  76. package/template/apps/web/test/rich-json-field.test.ts +0 -53
  77. package/template/apps/web/test/signature-serial-field.test.ts +0 -40
  78. package/template/apps/web/test/subtable-value.test.ts +0 -144
@@ -1,445 +0,0 @@
1
- import {
2
- AppstoreOutlined,
3
- DatabaseOutlined,
4
- DownOutlined,
5
- LeftOutlined,
6
- LogoutOutlined,
7
- MenuUnfoldOutlined,
8
- SettingOutlined,
9
- SwapOutlined,
10
- } from '@ant-design/icons';
11
- import {
12
- App,
13
- Avatar,
14
- Breadcrumb,
15
- Button,
16
- Dropdown,
17
- Empty,
18
- Layout,
19
- Menu,
20
- Modal,
21
- Segmented,
22
- Tabs,
23
- Typography,
24
- type MenuProps,
25
- } from 'antd';
26
- import type { ReactNode } from 'react';
27
- import { useEffect, useMemo, useState } from 'react';
28
- import { useLocation, useNavigate } from 'react-router-dom';
29
- import { resourceDefinitions } from '../../../packages/contracts/src/generated.js';
30
- import { useAppearance, type AppearancePreference } from './appearance';
31
- import { logoutCurrentUser, resolveDirectory } from './platform-client';
32
- import { RoleSubjectSelect } from './RoleSubjectSelect';
33
- import { useRuntime } from './runtime';
34
-
35
- const { Content, Header, Sider } = Layout;
36
- const HISTORY_STORAGE_KEY = 'openxiangda.admin.route-history';
37
- const MAX_HISTORY_ITEMS = 8;
38
-
39
- type Definition = {
40
- code: string;
41
- name: string;
42
- capabilities: { read: string };
43
- surface: { generated?: boolean };
44
- };
45
- type HistoryItem = { path: string; label: string };
46
-
47
- const definitions = resourceDefinitions as unknown as Record<string, Definition>;
48
-
49
- function currentDefinition(pathname: string) {
50
- return Object.values(definitions).find(
51
- definition =>
52
- pathname === `/${definition.code}` ||
53
- pathname.startsWith(`/${definition.code}/`) ||
54
- pathname === `/m/${definition.code}` ||
55
- pathname.startsWith(`/m/${definition.code}/`),
56
- );
57
- }
58
-
59
- function routeLabel(pathname: string, definition?: Definition) {
60
- if (!definition) return '应用首页';
61
- if (pathname.endsWith('/new')) return `新增${definition.name}`;
62
- if (pathname.endsWith('/edit')) return `编辑${definition.name}`;
63
- if (pathname === `/${definition.code}`) return definition.name;
64
- return `${definition.name}详情`;
65
- }
66
-
67
- function readHistory(): HistoryItem[] {
68
- try {
69
- const parsed = JSON.parse(
70
- window.sessionStorage.getItem(HISTORY_STORAGE_KEY) || '[]',
71
- );
72
- if (!Array.isArray(parsed)) return [];
73
- return parsed
74
- .filter(
75
- item =>
76
- item &&
77
- typeof item.path === 'string' &&
78
- typeof item.label === 'string',
79
- )
80
- .slice(-MAX_HISTORY_ITEMS);
81
- } catch {
82
- return [];
83
- }
84
- }
85
-
86
- function storeHistory(items: HistoryItem[]) {
87
- try {
88
- window.sessionStorage.setItem(HISTORY_STORAGE_KEY, JSON.stringify(items));
89
- } catch {
90
- // Route history is a convenience only; navigation remains authoritative.
91
- }
92
- }
93
-
94
- export function Shell({ children }: { children: ReactNode }) {
95
- const {
96
- identity,
97
- hasCapability,
98
- switchRole,
99
- switchingRole,
100
- } = useRuntime();
101
- const { effectiveAppearance, preference, setPreference } = useAppearance();
102
- const { message } = App.useApp();
103
- const location = useLocation();
104
- const navigate = useNavigate();
105
- const [collapsed, setCollapsed] = useState(false);
106
- const [loggingOut, setLoggingOut] = useState(false);
107
- const [roleModalOpen, setRoleModalOpen] = useState(false);
108
- const [selectedRoleSubjectKey, setSelectedRoleSubjectKey] = useState(
109
- identity.currentRoleSubject?.subjectKey || ''
110
- );
111
- const [history, setHistory] = useState<HistoryItem[]>(readHistory);
112
- const [user, setUser] = useState({
113
- label: '当前用户',
114
- description: '平台用户',
115
- });
116
- const definition = currentDefinition(location.pathname);
117
- const currentLabel = routeLabel(location.pathname, definition);
118
- const accessibleDefinitions = useMemo(
119
- () =>
120
- Object.values(definitions).filter(
121
- item =>
122
- item.surface.generated !== false &&
123
- hasCapability(item.capabilities.read),
124
- ),
125
- [hasCapability],
126
- );
127
- const homePath = accessibleDefinitions[0]
128
- ? `/${accessibleDefinitions[0].code}`
129
- : '/';
130
-
131
- useEffect(() => {
132
- if (identity.subjectProfile) {
133
- setUser({
134
- label: identity.subjectProfile.displayName || '当前用户',
135
- description:
136
- identity.subjectProfile.affiliatedDepartment?.name || '平台用户',
137
- });
138
- return;
139
- }
140
- let active = true;
141
- void resolveDirectory('user', [identity.userId])
142
- .then(page => {
143
- const entry = page.items[0];
144
- if (active && entry) {
145
- setUser({
146
- label: entry.label || '当前用户',
147
- description: entry.description || '平台用户',
148
- });
149
- }
150
- })
151
- .catch(() => undefined);
152
- return () => {
153
- active = false;
154
- };
155
- }, [identity.subjectProfile, identity.userId]);
156
-
157
- useEffect(() => {
158
- if (!definition || location.pathname.startsWith('/m/')) return;
159
- setHistory(current => {
160
- const next = [
161
- ...current.filter(item => item.path !== location.pathname),
162
- { path: location.pathname, label: currentLabel },
163
- ].slice(-MAX_HISTORY_ITEMS);
164
- storeHistory(next);
165
- return next;
166
- });
167
- }, [currentLabel, definition, location.pathname]);
168
-
169
- const menuItems = useMemo<MenuProps['items']>(
170
- () => [
171
- {
172
- key: 'overview',
173
- icon: <AppstoreOutlined />,
174
- label: '总览',
175
- children: [{ key: 'home', label: '首页' }],
176
- },
177
- {
178
- key: 'resources',
179
- icon: <DatabaseOutlined />,
180
- label: '数据管理',
181
- children: accessibleDefinitions.map(item => ({
182
- key: `/${item.code}`,
183
- icon: <DatabaseOutlined />,
184
- label: item.name,
185
- })),
186
- },
187
- {
188
- key: 'settings',
189
- icon: <SettingOutlined />,
190
- label: '系统设置',
191
- children: [
192
- { key: 'settings-disabled', label: '应用设置', disabled: true },
193
- ],
194
- },
195
- ],
196
- [accessibleDefinitions],
197
- );
198
-
199
- const closeHistory = (targetPath: string) => {
200
- const targetIndex = history.findIndex(item => item.path === targetPath);
201
- const remaining = history.filter(item => item.path !== targetPath);
202
- storeHistory(remaining);
203
- setHistory(remaining);
204
- if (targetPath !== location.pathname) return;
205
- const fallback =
206
- remaining[Math.min(Math.max(targetIndex, 0), remaining.length - 1)] ||
207
- remaining.at(-1);
208
- navigate(fallback?.path || homePath);
209
- };
210
-
211
- const handleLogout = async () => {
212
- setLoggingOut(true);
213
- try {
214
- await logoutCurrentUser();
215
- window.location.assign('/login');
216
- } catch (error) {
217
- message.error(error instanceof Error ? error.message : '退出登录失败');
218
- setLoggingOut(false);
219
- }
220
- };
221
-
222
- const handleRoleSwitch = async () => {
223
- if (
224
- !selectedRoleSubjectKey ||
225
- selectedRoleSubjectKey === identity.currentRoleSubject?.subjectKey
226
- ) {
227
- return;
228
- }
229
- try {
230
- await switchRole(selectedRoleSubjectKey);
231
- setRoleModalOpen(false);
232
- message.success('应用角色已切换');
233
- } catch (reason) {
234
- message.warning(
235
- reason instanceof Error
236
- ? `${reason.message};已重新读取当前角色`
237
- : '应用角色上下文已变化,请重试'
238
- );
239
- }
240
- };
241
-
242
- const currentRoleLabel =
243
- identity.currentRoleSubject?.role.name ||
244
- (identity.roleSwitchingAvailable
245
- ? identity.roleCodes.join('、') || '普通成员'
246
- : '开发者联合权限');
247
-
248
- const userMenu: MenuProps = {
249
- items: [
250
- ...(identity.roleSwitchingAvailable &&
251
- (identity.roleSubjectPage?.total || 0) > 1
252
- ? [
253
- {
254
- icon: <SwapOutlined />,
255
- key: 'switch-role',
256
- label: '切换应用角色',
257
- } as NonNullable<MenuProps['items']>[number],
258
- ]
259
- : []),
260
- { type: 'divider' },
261
- {
262
- danger: true,
263
- icon: <LogoutOutlined />,
264
- key: 'logout',
265
- label: loggingOut ? '正在退出…' : '退出登录',
266
- },
267
- ],
268
- onClick: ({ key }) => {
269
- if (key === 'switch-role') {
270
- setSelectedRoleSubjectKey(
271
- identity.currentRoleSubject?.subjectKey || ''
272
- );
273
- setRoleModalOpen(true);
274
- }
275
- if (key === 'logout' && !loggingOut) void handleLogout();
276
- },
277
- };
278
-
279
- return (
280
- <Layout className="oxa-app-layout">
281
- <Sider
282
- className="oxa-sider"
283
- collapsed={collapsed}
284
- collapsedWidth={64}
285
- collapsible
286
- onBreakpoint={setCollapsed}
287
- theme={effectiveAppearance}
288
- trigger={null}
289
- width={228}
290
- >
291
- <div className="oxa-brand">
292
- <div aria-hidden className="oxa-brand-mark">
293
- <span />
294
- </div>
295
- {!collapsed && (
296
- <div className="oxa-brand-copy">
297
- <strong>OpenXiangda 应用</strong>
298
- <span>标准资源管理工作台</span>
299
- </div>
300
- )}
301
- </div>
302
- <Menu
303
- className="oxa-menu"
304
- defaultOpenKeys={['overview', 'resources', 'settings']}
305
- inlineCollapsed={collapsed}
306
- items={menuItems}
307
- mode="inline"
308
- onClick={({ key }) => {
309
- if (key === 'home') navigate(homePath);
310
- else if (typeof key === 'string' && key.startsWith('/')) navigate(key);
311
- }}
312
- selectedKeys={definition ? [`/${definition.code}`] : []}
313
- theme={effectiveAppearance}
314
- />
315
- <div className="oxa-sider-footer">
316
- <Button
317
- aria-label={collapsed ? '展开侧边栏' : '收起侧边栏'}
318
- block
319
- icon={collapsed ? <MenuUnfoldOutlined /> : <LeftOutlined />}
320
- onClick={() => setCollapsed(value => !value)}
321
- type="text"
322
- >
323
- {!collapsed && '收起侧边栏'}
324
- </Button>
325
- </div>
326
- </Sider>
327
- <Layout className="oxa-workspace">
328
- <Header className="oxa-topbar">
329
- <Breadcrumb
330
- items={[
331
- { title: '首页', onClick: () => navigate(homePath) },
332
- ...(definition ? [{ title: currentLabel }] : []),
333
- ]}
334
- />
335
- <Dropdown
336
- menu={userMenu}
337
- popupRender={menus => (
338
- <div className="oxa-user-dropdown">
339
- <div className="oxa-user-dropdown-profile">
340
- <Avatar className="oxa-current-user-avatar" size={40}>
341
- {user.label.slice(0, 1)}
342
- </Avatar>
343
- <div>
344
- <strong>{user.label}</strong>
345
- <span>{user.description}</span>
346
- <small>{identity.userId}</small>
347
- </div>
348
- </div>
349
- <div className="oxa-user-dropdown-roles">
350
- <span>当前应用角色</span>
351
- <Typography.Text ellipsis>
352
- {currentRoleLabel}
353
- </Typography.Text>
354
- </div>
355
- <div className="oxa-user-dropdown-theme">
356
- <span>外观</span>
357
- <Segmented
358
- block
359
- onChange={value =>
360
- setPreference(value as AppearancePreference)
361
- }
362
- options={[
363
- { label: '跟随系统', value: 'system' },
364
- { label: '浅色', value: 'light' },
365
- { label: '深色', value: 'dark' },
366
- ]}
367
- size="small"
368
- value={preference}
369
- />
370
- </div>
371
- {menus}
372
- </div>
373
- )}
374
- trigger={['hover', 'click']}
375
- >
376
- <button className="oxa-current-user" type="button">
377
- <Avatar className="oxa-current-user-avatar" size={30}>
378
- {user.label.slice(0, 1)}
379
- </Avatar>
380
- <strong>{user.label}</strong>
381
- <DownOutlined className="oxa-current-user-chevron" />
382
- </button>
383
- </Dropdown>
384
- </Header>
385
- {history.length > 0 && (
386
- <nav aria-label="页面访问历史" className="oxa-route-history">
387
- <Tabs
388
- activeKey={location.pathname}
389
- hideAdd
390
- items={history.map(item => ({
391
- closable: history.length > 1,
392
- key: item.path,
393
- label: item.label,
394
- }))}
395
- onChange={navigate}
396
- onEdit={(target, action) => {
397
- if (action === 'remove') closeHistory(String(target));
398
- }}
399
- size="small"
400
- type="editable-card"
401
- />
402
- </nav>
403
- )}
404
- <Content className="oxa-content">
405
- <main className="oxa-main">{children}</main>
406
- </Content>
407
- </Layout>
408
- <Modal
409
- cancelText="取消"
410
- centered
411
- destroyOnHidden
412
- okButtonProps={{
413
- disabled:
414
- !selectedRoleSubjectKey ||
415
- selectedRoleSubjectKey === identity.currentRoleSubject?.subjectKey,
416
- }}
417
- okText="切换角色"
418
- onCancel={() => setRoleModalOpen(false)}
419
- onOk={() => void handleRoleSwitch()}
420
- open={roleModalOpen}
421
- confirmLoading={switchingRole}
422
- title="切换当前应用角色"
423
- >
424
- {identity.roleSubjectPage && (
425
- <RoleSubjectSelect
426
- initialPage={identity.roleSubjectPage}
427
- onChange={setSelectedRoleSubjectKey}
428
- value={selectedRoleSubjectKey || undefined}
429
- />
430
- )}
431
- <Typography.Paragraph className="oxa-role-switch-help" type="secondary">
432
- 切换后页面、数据和操作权限会按目标角色重新加载。
433
- </Typography.Paragraph>
434
- </Modal>
435
- </Layout>
436
- );
437
- }
438
-
439
- export function EmptyApplicationPage() {
440
- return (
441
- <Shell>
442
- <Empty description="还没有声明数据资源;请在 openxiangda.config.ts 中添加资源和字段" />
443
- </Shell>
444
- );
445
- }
@@ -1,147 +0,0 @@
1
- import {
2
- App as AntdApp,
3
- ConfigProvider,
4
- theme as antdTheme,
5
- type ThemeConfig,
6
- } from 'antd';
7
- import zhCN from 'antd/locale/zh_CN';
8
- import {
9
- createContext,
10
- useContext,
11
- useEffect,
12
- useLayoutEffect,
13
- useMemo,
14
- useState,
15
- type ReactNode,
16
- } from 'react';
17
-
18
- export type AppearancePreference = 'system' | 'light' | 'dark';
19
- export type EffectiveAppearance = 'light' | 'dark';
20
-
21
- const STORAGE_KEY = 'openxiangda.admin.appearance';
22
- const AppearanceContext = createContext<{
23
- preference: AppearancePreference;
24
- effectiveAppearance: EffectiveAppearance;
25
- setPreference: (value: AppearancePreference) => void;
26
- } | null>(null);
27
-
28
- function storedPreference(): AppearancePreference {
29
- try {
30
- const value = window.localStorage.getItem(STORAGE_KEY);
31
- return value === 'light' || value === 'dark' || value === 'system'
32
- ? value
33
- : 'system';
34
- } catch {
35
- return 'system';
36
- }
37
- }
38
-
39
- function systemPrefersDark() {
40
- return window.matchMedia?.('(prefers-color-scheme: dark)').matches ?? false;
41
- }
42
-
43
- function PlatformThemeCssVariables() {
44
- const { token } = antdTheme.useToken();
45
- useLayoutEffect(() => {
46
- const root = document.documentElement;
47
- const variables: Record<string, string> = {
48
- '--oxa-shell-bg': token.colorBgLayout,
49
- '--oxa-shell-surface': token.colorBgContainer,
50
- '--oxa-shell-elevated': token.colorBgElevated,
51
- '--oxa-shell-subtle': token.colorFillAlter,
52
- '--oxa-shell-hover': token.colorFillSecondary,
53
- '--oxa-shell-border': token.colorBorderSecondary,
54
- '--oxa-shell-border-strong': token.colorBorder,
55
- '--oxa-shell-text': token.colorText,
56
- '--oxa-shell-text-secondary': token.colorTextSecondary,
57
- '--oxa-shell-text-tertiary': token.colorTextTertiary,
58
- '--oxa-shell-text-disabled': token.colorTextDisabled,
59
- '--oxa-shell-primary': token.colorPrimary,
60
- '--oxa-shell-primary-bg': token.colorPrimaryBg,
61
- '--oxa-shell-primary-border': token.colorPrimaryBorder,
62
- '--oxa-shell-error': token.colorError,
63
- '--oxa-shell-error-bg': token.colorErrorBg,
64
- '--oxa-shell-error-border': token.colorErrorBorder,
65
- '--oxa-shell-mask': token.colorBgMask,
66
- '--oxa-shell-shadow': token.boxShadowTertiary,
67
- };
68
- for (const [name, value] of Object.entries(variables)) {
69
- root.style.setProperty(name, value);
70
- }
71
- }, [token]);
72
- return null;
73
- }
74
-
75
- export function AppearanceProvider({ children }: { children: ReactNode }) {
76
- const [preference, setPreferenceState] = useState<AppearancePreference>(
77
- storedPreference,
78
- );
79
- const [systemDark, setSystemDark] = useState(systemPrefersDark);
80
-
81
- useEffect(() => {
82
- const query = window.matchMedia('(prefers-color-scheme: dark)');
83
- const onChange = (event: MediaQueryListEvent) => setSystemDark(event.matches);
84
- query.addEventListener('change', onChange);
85
- return () => query.removeEventListener('change', onChange);
86
- }, []);
87
-
88
- const effectiveAppearance: EffectiveAppearance =
89
- preference === 'system' ? (systemDark ? 'dark' : 'light') : preference;
90
-
91
- useLayoutEffect(() => {
92
- document.documentElement.dataset.oxaTheme = effectiveAppearance;
93
- document.documentElement.style.colorScheme = effectiveAppearance;
94
- }, [effectiveAppearance]);
95
-
96
- const setPreference = (value: AppearancePreference) => {
97
- setPreferenceState(value);
98
- try {
99
- window.localStorage.setItem(STORAGE_KEY, value);
100
- } catch {
101
- // A blocked storage API must not prevent the in-memory theme switch.
102
- }
103
- };
104
-
105
- const theme = useMemo<ThemeConfig>(
106
- () => ({
107
- algorithm:
108
- effectiveAppearance === 'dark'
109
- ? antdTheme.darkAlgorithm
110
- : antdTheme.defaultAlgorithm,
111
- cssVar: { key: effectiveAppearance, prefix: 'oxa' },
112
- token: {
113
- borderRadius: 6,
114
- colorPrimary: '#1677ff',
115
- fontSize: 14,
116
- },
117
- components: {
118
- Card: { headerFontSize: 16 },
119
- Layout: { headerHeight: 52 },
120
- Menu: { itemBorderRadius: 6, itemHeight: 40 },
121
- },
122
- }),
123
- [effectiveAppearance],
124
- );
125
-
126
- const value = useMemo(
127
- () => ({ preference, effectiveAppearance, setPreference }),
128
- [effectiveAppearance, preference],
129
- );
130
-
131
- return (
132
- <AppearanceContext.Provider value={value}>
133
- <ConfigProvider componentSize="medium" locale={zhCN} theme={theme}>
134
- <AntdApp>
135
- <PlatformThemeCssVariables />
136
- {children}
137
- </AntdApp>
138
- </ConfigProvider>
139
- </AppearanceContext.Provider>
140
- );
141
- }
142
-
143
- export function useAppearance() {
144
- const value = useContext(AppearanceContext);
145
- if (!value) throw new Error('OPENXIANGDA_APPEARANCE_NOT_READY');
146
- return value;
147
- }