@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.
Files changed (59) hide show
  1. package/package.json +3 -3
  2. package/src/framework/GlobalData.ts +4 -0
  3. package/src/framework/biz/OrgTree.tsx +1 -1
  4. package/src/framework/biz/RoleTree.tsx +1 -1
  5. package/src/framework/biz/UserPicker.tsx +4 -4
  6. package/src/framework/biz/index.tsx +4 -4
  7. package/src/framework/components/CronEditor/index.tsx +318 -0
  8. package/src/framework/components/LinkButton/index.tsx +4 -0
  9. package/src/framework/components/OrgSwitcher/index.tsx +2 -2
  10. package/src/framework/components/Page/index.tsx +6 -14
  11. package/src/framework/components/ProTable/index.tsx +3 -1
  12. package/src/framework/components/ThemeSettings/index.tsx +5 -38
  13. package/src/framework/components/index.ts +1 -0
  14. package/src/framework/config/index.ts +12 -0
  15. package/src/framework/config/layoutConfig.ts +47 -8
  16. package/src/framework/config/themeConfig.ts +93 -25
  17. package/src/framework/config/uiDefaults.ts +26 -0
  18. package/src/framework/fields/FieldEditor/index.tsx +1 -1
  19. package/src/framework/fields/FieldUploadFile/index.tsx +1 -1
  20. package/src/framework/fields/FieldUploadImage/index.tsx +42 -10
  21. package/src/framework/fields/FieldUserPicker/index.tsx +3 -3
  22. package/src/framework/fields/orgTree.ts +1 -1
  23. package/src/framework/router/index.ts +1 -0
  24. package/src/framework/router/matcher.ts +19 -4
  25. package/src/framework/utils/AvatarUtils.ts +64 -0
  26. package/src/framework/utils/CronUtils.ts +562 -0
  27. package/src/framework/utils/HttpClient.ts +5 -5
  28. package/src/framework/utils/PageUtils.ts +38 -7
  29. package/src/framework/utils/TitleUtils.ts +38 -0
  30. package/src/framework/utils/UrlUtils.ts +3 -3
  31. package/src/framework/utils/index.ts +3 -0
  32. package/src/layouts/admin/HeaderRight.jsx +62 -16
  33. package/src/layouts/admin/SinglePageLayout.jsx +13 -2
  34. package/src/layouts/admin/TabLayout.jsx +38 -19
  35. package/src/layouts/admin/index.jsx +12 -3
  36. package/src/layouts/index.tsx +52 -15
  37. package/src/pages/404.jsx +4 -0
  38. package/src/pages/article/$code.jsx +5 -1
  39. package/src/pages/login.jsx +157 -0
  40. package/src/pages/{public/login.less → login.less} +16 -1
  41. package/src/pages/public/login.jsx +11 -145
  42. package/src/pages/standalone/{forceUpdatePwd.jsx → force-update-pwd.jsx} +10 -4
  43. package/src/pages/system/article/index.jsx +1 -1
  44. package/src/pages/system/config/index.jsx +335 -0
  45. package/src/pages/system/file/index.jsx +3 -3
  46. package/src/pages/system/job/index.jsx +62 -19
  47. package/src/pages/system/log/index.jsx +1 -1
  48. package/src/pages/system/online/index.jsx +95 -0
  49. package/src/pages/system/org/index.jsx +7 -7
  50. package/src/pages/system/role/index.jsx +8 -8
  51. package/src/pages/system/role/{rolePerm.jsx → role-perm.jsx} +9 -4
  52. package/src/pages/system/user/{userPerm.jsx → UserPermModal.jsx} +3 -3
  53. package/src/pages/system/user/index.jsx +6 -6
  54. package/src/pages/user-center/AvatarPicker.jsx +76 -0
  55. package/src/pages/{userCenter/permView.jsx → user-center/PermView.jsx} +48 -25
  56. package/src/pages/user-center/RecentOps.jsx +36 -0
  57. package/src/pages/user-center/index.jsx +393 -0
  58. package/vite-plugin/index.js +64 -6
  59. 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 buffer: string[] = [];
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
- buffer.push(`${k}=${v}`);
34
+ search.append(k, String(v));
35
35
  }
36
36
  }
37
37
  }
38
- return buffer.join('&');
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, getToken, history, EventBus, ThemeSettings, ARTICLE_HEADER_AVATAR_DROPDOWN, ARTICLE_HEADER_RIGHT} from "../../framework";
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('/userCenter', '个人中心')
61
+ PageUtils.open('/user-center')
62
+ }
63
+
64
+ openArticle = (code) => {
65
+ PageUtils.open('/article/' + code)
40
66
  }
41
67
 
42
- openArticle = (code, title) => {
43
- PageUtils.open('/article/' + code, title)
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: 'userCenter', label: '个人中心'},
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, a.title)}>
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 === 'userCenter') {
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
- const code = key.substring(8)
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" style={{backgroundColor: getToken().colorPrimary}}>
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('/public/login')
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('/public/login')
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, UrlUtils, EventBus} from "../../framework";
2
+ import {history, PageFrame, EventBus, matchRoute, setPageTitle} from "../../framework";
3
3
  import {AdminTabs} from "./AdminTabs";
4
4
 
5
- function getLabel(pathname, search, pathMenuMap) {
6
- if (pathname === '/') return '首页';
7
- const params = UrlUtils.getParams(search);
8
- if (params._label) return params._label;
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 qIndex = initUrl.indexOf('?');
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 = (url) => {
33
- const qIndex = url.indexOf('?');
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(pathname, search, this.props.pathMenuMap), refreshKey: 0}],
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 qIndex = t.key.indexOf('?');
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, menu.name);
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, a.title)}>{a.title}</button>
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]}>
@@ -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
- getPageType(pathname): 'public' | 'standalone' | 'protected' {
98
- if (pathname === '/' || pathname === '/index') return 'protected';
99
- if (pathname.startsWith('/public/')) return 'public';
100
- if (pathname.startsWith('/standalone/')) return 'standalone';
101
- return 'protected';
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 pageType = this.getPageType(this.state.location.pathname);
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 ((pageType === 'protected' || pageType === 'standalone') && !loginChecked) {
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/forceUpdatePwd');
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 pageType = this.getPageType(pathname);
198
- const showPageFrame = pageType === 'public' || (pageType === 'standalone' && ready);
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 {Result} from "antd";
3
3
  import {Page} from "../framework";
4
4
 
5
+ export const pageMeta = {
6
+ title: '页面不存在',
7
+ };
8
+
5
9
  export default class NotFound extends React.Component {
6
10
 
7
11
  render() {
@@ -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/getByCode', {code}, {toastError: false}).then(rs => {
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})