@jiangood/open-admin 3.1.11 → 3.1.12
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/package.json +3 -3
- package/src/framework/GlobalData.ts +3 -1
- package/src/framework/biz/OrgTree.tsx +1 -1
- package/src/framework/biz/RoleTree.tsx +1 -1
- package/src/framework/biz/UserPicker.tsx +4 -4
- package/src/framework/biz/index.tsx +4 -4
- package/src/framework/components/LinkButton/index.tsx +4 -0
- package/src/framework/components/OrgSwitcher/index.tsx +2 -2
- package/src/framework/components/Page/index.tsx +6 -14
- package/src/framework/components/ProTable/index.tsx +3 -1
- package/src/framework/components/ThemeSettings/index.tsx +5 -38
- package/src/framework/config/index.ts +12 -0
- package/src/framework/config/layoutConfig.ts +47 -8
- package/src/framework/config/themeConfig.ts +93 -25
- package/src/framework/config/uiDefaults.ts +26 -0
- package/src/framework/fields/FieldEditor/index.tsx +1 -1
- package/src/framework/fields/FieldUploadFile/index.tsx +1 -1
- package/src/framework/fields/FieldUploadImage/index.tsx +42 -10
- package/src/framework/fields/FieldUserPicker/index.tsx +3 -3
- package/src/framework/fields/orgTree.ts +1 -1
- package/src/framework/router/index.ts +1 -0
- package/src/framework/router/matcher.ts +19 -4
- package/src/framework/utils/AvatarUtils.ts +64 -0
- package/src/framework/utils/HttpClient.ts +5 -5
- package/src/framework/utils/PageUtils.ts +38 -7
- package/src/framework/utils/TitleUtils.ts +38 -0
- package/src/framework/utils/UrlUtils.ts +3 -3
- package/src/framework/utils/index.ts +2 -0
- package/src/layouts/admin/HeaderRight.jsx +62 -16
- package/src/layouts/admin/SinglePageLayout.jsx +13 -2
- package/src/layouts/admin/TabLayout.jsx +38 -19
- package/src/layouts/admin/index.jsx +4 -4
- package/src/layouts/index.tsx +52 -15
- package/src/pages/404.jsx +4 -0
- package/src/pages/article/$code.jsx +5 -1
- package/src/pages/login.jsx +157 -0
- package/src/pages/{public/login.less → login.less} +16 -1
- package/src/pages/public/login.jsx +11 -145
- package/src/pages/standalone/{forceUpdatePwd.jsx → force-update-pwd.jsx} +10 -4
- package/src/pages/system/article/index.jsx +1 -1
- package/src/pages/system/config/index.jsx +46 -20
- package/src/pages/system/file/index.jsx +3 -3
- package/src/pages/system/job/index.jsx +1 -1
- package/src/pages/system/log/index.jsx +1 -1
- package/src/pages/system/online/index.jsx +95 -0
- package/src/pages/system/org/index.jsx +7 -7
- package/src/pages/system/role/index.jsx +8 -8
- package/src/pages/system/role/{rolePerm.jsx → role-perm.jsx} +9 -4
- package/src/pages/system/user/{userPerm.jsx → UserPermModal.jsx} +3 -3
- package/src/pages/system/user/index.jsx +6 -6
- package/src/pages/user-center/AvatarPicker.jsx +76 -0
- package/src/pages/{userCenter/permView.jsx → user-center/PermView.jsx} +48 -25
- package/src/pages/user-center/RecentOps.jsx +36 -0
- package/src/pages/user-center/index.jsx +393 -0
- package/vite-plugin/index.js +64 -6
- package/src/pages/userCenter/index.jsx +0 -93
package/package.json
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"less": "^4.2.0",
|
|
16
16
|
"typescript": "^5.9.3",
|
|
17
17
|
"typescript-eslint": "^8.67.0",
|
|
18
|
-
"vite": "^8.
|
|
18
|
+
"vite": "^8.3.3"
|
|
19
19
|
},
|
|
20
20
|
"peerDependencies": {
|
|
21
21
|
"@ant-design/icons": "^6.0.0",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"cropperjs": "^1.6.2",
|
|
27
27
|
"dayjs": "^1.11.13",
|
|
28
28
|
"lodash": "^4.17.23",
|
|
29
|
-
"qs": "^6.
|
|
29
|
+
"qs": "^6.16.1",
|
|
30
30
|
"react": "^19.0.0",
|
|
31
31
|
"react-dom": "^19.0.0"
|
|
32
32
|
},
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"lint": "eslint src --max-warnings=0",
|
|
56
56
|
"lint:fix": "eslint src --fix"
|
|
57
57
|
},
|
|
58
|
-
"version": "3.1.
|
|
58
|
+
"version": "3.1.12",
|
|
59
59
|
"overrides": {
|
|
60
60
|
"zod-validation-error": "^4.0.2"
|
|
61
61
|
}
|
|
@@ -12,7 +12,7 @@ export interface SiteInfo {
|
|
|
12
12
|
waterMark?: boolean;
|
|
13
13
|
/** 登录失效(空闲超时/未登录)时全局弹窗的提示文案,对应 sys.login-expired-message */
|
|
14
14
|
loginExpiredMessage?: string;
|
|
15
|
-
/** 后端声明为 publicVisible
|
|
15
|
+
/** 后端声明为 publicVisible 的参数项,key → value */
|
|
16
16
|
config?: Record<string, unknown>;
|
|
17
17
|
[key: string]: unknown;
|
|
18
18
|
}
|
|
@@ -22,6 +22,8 @@ export interface LoginInfo {
|
|
|
22
22
|
account?: string;
|
|
23
23
|
phone?: string;
|
|
24
24
|
email?: string;
|
|
25
|
+
/** 头像文件 objectName,页面按 /file/{avatar} 访问 */
|
|
26
|
+
avatar?: string;
|
|
25
27
|
unit?: string;
|
|
26
28
|
dept?: string;
|
|
27
29
|
roles?: string;
|
|
@@ -22,7 +22,7 @@ export class OrgTree extends React.Component<OrgTreeProps, OrgTreeState> {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
componentDidMount() {
|
|
25
|
-
HttpClient.get('admin/
|
|
25
|
+
HttpClient.get('admin/sys-org/tree', null).then(tree => {
|
|
26
26
|
this.setState({treeData: tree.data, treeDataLoading: false})
|
|
27
27
|
}).catch(() => {
|
|
28
28
|
// 失败时结束 loading 并置空(组件会显示空状态提示),避免一直停留在骨架屏;
|
|
@@ -22,7 +22,7 @@ export class RoleTree extends React.Component<RoleTreeProps, RoleTreeState> {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
componentDidMount() {
|
|
25
|
-
HttpClient.get('admin/
|
|
25
|
+
HttpClient.get('admin/sys-role/biz-tree', null).then(tree => {
|
|
26
26
|
this.setState({treeData: tree.data, treeDataLoading: false})
|
|
27
27
|
}).catch(() => {
|
|
28
28
|
// 失败时结束 loading 并置空(组件会显示空状态提示),避免一直停留在骨架屏;
|
|
@@ -20,9 +20,9 @@ export interface UserPickerProps {
|
|
|
20
20
|
width?: number;
|
|
21
21
|
/** 内容区高度,默认 530(刚好容纳默认每页 10 行,不出现表格内部纵向滚动) */
|
|
22
22
|
height?: number;
|
|
23
|
-
/** 分页数据源地址,默认 admin/
|
|
23
|
+
/** 分页数据源地址,默认 admin/sys-user/page */
|
|
24
24
|
requestUrl?: string;
|
|
25
|
-
/** 按 id 查询用户详情地址(用于回显),默认 admin/
|
|
25
|
+
/** 按 id 查询用户详情地址(用于回显),默认 admin/sys-user/by-ids */
|
|
26
26
|
usersUrl?: string;
|
|
27
27
|
/** 确定按钮 loading */
|
|
28
28
|
confirmLoading?: boolean;
|
|
@@ -88,9 +88,9 @@ export class UserPicker extends React.Component<UserPickerProps, UserPickerState
|
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
getRequestUrl = () => this.props.requestUrl || 'admin/
|
|
91
|
+
getRequestUrl = () => this.props.requestUrl || 'admin/sys-user/page';
|
|
92
92
|
|
|
93
|
-
getUsersUrl = () => this.props.usersUrl || 'admin/
|
|
93
|
+
getUsersUrl = () => this.props.usersUrl || 'admin/sys-user/by-ids';
|
|
94
94
|
|
|
95
95
|
init = () => {
|
|
96
96
|
this.formRef.current?.resetFields();
|
|
@@ -11,22 +11,22 @@ export * from './pickerUser'
|
|
|
11
11
|
|
|
12
12
|
class InnerFieldUserSelect extends React.Component<ComponentProps<typeof FieldRemoteSelect>> {
|
|
13
13
|
render() {
|
|
14
|
-
return <FieldRemoteSelect url="admin/
|
|
14
|
+
return <FieldRemoteSelect url="admin/sys-user/options" {...this.props} />;
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
17
|
class InnerFieldUserSelectMultiple extends React.Component<ComponentProps<typeof FieldRemoteSelect>> {
|
|
18
18
|
render() {
|
|
19
|
-
return <FieldRemoteSelect url="admin/
|
|
19
|
+
return <FieldRemoteSelect url="admin/sys-user/options" multiple {...this.props} />;
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
22
|
class InnerFieldUnitTreeSelect extends React.Component<ComponentProps<typeof FieldRemoteTreeSelect>> {
|
|
23
23
|
render() {
|
|
24
|
-
return <FieldRemoteTreeSelect url="admin/
|
|
24
|
+
return <FieldRemoteTreeSelect url="admin/sys-org/unit-tree" {...this.props} />;
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
27
|
class InnerFieldDeptTreeSelect extends React.Component<ComponentProps<typeof FieldRemoteTreeSelect>> {
|
|
28
28
|
render() {
|
|
29
|
-
return <FieldRemoteTreeSelect url="admin/
|
|
29
|
+
return <FieldRemoteTreeSelect url="admin/sys-org/dept-tree" {...this.props} />;
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
class InnerFieldOrgTreeMultipleSelect extends React.Component<ComponentProps<typeof FieldRemoteTreeSelect>> {
|
|
@@ -2,6 +2,10 @@ import React from "react";
|
|
|
2
2
|
import {Button} from "antd";
|
|
3
3
|
import {PageUtils} from "../../utils";
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* 链接跳转按钮。
|
|
7
|
+
* `label` 参数已弃用:目标页面的标题请使用 pageMeta.title。
|
|
8
|
+
*/
|
|
5
9
|
export function LinkButton(props) {
|
|
6
10
|
const {path, label, children, size = 'small', ...rest} = props;
|
|
7
11
|
return <Button size={size} {...rest} onClick={() => {
|
|
@@ -18,7 +18,7 @@ export class OrgSwitcher extends React.Component<Record<string, never>, {tree: O
|
|
|
18
18
|
state = {tree: [], currentOrgId: null};
|
|
19
19
|
|
|
20
20
|
componentDidMount() {
|
|
21
|
-
HttpClient.get("admin/
|
|
21
|
+
HttpClient.get("admin/my-orgs", null).then(data => {
|
|
22
22
|
if (data.data) {
|
|
23
23
|
this.setState({tree: data.data.tree || [], currentOrgId: data.data.currentOrgId || null});
|
|
24
24
|
}
|
|
@@ -26,7 +26,7 @@ export class OrgSwitcher extends React.Component<Record<string, never>, {tree: O
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
handleChange = (orgId: string) => {
|
|
29
|
-
HttpClient.post("admin/
|
|
29
|
+
HttpClient.post("admin/switch-org", {orgId}, null).then(() => {
|
|
30
30
|
setTimeout(() => location.reload(), 1000);
|
|
31
31
|
});
|
|
32
32
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import { Space
|
|
2
|
+
import { Space } from "antd";
|
|
3
3
|
import './index.less'
|
|
4
4
|
import { PermActions, type PermAction } from "../../biz/PermActions";
|
|
5
5
|
import { PermUtils } from "../../utils";
|
|
@@ -19,8 +19,6 @@ interface PageProps {
|
|
|
19
19
|
/** @deprecated 页面底色已默认跟随布局底色,该参数不再产生差异;保留仅为兼容旧写法 */
|
|
20
20
|
backgroundGray?: boolean;
|
|
21
21
|
debug?: boolean;
|
|
22
|
-
title?: React.ReactNode;
|
|
23
|
-
description?: React.ReactNode;
|
|
24
22
|
/** 右侧操作区:推荐传 PermAction[](数据驱动),兼容带 perm 属性的节点 */
|
|
25
23
|
actions?: PermAction[] | React.ReactNode;
|
|
26
24
|
children?: React.ReactNode;
|
|
@@ -28,8 +26,7 @@ interface PageProps {
|
|
|
28
26
|
|
|
29
27
|
export class Page extends React.Component<PageProps> {
|
|
30
28
|
render() {
|
|
31
|
-
const { padding = true, debug = false,
|
|
32
|
-
const hasHeader = title != null || description != null || actions != null;
|
|
29
|
+
const { padding = true, debug = false, actions, children } = this.props;
|
|
33
30
|
const style: React.CSSProperties = {};
|
|
34
31
|
|
|
35
32
|
if (padding) {
|
|
@@ -41,18 +38,13 @@ export class Page extends React.Component<PageProps> {
|
|
|
41
38
|
}
|
|
42
39
|
|
|
43
40
|
return <div className={'oa-page'} style={style}>
|
|
44
|
-
{
|
|
41
|
+
{actions != null && (
|
|
45
42
|
<div className="oa-page-header">
|
|
46
43
|
<div className="oa-page-header-top">
|
|
47
|
-
<div className="oa-page-header-left"
|
|
48
|
-
|
|
49
|
-
{title && <Typography.Title level={5} style={{ margin: 0 }}>{title}</Typography.Title>}
|
|
50
|
-
{description && <div><Typography.Text type="secondary">{description}</Typography.Text></div>}
|
|
51
|
-
</div>
|
|
52
|
-
</div>
|
|
53
|
-
{actions != null && (Array.isArray(actions)
|
|
44
|
+
<div className="oa-page-header-left"/>
|
|
45
|
+
{Array.isArray(actions)
|
|
54
46
|
? <PermActions actions={actions}/>
|
|
55
|
-
: renderActionNodes(actions)
|
|
47
|
+
: renderActionNodes(actions)}
|
|
56
48
|
</div>
|
|
57
49
|
</div>
|
|
58
50
|
)}
|
|
@@ -71,6 +71,8 @@ export interface ProTableProps<T = unknown> {
|
|
|
71
71
|
bordered?: boolean;
|
|
72
72
|
/** 树形数据模式,关闭分页且不传 page/size 参数 */
|
|
73
73
|
treeMode?: boolean;
|
|
74
|
+
/** 无数据时的提示文案,默认“暂无数据” */
|
|
75
|
+
emptyText?: React.ReactNode;
|
|
74
76
|
}
|
|
75
77
|
|
|
76
78
|
interface ProTableState<T = unknown> {
|
|
@@ -256,7 +258,7 @@ this.setState({loading: true})
|
|
|
256
258
|
bordered={this.props.bordered ?? false}
|
|
257
259
|
locale={{
|
|
258
260
|
emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE}
|
|
259
|
-
description={hasFilter ? '没有符合条件的数据' : '暂无数据'}/>,
|
|
261
|
+
description={hasFilter ? '没有符合条件的数据' : (this.props.emptyText ?? '暂无数据')}/>,
|
|
260
262
|
}}
|
|
261
263
|
/>
|
|
262
264
|
</div>
|
|
@@ -7,19 +7,15 @@ import {
|
|
|
7
7
|
DEFAULT_SIDER_BG,
|
|
8
8
|
applyThemePreset,
|
|
9
9
|
getActivePresetKey,
|
|
10
|
-
getThemeMode,
|
|
11
10
|
resetAllSettings,
|
|
12
|
-
setThemeMode,
|
|
13
11
|
DENSITY_PRESETS,
|
|
14
12
|
RADIUS_PRESETS,
|
|
15
13
|
applyDensityPreset,
|
|
16
14
|
getActiveDensityKey,
|
|
17
15
|
applyRadiusPreset,
|
|
18
16
|
getActiveRadiusKey,
|
|
19
|
-
getLayoutMode,
|
|
20
|
-
setLayoutMode,
|
|
21
17
|
} from '../../config';
|
|
22
|
-
import type {
|
|
18
|
+
import type { ThemeDensity, ThemeRadius } from '../../config';
|
|
23
19
|
import { EventBus } from '../../utils/EventBus';
|
|
24
20
|
|
|
25
21
|
export interface ThemeSettingsProps {
|
|
@@ -32,29 +28,23 @@ function SectionTitle({ children }: { children: React.ReactNode }) {
|
|
|
32
28
|
}
|
|
33
29
|
|
|
34
30
|
/**
|
|
35
|
-
*
|
|
31
|
+
* 界面设置面板:主题色、界面密度、圆角。
|
|
32
|
+
* 明暗模式由顶栏的太阳/月亮按钮切换,此处不再重复提供。
|
|
36
33
|
* 所有变更即时生效并持久化到 localStorage。
|
|
37
34
|
*/
|
|
38
35
|
export function ThemeSettings({ open, onClose }: ThemeSettingsProps) {
|
|
39
36
|
const [activeKey, setActiveKey] = React.useState(getActivePresetKey());
|
|
40
|
-
const [mode, setMode] = React.useState<ThemeMode>(getThemeMode());
|
|
41
37
|
const [density, setDensity] = React.useState<ThemeDensity>(getActiveDensityKey());
|
|
42
38
|
const [radius, setRadius] = React.useState<ThemeRadius>(getActiveRadiusKey());
|
|
43
|
-
const [layout, setLayout] = React.useState<LayoutMode>(getLayoutMode());
|
|
44
39
|
|
|
45
40
|
React.useEffect(() => {
|
|
46
41
|
const offTheme = EventBus.on('themeChange', () => {
|
|
47
42
|
setActiveKey(getActivePresetKey());
|
|
48
|
-
setMode(getThemeMode());
|
|
49
43
|
setDensity(getActiveDensityKey());
|
|
50
44
|
setRadius(getActiveRadiusKey());
|
|
51
45
|
});
|
|
52
|
-
const offLayout = EventBus.on('layoutChange', () => {
|
|
53
|
-
setLayout(getLayoutMode());
|
|
54
|
-
});
|
|
55
46
|
return () => {
|
|
56
47
|
offTheme();
|
|
57
|
-
offLayout();
|
|
58
48
|
};
|
|
59
49
|
}, []);
|
|
60
50
|
|
|
@@ -63,20 +53,8 @@ export function ThemeSettings({ open, onClose }: ThemeSettingsProps) {
|
|
|
63
53
|
|
|
64
54
|
return (
|
|
65
55
|
<Drawer title="界面设置" open={open} onClose={onClose} size={340}>
|
|
66
|
-
<div style={{ fontWeight: 600 }}
|
|
67
|
-
<
|
|
68
|
-
block
|
|
69
|
-
style={{ marginTop: 12 }}
|
|
70
|
-
value={mode}
|
|
71
|
-
onChange={value => setThemeMode(value as ThemeMode)}
|
|
72
|
-
options={[
|
|
73
|
-
{ label: '浅色', value: 'light' },
|
|
74
|
-
{ label: '深色', value: 'dark' },
|
|
75
|
-
]}
|
|
76
|
-
/>
|
|
77
|
-
|
|
78
|
-
<SectionTitle>主题色</SectionTitle>
|
|
79
|
-
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: 12 }}>
|
|
56
|
+
<div style={{ fontWeight: 600 }}>主题色</div>
|
|
57
|
+
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: 12, marginTop: 12 }}>
|
|
80
58
|
{THEME_PRESETS.map(preset => {
|
|
81
59
|
const primary = preset.colors.colorPrimary || DEFAULT_COLORS.colorPrimary || '#1677ff';
|
|
82
60
|
const sider = preset.colors.siderBg || DEFAULT_SIDER_BG;
|
|
@@ -152,17 +130,6 @@ export function ThemeSettings({ open, onClose }: ThemeSettingsProps) {
|
|
|
152
130
|
}))}
|
|
153
131
|
/>
|
|
154
132
|
|
|
155
|
-
<SectionTitle>界面布局</SectionTitle>
|
|
156
|
-
<Segmented
|
|
157
|
-
block
|
|
158
|
-
value={layout}
|
|
159
|
-
onChange={value => setLayoutMode(value as LayoutMode)}
|
|
160
|
-
options={[
|
|
161
|
-
{ label: '多标签页', value: 'tabs' },
|
|
162
|
-
{ label: '单页', value: 'single' },
|
|
163
|
-
]}
|
|
164
|
-
/>
|
|
165
|
-
|
|
166
133
|
<Button block style={{ marginTop: 20 }} onClick={() => resetAllSettings()}>
|
|
167
134
|
恢复默认
|
|
168
135
|
</Button>
|
|
@@ -8,6 +8,9 @@ export {
|
|
|
8
8
|
toggleThemeMode,
|
|
9
9
|
applyThemePreset,
|
|
10
10
|
getActivePresetKey,
|
|
11
|
+
getDefaultPresetKey,
|
|
12
|
+
setDefaultThemePreset,
|
|
13
|
+
hasStoredPreset,
|
|
11
14
|
applyDensityPreset,
|
|
12
15
|
getActiveDensityKey,
|
|
13
16
|
applyRadiusPreset,
|
|
@@ -27,13 +30,22 @@ export type { ThemeColors, ThemeMode, ThemePreset, ThemeDensity, ThemeRadius, Th
|
|
|
27
30
|
|
|
28
31
|
export {
|
|
29
32
|
getLayoutMode,
|
|
33
|
+
getDefaultLayoutMode,
|
|
30
34
|
setLayoutMode,
|
|
35
|
+
setDefaultLayoutMode,
|
|
31
36
|
toggleLayoutMode,
|
|
32
37
|
resetLayoutMode,
|
|
38
|
+
hasStoredLayoutMode,
|
|
33
39
|
DEFAULT_LAYOUT_MODE,
|
|
34
40
|
} from './layoutConfig'
|
|
35
41
|
export type { LayoutMode } from './layoutConfig'
|
|
36
42
|
|
|
43
|
+
export {
|
|
44
|
+
applyServerUiDefaults,
|
|
45
|
+
UI_THEME_PRESET_KEY,
|
|
46
|
+
UI_TABS_ENABLED_KEY,
|
|
47
|
+
} from './uiDefaults'
|
|
48
|
+
|
|
37
49
|
import { resetUserTheme } from './themeConfig'
|
|
38
50
|
import { resetLayoutMode } from './layoutConfig'
|
|
39
51
|
|
|
@@ -4,29 +4,64 @@ export type LayoutMode = 'tabs' | 'single';
|
|
|
4
4
|
|
|
5
5
|
const LAYOUT_MODE_KEY = 'oa-layout-tabs';
|
|
6
6
|
|
|
7
|
+
/** 页签模式(多页签)/ 单页模式 */
|
|
7
8
|
export const DEFAULT_LAYOUT_MODE: LayoutMode = 'tabs';
|
|
8
9
|
|
|
9
10
|
const LAYOUT_MODES: LayoutMode[] = ['tabs', 'single'];
|
|
10
11
|
|
|
11
|
-
function readStoredLayoutMode(): LayoutMode {
|
|
12
|
-
if (typeof window === 'undefined') return
|
|
12
|
+
function readStoredLayoutMode(): LayoutMode | null {
|
|
13
|
+
if (typeof window === 'undefined') return null;
|
|
13
14
|
try {
|
|
14
15
|
const mode = localStorage.getItem(LAYOUT_MODE_KEY);
|
|
15
16
|
if (mode && LAYOUT_MODES.includes(mode as LayoutMode)) return mode as LayoutMode;
|
|
16
17
|
} catch {
|
|
17
18
|
// 隐私模式下 localStorage 不可用,忽略
|
|
18
19
|
}
|
|
19
|
-
return
|
|
20
|
+
return null;
|
|
20
21
|
}
|
|
21
22
|
|
|
22
|
-
|
|
23
|
+
/** 用户是否在本地自定义过页签模式 */
|
|
24
|
+
export function hasStoredLayoutMode(): boolean {
|
|
25
|
+
return readStoredLayoutMode() !== null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function removeStoredLayoutMode() {
|
|
29
|
+
if (typeof window === 'undefined') return;
|
|
30
|
+
try {
|
|
31
|
+
localStorage.removeItem(LAYOUT_MODE_KEY);
|
|
32
|
+
} catch {
|
|
33
|
+
// 忽略
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** 全局默认布局(来自参数设置,未自定义用户使用) */
|
|
38
|
+
let defaultLayoutMode: LayoutMode = DEFAULT_LAYOUT_MODE;
|
|
39
|
+
|
|
40
|
+
let currentLayoutMode: LayoutMode = readStoredLayoutMode() ?? DEFAULT_LAYOUT_MODE;
|
|
23
41
|
|
|
24
|
-
/**
|
|
42
|
+
/** 当前页签模式:'tabs' 多页签 / 'single' 单页 */
|
|
25
43
|
export function getLayoutMode(): LayoutMode {
|
|
26
44
|
return currentLayoutMode;
|
|
27
45
|
}
|
|
28
46
|
|
|
29
|
-
|
|
47
|
+
export function getDefaultLayoutMode(): LayoutMode {
|
|
48
|
+
return defaultLayoutMode;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 应用参数设置下发的全局默认页签模式。
|
|
53
|
+
* 仅在用户本地没有自定义时生效;已自定义用户保持个人选择。
|
|
54
|
+
*/
|
|
55
|
+
export function setDefaultLayoutMode(mode: LayoutMode) {
|
|
56
|
+
if (!LAYOUT_MODES.includes(mode)) return;
|
|
57
|
+
defaultLayoutMode = mode;
|
|
58
|
+
if (hasStoredLayoutMode() || currentLayoutMode === mode) return;
|
|
59
|
+
|
|
60
|
+
currentLayoutMode = mode;
|
|
61
|
+
EventBus.emit('layoutChange', mode);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** 设置页签模式,持久化并广播 layoutChange 供管理布局重渲染 */
|
|
30
65
|
export function setLayoutMode(mode: LayoutMode) {
|
|
31
66
|
if (!LAYOUT_MODES.includes(mode) || currentLayoutMode === mode) return;
|
|
32
67
|
currentLayoutMode = mode;
|
|
@@ -46,7 +81,11 @@ export function toggleLayoutMode(): LayoutMode {
|
|
|
46
81
|
return next;
|
|
47
82
|
}
|
|
48
83
|
|
|
49
|
-
/**
|
|
84
|
+
/** 恢复默认页签模式:清除本地覆盖,回到参数设置的全局默认 */
|
|
50
85
|
export function resetLayoutMode() {
|
|
51
|
-
|
|
86
|
+
removeStoredLayoutMode();
|
|
87
|
+
if (currentLayoutMode === defaultLayoutMode) return;
|
|
88
|
+
|
|
89
|
+
currentLayoutMode = defaultLayoutMode;
|
|
90
|
+
EventBus.emit('layoutChange', currentLayoutMode);
|
|
52
91
|
}
|
|
@@ -92,7 +92,10 @@ export const RADIUS_PRESETS: ThemeRadiusPreset[] = [
|
|
|
92
92
|
|
|
93
93
|
export const DEFAULT_RADIUS_KEY: ThemeRadius = 'default';
|
|
94
94
|
|
|
95
|
-
/**
|
|
95
|
+
/**
|
|
96
|
+
* 可选主题预设;antd 预设不做任何颜色覆盖,保持 100% 原生观感。
|
|
97
|
+
* 按色相环顺序排列:蓝 → 蓝灰 → 靛蓝 → 紫 → 粉 → 红 → 橙 → 绿 → 青,便于用户按相近色比较选择。
|
|
98
|
+
*/
|
|
96
99
|
export const THEME_PRESETS: ThemePreset[] = [
|
|
97
100
|
{
|
|
98
101
|
key: 'antd',
|
|
@@ -100,24 +103,24 @@ export const THEME_PRESETS: ThemePreset[] = [
|
|
|
100
103
|
colors: {},
|
|
101
104
|
},
|
|
102
105
|
{
|
|
103
|
-
key: '
|
|
104
|
-
name: '
|
|
106
|
+
key: 'graphite',
|
|
107
|
+
name: '石墨灰',
|
|
105
108
|
colors: {
|
|
106
|
-
colorPrimary: '#
|
|
107
|
-
siderBg: '#
|
|
108
|
-
siderSubBg: '#
|
|
109
|
-
siderTriggerBg: '#
|
|
109
|
+
colorPrimary: '#2f3542',
|
|
110
|
+
siderBg: '#1B2026',
|
|
111
|
+
siderSubBg: '#1B2026',
|
|
112
|
+
siderTriggerBg: '#141A20',
|
|
110
113
|
siderHoverBg: 'rgba(255,255,255,0.06)',
|
|
111
114
|
},
|
|
112
115
|
},
|
|
113
116
|
{
|
|
114
|
-
key: '
|
|
115
|
-
name: '
|
|
117
|
+
key: 'indigo',
|
|
118
|
+
name: '靛蓝',
|
|
116
119
|
colors: {
|
|
117
|
-
colorPrimary: '#
|
|
118
|
-
siderBg: '#
|
|
119
|
-
siderSubBg: '#
|
|
120
|
-
siderTriggerBg: '#
|
|
120
|
+
colorPrimary: '#4f46e5',
|
|
121
|
+
siderBg: '#1E1B4B',
|
|
122
|
+
siderSubBg: '#1E1B4B',
|
|
123
|
+
siderTriggerBg: '#17143C',
|
|
121
124
|
siderHoverBg: 'rgba(255,255,255,0.06)',
|
|
122
125
|
},
|
|
123
126
|
},
|
|
@@ -132,6 +135,17 @@ export const THEME_PRESETS: ThemePreset[] = [
|
|
|
132
135
|
siderHoverBg: 'rgba(255,255,255,0.06)',
|
|
133
136
|
},
|
|
134
137
|
},
|
|
138
|
+
{
|
|
139
|
+
key: 'pink',
|
|
140
|
+
name: '樱花粉',
|
|
141
|
+
colors: {
|
|
142
|
+
colorPrimary: '#eb2f96',
|
|
143
|
+
siderBg: '#2B1024',
|
|
144
|
+
siderSubBg: '#2B1024',
|
|
145
|
+
siderTriggerBg: '#200B1A',
|
|
146
|
+
siderHoverBg: 'rgba(255,255,255,0.06)',
|
|
147
|
+
},
|
|
148
|
+
},
|
|
135
149
|
{
|
|
136
150
|
key: 'red',
|
|
137
151
|
name: '中国红',
|
|
@@ -155,13 +169,24 @@ export const THEME_PRESETS: ThemePreset[] = [
|
|
|
155
169
|
},
|
|
156
170
|
},
|
|
157
171
|
{
|
|
158
|
-
key: '
|
|
159
|
-
name: '
|
|
172
|
+
key: 'green',
|
|
173
|
+
name: '翠竹',
|
|
160
174
|
colors: {
|
|
161
|
-
colorPrimary: '#
|
|
162
|
-
siderBg: '#
|
|
163
|
-
siderSubBg: '#
|
|
164
|
-
siderTriggerBg: '#
|
|
175
|
+
colorPrimary: '#389e0d',
|
|
176
|
+
siderBg: '#10291A',
|
|
177
|
+
siderSubBg: '#10291A',
|
|
178
|
+
siderTriggerBg: '#0B1E13',
|
|
179
|
+
siderHoverBg: 'rgba(255,255,255,0.06)',
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
key: 'cyan',
|
|
184
|
+
name: '青碧',
|
|
185
|
+
colors: {
|
|
186
|
+
colorPrimary: '#13c2c2',
|
|
187
|
+
siderBg: '#0C2E2E',
|
|
188
|
+
siderSubBg: '#0C2E2E',
|
|
189
|
+
siderTriggerBg: '#082222',
|
|
165
190
|
siderHoverBg: 'rgba(255,255,255,0.06)',
|
|
166
191
|
},
|
|
167
192
|
},
|
|
@@ -174,6 +199,8 @@ const THEME_RADIUS_KEY = 'oa-theme-radius';
|
|
|
174
199
|
|
|
175
200
|
/** 业务侧传入的颜色基线(由 setThemeColors 设置) */
|
|
176
201
|
let businessColors: Partial<ThemeColors> | null = null;
|
|
202
|
+
/** 全局默认预设 key(来自参数设置,未自定义用户使用) */
|
|
203
|
+
let defaultPresetKey: string = DEFAULT_PRESET_KEY;
|
|
177
204
|
/** 用户选择的预设 key(持久化到 localStorage) */
|
|
178
205
|
let presetKey: string = readStoredPreset();
|
|
179
206
|
let densityKey: ThemeDensity = readStoredDensity();
|
|
@@ -192,15 +219,24 @@ function readStoredMode(): ThemeMode {
|
|
|
192
219
|
}
|
|
193
220
|
}
|
|
194
221
|
|
|
195
|
-
function
|
|
196
|
-
if (typeof window === 'undefined') return
|
|
222
|
+
function readStoredPresetKey(): string | null {
|
|
223
|
+
if (typeof window === 'undefined') return null;
|
|
197
224
|
try {
|
|
198
225
|
const key = localStorage.getItem(THEME_PRESET_KEY);
|
|
199
226
|
if (key && THEME_PRESETS.some(p => p.key === key)) return key;
|
|
200
227
|
} catch {
|
|
201
228
|
// 隐私模式下 localStorage 不可用,忽略
|
|
202
229
|
}
|
|
203
|
-
return
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function readStoredPreset(): string {
|
|
234
|
+
return readStoredPresetKey() ?? DEFAULT_PRESET_KEY;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** 用户是否在本地自定义过主题色预设 */
|
|
238
|
+
export function hasStoredPreset(): boolean {
|
|
239
|
+
return readStoredPresetKey() !== null;
|
|
204
240
|
}
|
|
205
241
|
|
|
206
242
|
function persistPreset(key: string) {
|
|
@@ -243,6 +279,15 @@ function writeStored(key: string, value: string) {
|
|
|
243
279
|
}
|
|
244
280
|
}
|
|
245
281
|
|
|
282
|
+
function removeStored(key: string) {
|
|
283
|
+
if (typeof window === 'undefined') return;
|
|
284
|
+
try {
|
|
285
|
+
localStorage.removeItem(key);
|
|
286
|
+
} catch {
|
|
287
|
+
// 忽略
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
246
291
|
function getActiveDensity(): ThemeDensityPreset | undefined {
|
|
247
292
|
return DENSITY_PRESETS.find(p => p.key === densityKey);
|
|
248
293
|
}
|
|
@@ -454,6 +499,26 @@ export function getActivePresetKey(): string {
|
|
|
454
499
|
return presetKey;
|
|
455
500
|
}
|
|
456
501
|
|
|
502
|
+
export function getDefaultPresetKey(): string {
|
|
503
|
+
return defaultPresetKey;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* 应用参数设置下发的全局默认主题色。
|
|
508
|
+
* 仅在用户本地没有自定义预设时生效;已自定义用户保持个人选择,
|
|
509
|
+
* 但其「恢复默认」会回到这里设置的值。
|
|
510
|
+
*/
|
|
511
|
+
export function setDefaultThemePreset(key: string) {
|
|
512
|
+
if (!THEME_PRESETS.some(p => p.key === key)) return;
|
|
513
|
+
defaultPresetKey = key;
|
|
514
|
+
if (hasStoredPreset() || presetKey === key) return;
|
|
515
|
+
|
|
516
|
+
presetKey = key;
|
|
517
|
+
invalidate();
|
|
518
|
+
syncCssVars(getToken());
|
|
519
|
+
EventBus.emit('themeChange', currentMode);
|
|
520
|
+
}
|
|
521
|
+
|
|
457
522
|
/** 应用界面密度预设,持久化并广播 themeChange */
|
|
458
523
|
export function applyDensityPreset(key: ThemeDensity) {
|
|
459
524
|
if (!DENSITY_PRESETS.some(p => p.key === key)) return;
|
|
@@ -482,13 +547,16 @@ export function getActiveRadiusKey(): ThemeRadius {
|
|
|
482
547
|
return radiusKey;
|
|
483
548
|
}
|
|
484
549
|
|
|
485
|
-
/**
|
|
550
|
+
/**
|
|
551
|
+
* 恢复默认:主题色回到参数设置的全局默认,密度 / 圆角 / 明暗复位(不影响布局,布局由 resetAllSettings 统一处理)。
|
|
552
|
+
* 主题色会同时清除本地覆盖,之后管理员调整全局默认值时重新生效。
|
|
553
|
+
*/
|
|
486
554
|
export function resetUserTheme() {
|
|
487
|
-
presetKey =
|
|
555
|
+
presetKey = defaultPresetKey;
|
|
488
556
|
densityKey = DEFAULT_DENSITY_KEY;
|
|
489
557
|
radiusKey = DEFAULT_RADIUS_KEY;
|
|
490
558
|
currentMode = 'light';
|
|
491
|
-
|
|
559
|
+
removeStored(THEME_PRESET_KEY);
|
|
492
560
|
writeStored(THEME_DENSITY_KEY, densityKey);
|
|
493
561
|
writeStored(THEME_RADIUS_KEY, radiusKey);
|
|
494
562
|
writeStored(THEME_MODE_KEY, currentMode);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { setDefaultThemePreset } from './themeConfig';
|
|
2
|
+
import { setDefaultLayoutMode } from './layoutConfig';
|
|
3
|
+
|
|
4
|
+
/** 参数设置中「默认界面」分组的配置键 */
|
|
5
|
+
export const UI_THEME_PRESET_KEY = 'sys.ui.theme-preset';
|
|
6
|
+
export const UI_TABS_ENABLED_KEY = 'sys.ui.tabs-enabled';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 应用后端下发的全局默认界面配置(来自 site-info.config)。
|
|
10
|
+
*
|
|
11
|
+
* 只对未在本地自定义过的用户生效;已自定义用户保持个人设置,
|
|
12
|
+
* 其「恢复默认」会回到这里设置的全局默认值。
|
|
13
|
+
*/
|
|
14
|
+
export function applyServerUiDefaults(config?: Record<string, unknown> | null) {
|
|
15
|
+
if (!config) return;
|
|
16
|
+
|
|
17
|
+
const preset = config[UI_THEME_PRESET_KEY];
|
|
18
|
+
if (typeof preset === 'string') {
|
|
19
|
+
setDefaultThemePreset(preset);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const tabsEnabled = config[UI_TABS_ENABLED_KEY];
|
|
23
|
+
if (typeof tabsEnabled === 'boolean') {
|
|
24
|
+
setDefaultLayoutMode(tabsEnabled ? 'tabs' : 'single');
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -35,7 +35,7 @@ export class FieldEditor extends React.Component<FieldEditorProps, {loading: boo
|
|
|
35
35
|
render() {
|
|
36
36
|
const {loading} = this.state;
|
|
37
37
|
const isPublic = this.props.isPublic ?? true;
|
|
38
|
-
const uploadUrl = UrlUtils.contextPath('/admin/
|
|
38
|
+
const uploadUrl = UrlUtils.contextPath('/admin/sys-file/upload') + '?isPublic=' + isPublic;
|
|
39
39
|
const jsUrl = UrlUtils.contextPath('/admin/tinymce/tinymce.min.js');
|
|
40
40
|
const {value, onChange, height} = this.props;
|
|
41
41
|
const editorHeight = height || 300;
|