@jiangood/open-admin 3.1.6 → 3.1.7

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 (41) hide show
  1. package/package.json +2 -5
  2. package/src/framework/biz/UserPicker.less +17 -0
  3. package/src/framework/biz/UserPicker.tsx +296 -0
  4. package/src/framework/biz/index.tsx +2 -0
  5. package/src/framework/biz/pickerUser.ts +19 -0
  6. package/src/framework/components/ContextMenu/index.tsx +11 -8
  7. package/src/framework/components/DownloadModal/index.tsx +4 -4
  8. package/src/framework/components/ErrorBoundary/index.tsx +1 -1
  9. package/src/framework/components/OrgSwitcher/index.tsx +1 -1
  10. package/src/framework/components/ProTable/index.less +26 -8
  11. package/src/framework/components/ProTable/index.tsx +9 -3
  12. package/src/framework/components/ThemeSettings/index.tsx +173 -0
  13. package/src/framework/components/index.ts +1 -0
  14. package/src/framework/config/index.ts +44 -2
  15. package/src/framework/config/layoutConfig.ts +52 -0
  16. package/src/framework/config/themeConfig.ts +449 -14
  17. package/src/framework/fields/FieldEditor/index.tsx +2 -2
  18. package/src/framework/fields/FieldUploadFile/index.tsx +3 -3
  19. package/src/framework/fields/FieldUploadImage/index.tsx +15 -15
  20. package/src/framework/fields/FieldUserPicker/index.tsx +124 -0
  21. package/src/framework/fields/index.ts +1 -0
  22. package/src/framework/utils/HttpClient.ts +15 -4
  23. package/src/framework/utils/MessageApi.ts +16 -3
  24. package/src/framework/views/ViewText/index.tsx +1 -1
  25. package/src/layouts/admin/AdminTabs.less +11 -11
  26. package/src/layouts/admin/HeaderRight.jsx +23 -12
  27. package/src/layouts/admin/SinglePageLayout.jsx +36 -0
  28. package/src/layouts/admin/TabLayout.jsx +21 -1
  29. package/src/layouts/admin/index.jsx +45 -25
  30. package/src/layouts/admin/index.less +43 -23
  31. package/src/layouts/index.tsx +29 -4
  32. package/src/pages/article/article.less +13 -13
  33. package/src/pages/public/login.jsx +29 -15
  34. package/src/pages/public/login.less +243 -22
  35. package/src/pages/standalone/forceUpdatePwd.jsx +8 -7
  36. package/src/pages/system/codegen/index.jsx +56 -51
  37. package/src/pages/system/job/index.jsx +44 -7
  38. package/src/pages/system/role/index.jsx +17 -43
  39. package/src/pages/system/user/index.jsx +0 -4
  40. package/src/pages/system/user/userPerm.jsx +3 -3
  41. package/src/style/global.less +6 -6
@@ -1,49 +1,401 @@
1
1
  import { theme } from 'antd';
2
2
  import type { ThemeConfig } from 'antd';
3
+ import { EventBus } from '../utils/EventBus';
3
4
 
4
5
  const { getDesignToken } = theme;
5
6
 
7
+ export type ThemeMode = 'light' | 'dark';
8
+
6
9
  export interface ThemeColors {
7
10
  colorPrimary?: string;
8
11
  colorSuccess?: string;
9
12
  colorWarning?: string;
10
13
  colorError?: string;
14
+ colorInfo?: string;
11
15
  colorBgLayout?: string;
16
+ /** 侧栏底色(深色系);不设置时使用 antd 默认值 */
17
+ siderBg?: string;
18
+ /** 侧栏子菜单底色;缺省与 siderBg 相同 */
19
+ siderSubBg?: string;
20
+ /** 侧栏菜单项悬停底色;缺省 antd 默认(transparent) */
21
+ siderHoverBg?: string;
22
+ /** 侧栏折叠触发条底色;不设置时使用 antd 默认值 */
23
+ siderTriggerBg?: string;
24
+ }
25
+
26
+ export interface ThemePreset {
27
+ key: string;
28
+ name: string;
29
+ colors: ThemeColors;
12
30
  }
13
31
 
14
- export const DEFAULT_COLORS: Required<ThemeColors> = {
15
- colorPrimary: '#1961AC',
32
+ export type ThemeDensity = 'default' | 'compact';
33
+ export type ThemeRadius = 'sharp' | 'default' | 'round';
34
+
35
+ export interface ThemeDensityPreset {
36
+ key: ThemeDensity;
37
+ name: string;
38
+ /** 是否叠加 antd 紧凑算法 */
39
+ compact?: boolean;
40
+ /** 追加的 Seed/Map Token 覆盖 */
41
+ token?: Record<string, unknown>;
42
+ }
43
+
44
+ export interface ThemeRadiusPreset {
45
+ key: ThemeRadius;
46
+ name: string;
47
+ token: Record<string, unknown>;
48
+ }
49
+
50
+ /** 框架默认颜色,回归 antd 原生主题(不再覆盖主色) */
51
+ export const DEFAULT_COLORS: ThemeColors = {
52
+ colorPrimary: '#1677ff',
16
53
  colorSuccess: '#52c41a',
17
54
  colorWarning: '#faad14',
18
55
  colorError: '#ff4d4f',
56
+ colorInfo: '#1677ff',
19
57
  colorBgLayout: '#f5f5f5',
20
58
  };
21
59
 
22
- let overrideColors: Partial<ThemeColors> | null = null;
60
+ export const DEFAULT_PRESET_KEY = 'antd';
61
+
62
+ /** 侧栏默认底色(预设未指定 siderBg 时的预览/回退值) */
63
+ export const DEFAULT_SIDER_BG = '#001529';
64
+ /** 侧栏折叠触发条默认底色 */
65
+ export const DEFAULT_SIDER_TRIGGER_BG = '#002140';
66
+
67
+ /** 界面密度预设:默认保持 antd 原生,紧凑叠加 compactAlgorithm */
68
+ export const DENSITY_PRESETS: ThemeDensityPreset[] = [
69
+ { key: 'default', name: '默认' },
70
+ {
71
+ key: 'compact',
72
+ name: '紧凑',
73
+ compact: true,
74
+ token: { sizeUnit: 4, sizeStep: 4, controlHeight: 28, fontSize: 13 },
75
+ },
76
+ ];
77
+
78
+ export const DEFAULT_DENSITY_KEY: ThemeDensity = 'default';
79
+
80
+ /** 圆角预设:默认回归 antd 原生 6 */
81
+ export const RADIUS_PRESETS: ThemeRadiusPreset[] = [
82
+ { key: 'default', name: '默认', token: { borderRadius: 6 } },
83
+ { key: 'sharp', name: '直角', token: { borderRadius: 0 } },
84
+ { key: 'round', name: '圆润', token: { borderRadius: 10 } },
85
+ ];
86
+
87
+ export const DEFAULT_RADIUS_KEY: ThemeRadius = 'default';
88
+
89
+ /** 可选主题预设;antd 预设不做任何颜色覆盖,保持 100% 原生观感 */
90
+ export const THEME_PRESETS: ThemePreset[] = [
91
+ {
92
+ key: 'antd',
93
+ name: 'Ant Design 默认',
94
+ colors: {},
95
+ },
96
+ {
97
+ key: 'cyan',
98
+ name: '青碧',
99
+ colors: {
100
+ colorPrimary: '#13c2c2',
101
+ siderBg: '#0C2E2E',
102
+ siderSubBg: '#0C2E2E',
103
+ siderTriggerBg: '#082222',
104
+ siderHoverBg: 'rgba(255,255,255,0.06)',
105
+ },
106
+ },
107
+ {
108
+ key: 'green',
109
+ name: '翠竹',
110
+ colors: {
111
+ colorPrimary: '#389e0d',
112
+ siderBg: '#10291A',
113
+ siderSubBg: '#10291A',
114
+ siderTriggerBg: '#0B1E13',
115
+ siderHoverBg: 'rgba(255,255,255,0.06)',
116
+ },
117
+ },
118
+ {
119
+ key: 'purple',
120
+ name: '紫罗兰',
121
+ colors: {
122
+ colorPrimary: '#722ed1',
123
+ siderBg: '#1E1440',
124
+ siderSubBg: '#1E1440',
125
+ siderTriggerBg: '#170F31',
126
+ siderHoverBg: 'rgba(255,255,255,0.06)',
127
+ },
128
+ },
129
+ {
130
+ key: 'red',
131
+ name: '中国红',
132
+ colors: {
133
+ colorPrimary: '#cf1322',
134
+ siderBg: '#2A1418',
135
+ siderSubBg: '#2A1418',
136
+ siderTriggerBg: '#201014',
137
+ siderHoverBg: 'rgba(255,255,255,0.06)',
138
+ },
139
+ },
140
+ {
141
+ key: 'orange',
142
+ name: '暖橙',
143
+ colors: {
144
+ colorPrimary: '#d46b08',
145
+ siderBg: '#2A1D10',
146
+ siderSubBg: '#2A1D10',
147
+ siderTriggerBg: '#20160C',
148
+ siderHoverBg: 'rgba(255,255,255,0.06)',
149
+ },
150
+ },
151
+ {
152
+ key: 'graphite',
153
+ name: '石墨灰',
154
+ colors: {
155
+ colorPrimary: '#2f3542',
156
+ siderBg: '#1B2026',
157
+ siderSubBg: '#1B2026',
158
+ siderTriggerBg: '#141A20',
159
+ siderHoverBg: 'rgba(255,255,255,0.06)',
160
+ },
161
+ },
162
+ ];
163
+
164
+ const THEME_MODE_KEY = 'oa-theme-mode';
165
+ const THEME_PRESET_KEY = 'oa-theme-preset';
166
+ const THEME_DENSITY_KEY = 'oa-theme-density';
167
+ const THEME_RADIUS_KEY = 'oa-theme-radius';
168
+
169
+ /** 业务侧传入的颜色基线(由 setThemeColors 设置) */
170
+ let businessColors: Partial<ThemeColors> | null = null;
171
+ /** 用户选择的预设 key(持久化到 localStorage) */
172
+ let presetKey: string = readStoredPreset();
173
+ let densityKey: ThemeDensity = readStoredDensity();
174
+ let radiusKey: ThemeRadius = readStoredRadius();
175
+ let currentMode: ThemeMode = readStoredMode();
23
176
  let cachedConfig: ThemeConfig | null = null;
177
+ let cachedSiderConfig: ThemeConfig | null = null;
24
178
  let cachedToken: Record<string, unknown> | null = null;
25
179
 
180
+ function readStoredMode(): ThemeMode {
181
+ if (typeof window === 'undefined') return 'light';
182
+ try {
183
+ return localStorage.getItem(THEME_MODE_KEY) === 'dark' ? 'dark' : 'light';
184
+ } catch {
185
+ return 'light';
186
+ }
187
+ }
188
+
189
+ function readStoredPreset(): string {
190
+ if (typeof window === 'undefined') return DEFAULT_PRESET_KEY;
191
+ try {
192
+ const key = localStorage.getItem(THEME_PRESET_KEY);
193
+ if (key && THEME_PRESETS.some(p => p.key === key)) return key;
194
+ } catch {
195
+ // 隐私模式下 localStorage 不可用,忽略
196
+ }
197
+ return DEFAULT_PRESET_KEY;
198
+ }
199
+
200
+ function persistPreset(key: string) {
201
+ if (typeof window === 'undefined') return;
202
+ try {
203
+ localStorage.setItem(THEME_PRESET_KEY, key);
204
+ } catch {
205
+ // 忽略
206
+ }
207
+ }
208
+
209
+ function readStoredDensity(): ThemeDensity {
210
+ if (typeof window === 'undefined') return DEFAULT_DENSITY_KEY;
211
+ try {
212
+ const key = localStorage.getItem(THEME_DENSITY_KEY);
213
+ if (key && DENSITY_PRESETS.some(p => p.key === key)) return key as ThemeDensity;
214
+ } catch {
215
+ // 隐私模式下 localStorage 不可用,忽略
216
+ }
217
+ return DEFAULT_DENSITY_KEY;
218
+ }
219
+
220
+ function readStoredRadius(): ThemeRadius {
221
+ if (typeof window === 'undefined') return DEFAULT_RADIUS_KEY;
222
+ try {
223
+ const key = localStorage.getItem(THEME_RADIUS_KEY);
224
+ if (key && RADIUS_PRESETS.some(p => p.key === key)) return key as ThemeRadius;
225
+ } catch {
226
+ // 隐私模式下 localStorage 不可用,忽略
227
+ }
228
+ return DEFAULT_RADIUS_KEY;
229
+ }
230
+
231
+ function writeStored(key: string, value: string) {
232
+ if (typeof window === 'undefined') return;
233
+ try {
234
+ localStorage.setItem(key, value);
235
+ } catch {
236
+ // 忽略
237
+ }
238
+ }
239
+
240
+ function getActiveDensity(): ThemeDensityPreset | undefined {
241
+ return DENSITY_PRESETS.find(p => p.key === densityKey);
242
+ }
243
+
244
+ function getActiveRadius(): ThemeRadiusPreset | undefined {
245
+ return RADIUS_PRESETS.find(p => p.key === radiusKey);
246
+ }
247
+
248
+ /** 尺寸维度 Token 合并:密度 ← 圆角(用户显式选择优先) */
249
+ function getMergedSizeToken(): Record<string, unknown> {
250
+ return {
251
+ ...(getActiveDensity()?.token || {}),
252
+ ...(getActiveRadius()?.token || {}),
253
+ };
254
+ }
255
+
256
+ function getActivePreset(): ThemePreset | undefined {
257
+ return THEME_PRESETS.find(p => p.key === presetKey);
258
+ }
259
+
260
+ /** 合并优先级:antd 默认 ← 业务基线 ← 用户预设 */
261
+ function getMergedColors(): ThemeColors {
262
+ return {
263
+ ...DEFAULT_COLORS,
264
+ ...(businessColors || {}),
265
+ ...(getActivePreset()?.colors || {}),
266
+ };
267
+ }
268
+
269
+ function hasExplicitBgLayout(): boolean {
270
+ return businessColors?.colorBgLayout != null || getActivePreset()?.colors.colorBgLayout != null;
271
+ }
272
+
273
+ function invalidate() {
274
+ cachedConfig = null;
275
+ cachedToken = null;
276
+ cachedSiderConfig = null;
277
+ }
278
+
279
+ function buildAlgorithm(): ThemeConfig['algorithm'] {
280
+ const algorithms = [currentMode === 'dark' ? theme.darkAlgorithm : theme.defaultAlgorithm];
281
+ if (getActiveDensity()?.compact) {
282
+ algorithms.push(theme.compactAlgorithm);
283
+ }
284
+ return algorithms;
285
+ }
286
+
26
287
  function buildConfig(): ThemeConfig {
27
- const token: Record<string, unknown> = { borderRadius: 4, ...DEFAULT_COLORS, ...overrideColors };
288
+ const merged = getMergedColors();
289
+ const algorithm = buildAlgorithm();
290
+ const token: Record<string, unknown> = {
291
+ ...getMergedSizeToken(),
292
+ colorPrimary: merged.colorPrimary,
293
+ colorSuccess: merged.colorSuccess,
294
+ colorWarning: merged.colorWarning,
295
+ colorError: merged.colorError,
296
+ colorInfo: merged.colorInfo,
297
+ };
298
+ // colorBgLayout 仅浅色模式默认覆盖;暗色模式交给算法推导,避免被强制成浅灰。
299
+ // 业务/预设显式传入 colorBgLayout 时两种模式都尊重。
300
+ if (currentMode === 'light' || hasExplicitBgLayout()) {
301
+ token.colorBgLayout = merged.colorBgLayout;
302
+ }
303
+
304
+ const resolved = getDesignToken({ token, algorithm } as ThemeConfig);
28
305
  const components: Record<string, unknown> = {
29
- Layout: { headerBg: 'white', triggerHeight: 32 },
306
+ // 顶栏用容器色,跟随明/暗主题
307
+ Layout: { headerBg: resolved.colorBgContainer, triggerHeight: 32 },
30
308
  };
31
309
 
32
- return { token, components };
310
+ return { token, algorithm, components } as ThemeConfig;
311
+ }
312
+
313
+ /**
314
+ * 侧栏专用主题:单独套一层暗色算法,使分割线、边框、填充等按暗色派生,
315
+ * 避免浅色算法下色值不可见;同时按预设覆盖侧栏底色。
316
+ */
317
+ export function getSiderThemeConfig(): ThemeConfig {
318
+ if (cachedSiderConfig) return cachedSiderConfig;
319
+
320
+ const merged = getMergedColors();
321
+ const token: Record<string, unknown> = {
322
+ ...getMergedSizeToken(),
323
+ colorPrimary: merged.colorPrimary,
324
+ };
325
+ const layout: Record<string, unknown> = {};
326
+ const menu: Record<string, unknown> = {
327
+ darkItemSelectedBg: merged.colorPrimary,
328
+ };
329
+
330
+ if (merged.siderBg) {
331
+ layout.siderBg = merged.siderBg;
332
+ menu.darkItemBg = merged.siderBg;
333
+ menu.darkSubMenuItemBg = merged.siderSubBg || merged.siderBg;
334
+ menu.darkItemHoverBg = merged.siderHoverBg || 'rgba(255,255,255,0.06)';
335
+ }
336
+ if (merged.siderTriggerBg) {
337
+ layout.triggerBg = merged.siderTriggerBg;
338
+ }
339
+
340
+ const siderAlgorithms = [theme.darkAlgorithm];
341
+ if (getActiveDensity()?.compact) {
342
+ siderAlgorithms.push(theme.compactAlgorithm);
343
+ }
344
+
345
+ cachedSiderConfig = {
346
+ algorithm: siderAlgorithms,
347
+ inherit: true,
348
+ token,
349
+ components: { Layout: layout, Menu: menu },
350
+ } as ThemeConfig;
351
+ return cachedSiderConfig;
352
+ }
353
+
354
+ function setVar(el: HTMLElement, name: string, value: unknown) {
355
+ if (value == null) return;
356
+ el.style.setProperty(name, String(value));
357
+ }
358
+
359
+ /** 像素类 Token 以数值返回(如 32),需补 px 后写入,避免 line-height 被当作倍数 */
360
+ function setPxVar(el: HTMLElement, name: string, value: unknown) {
361
+ if (value == null) return;
362
+ const str = typeof value === 'number' ? `${value}px` : String(value);
363
+ el.style.setProperty(name, str);
33
364
  }
34
365
 
35
366
  function syncCssVars(token: Record<string, unknown>) {
36
367
  if (typeof document === 'undefined') return;
368
+ const merged = getMergedColors();
37
369
  const el = document.documentElement;
38
- el.style.setProperty('--primary-color', token.colorPrimary);
39
- el.style.setProperty('--primary-color-hover', token.colorPrimaryHover || token.colorPrimary);
370
+ setVar(el, '--primary-color', token.colorPrimary);
371
+ setVar(el, '--primary-color-hover', token.colorPrimaryHover || token.colorPrimary);
372
+ // 供 less 中替代硬编码颜色,使自定义样式跟随明/暗主题
373
+ setVar(el, '--oa-color-bg-container', token.colorBgContainer);
374
+ setVar(el, '--oa-color-bg-layout', token.colorBgLayout);
375
+ setVar(el, '--oa-color-border', token.colorBorderSecondary ?? token.colorBorder);
376
+ setVar(el, '--oa-color-text', token.colorText);
377
+ setVar(el, '--oa-color-text-secondary', token.colorTextSecondary);
378
+ setVar(el, '--oa-color-text-tertiary', token.colorTextTertiary);
379
+ setVar(el, '--oa-color-fill', token.colorFillTertiary);
380
+ setVar(el, '--oa-color-primary-bg', token.colorPrimaryBg);
381
+ setVar(el, '--oa-color-error', token.colorError);
382
+ // 尺寸/圆角变量,供 less 复用,使自定义样式跟随密度与圆角预设
383
+ setPxVar(el, '--oa-border-radius', token.borderRadius);
384
+ setPxVar(el, '--oa-control-height', token.controlHeight);
385
+ setPxVar(el, '--oa-font-size', token.fontSize);
386
+ // 侧栏相关变量
387
+ setVar(el, '--oa-sider-bg', merged.siderBg || DEFAULT_SIDER_BG);
388
+ setVar(el, '--oa-sider-text', 'rgba(255,255,255,0.85)');
389
+ setVar(el, '--oa-sider-hover-bg', merged.siderHoverBg || 'transparent');
390
+ setVar(el, '--oa-sider-trigger-bg', merged.siderTriggerBg || DEFAULT_SIDER_TRIGGER_BG);
391
+ el.dataset.oaTheme = currentMode;
392
+ el.style.colorScheme = currentMode;
40
393
  }
41
394
 
42
- /** 由 Layouts 在渲染前调用,传入业务侧颜色覆盖;不传则恢复默认主题 */
395
+ /** 由 Layouts 在渲染前调用,传入业务侧颜色基线;不传则恢复 antd 默认主题 */
43
396
  export function setThemeColors(colors?: Partial<ThemeColors>) {
44
- overrideColors = colors || null;
45
- cachedConfig = null;
46
- cachedToken = null;
397
+ businessColors = colors || null;
398
+ invalidate();
47
399
  syncCssVars(getToken());
48
400
  }
49
401
 
@@ -56,7 +408,90 @@ export function getThemeConfig(): ThemeConfig {
56
408
 
57
409
  export function getToken(): ThemeColors & Record<string, unknown> {
58
410
  if (!cachedToken) {
59
- cachedToken = getDesignToken(getThemeConfig());
411
+ cachedToken = getDesignToken(getThemeConfig()) as unknown as Record<string, unknown>;
412
+ }
413
+ return cachedToken as ThemeColors & Record<string, unknown>;
414
+ }
415
+
416
+ export function getThemeMode(): ThemeMode {
417
+ return currentMode;
418
+ }
419
+
420
+ /** 应用预设主题色,持久化并广播 themeChange 供布局重渲染 */
421
+ export function applyThemePreset(key: string) {
422
+ if (!THEME_PRESETS.some(p => p.key === key)) return;
423
+ presetKey = key;
424
+ persistPreset(key);
425
+ invalidate();
426
+ syncCssVars(getToken());
427
+ EventBus.emit('themeChange', currentMode);
428
+ }
429
+
430
+ export function getActivePresetKey(): string {
431
+ return presetKey;
432
+ }
433
+
434
+ /** 应用界面密度预设,持久化并广播 themeChange */
435
+ export function applyDensityPreset(key: ThemeDensity) {
436
+ if (!DENSITY_PRESETS.some(p => p.key === key)) return;
437
+ densityKey = key;
438
+ writeStored(THEME_DENSITY_KEY, key);
439
+ invalidate();
440
+ syncCssVars(getToken());
441
+ EventBus.emit('themeChange', currentMode);
442
+ }
443
+
444
+ export function getActiveDensityKey(): ThemeDensity {
445
+ return densityKey;
446
+ }
447
+
448
+ /** 应用圆角预设,持久化并广播 themeChange */
449
+ export function applyRadiusPreset(key: ThemeRadius) {
450
+ if (!RADIUS_PRESETS.some(p => p.key === key)) return;
451
+ radiusKey = key;
452
+ writeStored(THEME_RADIUS_KEY, key);
453
+ invalidate();
454
+ syncCssVars(getToken());
455
+ EventBus.emit('themeChange', currentMode);
456
+ }
457
+
458
+ export function getActiveRadiusKey(): ThemeRadius {
459
+ return radiusKey;
460
+ }
461
+
462
+ /** 恢复默认:颜色 / 密度 / 圆角 / 明暗 全部复位(不影响布局,布局由 resetAllSettings 统一处理) */
463
+ export function resetUserTheme() {
464
+ presetKey = DEFAULT_PRESET_KEY;
465
+ densityKey = DEFAULT_DENSITY_KEY;
466
+ radiusKey = DEFAULT_RADIUS_KEY;
467
+ currentMode = 'light';
468
+ persistPreset(presetKey);
469
+ writeStored(THEME_DENSITY_KEY, densityKey);
470
+ writeStored(THEME_RADIUS_KEY, radiusKey);
471
+ writeStored(THEME_MODE_KEY, currentMode);
472
+ invalidate();
473
+ syncCssVars(getToken());
474
+ EventBus.emit('themeChange', currentMode);
475
+ }
476
+
477
+ /** 切换明/暗模式:清缓存、同步 CSS 变量并广播 themeChange 供布局重渲染 */
478
+ export function setThemeMode(mode: ThemeMode) {
479
+ if (currentMode === mode) return;
480
+ currentMode = mode;
481
+ if (typeof window !== 'undefined') {
482
+ try {
483
+ localStorage.setItem(THEME_MODE_KEY, mode);
484
+ } catch {
485
+ // 隐私模式下 localStorage 不可用,忽略
486
+ }
60
487
  }
61
- return cachedToken;
488
+ invalidate();
489
+ syncCssVars(getToken());
490
+ EventBus.emit('themeChange', mode);
491
+ }
492
+
493
+ export function toggleThemeMode(): ThemeMode {
494
+ const next: ThemeMode = currentMode === 'dark' ? 'light' : 'dark';
495
+ setThemeMode(next);
496
+ return next;
62
497
  }
@@ -94,11 +94,11 @@ export class FieldEditor extends React.Component<FieldEditorProps, {loading: boo
94
94
  alignItems: 'center',
95
95
  justifyContent: 'center',
96
96
  gap: 8,
97
- background: '#fff',
97
+ background: 'var(--oa-color-bg-container, #fff)',
98
98
  zIndex: 1,
99
99
  }}>
100
100
  <Spin/>
101
- <span style={{color: '#999'}}>编辑器加载中...</span>
101
+ <span style={{color: 'var(--oa-color-text-tertiary, #999)'}}>编辑器加载中...</span>
102
102
  </div>
103
103
  )}
104
104
  </div>
@@ -1,9 +1,9 @@
1
1
  import React from "react";
2
- import {message, Modal, Upload} from "antd";
2
+ import {Modal, Upload} from "antd";
3
3
  import type {UploadChangeParam, UploadFile, UploadProps} from "antd";
4
4
  import { UploadOutlined } from "@ant-design/icons";
5
5
  import {ViewFile} from "../../views";
6
- import {ObjectUtils, UrlUtils} from "../../utils";
6
+ import {getMessageApi, ObjectUtils, UrlUtils} from "../../utils";
7
7
  import type {FieldProps} from '../types';
8
8
 
9
9
  /** 框架内使用的上传文件对象,额外携带 sysFile 的 objectName */
@@ -110,7 +110,7 @@ export class FieldUploadFile extends React.Component<FieldUploadFileProps, Field
110
110
  }
111
111
 
112
112
  if (file.status === 'done' && rs?.success) {
113
- message.success(`文件「${rs.data?.name || ''}」上传成功`);
113
+ getMessageApi().success(`文件「${rs.data?.name || ''}」上传成功`);
114
114
  }
115
115
 
116
116
  const newIds = this.convertComponentValueToOutput(fileList);
@@ -1,10 +1,10 @@
1
1
  import React from "react";
2
- import {Button, Divider, Modal, Radio, Select, Space, Upload, message} from "antd";
2
+ import {Button, Divider, Modal, Radio, Select, Space, Upload} from "antd";
3
3
  import {DeleteOutlined, EyeOutlined, PlusOutlined} from "@ant-design/icons";
4
4
  import Compressor from "compressorjs";
5
5
  import Cropper from "cropperjs";
6
6
  import "cropperjs/dist/cropper.css";
7
- import {HttpClient, ObjectUtils, UrlUtils} from "../../utils";
7
+ import {HttpClient, getMessageApi, ObjectUtils, UrlUtils} from "../../utils";
8
8
  import type {FieldProps} from '../types';
9
9
 
10
10
  export interface FieldUploadImageProps extends FieldProps<string> {
@@ -290,19 +290,19 @@ export class FieldUploadImage extends React.Component<FieldUploadImageProps, Fie
290
290
  this.revokePreviewUrls();
291
291
  this.previewUrlsRef = [cUrl];
292
292
  this.setState({preview: {cUrl, cFile, cSize: cFile.size, cdims}, tool: undefined});
293
- message.success(`已压缩:${cdims.width} x ${cdims.height} / ${formatSize(cFile.size)}`);
293
+ getMessageApi().success(`已压缩:${cdims.width} x ${cdims.height} / ${formatSize(cFile.size)}`);
294
294
  } catch {
295
- message.error('压缩失败');
295
+ getMessageApi().error('压缩失败');
296
296
  }
297
297
  };
298
298
 
299
299
  private handleBeforeUpload = async (file: File) => {
300
300
  if (this.state.objectNames.length >= this.state.maxCount) {
301
- message.warning('已达到最大上传数量');
301
+ getMessageApi().warning('已达到最大上传数量');
302
302
  return Upload.LIST_IGNORE;
303
303
  }
304
304
  if (!file.type.startsWith('image/')) {
305
- message.error('请选择图片文件');
305
+ getMessageApi().error('请选择图片文件');
306
306
  return Upload.LIST_IGNORE;
307
307
  }
308
308
 
@@ -319,7 +319,7 @@ export class FieldUploadImage extends React.Component<FieldUploadImageProps, Fie
319
319
  // 默认展示原图,不做压缩处理,用户可点击「压缩」按钮手动压缩
320
320
  await this.loadOriginalPreview(file, url, dims);
321
321
  } catch {
322
- message.error('读取图片失败');
322
+ getMessageApi().error('读取图片失败');
323
323
  this.closeModal();
324
324
  return;
325
325
  }
@@ -370,7 +370,7 @@ export class FieldUploadImage extends React.Component<FieldUploadImageProps, Fie
370
370
  );
371
371
  this.closeModal();
372
372
  } catch (e) {
373
- message.error(HttpClient.errToMsg(e));
373
+ getMessageApi().error(HttpClient.errToMsg(e));
374
374
  } finally {
375
375
  this.setState({uploading: false});
376
376
  }
@@ -390,7 +390,7 @@ export class FieldUploadImage extends React.Component<FieldUploadImageProps, Fie
390
390
  if (canvasImg?.url) {
391
391
  return <img src={canvasImg.url} style={{maxWidth: '100%', maxHeight: '100%'}} alt="预览"/>;
392
392
  }
393
- return <div style={{color: '#999'}}>生成中...</div>;
393
+ return <div style={{color: 'var(--oa-color-text-tertiary, #999)'}}>生成中...</div>;
394
394
  };
395
395
 
396
396
  render() {
@@ -487,14 +487,14 @@ export class FieldUploadImage extends React.Component<FieldUploadImageProps, Fie
487
487
  <div style={{flex: 1, minWidth: 0, display: 'flex', flexDirection: 'column', gap: 12}}>
488
488
  <div style={{
489
489
  flex: 1, minHeight: 0, display: 'flex', alignItems: 'center', justifyContent: 'center',
490
- background: '#f5f5f5', borderRadius: 8, border: '1px dashed #d9d9d9', position: 'relative', overflow: 'hidden',
490
+ background: 'var(--oa-color-fill, #f5f5f5)', borderRadius: 8, border: '1px dashed var(--oa-color-border, #d9d9d9)', position: 'relative', overflow: 'hidden',
491
491
  }}>
492
492
  {this.renderCanvas(preview, canvasImg, originalUrl)}
493
493
  </div>
494
494
  </div>
495
495
 
496
496
  {/* 右侧属性栏 */}
497
- <div style={{width: 240, flexShrink: 0, borderLeft: '1px solid #f0f0f0', paddingLeft: 16, display: 'flex', flexDirection: 'column', overflow: 'hidden'}}>
497
+ <div style={{width: 240, flexShrink: 0, borderLeft: '1px solid var(--oa-color-border, #f0f0f0)', paddingLeft: 16, display: 'flex', flexDirection: 'column', overflow: 'hidden'}}>
498
498
  <div style={{flex: 1, overflowY: 'auto'}}>
499
499
  {/* 工具栏(横排) */}
500
500
  {tool !== 'crop' && (
@@ -520,14 +520,14 @@ export class FieldUploadImage extends React.Component<FieldUploadImageProps, Fie
520
520
  >
521
521
  {CROP_RATIOS.map((r) => (
522
522
  <Radio key={r.ratio ? `${r.ratio.width}:${r.ratio.height}` : 'free'} value={r.ratio ? `${r.ratio.width}:${r.ratio.height}` : 'free'}>
523
- {r.label}{r.desc ? <span style={{color: '#999', fontSize: 12}}>({r.desc})</span> : null}
523
+ {r.label}{r.desc ? <span style={{color: 'var(--oa-color-text-tertiary, #999)', fontSize: 12}}>({r.desc})</span> : null}
524
524
  </Radio>
525
525
  ))}
526
526
  </Radio.Group>
527
527
  ) : (
528
528
  <>
529
529
  {canvasImg && (
530
- <div style={{color: '#666'}}>
530
+ <div style={{color: 'var(--oa-color-text-secondary, #666)'}}>
531
531
  <div>尺寸:{canvasImg.dims ? `${canvasImg.dims.width} x ${canvasImg.dims.height}` : '--'}</div>
532
532
  <div>体积:{formatSize(canvasImg.size)}</div>
533
533
  </div>
@@ -537,7 +537,7 @@ export class FieldUploadImage extends React.Component<FieldUploadImageProps, Fie
537
537
  <Divider style={{margin: '12px 0'}}/>
538
538
  <div style={{fontWeight: 600, marginBottom: 8}}>压缩处理</div>
539
539
  <div style={{marginBottom: 8, display: 'flex', alignItems: 'center', gap: 8}}>
540
- <span style={{color: '#666', flexShrink: 0}}>最大宽度</span>
540
+ <span style={{color: 'var(--oa-color-text-secondary, #666)', flexShrink: 0}}>最大宽度</span>
541
541
  <Select
542
542
  value={compressWidth}
543
543
  onChange={(v) => this.setState({compressWidth: v})}
@@ -555,7 +555,7 @@ export class FieldUploadImage extends React.Component<FieldUploadImageProps, Fie
555
555
  />
556
556
  </div>
557
557
  <div style={{marginBottom: 12, display: 'flex', alignItems: 'center', gap: 8}}>
558
- <span style={{color: '#666', flexShrink: 0}}>最大体积</span>
558
+ <span style={{color: 'var(--oa-color-text-secondary, #666)', flexShrink: 0}}>最大体积</span>
559
559
  <Select
560
560
  value={compressSize}
561
561
  onChange={(v) => this.setState({compressSize: v})}