@nocobase/client-v2 2.3.0-beta.6 → 2.4.0-alpha.2
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.
- package/es/acl/ACLProvider.d.ts +10 -0
- package/es/acl/aclCheckReadiness.d.ts +11 -0
- package/es/authRedirect.d.ts +11 -2
- package/es/components/AttachmentUpload.d.ts +51 -0
- package/es/components/index.d.ts +1 -0
- package/es/flow/admin-shell/admin-layout/NocoBaseLogo.d.ts +10 -0
- package/es/index.d.ts +4 -1
- package/es/index.mjs +129 -118
- package/es/nocobase-buildin-plugin/currentUserAuthStatus.d.ts +11 -0
- package/es/nocobase-buildin-plugin/index.d.ts +10 -0
- package/es/settings-app/SettingsApplication.d.ts +18 -0
- package/es/settings-app/SettingsBrand.d.ts +16 -0
- package/es/settings-app/SettingsBuildInPlugin.d.ts +9 -0
- package/es/settings-app/SettingsDocumentRedirect.d.ts +10 -0
- package/es/settings-app/SettingsGroupNav.d.ts +18 -0
- package/es/settings-app/SettingsPluginSettingsManager.d.ts +14 -0
- package/es/settings-app/SettingsRouterManager.d.ts +15 -0
- package/es/settings-app/SettingsSearch.d.ts +17 -0
- package/es/settings-app/SettingsShell.d.ts +10 -0
- package/es/settings-app/isSettingsApp.d.ts +18 -0
- package/es/settings-app/runtimePaths.d.ts +18 -0
- package/es/settings-app/settingsDocumentPath.d.ts +14 -0
- package/es/settings-app/settingsTheme.d.ts +87 -0
- package/es/settings-app/useSettingsThemeConfig.d.ts +22 -0
- package/es/settings-center/AdminSettingsLayout.d.ts +2 -2
- package/es/settings-center/groups.d.ts +66 -0
- package/es/settings-center/useSettingsGroups.d.ts +36 -0
- package/es/settings-center/useSettingsSearch.d.ts +35 -0
- package/es/settings-center/utils.d.ts +24 -0
- package/es/theme/index.d.ts +1 -0
- package/es/theme/itemActive.d.ts +24 -0
- package/es/theme/type.d.ts +9 -0
- package/lib/index.js +157 -146
- package/package.json +7 -7
- package/src/__tests__/app.test.tsx +122 -3
- package/src/__tests__/authRedirect.test.ts +154 -0
- package/src/__tests__/browserChecker.test.ts +172 -6
- package/src/__tests__/mockSettingsApplication.ts +29 -0
- package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +81 -8
- package/src/__tests__/plugin-manager.test.tsx +7 -6
- package/src/__tests__/settings-application.test.ts +164 -0
- package/src/__tests__/settings-auth-routes.test.ts +71 -0
- package/src/__tests__/settings-center.test.tsx +168 -130
- package/src/__tests__/settings-layout-root.test.tsx +600 -0
- package/src/__tests__/settings-runtime-paths.test.ts +99 -0
- package/src/__tests__/settings-search-shortcut.test.tsx +321 -0
- package/src/__tests__/settings-shell.test.tsx +261 -0
- package/src/acl/ACLProvider.tsx +53 -3
- package/src/acl/aclCheckReadiness.ts +44 -0
- package/src/authRedirect.ts +85 -8
- package/src/components/AppComponents.tsx +16 -7
- package/src/components/AttachmentUpload.tsx +275 -0
- package/src/components/index.ts +1 -0
- package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +1 -56
- package/src/flow/admin-shell/admin-layout/NocoBaseLogo.tsx +77 -0
- package/src/flow/admin-shell/admin-layout/TopbarActionsBar.tsx +37 -14
- package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +77 -13
- package/src/flow/internal/components/Markdown/DisplayMarkdown.tsx +14 -9
- package/src/flow/internal/components/Markdown/__tests__/DisplayMarkdown.test.tsx +46 -0
- package/src/flow/models/fields/DisplayHtmlFieldModel.tsx +2 -1
- package/src/flow/models/fields/RichTextFieldModel/__tests__/RichTextFieldModel.test.tsx +74 -0
- package/src/flow/models/fields/RichTextFieldModel/index.tsx +27 -6
- package/src/flow/models/topbar/TopbarActionModel.tsx +52 -180
- package/src/index.ts +11 -1
- package/src/nocobase-buildin-plugin/currentUserAuthStatus.ts +58 -0
- package/src/nocobase-buildin-plugin/index.tsx +112 -71
- package/src/settings-app/SettingsApplication.ts +31 -0
- package/src/settings-app/SettingsBrand.tsx +108 -0
- package/src/settings-app/SettingsBuildInPlugin.ts +10 -0
- package/src/settings-app/SettingsDocumentRedirect.tsx +26 -0
- package/src/settings-app/SettingsGroupNav.tsx +124 -0
- package/src/settings-app/SettingsPluginSettingsManager.ts +38 -0
- package/src/settings-app/SettingsRouterManager.ts +92 -0
- package/src/settings-app/SettingsSearch.tsx +343 -0
- package/src/settings-app/SettingsShell.tsx +184 -0
- package/src/settings-app/isSettingsApp.ts +21 -0
- package/src/settings-app/runtimePaths.ts +103 -0
- package/src/settings-app/settingsDocumentPath.ts +66 -0
- package/src/settings-app/settingsTheme.ts +369 -0
- package/src/settings-app/useSettingsThemeConfig.ts +100 -0
- package/src/settings-center/AdminSettingsLayout.tsx +154 -169
- package/src/settings-center/SystemSettingsPage.tsx +4 -170
- package/src/settings-center/__tests__/groups.test.ts +25 -0
- package/src/settings-center/groups.ts +108 -0
- package/src/settings-center/useSettingsGroups.ts +105 -0
- package/src/settings-center/useSettingsSearch.ts +173 -0
- package/src/settings-center/utils.tsx +50 -1
- package/src/theme/index.tsx +1 -0
- package/src/theme/itemActive.ts +31 -0
- package/src/theme/type.ts +10 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export type SettingsAppScope = '' | `/${'apps' | '_app'}/${string}`;
|
|
11
|
+
|
|
12
|
+
function ensureLeadingSlash(value: string) {
|
|
13
|
+
return value.startsWith('/') ? value : `/${value}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function normalizeSettingsRootPublicPath(value?: string) {
|
|
17
|
+
const normalized = ensureLeadingSlash(String(value || '/').trim() || '/').replace(/\/{2,}/g, '/');
|
|
18
|
+
return normalized.endsWith('/') ? normalized : `${normalized}/`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function resolveSettingsAppScope(pathname?: string): SettingsAppScope {
|
|
22
|
+
const match = /\/(apps|_app)\/([^/?#]+)(?=\/|[?#]|$)/.exec(ensureLeadingSlash(String(pathname || '/')));
|
|
23
|
+
return match ? (`/${match[1]}/${match[2]}` as SettingsAppScope) : '';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function resolveSettingsAppScopeWithinPublicPath(publicPath: string, pathname?: string): SettingsAppScope {
|
|
27
|
+
const root = normalizeSettingsRootPublicPath(publicPath).replace(/\/+$/, '');
|
|
28
|
+
const path = ensureLeadingSlash(String(pathname || '/').split(/[?#]/)[0]).replace(/\/{2,}/g, '/');
|
|
29
|
+
const relativePath = root && (path === root || path.startsWith(`${root}/`)) ? path.slice(root.length) || '/' : path;
|
|
30
|
+
const match = /^\/(?:settings\/)?(apps|_app)\/([^/]+)(?=\/|$)/.exec(relativePath);
|
|
31
|
+
return match ? (`/${match[1]}/${match[2]}` as SettingsAppScope) : '';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function normalizeSettingsRoutePath(value: string) {
|
|
35
|
+
const normalized = ensureLeadingSlash(String(value || '/settings')).replace(/\/{2,}/g, '/');
|
|
36
|
+
return normalized === '/' ? '/settings' : normalized.replace(/\/+$/, '');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function removeSettingsRouteRoot(settingsRoute: string) {
|
|
40
|
+
if (settingsRoute === '/settings') {
|
|
41
|
+
return '';
|
|
42
|
+
}
|
|
43
|
+
return settingsRoute.replace(/^\/settings(?=\/|$)/, '');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function resolveSettingsDocumentPath(
|
|
47
|
+
rootPublicPath: string,
|
|
48
|
+
appScope: SettingsAppScope,
|
|
49
|
+
settingsRoutePath: string,
|
|
50
|
+
) {
|
|
51
|
+
const root = normalizeSettingsRootPublicPath(rootPublicPath);
|
|
52
|
+
const rootPrefix = root === '/' ? '' : root.replace(/\/+$/, '');
|
|
53
|
+
const settingsRoute = normalizeSettingsRoutePath(settingsRoutePath);
|
|
54
|
+
if (!appScope) {
|
|
55
|
+
return `${rootPrefix}${settingsRoute}`;
|
|
56
|
+
}
|
|
57
|
+
return `${rootPrefix}/settings${appScope}${removeSettingsRouteRoot(settingsRoute)}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function resolveSettingsDocumentBasename(rootPublicPath: string, appScope: SettingsAppScope) {
|
|
61
|
+
const root = normalizeSettingsRootPublicPath(rootPublicPath);
|
|
62
|
+
if (!appScope) {
|
|
63
|
+
return root;
|
|
64
|
+
}
|
|
65
|
+
return normalizeSettingsRootPublicPath(`${root}settings${appScope}`);
|
|
66
|
+
}
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { GlobalToken, ThemeConfig } from 'antd';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 粗略判断一个颜色是不是深色背景。
|
|
14
|
+
*
|
|
15
|
+
* 只支持 `#rgb` / `#rrggbb`,解析不出来时按浅色处理。
|
|
16
|
+
*
|
|
17
|
+
* @param {string | undefined} color 背景色
|
|
18
|
+
* @returns {boolean} 是否为深色
|
|
19
|
+
*/
|
|
20
|
+
export function isDarkColor(color?: string) {
|
|
21
|
+
if (typeof color !== 'string') {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const hex = color.trim().replace('#', '');
|
|
26
|
+
const normalized =
|
|
27
|
+
hex.length === 3
|
|
28
|
+
? hex
|
|
29
|
+
.split('')
|
|
30
|
+
.map((char) => char + char)
|
|
31
|
+
.join('')
|
|
32
|
+
: hex;
|
|
33
|
+
|
|
34
|
+
if (normalized.length !== 6 || /[^0-9a-f]/i.test(normalized)) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const r = parseInt(normalized.slice(0, 2), 16);
|
|
39
|
+
const g = parseInt(normalized.slice(2, 4), 16);
|
|
40
|
+
const b = parseInt(normalized.slice(4, 6), 16);
|
|
41
|
+
|
|
42
|
+
// Rec. 601 亮度
|
|
43
|
+
return (r * 299 + g * 587 + b * 114) / 1000 < 128;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 取顶栏配色。
|
|
48
|
+
*
|
|
49
|
+
* 设置中心的顶栏刻意**不跟随**主题编辑器里的 `colorBgHeader`:那套深色顶栏是给业务端的,
|
|
50
|
+
* 设置中心用容器底色(浅色主题下就是白色)+ 一条细分割线,和业务端在视觉上分开。
|
|
51
|
+
* 文字和图标一律走正文色,深色主题下由 `colorBgContainer` 自动翻成浅色。
|
|
52
|
+
*
|
|
53
|
+
* @param {GlobalToken} token 外层主题 token
|
|
54
|
+
* @returns 顶栏底色、文字色、hover / active 色,以及顶栏是否为深色
|
|
55
|
+
*/
|
|
56
|
+
export function getSettingsHeaderColors(token: GlobalToken) {
|
|
57
|
+
const bg = token.colorBgContainer;
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
bg,
|
|
61
|
+
dark: isDarkColor(bg),
|
|
62
|
+
text: token.colorText,
|
|
63
|
+
textHover: token.colorText,
|
|
64
|
+
textActive: token.colorText,
|
|
65
|
+
bgHover: token.colorFillQuaternary,
|
|
66
|
+
bgActive: token.colorFillQuaternary,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 顶栏一级导航专用主题。
|
|
72
|
+
*
|
|
73
|
+
* 顶栏自己是透明底(底色由 Layout.Header 给),下拉浮层跟着顶栏用容器色;
|
|
74
|
+
* 选中 / hover 的颜色一律交给外层主题,也就是默认蓝。
|
|
75
|
+
*
|
|
76
|
+
* @param {GlobalToken} token 外层主题 token
|
|
77
|
+
* @returns {ThemeConfig} 顶栏菜单主题
|
|
78
|
+
*/
|
|
79
|
+
export function buildSettingsHeaderMenuTheme(token: GlobalToken): ThemeConfig {
|
|
80
|
+
const colors = getSettingsHeaderColors(token);
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
components: {
|
|
84
|
+
Menu: {
|
|
85
|
+
itemBg: 'transparent',
|
|
86
|
+
popupBg: colors.bg,
|
|
87
|
+
subMenuItemBg: 'transparent',
|
|
88
|
+
// 顶栏表达的是「在哪个模块」,属于背景信息:选中态用一条下划线 + 加粗就够,
|
|
89
|
+
// 实心色块太重,会跟左栏那个「当前页面」的强指示打架。
|
|
90
|
+
// 未选中项也用正文色——次要灰在白底顶栏上偏淡,看不清;选中态靠下划线 + 加粗区分。
|
|
91
|
+
itemColor: token.colorText,
|
|
92
|
+
itemHoverColor: token.colorText,
|
|
93
|
+
horizontalItemSelectedColor: token.colorText,
|
|
94
|
+
horizontalItemSelectedBg: 'transparent',
|
|
95
|
+
horizontalItemHoverColor: token.colorText,
|
|
96
|
+
horizontalItemBorderRadius: 0,
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* 构造设置中心的外壳主题。
|
|
104
|
+
*
|
|
105
|
+
* 只做**顶栏**这一层:白(或深色主题下的容器色)底 + 细分割线,文字与图标走正文色,
|
|
106
|
+
* 和业务端那条深色顶栏区分开。
|
|
107
|
+
*
|
|
108
|
+
* 选中态一律用浅灰底 + 正文色,只有开关这类「开/关」控件才用纯黑填充。
|
|
109
|
+
* 各插件页面里 `controlItemBgActive` 常被当成纯背景用、文字色不跟着走,所以那个 token
|
|
110
|
+
* 也保持浅色,不然列表行会变成深底深字。
|
|
111
|
+
*
|
|
112
|
+
* @param {GlobalToken} token 外层主题 token
|
|
113
|
+
* @returns {ThemeConfig} 设置中心外壳主题
|
|
114
|
+
*/
|
|
115
|
+
export function buildSettingsNeutralTheme(token: GlobalToken): ThemeConfig {
|
|
116
|
+
const headerColors = getSettingsHeaderColors(token);
|
|
117
|
+
const dark = headerColors.dark;
|
|
118
|
+
/** 选中态底色:浅色主题下是黑,深色主题下翻成白 */
|
|
119
|
+
const activeBg = dark ? '#ffffff' : '#000000';
|
|
120
|
+
const activeText = dark ? '#000000' : '#ffffff';
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
token: {
|
|
124
|
+
// 顶栏图标(帮助、用户中心、pinned 插件)默认按深色顶栏取浅色;
|
|
125
|
+
// 这里的顶栏是灰白的,必须翻成正文色,否则整排图标白底白字看不见。
|
|
126
|
+
colorTextHeaderMenu: headerColors.text,
|
|
127
|
+
colorTextHeaderMenuHover: headerColors.textHover,
|
|
128
|
+
colorTextHeaderMenuActive: headerColors.textActive,
|
|
129
|
+
colorBgHeaderMenuHover: headerColors.bgHover,
|
|
130
|
+
colorBgHeaderMenuActive: headerColors.bgActive,
|
|
131
|
+
// 主色收成黑白。antd 会拿它派生一整套色板,而近黑色派生出来的「浅色背景」是 #5e5e5e
|
|
132
|
+
// 这种中灰,列表行、Select 选项选中后就成了深底深字,所以派生的那几个必须显式钉住。
|
|
133
|
+
colorPrimary: activeBg,
|
|
134
|
+
colorInfo: activeBg,
|
|
135
|
+
colorPrimaryHover: dark ? '#ffffff' : '#333333',
|
|
136
|
+
colorPrimaryActive: dark ? '#d4d4d4' : '#000000',
|
|
137
|
+
colorPrimaryBg: token.colorFillQuaternary,
|
|
138
|
+
colorPrimaryBgHover: token.colorFillTertiary,
|
|
139
|
+
colorPrimaryBorder: token.colorBorder,
|
|
140
|
+
colorPrimaryBorderHover: token.colorTextTertiary,
|
|
141
|
+
// 提示类(Alert / Tag 的 info 态)同理:主色一黑,派生的底色就成了脏灰配黑字。
|
|
142
|
+
colorInfoBg: token.colorFillQuaternary,
|
|
143
|
+
colorInfoBgHover: token.colorFillTertiary,
|
|
144
|
+
colorInfoBorder: token.colorBorderSecondary,
|
|
145
|
+
colorInfoBorderHover: token.colorBorder,
|
|
146
|
+
colorInfoText: token.colorText,
|
|
147
|
+
controlItemBgActive: token.colorFillQuaternary,
|
|
148
|
+
controlItemBgActiveHover: token.colorFillTertiary,
|
|
149
|
+
// 压在主色上的文字:浅色主题下主色是黑,文字白;深色主题反过来。
|
|
150
|
+
colorTextLightSolid: activeText,
|
|
151
|
+
// 链接:灰色 + 下划线(下划线在 settingsGlobalCss 里补,token 层没有这一项)
|
|
152
|
+
colorLink: token.colorTextSecondary,
|
|
153
|
+
colorLinkHover: token.colorText,
|
|
154
|
+
colorLinkActive: token.colorText,
|
|
155
|
+
// 自绘选中态(列表行、树节点)读这一对,见 CustomToken 里的说明。
|
|
156
|
+
// 这里用浅底:整行纯黑太重,行内的图标、标签都得跟着反色,得不偿失。
|
|
157
|
+
// 文字色留空表示「不覆盖」,各页面保持正文色即可。
|
|
158
|
+
colorBgItemActive: token.colorFillSecondary,
|
|
159
|
+
colorTextItemActive: '',
|
|
160
|
+
} as ThemeConfig['token'],
|
|
161
|
+
components: {
|
|
162
|
+
Layout: {
|
|
163
|
+
headerBg: headerColors.bg,
|
|
164
|
+
headerColor: headerColors.text,
|
|
165
|
+
bodyBg: token.colorBgLayout,
|
|
166
|
+
},
|
|
167
|
+
Menu: {
|
|
168
|
+
// 左栏选中态:浅底 + 加粗,不再画左侧竖线。
|
|
169
|
+
itemSelectedBg: token.colorFillSecondary,
|
|
170
|
+
itemSelectedColor: token.colorText,
|
|
171
|
+
// 子菜单容器保留一层很淡的底色标记「这是一组」,但要比选中项浅一档,
|
|
172
|
+
// 否则两层灰叠在一起就分不出哪个是选中的。
|
|
173
|
+
subMenuItemBg: token.colorFillQuaternary,
|
|
174
|
+
subMenuItemSelectedColor: token.colorText,
|
|
175
|
+
horizontalItemSelectedColor: token.colorText,
|
|
176
|
+
horizontalItemSelectedBg: 'transparent',
|
|
177
|
+
activeBarBorderWidth: 0,
|
|
178
|
+
},
|
|
179
|
+
Spin: {
|
|
180
|
+
// 加载态跟着中性配色走,别在一片黑白灰里闪一个蓝点。
|
|
181
|
+
colorPrimary: token.colorTextTertiary,
|
|
182
|
+
},
|
|
183
|
+
Select: {
|
|
184
|
+
// 下拉选中项用浅灰底 + 正文色,别在下拉里压一条纯黑。
|
|
185
|
+
optionSelectedBg: token.colorFillSecondary,
|
|
186
|
+
optionSelectedColor: token.colorText,
|
|
187
|
+
},
|
|
188
|
+
Switch: {
|
|
189
|
+
colorPrimary: activeBg,
|
|
190
|
+
colorPrimaryHover: activeBg,
|
|
191
|
+
},
|
|
192
|
+
Tabs: {
|
|
193
|
+
// 页签选中态跟着黑白走,不然一页里既有黑色选中项又有蓝色页签。
|
|
194
|
+
itemSelectedColor: token.colorText,
|
|
195
|
+
itemHoverColor: token.colorText,
|
|
196
|
+
itemActiveColor: token.colorText,
|
|
197
|
+
inkBarColor: token.colorText,
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* 给外部主题补上设置中心的顶栏配色。
|
|
205
|
+
*
|
|
206
|
+
* 顶栏那几个 token(`colorBgHeader`、`colorTextHeaderMenu` 系列、`Layout.headerBg`)
|
|
207
|
+
* 刻意不放进「简约」主题记录里:那条记录是通用的,业务端顶栏是深色的、logo 和图标都按
|
|
208
|
+
* 深底做的,一旦被改成白底就得连 logo 一起反色。设置中心的顶栏是白的,所以在这一层补。
|
|
209
|
+
*
|
|
210
|
+
* @param {ThemeConfig} theme 主题记录里读到的配置
|
|
211
|
+
* @param {GlobalToken} token 外层主题 token
|
|
212
|
+
* @returns {ThemeConfig} 补齐顶栏配色后的主题
|
|
213
|
+
*/
|
|
214
|
+
export function withSettingsHeaderTheme(theme: ThemeConfig, token: GlobalToken): ThemeConfig {
|
|
215
|
+
const colors = getSettingsHeaderColors(token);
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
...theme,
|
|
219
|
+
token: {
|
|
220
|
+
...theme.token,
|
|
221
|
+
colorTextHeaderMenu: colors.text,
|
|
222
|
+
colorTextHeaderMenuHover: colors.textHover,
|
|
223
|
+
colorTextHeaderMenuActive: colors.textActive,
|
|
224
|
+
colorBgHeaderMenuHover: colors.bgHover,
|
|
225
|
+
colorBgHeaderMenuActive: colors.bgActive,
|
|
226
|
+
} as ThemeConfig['token'],
|
|
227
|
+
components: {
|
|
228
|
+
...theme.components,
|
|
229
|
+
Layout: {
|
|
230
|
+
...theme.components?.Layout,
|
|
231
|
+
headerBg: colors.bg,
|
|
232
|
+
headerColor: colors.text,
|
|
233
|
+
bodyBg: token.colorBgLayout,
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Height of one entry in the top bar dropdown.
|
|
241
|
+
*
|
|
242
|
+
* Taken from the user center dropdown (`nb-user-center-item`, 30px) so that the
|
|
243
|
+
* two dropdowns hanging off the same top bar line up.
|
|
244
|
+
*/
|
|
245
|
+
const SETTINGS_MENU_ITEM_HEIGHT = 30;
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* 设置中心的全局补丁样式。
|
|
249
|
+
*
|
|
250
|
+
* 这几件事 antd 没给 token,只能落到 CSS:
|
|
251
|
+
* - 裸 `<a>` 的颜色。它吃的是**最外层** ConfigProvider 生成的全局 reset 样式,
|
|
252
|
+
* 我们这层嵌套 ConfigProvider 的 `colorLink` 够不着,只能在 CSS 里压一次。
|
|
253
|
+
* - 链接下划线(`colorLink` 只管颜色)
|
|
254
|
+
* - Switch 禁用态压得更淡,跟「未启用但可点」区分开
|
|
255
|
+
* - 顶栏选中项的下划线、左栏选中项的加粗
|
|
256
|
+
*
|
|
257
|
+
* @param {GlobalToken} token 外层主题 token
|
|
258
|
+
* @returns {string} 可直接塞进 `<style>` 的 CSS
|
|
259
|
+
*/
|
|
260
|
+
export function buildSettingsGlobalCss(token: GlobalToken): string {
|
|
261
|
+
return `
|
|
262
|
+
/* The rules below deliberately drop the .nb-settings-shell prefix: antd renders
|
|
263
|
+
drawers, modals and dropdowns into portals under body, where a shell-scoped
|
|
264
|
+
selector never matches — that is how the edit link inside the collection drawer
|
|
265
|
+
kept antd's default blue. This stylesheet is only injected by the standalone
|
|
266
|
+
settings SPA, so unscoped selectors cannot reach the business side. */
|
|
267
|
+
|
|
268
|
+
/* 最外层 body 的底色来自业务端主题(暗黑下是 #141414)。内容区一般把它整块盖住,
|
|
269
|
+
但内容不满屏、或者滚动橡皮筋回弹时会漏出来,所以这里一并接管。
|
|
270
|
+
设置中心是独立入口的单页应用,这条全局规则不会影响业务端。 */
|
|
271
|
+
body {
|
|
272
|
+
background: ${token.colorBgLayout};
|
|
273
|
+
}
|
|
274
|
+
a:not(.ant-btn):not(.ant-menu-item):not(.ant-tabs-tab-btn):not([class*="ant-"]) {
|
|
275
|
+
color: ${token.colorTextSecondary};
|
|
276
|
+
}
|
|
277
|
+
a:not(.ant-btn):not(.ant-menu-item):not(.ant-tabs-tab-btn):not([class*="ant-"]):hover {
|
|
278
|
+
color: ${token.colorText};
|
|
279
|
+
}
|
|
280
|
+
a:not(.ant-btn):not(.ant-menu-item):not(.ant-tabs-tab-btn),
|
|
281
|
+
.ant-btn-link:not(:disabled) {
|
|
282
|
+
text-decoration: underline;
|
|
283
|
+
text-underline-offset: 2px;
|
|
284
|
+
}
|
|
285
|
+
.nb-settings-shell .ant-layout-header .ant-menu-horizontal > .ant-menu-item::after,
|
|
286
|
+
.nb-settings-shell .ant-layout-header .ant-menu-horizontal > .ant-menu-submenu::after {
|
|
287
|
+
border-bottom: none !important;
|
|
288
|
+
}
|
|
289
|
+
.nb-settings-shell .ant-layout-header .ant-menu-horizontal > .ant-menu-item-selected,
|
|
290
|
+
.nb-settings-shell .ant-layout-header .ant-menu-horizontal > .ant-menu-submenu-selected {
|
|
291
|
+
font-weight: 600;
|
|
292
|
+
box-shadow: inset 0 -2px 0 0 ${token.colorText};
|
|
293
|
+
}
|
|
294
|
+
.nb-settings-shell .ant-layout-sider .ant-menu-item-selected {
|
|
295
|
+
font-weight: 600;
|
|
296
|
+
}
|
|
297
|
+
.nb-settings-shell .ant-layout-sider .ant-menu-item::after,
|
|
298
|
+
.nb-settings-shell .ant-layout-sider .ant-menu-submenu-title::after {
|
|
299
|
+
display: none !important;
|
|
300
|
+
}
|
|
301
|
+
/* Entry height matches the other dropdown on the same top bar (the user center,
|
|
302
|
+
whose nb-user-center-item is 30px). antd sizes menu entries with
|
|
303
|
+
controlHeightLG (35px under compact), which is the height of a single clickable
|
|
304
|
+
control; this dropdown is a list of a dozen links and reads loose at 35.
|
|
305
|
+
|
|
306
|
+
Scoped to entries inside the popup only: the horizontal top bar menu shares the
|
|
307
|
+
same Menu tokens, so changing tokens would eat its horizontal padding too. The
|
|
308
|
+
selector needs three classes; two do not outweigh antd's own rule. */
|
|
309
|
+
.ant-menu-submenu-popup .ant-menu.ant-menu-sub.ant-menu-vertical {
|
|
310
|
+
/* With the 4px margin between entries gone, the first and last would sit flush
|
|
311
|
+
against the popup edge, so the container gives the padding back. */
|
|
312
|
+
padding-block: ${token.paddingXXS}px;
|
|
313
|
+
}
|
|
314
|
+
.ant-menu-submenu-popup .ant-menu-sub > .ant-menu-item,
|
|
315
|
+
.ant-menu-submenu-popup .ant-menu-sub > .ant-menu-submenu > .ant-menu-submenu-title {
|
|
316
|
+
height: ${SETTINGS_MENU_ITEM_HEIGHT}px;
|
|
317
|
+
line-height: ${SETTINGS_MENU_ITEM_HEIGHT}px;
|
|
318
|
+
margin-block: 0;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/* Info alerts: with the primary color collapsed to black and white, their derived
|
|
322
|
+
background lands on rgba(0,0,0,0.02) — faint enough to read as empty space.
|
|
323
|
+
Rather than reaching for color (which would drag the palette back in), the shape
|
|
324
|
+
carries the weight: a 4px bar on the left, a firmer background and border, a
|
|
325
|
+
bold title and a larger icon. Warning / error / success keep their semantic
|
|
326
|
+
colors and are left alone. */
|
|
327
|
+
.ant-alert-info {
|
|
328
|
+
background: ${token.colorFillSecondary};
|
|
329
|
+
border: ${token.lineWidth}px solid ${token.colorBorder};
|
|
330
|
+
border-inline-start: 4px solid ${token.colorText};
|
|
331
|
+
padding-block: ${token.paddingSM}px;
|
|
332
|
+
}
|
|
333
|
+
.ant-alert-info .ant-alert-icon,
|
|
334
|
+
.ant-alert-info .ant-alert-message {
|
|
335
|
+
color: ${token.colorText};
|
|
336
|
+
}
|
|
337
|
+
.ant-alert-info .ant-alert-icon {
|
|
338
|
+
font-size: ${token.fontSizeLG}px;
|
|
339
|
+
}
|
|
340
|
+
.ant-alert-info .ant-alert-message {
|
|
341
|
+
font-weight: 600;
|
|
342
|
+
}
|
|
343
|
+
.ant-alert-info .ant-alert-description {
|
|
344
|
+
color: ${token.colorTextSecondary};
|
|
345
|
+
}
|
|
346
|
+
/* Three switch shades: off is light grey (colorFill, 0.15), disabled is dark grey
|
|
347
|
+
(colorTextTertiary, 0.45), on is black. Disabled being darker than off is
|
|
348
|
+
deliberate — here "darker" reads as "pinned", not "stronger" — and one light
|
|
349
|
+
against one dark is told apart at a glance, without comparing subtle
|
|
350
|
+
brightness steps. Disabled is not dimmed as a whole: the plugin manager shows
|
|
351
|
+
over a hundred built-in plugins in that state, and dimming makes the page look
|
|
352
|
+
broken. */
|
|
353
|
+
.ant-switch {
|
|
354
|
+
background: ${token.colorFill};
|
|
355
|
+
}
|
|
356
|
+
.ant-switch.ant-switch-checked {
|
|
357
|
+
background: ${token.colorText};
|
|
358
|
+
}
|
|
359
|
+
/* This selector has to outweigh antd's own .ant-switch.ant-switch-disabled (a
|
|
360
|
+
single class no longer does, now that the shell prefix is gone), otherwise its
|
|
361
|
+
opacity: 0.65 survives and washes the dark grey out onto the off state, where
|
|
362
|
+
the two read as the same color. */
|
|
363
|
+
.ant-switch.ant-switch-disabled,
|
|
364
|
+
.ant-switch.ant-switch-disabled.ant-switch-checked {
|
|
365
|
+
background: ${token.colorTextTertiary};
|
|
366
|
+
opacity: 1;
|
|
367
|
+
}
|
|
368
|
+
`;
|
|
369
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { theme as antdTheme, type ThemeConfig } from 'antd';
|
|
11
|
+
import { useEffect, useState } from 'react';
|
|
12
|
+
import { useApp } from '../hooks/useApp';
|
|
13
|
+
|
|
14
|
+
/** 简约主题在 `themeConfig` 表里的固定标识,见 plugin-theme-editor 的 builtinThemes */
|
|
15
|
+
export const MINIMAL_THEME_UID = 'minimal';
|
|
16
|
+
|
|
17
|
+
const ALGORITHMS = {
|
|
18
|
+
defaultAlgorithm: antdTheme.defaultAlgorithm,
|
|
19
|
+
darkAlgorithm: antdTheme.darkAlgorithm,
|
|
20
|
+
compactAlgorithm: antdTheme.compactAlgorithm,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 把库里存的 algorithm 名字换成 antd 的算法函数。
|
|
25
|
+
*
|
|
26
|
+
* 主题记录里存的是字符串(JSON 存不下函数),可能是单个也可能是数组。
|
|
27
|
+
*
|
|
28
|
+
* @param {unknown} value 库里存的 algorithm 字段
|
|
29
|
+
* @returns {ThemeConfig['algorithm'] | undefined} antd 算法
|
|
30
|
+
*/
|
|
31
|
+
function toAlgorithm(value: unknown): ThemeConfig['algorithm'] | undefined {
|
|
32
|
+
const names = (Array.isArray(value) ? value : [value]).filter((name): name is keyof typeof ALGORITHMS =>
|
|
33
|
+
Boolean(name && typeof name === 'string' && ALGORITHMS[name]),
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
if (!names.length) {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return names.length === 1 ? ALGORITHMS[names[0]] : names.map((name) => ALGORITHMS[name]);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 读取设置中心主题。
|
|
45
|
+
*
|
|
46
|
+
* 设置中心的外观由 `themeConfig` 里那条「简约」记录约束,在主题编辑器里就能改;
|
|
47
|
+
* 它本身是一条普通的可选主题,别的应用要同一套观感直接选它即可。
|
|
48
|
+
* 读不到(主题编辑器插件没启、老实例还没迁移、请求失败)时返回 `null`,
|
|
49
|
+
* 由调用方回落到代码里的默认中性配色,避免设置中心突然变回一片蓝。
|
|
50
|
+
*
|
|
51
|
+
* @returns {ThemeConfig | null | undefined} 主题配置;`undefined` 表示还没读完
|
|
52
|
+
*/
|
|
53
|
+
export function useSettingsThemeConfig(): ThemeConfig | null | undefined {
|
|
54
|
+
const app = useApp();
|
|
55
|
+
const [themeConfig, setThemeConfig] = useState<ThemeConfig | null | undefined>(undefined);
|
|
56
|
+
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
let cancelled = false;
|
|
59
|
+
|
|
60
|
+
const load = async () => {
|
|
61
|
+
try {
|
|
62
|
+
const response = await app.apiClient.request({
|
|
63
|
+
resource: 'themeConfig',
|
|
64
|
+
action: 'list',
|
|
65
|
+
params: { paginate: false },
|
|
66
|
+
});
|
|
67
|
+
const items: any[] = response?.data?.data || [];
|
|
68
|
+
const matched = items.find((item) => item?.uid === MINIMAL_THEME_UID);
|
|
69
|
+
const config = matched?.config;
|
|
70
|
+
|
|
71
|
+
if (cancelled) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!config) {
|
|
76
|
+
setThemeConfig(null);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
setThemeConfig({
|
|
81
|
+
token: config.token,
|
|
82
|
+
components: config.components,
|
|
83
|
+
algorithm: toAlgorithm(config.algorithm),
|
|
84
|
+
});
|
|
85
|
+
} catch (error) {
|
|
86
|
+
if (!cancelled) {
|
|
87
|
+
setThemeConfig(null);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
void load();
|
|
93
|
+
|
|
94
|
+
return () => {
|
|
95
|
+
cancelled = true;
|
|
96
|
+
};
|
|
97
|
+
}, [app.apiClient]);
|
|
98
|
+
|
|
99
|
+
return themeConfig;
|
|
100
|
+
}
|