@jiangood/open-admin 3.1.10 → 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 +4 -0
- 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/CronEditor/index.tsx +318 -0
- 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/components/index.ts +1 -0
- 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/CronUtils.ts +562 -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 +3 -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 +12 -3
- 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 +335 -0
- package/src/pages/system/file/index.jsx +3 -3
- package/src/pages/system/job/index.jsx +62 -19
- 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
|
@@ -26,16 +26,16 @@ export class UrlUtils {
|
|
|
26
26
|
|
|
27
27
|
static paramsToSearch(params: Record<string, string | number | boolean | null | undefined> | null | undefined): string {
|
|
28
28
|
if (!params) return "";
|
|
29
|
-
const
|
|
29
|
+
const search = new URLSearchParams();
|
|
30
30
|
for (const k in params) {
|
|
31
31
|
if (Object.hasOwn(params, k)) {
|
|
32
32
|
const v = params[k];
|
|
33
33
|
if (v !== null && v !== undefined) {
|
|
34
|
-
|
|
34
|
+
search.append(k, String(v));
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
|
-
return
|
|
38
|
+
return search.toString();
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
static setParam(url: string, key: string, value: string | number | boolean | null | undefined): string {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export * from "./DateUtils";
|
|
2
|
+
export * from "./CronUtils";
|
|
2
3
|
export * from "./ArrayUtils";
|
|
3
4
|
export * from "./UrlUtils"
|
|
4
5
|
export * from "./StringUtils"
|
|
@@ -10,6 +11,8 @@ export * from './ObjectUtils'
|
|
|
10
11
|
export * from './DictUtils'
|
|
11
12
|
export * from './PermUtils'
|
|
12
13
|
export * from './PageUtils'
|
|
14
|
+
export * from './TitleUtils'
|
|
13
15
|
export * from './HttpClient'
|
|
14
16
|
export * from './MessageApi'
|
|
15
17
|
export * from './ModalApi'
|
|
18
|
+
export * from './AvatarUtils'
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {Avatar, Dropdown, Modal} from "antd";
|
|
2
|
-
import {BgColorsOutlined} from "@ant-design/icons";
|
|
2
|
+
import {BgColorsOutlined, FileOutlined, MoonOutlined, ProfileOutlined, SunOutlined, UserOutlined} from "@ant-design/icons";
|
|
3
3
|
import React from "react";
|
|
4
|
-
import {HttpClient, PageUtils, GlobalData,
|
|
4
|
+
import {HttpClient, PageUtils, GlobalData, history, EventBus, LOGIN_PATH, ThemeSettings, AvatarUtils, ARTICLE_HEADER_AVATAR_DROPDOWN, ARTICLE_HEADER_RIGHT, getThemeMode, toggleThemeMode, getLayoutMode, toggleLayoutMode} from "../../framework";
|
|
5
5
|
|
|
6
6
|
export class HeaderRight extends React.Component {
|
|
7
7
|
|
|
@@ -13,8 +13,30 @@ export class HeaderRight extends React.Component {
|
|
|
13
13
|
themeSettingsVisible: false,
|
|
14
14
|
};
|
|
15
15
|
|
|
16
|
+
unsubscribeLoginInfo = null;
|
|
17
|
+
unsubscribeTheme = null;
|
|
18
|
+
unsubscribeLayout = null;
|
|
19
|
+
|
|
16
20
|
componentDidMount() {
|
|
17
21
|
this.loadArticles()
|
|
22
|
+
// 个人中心修改头像/资料后刷新顶栏显示
|
|
23
|
+
this.unsubscribeLoginInfo = EventBus.on('loginInfoChange', () => this.setState({}))
|
|
24
|
+
// 明暗切换后刷新太阳/月亮图标(其它入口切换时也能同步)
|
|
25
|
+
this.unsubscribeTheme = EventBus.on('themeChange', () => this.setState({}))
|
|
26
|
+
// 布局切换后刷新图标(其它入口切换时也能同步)
|
|
27
|
+
this.unsubscribeLayout = EventBus.on('layoutChange', () => this.setState({}))
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
componentWillUnmount() {
|
|
31
|
+
if (this.unsubscribeLoginInfo) {
|
|
32
|
+
this.unsubscribeLoginInfo()
|
|
33
|
+
}
|
|
34
|
+
if (this.unsubscribeTheme) {
|
|
35
|
+
this.unsubscribeTheme()
|
|
36
|
+
}
|
|
37
|
+
if (this.unsubscribeLayout) {
|
|
38
|
+
this.unsubscribeLayout()
|
|
39
|
+
}
|
|
18
40
|
}
|
|
19
41
|
|
|
20
42
|
loadArticles = () => {
|
|
@@ -36,11 +58,34 @@ export class HeaderRight extends React.Component {
|
|
|
36
58
|
}
|
|
37
59
|
|
|
38
60
|
account = () => {
|
|
39
|
-
PageUtils.open('/
|
|
61
|
+
PageUtils.open('/user-center')
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
openArticle = (code) => {
|
|
65
|
+
PageUtils.open('/article/' + code)
|
|
40
66
|
}
|
|
41
67
|
|
|
42
|
-
|
|
43
|
-
|
|
68
|
+
renderThemeModeToggle = () => {
|
|
69
|
+
const isDark = getThemeMode() === 'dark';
|
|
70
|
+
const title = isDark ? '切换到浅色模式' : '切换到深色模式';
|
|
71
|
+
return (
|
|
72
|
+
<button type="button" className='item' title={title} aria-label={title}
|
|
73
|
+
onClick={() => toggleThemeMode()}>
|
|
74
|
+
{isDark ? <SunOutlined/> : <MoonOutlined/>}
|
|
75
|
+
</button>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
renderLayoutModeToggle = () => {
|
|
80
|
+
const isTabs = getLayoutMode() === 'tabs';
|
|
81
|
+
const title = isTabs ? '切换为单页模式' : '切换为页签模式';
|
|
82
|
+
const Icon = isTabs ? FileOutlined : ProfileOutlined;
|
|
83
|
+
return (
|
|
84
|
+
<button type="button" className='item' title={title} aria-label={title}
|
|
85
|
+
onClick={() => toggleLayoutMode()}>
|
|
86
|
+
<Icon/>
|
|
87
|
+
</button>
|
|
88
|
+
)
|
|
44
89
|
}
|
|
45
90
|
|
|
46
91
|
renderThemeSettings = () => {
|
|
@@ -61,6 +106,7 @@ export class HeaderRight extends React.Component {
|
|
|
61
106
|
|
|
62
107
|
render() {
|
|
63
108
|
const info = GlobalData.getLoginInfo()
|
|
109
|
+
const avatarSrc = AvatarUtils.resolve(info.avatar)
|
|
64
110
|
const {dropdownArticles, headerArticles} = this.state
|
|
65
111
|
|
|
66
112
|
const articleItems = dropdownArticles.map(a => ({
|
|
@@ -69,7 +115,7 @@ export class HeaderRight extends React.Component {
|
|
|
69
115
|
}))
|
|
70
116
|
|
|
71
117
|
const menuItems = [
|
|
72
|
-
{key: '
|
|
118
|
+
{key: 'user-center', label: '个人中心'},
|
|
73
119
|
...articleItems,
|
|
74
120
|
{key: 'logout', label: '退出登录'},
|
|
75
121
|
]
|
|
@@ -77,31 +123,31 @@ export class HeaderRight extends React.Component {
|
|
|
77
123
|
return <div className='header-right'>
|
|
78
124
|
{headerArticles.map(a => (
|
|
79
125
|
<button key={a.code} type="button" className='item'
|
|
80
|
-
onClick={() => this.openArticle(a.code
|
|
126
|
+
onClick={() => this.openArticle(a.code)}>
|
|
81
127
|
{a.title}
|
|
82
128
|
</button>
|
|
83
129
|
))}
|
|
84
130
|
|
|
131
|
+
{this.renderLayoutModeToggle()}
|
|
132
|
+
|
|
133
|
+
{this.renderThemeModeToggle()}
|
|
134
|
+
|
|
85
135
|
{this.renderThemeSettings()}
|
|
86
136
|
|
|
87
137
|
<Dropdown menu={{
|
|
88
138
|
onClick: ({key}) => {
|
|
89
|
-
if (key === '
|
|
139
|
+
if (key === 'user-center') {
|
|
90
140
|
this.account()
|
|
91
141
|
} else if (key === 'logout') {
|
|
92
142
|
this.logout();
|
|
93
143
|
} else if (key.startsWith('article:')) {
|
|
94
|
-
|
|
95
|
-
const article = dropdownArticles.find(a => a.code === code)
|
|
96
|
-
this.openArticle(code, article ? article.title : code)
|
|
144
|
+
this.openArticle(key.substring(8))
|
|
97
145
|
}
|
|
98
146
|
},
|
|
99
147
|
items: menuItems,
|
|
100
148
|
}}>
|
|
101
149
|
<div className='item' style={{cursor: 'pointer'}}>
|
|
102
|
-
<Avatar size="default"
|
|
103
|
-
{info.name?.charAt(0)}
|
|
104
|
-
</Avatar>
|
|
150
|
+
<Avatar size="default" src={avatarSrc || undefined} icon={<UserOutlined/>}/>
|
|
105
151
|
<span style={{marginLeft: 8}}>{info.name}</span>
|
|
106
152
|
</div>
|
|
107
153
|
</Dropdown>
|
|
@@ -111,7 +157,7 @@ export class HeaderRight extends React.Component {
|
|
|
111
157
|
onOk={() => {
|
|
112
158
|
this.setState({alertVisible: false});
|
|
113
159
|
EventBus.emit('logoutSuccess');
|
|
114
|
-
history.replace(
|
|
160
|
+
history.replace(LOGIN_PATH)
|
|
115
161
|
}}>
|
|
116
162
|
退出登录成功
|
|
117
163
|
</Modal>
|
|
@@ -121,7 +167,7 @@ export class HeaderRight extends React.Component {
|
|
|
121
167
|
this.setState({confirmVisible: false});
|
|
122
168
|
localStorage.clear();
|
|
123
169
|
EventBus.emit('logoutSuccess');
|
|
124
|
-
history.replace(
|
|
170
|
+
history.replace(LOGIN_PATH)
|
|
125
171
|
}}>
|
|
126
172
|
退出登录失败,是否清空缓存
|
|
127
173
|
</Modal>
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import {history, PageFrame} from "../../framework";
|
|
2
|
+
import {history, PageFrame, matchRoute, setPageTitle} from "../../framework";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
5
|
+
* 单页布局:不使用页签,仅渲染当前路由对应页面。
|
|
6
6
|
* 路由变化时按 url 重新挂载 PageFrame,页面状态随之重置。
|
|
7
7
|
*/
|
|
8
8
|
export class SinglePageLayout extends React.Component {
|
|
@@ -14,6 +14,11 @@ export class SinglePageLayout extends React.Component {
|
|
|
14
14
|
this.unlisten = history.listen(({location}) => {
|
|
15
15
|
this.setState({location});
|
|
16
16
|
});
|
|
17
|
+
this.syncDocumentTitle();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
componentDidUpdate() {
|
|
21
|
+
this.syncDocumentTitle();
|
|
17
22
|
}
|
|
18
23
|
|
|
19
24
|
componentWillUnmount() {
|
|
@@ -22,6 +27,12 @@ export class SinglePageLayout extends React.Component {
|
|
|
22
27
|
}
|
|
23
28
|
}
|
|
24
29
|
|
|
30
|
+
syncDocumentTitle() {
|
|
31
|
+
const {pathname} = this.state.location;
|
|
32
|
+
const label = matchRoute(pathname)?.meta?.title || this.props.pathMenuMap?.[pathname]?.name;
|
|
33
|
+
setPageTitle(label);
|
|
34
|
+
}
|
|
35
|
+
|
|
25
36
|
render() {
|
|
26
37
|
const {pathname, search} = this.state.location;
|
|
27
38
|
const url = pathname + search;
|
|
@@ -1,24 +1,39 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import {history, PageFrame,
|
|
2
|
+
import {history, PageFrame, EventBus, matchRoute, setPageTitle} from "../../framework";
|
|
3
3
|
import {AdminTabs} from "./AdminTabs";
|
|
4
4
|
|
|
5
|
-
function
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
function getPathname(url) {
|
|
6
|
+
const qIndex = url.indexOf('?');
|
|
7
|
+
return qIndex === -1 ? url : url.substring(0, qIndex);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** 移除已弃用的 _label 参数,避免旧 URL 产生重复 Tab;其余 query 原样保留 */
|
|
11
|
+
function normalizeUrl(url) {
|
|
12
|
+
const qIndex = url.indexOf('?');
|
|
13
|
+
if (qIndex === -1) return url;
|
|
14
|
+
const params = new URLSearchParams(url.substring(qIndex));
|
|
15
|
+
if (!params.has('_label')) return url;
|
|
16
|
+
params.delete('_label');
|
|
17
|
+
const search = params.toString();
|
|
18
|
+
return search ? `${url.substring(0, qIndex)}?${search}` : url.substring(0, qIndex);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function getLabel(pathname, pathMenuMap) {
|
|
22
|
+
if (pathname === '/' || pathname === '/index') return '首页';
|
|
23
|
+
const route = matchRoute(pathname);
|
|
24
|
+
// 页面显式声明的标题优先;未声明时回退到后端菜单名
|
|
25
|
+
if (route?.meta?.title) return route.meta.title;
|
|
9
26
|
const menu = pathMenuMap[pathname];
|
|
10
27
|
if (menu) return menu.name;
|
|
28
|
+
if (!route) return '页面不存在';
|
|
11
29
|
return '未命名';
|
|
12
30
|
}
|
|
13
31
|
|
|
14
32
|
export class TabLayout extends React.Component {
|
|
15
33
|
constructor(props) {
|
|
16
34
|
super(props);
|
|
17
|
-
const initUrl = history.location.pathname + history.location.search;
|
|
18
|
-
const
|
|
19
|
-
const initPathname = qIndex === -1 ? initUrl : initUrl.substring(0, qIndex);
|
|
20
|
-
const initSearch = qIndex === -1 ? '' : initUrl.substring(qIndex);
|
|
21
|
-
const initLabel = getLabel(initPathname, initSearch, props.pathMenuMap);
|
|
35
|
+
const initUrl = normalizeUrl(history.location.pathname + history.location.search);
|
|
36
|
+
const initLabel = getLabel(getPathname(initUrl), props.pathMenuMap);
|
|
22
37
|
this.state = {
|
|
23
38
|
tabs: [{key: initUrl, label: initLabel, refreshKey: 0}],
|
|
24
39
|
activeKey: initUrl,
|
|
@@ -29,13 +44,11 @@ export class TabLayout extends React.Component {
|
|
|
29
44
|
unlisten = null;
|
|
30
45
|
unsubscribeClosePage = null;
|
|
31
46
|
|
|
32
|
-
openTab = (
|
|
33
|
-
const
|
|
34
|
-
const pathname = qIndex === -1 ? url : url.substring(0, qIndex);
|
|
35
|
-
const search = qIndex === -1 ? '' : url.substring(qIndex);
|
|
47
|
+
openTab = (rawUrl) => {
|
|
48
|
+
const url = normalizeUrl(rawUrl);
|
|
36
49
|
this.setState(prev => ({
|
|
37
50
|
tabs: prev.tabs.some(t => t.key === url) ? prev.tabs
|
|
38
|
-
: [...prev.tabs, {key: url, label: getLabel(
|
|
51
|
+
: [...prev.tabs, {key: url, label: getLabel(getPathname(url), this.props.pathMenuMap), refreshKey: 0}],
|
|
39
52
|
activeKey: url,
|
|
40
53
|
}));
|
|
41
54
|
};
|
|
@@ -64,6 +77,13 @@ export class TabLayout extends React.Component {
|
|
|
64
77
|
this.closeTab(url);
|
|
65
78
|
};
|
|
66
79
|
|
|
80
|
+
/** 浏览器标题跟随当前激活标签 */
|
|
81
|
+
syncDocumentTitle() {
|
|
82
|
+
const {tabs, activeKey} = this.state;
|
|
83
|
+
const active = tabs.find(t => t.key === activeKey);
|
|
84
|
+
setPageTitle(active && active.label !== '未命名' ? active.label : undefined);
|
|
85
|
+
}
|
|
86
|
+
|
|
67
87
|
handleCloseOthers = (key) => {
|
|
68
88
|
this.setState(prev => ({tabs: prev.tabs.filter(t => t.key === key)}));
|
|
69
89
|
if (key !== this.activeKey) {
|
|
@@ -81,6 +101,7 @@ export class TabLayout extends React.Component {
|
|
|
81
101
|
this.openTab(location.pathname + location.search);
|
|
82
102
|
});
|
|
83
103
|
this.unsubscribeClosePage = EventBus.on('closePage', this.handleClosePageEvent);
|
|
104
|
+
this.syncDocumentTitle();
|
|
84
105
|
}
|
|
85
106
|
|
|
86
107
|
componentDidUpdate(prevProps) {
|
|
@@ -89,16 +110,14 @@ export class TabLayout extends React.Component {
|
|
|
89
110
|
if (this.props.pathMenuMap !== prevProps.pathMenuMap) {
|
|
90
111
|
this.refreshTabLabels();
|
|
91
112
|
}
|
|
113
|
+
this.syncDocumentTitle();
|
|
92
114
|
}
|
|
93
115
|
|
|
94
116
|
refreshTabLabels() {
|
|
95
117
|
this.setState(prev => {
|
|
96
118
|
let changed = false;
|
|
97
119
|
const tabs = prev.tabs.map(t => {
|
|
98
|
-
const
|
|
99
|
-
const pathname = qIndex === -1 ? t.key : t.key.substring(0, qIndex);
|
|
100
|
-
const search = qIndex === -1 ? '' : t.key.substring(qIndex);
|
|
101
|
-
const label = getLabel(pathname, search, this.props.pathMenuMap);
|
|
120
|
+
const label = getLabel(getPathname(t.key), this.props.pathMenuMap);
|
|
102
121
|
if (label === t.label) return t;
|
|
103
122
|
changed = true;
|
|
104
123
|
return {...t, label};
|
|
@@ -37,6 +37,7 @@ export default class AdminLayout extends React.Component {
|
|
|
37
37
|
|
|
38
38
|
|
|
39
39
|
unsubscribeLayout = null;
|
|
40
|
+
unsubscribeSiteInfo = null;
|
|
40
41
|
|
|
41
42
|
componentDidMount() {
|
|
42
43
|
// 判断是否手机端,自动收起菜单
|
|
@@ -54,12 +55,20 @@ export default class AdminLayout extends React.Component {
|
|
|
54
55
|
this.unsubscribeLayout = EventBus.on('layoutChange', () => {
|
|
55
56
|
this.setState({layoutMode: getLayoutMode()});
|
|
56
57
|
})
|
|
58
|
+
|
|
59
|
+
// 参数设置保存后刷新标题、水印等站点信息
|
|
60
|
+
this.unsubscribeSiteInfo = EventBus.on('siteInfoChange', () => {
|
|
61
|
+
this.setState({siteInfo: GlobalData.getSiteInfo()});
|
|
62
|
+
})
|
|
57
63
|
}
|
|
58
64
|
|
|
59
65
|
componentWillUnmount() {
|
|
60
66
|
if (this.unsubscribeLayout) {
|
|
61
67
|
this.unsubscribeLayout();
|
|
62
68
|
}
|
|
69
|
+
if (this.unsubscribeSiteInfo) {
|
|
70
|
+
this.unsubscribeSiteInfo();
|
|
71
|
+
}
|
|
63
72
|
}
|
|
64
73
|
|
|
65
74
|
|
|
@@ -171,7 +180,7 @@ export default class AdminLayout extends React.Component {
|
|
|
171
180
|
if (!menu) return;
|
|
172
181
|
const {path} = menu;
|
|
173
182
|
this.setState({currentMenuKey: key});
|
|
174
|
-
PageUtils.open(path
|
|
183
|
+
PageUtils.open(path);
|
|
175
184
|
}
|
|
176
185
|
|
|
177
186
|
getActiveTopMenu() {
|
|
@@ -233,7 +242,7 @@ return <Layout className='main-layout'>
|
|
|
233
242
|
{this.renderTopMenu()}
|
|
234
243
|
{this.state.headerLeftArticles.map(a => (
|
|
235
244
|
<button key={a.code} type="button" className='item'
|
|
236
|
-
onClick={() => PageUtils.open('/article/' + a.code
|
|
245
|
+
onClick={() => PageUtils.open('/article/' + a.code)}>{a.title}</button>
|
|
237
246
|
))}
|
|
238
247
|
{this.props.showOrgSwitcher?.({
|
|
239
248
|
activeTopMenu: this.getActiveTopMenu(),
|
|
@@ -274,7 +283,7 @@ return <Layout className='main-layout'>
|
|
|
274
283
|
const {siteInfo, loginInfo, layoutMode} = this.state
|
|
275
284
|
// 即使菜单为空也直接显示页面内容,不要一直显示 Skeleton
|
|
276
285
|
const contentNode = layoutMode === 'single'
|
|
277
|
-
? <SinglePageLayout/>
|
|
286
|
+
? <SinglePageLayout pathMenuMap={this.state.pathMenuMap}/>
|
|
278
287
|
: <TabLayout pathMenuMap={this.state.pathMenuMap}/>;
|
|
279
288
|
if (siteInfo.waterMark === true) {
|
|
280
289
|
return <Watermark content={[loginInfo.name, loginInfo.account]}>
|
package/src/layouts/index.tsx
CHANGED
|
@@ -3,8 +3,8 @@ import {App, ConfigProvider, Modal} from "antd";
|
|
|
3
3
|
import zhCN from 'antd/locale/zh_CN';
|
|
4
4
|
import dayjs from 'dayjs';
|
|
5
5
|
import 'dayjs/locale/zh-cn';
|
|
6
|
-
import {ErrorBoundary, GlobalData, HttpClient, LOGIN_EXPIRED_REASON_CONCURRENT_LOGIN, PageFrame, PageLoading, PageUtils, history, getThemeConfig, getThemeMode, setThemeColors, setMessageApi, EventBus} from "../framework";
|
|
7
|
-
import type {LoginExpiredPayload, ThemeColors} from "../framework";
|
|
6
|
+
import {ErrorBoundary, GlobalData, HttpClient, LOGIN_EXPIRED_REASON_CONCURRENT_LOGIN, LOGIN_PATH, PageFrame, PageLoading, PageUtils, history, getThemeConfig, getThemeMode, setThemeColors, setMessageApi, EventBus, matchRoute, refreshDocumentTitle, setPageTitle, warnDeprecatedPageLabel, applyServerUiDefaults} from "../framework";
|
|
7
|
+
import type {LoginExpiredPayload, PageLayout, ThemeColors} from "../framework";
|
|
8
8
|
|
|
9
9
|
import AdminLayout from "./admin"
|
|
10
10
|
|
|
@@ -59,6 +59,7 @@ export class Layouts extends React.Component<LayoutsProps> {
|
|
|
59
59
|
unsubscribeLoginExpired: (() => void) | null = null;
|
|
60
60
|
unsubscribeLogoutSuccess: (() => void) | null = null;
|
|
61
61
|
unsubscribeTheme: (() => void) | null = null;
|
|
62
|
+
unsubscribeSiteInfo: (() => void) | null = null;
|
|
62
63
|
/** 本次登录失效弹窗的原因。同步字段(非 state):同一次失效会并发上报多个 401,setState 异步可能读到旧值 */
|
|
63
64
|
private loginExpiredReason: string = '';
|
|
64
65
|
|
|
@@ -94,22 +95,42 @@ export class Layouts extends React.Component<LayoutsProps> {
|
|
|
94
95
|
this.setState({loginExpiredVisible: false});
|
|
95
96
|
};
|
|
96
97
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
98
|
+
/**
|
|
99
|
+
* 解析页面登录与布局要求:
|
|
100
|
+
* 优先使用页面文件导出的 pageMeta;未匹配的路由按目录约定兜底
|
|
101
|
+
*/
|
|
102
|
+
getPageMeta(pathname: string): {requiresAuth: boolean; layout: PageLayout} {
|
|
103
|
+
const meta = matchRoute(pathname)?.meta;
|
|
104
|
+
const isLogin = pathname === LOGIN_PATH;
|
|
105
|
+
const inPublicDir = pathname.startsWith('/public/');
|
|
106
|
+
const inStandaloneDir = pathname.startsWith('/standalone/');
|
|
107
|
+
const requiresAuth = meta?.requiresAuth ?? (isLogin ? false : !inPublicDir);
|
|
108
|
+
const layout = requiresAuth
|
|
109
|
+
? (meta?.layout ?? (inStandaloneDir || isLogin ? 'blank' : 'admin'))
|
|
110
|
+
: 'blank';
|
|
111
|
+
return {requiresAuth, layout};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** 公共/standalone 页用 pageMeta.title;admin 页由 TabLayout / SinglePageLayout 设置 */
|
|
115
|
+
syncDocumentTitle() {
|
|
116
|
+
const {pathname, search} = this.state.location;
|
|
117
|
+
if (search && new URLSearchParams(search).has('_label')) {
|
|
118
|
+
warnDeprecatedPageLabel();
|
|
119
|
+
}
|
|
120
|
+
if (this.getPageMeta(pathname).layout === 'blank') {
|
|
121
|
+
setPageTitle(matchRoute(pathname)?.meta?.title);
|
|
122
|
+
}
|
|
102
123
|
}
|
|
103
124
|
|
|
104
125
|
loadData() {
|
|
105
126
|
const {siteInfoLoaded, loginChecked} = this.state;
|
|
106
|
-
const
|
|
127
|
+
const {requiresAuth} = this.getPageMeta(this.state.location.pathname);
|
|
107
128
|
|
|
108
129
|
if (!siteInfoLoaded) {
|
|
109
130
|
this.loadSiteInfo();
|
|
110
131
|
}
|
|
111
132
|
|
|
112
|
-
if (
|
|
133
|
+
if (requiresAuth && !loginChecked) {
|
|
113
134
|
this.loadLoginInfo();
|
|
114
135
|
}
|
|
115
136
|
}
|
|
@@ -117,6 +138,8 @@ export class Layouts extends React.Component<LayoutsProps> {
|
|
|
117
138
|
loadSiteInfo() {
|
|
118
139
|
HttpClient.get("/admin/public/site-info", null, {toastError: false}).then(data => {
|
|
119
140
|
GlobalData.setSiteInfo(data.data);
|
|
141
|
+
// 参数设置的默认主题/页签模式需在后台布局首次渲染前生效,避免闪一下默认样式
|
|
142
|
+
applyServerUiDefaults(data.data?.config);
|
|
120
143
|
this.setState({siteInfoLoaded: true});
|
|
121
144
|
}).catch(() => {
|
|
122
145
|
console.error('[Layout] 加载站点信息失败');
|
|
@@ -131,7 +154,7 @@ export class Layouts extends React.Component<LayoutsProps> {
|
|
|
131
154
|
|
|
132
155
|
if (data.data.needUpdatePwd) {
|
|
133
156
|
this.setState({loginChecked: true});
|
|
134
|
-
history.push('/standalone/
|
|
157
|
+
history.push('/standalone/force-update-pwd');
|
|
135
158
|
return;
|
|
136
159
|
}
|
|
137
160
|
|
|
@@ -154,12 +177,22 @@ export class Layouts extends React.Component<LayoutsProps> {
|
|
|
154
177
|
themeVersion: prevState.themeVersion + 1,
|
|
155
178
|
}));
|
|
156
179
|
});
|
|
180
|
+
this.unsubscribeSiteInfo = EventBus.on('siteInfoChange', () => {
|
|
181
|
+
// 参数设置保存后,未自定义用户即时跟随新的默认主题/页签模式
|
|
182
|
+
applyServerUiDefaults(GlobalData.getSiteInfo().config);
|
|
183
|
+
refreshDocumentTitle();
|
|
184
|
+
});
|
|
185
|
+
this.syncDocumentTitle();
|
|
157
186
|
this.loadData();
|
|
158
187
|
}
|
|
159
188
|
|
|
160
189
|
componentDidUpdate(prevProps: LayoutsProps, prevState: typeof this.state) {
|
|
161
190
|
if (this.state.location !== prevState.location) {
|
|
162
191
|
this.loadData();
|
|
192
|
+
this.syncDocumentTitle();
|
|
193
|
+
}
|
|
194
|
+
if (!prevState.siteInfoLoaded && this.state.siteInfoLoaded) {
|
|
195
|
+
refreshDocumentTitle();
|
|
163
196
|
}
|
|
164
197
|
}
|
|
165
198
|
|
|
@@ -176,6 +209,9 @@ export class Layouts extends React.Component<LayoutsProps> {
|
|
|
176
209
|
if (this.unsubscribeTheme) {
|
|
177
210
|
this.unsubscribeTheme();
|
|
178
211
|
}
|
|
212
|
+
if (this.unsubscribeSiteInfo) {
|
|
213
|
+
this.unsubscribeSiteInfo();
|
|
214
|
+
}
|
|
179
215
|
}
|
|
180
216
|
|
|
181
217
|
/**
|
|
@@ -194,15 +230,16 @@ export class Layouts extends React.Component<LayoutsProps> {
|
|
|
194
230
|
const {location, siteInfoLoaded, loginChecked} = this.state;
|
|
195
231
|
const {pathname, search} = location;
|
|
196
232
|
const ready = siteInfoLoaded && loginChecked;
|
|
197
|
-
const
|
|
198
|
-
|
|
233
|
+
const {requiresAuth, layout} = this.getPageMeta(pathname);
|
|
234
|
+
// 公共页直接渲染;standalone 页需登录后渲染;admin 页交给后台布局
|
|
235
|
+
const showPageFrame = !requiresAuth || (layout === 'blank' && ready);
|
|
199
236
|
|
|
200
237
|
return (
|
|
201
238
|
<ErrorBoundary minimal>
|
|
202
239
|
<ConfigProvider {...baseConfigProps} theme={getThemeConfig()}>
|
|
203
240
|
<App component={false}>
|
|
204
241
|
<MessageBridge/>
|
|
205
|
-
{this.renderContent(showPageFrame, ready, pathname, search)}
|
|
242
|
+
{this.renderContent(showPageFrame, ready, layout, pathname, search)}
|
|
206
243
|
{this.renderLoginExpiredModal()}
|
|
207
244
|
</App>
|
|
208
245
|
</ConfigProvider>
|
|
@@ -210,11 +247,11 @@ export class Layouts extends React.Component<LayoutsProps> {
|
|
|
210
247
|
);
|
|
211
248
|
}
|
|
212
249
|
|
|
213
|
-
renderContent(showPageFrame: boolean, ready: boolean, pathname: string, search: string) {
|
|
250
|
+
renderContent(showPageFrame: boolean, ready: boolean, layout: PageLayout, pathname: string, search: string) {
|
|
214
251
|
if (showPageFrame) {
|
|
215
252
|
return <PageFrame url={pathname + search}/>;
|
|
216
253
|
}
|
|
217
|
-
if (ready) {
|
|
254
|
+
if (ready && layout === 'admin') {
|
|
218
255
|
return <AdminLayout headerExtra={this.props.headerExtra} showOrgSwitcher={this.props.showOrgSwitcher} loginInfo={GlobalData.getLoginInfo()}/>;
|
|
219
256
|
}
|
|
220
257
|
return <PageLoading messages={[
|
package/src/pages/404.jsx
CHANGED
|
@@ -2,6 +2,10 @@ import React from "react";
|
|
|
2
2
|
import {HttpClient, Page, UrlUtils} from "../../framework";
|
|
3
3
|
import "./article.less";
|
|
4
4
|
|
|
5
|
+
export const pageMeta = {
|
|
6
|
+
title: '文章详情',
|
|
7
|
+
};
|
|
8
|
+
|
|
5
9
|
export default class ArticlePage extends React.Component {
|
|
6
10
|
|
|
7
11
|
state = {
|
|
@@ -12,7 +16,7 @@ export default class ArticlePage extends React.Component {
|
|
|
12
16
|
componentDidMount() {
|
|
13
17
|
const code = this.props.params?.code
|
|
14
18
|
if (code) {
|
|
15
|
-
HttpClient.get('admin/article/
|
|
19
|
+
HttpClient.get('admin/article/get-by-code', {code}, {toastError: false}).then(rs => {
|
|
16
20
|
this.setState({article: rs.data, loading: false})
|
|
17
21
|
}).catch(() => {
|
|
18
22
|
this.setState({loading: false})
|