@jiangood/open-admin 3.1.6 → 3.1.8
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 +2 -5
- package/src/framework/biz/PermActions.tsx +1 -1
- package/src/framework/biz/UserPicker.less +17 -0
- package/src/framework/biz/UserPicker.tsx +296 -0
- package/src/framework/biz/index.tsx +2 -0
- package/src/framework/biz/pickerUser.ts +19 -0
- package/src/framework/components/ContextMenu/index.tsx +11 -8
- package/src/framework/components/DownloadModal/index.tsx +7 -7
- package/src/framework/components/ErrorBoundary/index.tsx +1 -1
- package/src/framework/components/FormModal/index.tsx +4 -4
- package/src/framework/components/NamedIcon/index.tsx +1 -1
- package/src/framework/components/OrgSwitcher/index.tsx +1 -1
- package/src/framework/components/Page/index.tsx +18 -4
- package/src/framework/components/PageLoading/index.tsx +2 -2
- package/src/framework/components/ProTable/index.less +70 -8
- package/src/framework/components/ProTable/index.tsx +63 -16
- package/src/framework/components/ThemeSettings/index.tsx +173 -0
- package/src/framework/components/index.ts +1 -0
- package/src/framework/config/index.ts +44 -2
- package/src/framework/config/layoutConfig.ts +52 -0
- package/src/framework/config/themeConfig.ts +449 -14
- package/src/framework/fields/BaseRemoteSelect/index.tsx +4 -4
- package/src/framework/fields/FieldDateRange/index.tsx +1 -1
- package/src/framework/fields/FieldDictSelect/index.tsx +1 -1
- package/src/framework/fields/FieldEditor/index.tsx +2 -2
- package/src/framework/fields/FieldNumberRange/index.tsx +1 -1
- package/src/framework/fields/FieldUploadFile/index.tsx +3 -3
- package/src/framework/fields/FieldUploadImage/index.tsx +19 -19
- package/src/framework/fields/FieldUserPicker/index.tsx +124 -0
- package/src/framework/fields/index.ts +1 -0
- package/src/framework/utils/HttpClient.ts +15 -4
- package/src/framework/utils/MessageApi.ts +16 -3
- package/src/framework/utils/UrlUtils.ts +1 -1
- package/src/framework/views/ViewText/index.tsx +1 -1
- package/src/layouts/admin/AdminTabs.less +11 -11
- package/src/layouts/admin/HeaderRight.jsx +23 -12
- package/src/layouts/admin/SinglePageLayout.jsx +36 -0
- package/src/layouts/admin/TabLayout.jsx +21 -1
- package/src/layouts/admin/index.jsx +46 -26
- package/src/layouts/admin/index.less +68 -24
- package/src/layouts/index.tsx +29 -4
- package/src/pages/article/$code.jsx +2 -2
- package/src/pages/article/article.less +13 -13
- package/src/pages/index.jsx +1 -1
- package/src/pages/public/login.jsx +29 -15
- package/src/pages/public/login.less +243 -22
- package/src/pages/standalone/forceUpdatePwd.jsx +8 -7
- package/src/pages/system/article/index.jsx +13 -15
- package/src/pages/system/codegen/index.jsx +54 -51
- package/src/pages/system/dict/index.jsx +2 -2
- package/src/pages/system/file/index.jsx +3 -3
- package/src/pages/system/job/index.jsx +57 -20
- package/src/pages/system/log/index.jsx +2 -2
- package/src/pages/system/org/index.jsx +5 -5
- package/src/pages/system/role/index.jsx +28 -55
- package/src/pages/system/role/rolePerm.jsx +1 -1
- package/src/pages/system/user/index.jsx +7 -11
- package/src/pages/system/user/userPerm.jsx +5 -5
- package/src/pages/userCenter/permView.jsx +2 -2
- package/src/style/global.less +6 -6
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import {Button, Divider, Modal, Radio, Select, Space, Upload
|
|
2
|
+
import {Button, Divider, Modal, Radio, Select, Space, Upload} from "antd";
|
|
3
3
|
import {DeleteOutlined, EyeOutlined, PlusOutlined} from "@ant-design/icons";
|
|
4
4
|
import Compressor from "compressorjs";
|
|
5
5
|
import Cropper from "cropperjs";
|
|
6
6
|
import "cropperjs/dist/cropper.css";
|
|
7
|
-
import {HttpClient, ObjectUtils, UrlUtils} from "../../utils";
|
|
7
|
+
import {HttpClient, getMessageApi, ObjectUtils, UrlUtils} from "../../utils";
|
|
8
8
|
import type {FieldProps} from '../types';
|
|
9
9
|
|
|
10
10
|
export interface FieldUploadImageProps extends FieldProps<string> {
|
|
@@ -91,7 +91,7 @@ function readDims(url: string): Promise<Dims> {
|
|
|
91
91
|
|
|
92
92
|
function compressToFile(source: Blob, options: {maxWidth?: number; maxHeight?: number; width?: number; height?: number; quality?: number}): Promise<File> {
|
|
93
93
|
return new Promise((resolve, reject) => {
|
|
94
|
-
new Compressor(source, {
|
|
94
|
+
new Compressor(source, {
|
|
95
95
|
maxWidth: options.maxWidth,
|
|
96
96
|
maxHeight: options.maxHeight,
|
|
97
97
|
width: options.width,
|
|
@@ -139,9 +139,9 @@ async function compressToTarget(source: Blob, maxWidth: number | undefined, targ
|
|
|
139
139
|
return best;
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
-
export class FieldUploadImage extends React.Component<FieldUploadImageProps, FieldUploadImageState> {
|
|
142
|
+
export class FieldUploadImage extends React.Component<FieldUploadImageProps, FieldUploadImageState> {
|
|
143
143
|
|
|
144
|
-
state: FieldUploadImageState = {
|
|
144
|
+
state: FieldUploadImageState = {
|
|
145
145
|
// 传入的参数
|
|
146
146
|
maxCount: 1,
|
|
147
147
|
thumbWidth: 300,
|
|
@@ -170,7 +170,7 @@ export class FieldUploadImage extends React.Component<FieldUploadImageProps, Fie
|
|
|
170
170
|
if (props.value) this.state.objectNames = props.value.split(',');
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
-
componentDidUpdate(prevProps: FieldUploadImageProps, prevState: FieldUploadImageState) {
|
|
173
|
+
componentDidUpdate(prevProps: FieldUploadImageProps, prevState: FieldUploadImageState) {
|
|
174
174
|
const next: Partial<FieldUploadImageState> = {};
|
|
175
175
|
if (this.props.maxCount !== prevProps.maxCount) next.maxCount = this.props.maxCount;
|
|
176
176
|
if (this.props.thumbWidth !== prevProps.thumbWidth) next.thumbWidth = this.props.thumbWidth;
|
|
@@ -290,19 +290,19 @@ export class FieldUploadImage extends React.Component<FieldUploadImageProps, Fie
|
|
|
290
290
|
this.revokePreviewUrls();
|
|
291
291
|
this.previewUrlsRef = [cUrl];
|
|
292
292
|
this.setState({preview: {cUrl, cFile, cSize: cFile.size, cdims}, tool: undefined});
|
|
293
|
-
|
|
293
|
+
getMessageApi().success(`已压缩:${cdims.width} x ${cdims.height} / ${formatSize(cFile.size)}`);
|
|
294
294
|
} catch {
|
|
295
|
-
|
|
295
|
+
getMessageApi().error('压缩失败');
|
|
296
296
|
}
|
|
297
297
|
};
|
|
298
298
|
|
|
299
299
|
private handleBeforeUpload = async (file: File) => {
|
|
300
300
|
if (this.state.objectNames.length >= this.state.maxCount) {
|
|
301
|
-
|
|
301
|
+
getMessageApi().warning('已达到最大上传数量');
|
|
302
302
|
return Upload.LIST_IGNORE;
|
|
303
303
|
}
|
|
304
304
|
if (!file.type.startsWith('image/')) {
|
|
305
|
-
|
|
305
|
+
getMessageApi().error('请选择图片文件');
|
|
306
306
|
return Upload.LIST_IGNORE;
|
|
307
307
|
}
|
|
308
308
|
|
|
@@ -319,7 +319,7 @@ export class FieldUploadImage extends React.Component<FieldUploadImageProps, Fie
|
|
|
319
319
|
// 默认展示原图,不做压缩处理,用户可点击「压缩」按钮手动压缩
|
|
320
320
|
await this.loadOriginalPreview(file, url, dims);
|
|
321
321
|
} catch {
|
|
322
|
-
|
|
322
|
+
getMessageApi().error('读取图片失败');
|
|
323
323
|
this.closeModal();
|
|
324
324
|
return;
|
|
325
325
|
}
|
|
@@ -370,7 +370,7 @@ export class FieldUploadImage extends React.Component<FieldUploadImageProps, Fie
|
|
|
370
370
|
);
|
|
371
371
|
this.closeModal();
|
|
372
372
|
} catch (e) {
|
|
373
|
-
|
|
373
|
+
getMessageApi().error(HttpClient.errToMsg(e));
|
|
374
374
|
} finally {
|
|
375
375
|
this.setState({uploading: false});
|
|
376
376
|
}
|
|
@@ -390,7 +390,7 @@ export class FieldUploadImage extends React.Component<FieldUploadImageProps, Fie
|
|
|
390
390
|
if (canvasImg?.url) {
|
|
391
391
|
return <img src={canvasImg.url} style={{maxWidth: '100%', maxHeight: '100%'}} alt="预览"/>;
|
|
392
392
|
}
|
|
393
|
-
return <div style={{color: '#999'}}>生成中...</div>;
|
|
393
|
+
return <div style={{color: 'var(--oa-color-text-tertiary, #999)'}}>生成中...</div>;
|
|
394
394
|
};
|
|
395
395
|
|
|
396
396
|
render() {
|
|
@@ -487,14 +487,14 @@ export class FieldUploadImage extends React.Component<FieldUploadImageProps, Fie
|
|
|
487
487
|
<div style={{flex: 1, minWidth: 0, display: 'flex', flexDirection: 'column', gap: 12}}>
|
|
488
488
|
<div style={{
|
|
489
489
|
flex: 1, minHeight: 0, display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
490
|
-
background: '#f5f5f5', borderRadius: 8, border: '1px dashed #d9d9d9', position: 'relative', overflow: 'hidden',
|
|
490
|
+
background: 'var(--oa-color-fill, #f5f5f5)', borderRadius: 8, border: '1px dashed var(--oa-color-border, #d9d9d9)', position: 'relative', overflow: 'hidden',
|
|
491
491
|
}}>
|
|
492
492
|
{this.renderCanvas(preview, canvasImg, originalUrl)}
|
|
493
493
|
</div>
|
|
494
494
|
</div>
|
|
495
495
|
|
|
496
496
|
{/* 右侧属性栏 */}
|
|
497
|
-
<div style={{width: 240, flexShrink: 0, borderLeft: '1px solid #f0f0f0', paddingLeft: 16, display: 'flex', flexDirection: 'column', overflow: 'hidden'}}>
|
|
497
|
+
<div style={{width: 240, flexShrink: 0, borderLeft: '1px solid var(--oa-color-border, #f0f0f0)', paddingLeft: 16, display: 'flex', flexDirection: 'column', overflow: 'hidden'}}>
|
|
498
498
|
<div style={{flex: 1, overflowY: 'auto'}}>
|
|
499
499
|
{/* 工具栏(横排) */}
|
|
500
500
|
{tool !== 'crop' && (
|
|
@@ -520,14 +520,14 @@ export class FieldUploadImage extends React.Component<FieldUploadImageProps, Fie
|
|
|
520
520
|
>
|
|
521
521
|
{CROP_RATIOS.map((r) => (
|
|
522
522
|
<Radio key={r.ratio ? `${r.ratio.width}:${r.ratio.height}` : 'free'} value={r.ratio ? `${r.ratio.width}:${r.ratio.height}` : 'free'}>
|
|
523
|
-
{r.label}{r.desc ? <span style={{color: '#999', fontSize: 12}}>({r.desc})</span> : null}
|
|
523
|
+
{r.label}{r.desc ? <span style={{color: 'var(--oa-color-text-tertiary, #999)', fontSize: 12}}>({r.desc})</span> : null}
|
|
524
524
|
</Radio>
|
|
525
525
|
))}
|
|
526
526
|
</Radio.Group>
|
|
527
527
|
) : (
|
|
528
528
|
<>
|
|
529
529
|
{canvasImg && (
|
|
530
|
-
<div style={{color: '#666'}}>
|
|
530
|
+
<div style={{color: 'var(--oa-color-text-secondary, #666)'}}>
|
|
531
531
|
<div>尺寸:{canvasImg.dims ? `${canvasImg.dims.width} x ${canvasImg.dims.height}` : '--'}</div>
|
|
532
532
|
<div>体积:{formatSize(canvasImg.size)}</div>
|
|
533
533
|
</div>
|
|
@@ -537,7 +537,7 @@ export class FieldUploadImage extends React.Component<FieldUploadImageProps, Fie
|
|
|
537
537
|
<Divider style={{margin: '12px 0'}}/>
|
|
538
538
|
<div style={{fontWeight: 600, marginBottom: 8}}>压缩处理</div>
|
|
539
539
|
<div style={{marginBottom: 8, display: 'flex', alignItems: 'center', gap: 8}}>
|
|
540
|
-
<span style={{color: '#666', flexShrink: 0}}>最大宽度</span>
|
|
540
|
+
<span style={{color: 'var(--oa-color-text-secondary, #666)', flexShrink: 0}}>最大宽度</span>
|
|
541
541
|
<Select
|
|
542
542
|
value={compressWidth}
|
|
543
543
|
onChange={(v) => this.setState({compressWidth: v})}
|
|
@@ -555,7 +555,7 @@ export class FieldUploadImage extends React.Component<FieldUploadImageProps, Fie
|
|
|
555
555
|
/>
|
|
556
556
|
</div>
|
|
557
557
|
<div style={{marginBottom: 12, display: 'flex', alignItems: 'center', gap: 8}}>
|
|
558
|
-
<span style={{color: '#666', flexShrink: 0}}>最大体积</span>
|
|
558
|
+
<span style={{color: 'var(--oa-color-text-secondary, #666)', flexShrink: 0}}>最大体积</span>
|
|
559
559
|
<Select
|
|
560
560
|
value={compressSize}
|
|
561
561
|
onChange={(v) => this.setState({compressSize: v})}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {Tag} from 'antd';
|
|
3
|
+
import {HttpClient} from '../../utils';
|
|
4
|
+
import type {FieldProps} from '../types';
|
|
5
|
+
import {formatUserLabel, type PickerUser} from '../../biz/pickerUser';
|
|
6
|
+
import {UserPicker} from '../../biz/UserPicker';
|
|
7
|
+
|
|
8
|
+
export interface FieldUserPickerProps extends FieldProps<string[]> {
|
|
9
|
+
/** 是否多选,默认 true */
|
|
10
|
+
multiple?: boolean;
|
|
11
|
+
/** 禁用 */
|
|
12
|
+
disabled?: boolean;
|
|
13
|
+
/** 空值提示,默认 请选择用户 */
|
|
14
|
+
placeholder?: string;
|
|
15
|
+
/** 弹窗标题 */
|
|
16
|
+
title?: string;
|
|
17
|
+
/** 分页数据源地址,默认 admin/sysUser/page */
|
|
18
|
+
requestUrl?: string;
|
|
19
|
+
/** 按 id 查询用户详情地址(用于回显),默认 admin/sysUser/by-ids */
|
|
20
|
+
usersUrl?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface FieldUserPickerState {
|
|
24
|
+
open: boolean;
|
|
25
|
+
users: PickerUser[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 选人表单组件
|
|
30
|
+
*
|
|
31
|
+
* 展示已选用户标签并提供弹窗选择,遵循字段组件约定:value + onChange(value 为用户 id 数组)。
|
|
32
|
+
*/
|
|
33
|
+
export class FieldUserPicker extends React.Component<FieldUserPickerProps, FieldUserPickerState> {
|
|
34
|
+
|
|
35
|
+
static readonly defaultProps = {
|
|
36
|
+
multiple: true,
|
|
37
|
+
placeholder: '请选择用户',
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
state: FieldUserPickerState = {
|
|
41
|
+
open: false,
|
|
42
|
+
users: [],
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
componentDidMount() {
|
|
46
|
+
this.syncUsers();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
componentDidUpdate(prevProps: FieldUserPickerProps) {
|
|
50
|
+
if (prevProps.value !== this.props.value) {
|
|
51
|
+
this.syncUsers();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
getUsersUrl = () => this.props.usersUrl || 'admin/sysUser/by-ids';
|
|
56
|
+
|
|
57
|
+
// 根据 value(ids) 拉取并补全用户详情,按 ids 顺序展示
|
|
58
|
+
syncUsers = () => {
|
|
59
|
+
const ids = this.props.value || [];
|
|
60
|
+
const known = new Map(this.state.users.map(user => [user.id, user]));
|
|
61
|
+
const missing = ids.filter(id => !known.has(id));
|
|
62
|
+
if (missing.length === 0) {
|
|
63
|
+
this.setState({users: ids.map(id => known.get(id)).filter(Boolean) as PickerUser[]});
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
HttpClient.get<PickerUser[]>(this.getUsersUrl(), {ids}, {toastError: false}).then(rs => {
|
|
67
|
+
const merged = new Map(this.state.users.map(user => [user.id, user]));
|
|
68
|
+
(rs.data || []).forEach(user => merged.set(user.id, user));
|
|
69
|
+
const currentIds = this.props.value || [];
|
|
70
|
+
this.setState({users: currentIds.map(id => merged.get(id)).filter(Boolean) as PickerUser[]});
|
|
71
|
+
});
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
remove = (id: string) => {
|
|
75
|
+
this.props.onChange?.((this.props.value || []).filter(value => value !== id));
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
handleOk = (ids: string[], users: PickerUser[]) => {
|
|
79
|
+
this.setState({open: false, users});
|
|
80
|
+
this.props.onChange?.(ids);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
render() {
|
|
84
|
+
const {disabled, placeholder, multiple, title, requestUrl, usersUrl, value} = this.props;
|
|
85
|
+
const {open, users} = this.state;
|
|
86
|
+
return <>
|
|
87
|
+
<div style={{
|
|
88
|
+
minHeight: 32,
|
|
89
|
+
display: 'flex',
|
|
90
|
+
flexWrap: 'wrap',
|
|
91
|
+
alignItems: 'center',
|
|
92
|
+
gap: 4,
|
|
93
|
+
padding: '4px 8px',
|
|
94
|
+
border: '1px solid var(--oa-color-border, #d9d9d9)',
|
|
95
|
+
borderRadius: 'var(--oa-border-radius, 6px)',
|
|
96
|
+
background: disabled ? 'var(--oa-color-fill, #f5f5f5)' : 'var(--oa-color-bg-container, #fff)',
|
|
97
|
+
cursor: disabled ? 'not-allowed' : undefined,
|
|
98
|
+
}}>
|
|
99
|
+
{users.length === 0 && <span style={{color: 'var(--oa-color-text-tertiary, #999)'}}>{placeholder}</span>}
|
|
100
|
+
{users.map(user => (
|
|
101
|
+
<Tag key={user.id} closable={!disabled} onClose={(e) => {
|
|
102
|
+
e.preventDefault();
|
|
103
|
+
this.remove(user.id);
|
|
104
|
+
}}>
|
|
105
|
+
{formatUserLabel(user)}
|
|
106
|
+
</Tag>
|
|
107
|
+
))}
|
|
108
|
+
{!disabled && (
|
|
109
|
+
<a style={{marginLeft: users.length ? 'auto' : 0}} onClick={() => this.setState({open: true})}>选择</a>
|
|
110
|
+
)}
|
|
111
|
+
</div>
|
|
112
|
+
<UserPicker
|
|
113
|
+
open={open}
|
|
114
|
+
title={title}
|
|
115
|
+
multiple={multiple}
|
|
116
|
+
value={value}
|
|
117
|
+
requestUrl={requestUrl}
|
|
118
|
+
usersUrl={usersUrl}
|
|
119
|
+
onOk={this.handleOk}
|
|
120
|
+
onCancel={() => this.setState({open: false})}
|
|
121
|
+
/>
|
|
122
|
+
</>;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -14,6 +14,7 @@ export * from './FieldTable'
|
|
|
14
14
|
export * from './FieldTableSelect'
|
|
15
15
|
export * from './FieldSysOrgTreeSelect'
|
|
16
16
|
export * from './FieldSysOrgTree'
|
|
17
|
+
export * from './FieldUserPicker'
|
|
17
18
|
export * from './FieldPercent'
|
|
18
19
|
export * from './FieldUploadFile';
|
|
19
20
|
export * from './FieldUploadImage';
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import axios from "axios";
|
|
2
2
|
import type {AxiosProgressEvent, AxiosRequestConfig} from "axios";
|
|
3
3
|
import qs from 'qs';
|
|
4
|
-
import {message as messageApi} from "antd";
|
|
5
4
|
import {EventBus} from "./EventBus";
|
|
5
|
+
import {getMessageApi} from "./MessageApi";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* 单一 Promise 风格 HTTP 工具类(所有请求方法均返回 Promise,无 success/error 回调)
|
|
@@ -224,6 +224,11 @@ export class HttpClient {
|
|
|
224
224
|
return url.startsWith('admin') ? '/' + url : url;
|
|
225
225
|
}
|
|
226
226
|
|
|
227
|
+
/** 判断是否为普通对象(用于决定是否附加 X-Body-Fields) */
|
|
228
|
+
private static isPlainObject(data: unknown): data is Record<string, unknown> {
|
|
229
|
+
return data !== null && typeof data === 'object' && !Array.isArray(data);
|
|
230
|
+
}
|
|
231
|
+
|
|
227
232
|
private static coreRequest<T = unknown>(settings: AjaxSettings): Promise<AjaxBody<T>> {
|
|
228
233
|
const {url, method, params, data, headers, toastError = true, toastSuccess = true} = settings;
|
|
229
234
|
const config: AxiosRequestConfig = {url: HttpClient.prefixUrl(url), method, params, data, headers};
|
|
@@ -231,6 +236,12 @@ export class HttpClient {
|
|
|
231
236
|
// FormData 请求交由浏览器自动生成带 boundary 的 multipart Content-Type
|
|
232
237
|
if (typeof FormData !== 'undefined' && data instanceof FormData) {
|
|
233
238
|
config.headers = {...config.headers, 'Content-Type': undefined};
|
|
239
|
+
} else if (method === 'POST' && HttpClient.isPlainObject(data)) {
|
|
240
|
+
// 告知后端本次 JSON body 的顶层字段;值为 undefined 的键不会被 JSON.stringify 序列化,需排除
|
|
241
|
+
const bodyFields = Object.keys(data)
|
|
242
|
+
.filter((key) => data[key] !== undefined)
|
|
243
|
+
.join(',');
|
|
244
|
+
config.headers = {...config.headers, 'X-Body-Fields': bodyFields};
|
|
234
245
|
}
|
|
235
246
|
|
|
236
247
|
const promise = new Promise<AjaxBody<T>>((resolve, reject) => {
|
|
@@ -249,7 +260,7 @@ export class HttpClient {
|
|
|
249
260
|
return;
|
|
250
261
|
}
|
|
251
262
|
if (toastSuccess && message) {
|
|
252
|
-
|
|
263
|
+
getMessageApi().success(message);
|
|
253
264
|
}
|
|
254
265
|
resolve(body);
|
|
255
266
|
}).catch((e: unknown) => {
|
|
@@ -268,7 +279,7 @@ export class HttpClient {
|
|
|
268
279
|
EventBus.emit('loginExpired');
|
|
269
280
|
const err: AjaxError = {code: 401, message: '登录过期'};
|
|
270
281
|
if (toastError) {
|
|
271
|
-
|
|
282
|
+
getMessageApi().error('登录过期');
|
|
272
283
|
}
|
|
273
284
|
reject(err);
|
|
274
285
|
return;
|
|
@@ -276,7 +287,7 @@ export class HttpClient {
|
|
|
276
287
|
const err = HttpClient.toAjaxError(e);
|
|
277
288
|
if (toastError) {
|
|
278
289
|
console.warn(`[HttpClient] ${label}: ${url}`, err.message);
|
|
279
|
-
|
|
290
|
+
getMessageApi().error(err.message);
|
|
280
291
|
}
|
|
281
292
|
reject(err);
|
|
282
293
|
}
|
|
@@ -1,5 +1,18 @@
|
|
|
1
|
-
import {message} from 'antd';
|
|
1
|
+
import {message as staticMessage} from 'antd';
|
|
2
|
+
import type {MessageInstance} from 'antd/es/message/interface';
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
4
|
+
/**
|
|
5
|
+
* message 实例持有者。
|
|
6
|
+
* 静态 message 无法消费 ConfigProvider 主题(暗色模式下仍是浅色),
|
|
7
|
+
* 由布局内的 MessageBridge 通过 App.useApp() 注入跟随主题的实例。
|
|
8
|
+
*/
|
|
9
|
+
let messageInstance: MessageInstance = staticMessage;
|
|
10
|
+
|
|
11
|
+
/** 注入 App 上下文中的 message 实例;不传则恢复为静态 message */
|
|
12
|
+
export function setMessageApi(instance?: MessageInstance) {
|
|
13
|
+
messageInstance = instance ?? staticMessage;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function getMessageApi(): MessageInstance {
|
|
17
|
+
return messageInstance;
|
|
5
18
|
}
|
|
@@ -67,6 +67,6 @@ export class UrlUtils {
|
|
|
67
67
|
static contextPath(path: string): string {
|
|
68
68
|
const base = import.meta.env.VITE_SERVER_SERVLET_CONTEXT_PATH;
|
|
69
69
|
if (base === '/' || base === '') return path;
|
|
70
|
-
return base.replace(/\/+$/, '') + path;
|
|
70
|
+
return base.replace(/\/+$/, '') + path;
|
|
71
71
|
}
|
|
72
72
|
}
|
|
@@ -31,7 +31,7 @@ export class ViewText extends React.Component {
|
|
|
31
31
|
style={{
|
|
32
32
|
background: 'none',
|
|
33
33
|
border: 'none',
|
|
34
|
-
borderBottom: '1px dashed #d9d9d9',
|
|
34
|
+
borderBottom: '1px dashed var(--oa-color-border, #d9d9d9)',
|
|
35
35
|
padding: 0,
|
|
36
36
|
font: 'inherit',
|
|
37
37
|
cursor: 'pointer',
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
overflow-x: auto;
|
|
5
5
|
overflow-y: hidden;
|
|
6
6
|
flex-shrink: 0;
|
|
7
|
-
border-bottom: 1px solid #e8e8e8;
|
|
8
|
-
background: #fff;
|
|
7
|
+
border-bottom: 1px solid var(--oa-color-border, #e8e8e8);
|
|
8
|
+
background: var(--oa-color-bg-container, #fff);
|
|
9
9
|
|
|
10
10
|
&::-webkit-scrollbar {
|
|
11
11
|
height: 0;
|
|
@@ -16,14 +16,14 @@
|
|
|
16
16
|
align-items: center;
|
|
17
17
|
gap: 4px;
|
|
18
18
|
padding: 0 12px;
|
|
19
|
-
height: 32px;
|
|
20
|
-
line-height: 32px;
|
|
19
|
+
height: var(--oa-control-height, 32px);
|
|
20
|
+
line-height: var(--oa-control-height, 32px);
|
|
21
21
|
font-size: 13px;
|
|
22
22
|
cursor: pointer;
|
|
23
|
-
border-right: 1px solid #e8e8e8;
|
|
23
|
+
border-right: 1px solid var(--oa-color-border, #e8e8e8);
|
|
24
24
|
white-space: nowrap;
|
|
25
|
-
color: rgba(0, 0, 0, 0.65);
|
|
26
|
-
background: #fafafa;
|
|
25
|
+
color: var(--oa-color-text-secondary, rgba(0, 0, 0, 0.65));
|
|
26
|
+
background: var(--oa-color-fill, #fafafa);
|
|
27
27
|
transition: color 0.2s, background 0.2s;
|
|
28
28
|
position: relative;
|
|
29
29
|
flex-shrink: 0;
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
|
|
35
35
|
&.active {
|
|
36
36
|
color: var(--primary-color);
|
|
37
|
-
background: #fff;
|
|
37
|
+
background: var(--oa-color-bg-container, #fff);
|
|
38
38
|
font-weight: 500;
|
|
39
39
|
}
|
|
40
40
|
|
|
@@ -52,11 +52,11 @@
|
|
|
52
52
|
height: 16px;
|
|
53
53
|
font-size: 12px;
|
|
54
54
|
border-radius: 50%;
|
|
55
|
-
color: rgba(0, 0, 0, 0.45);
|
|
55
|
+
color: var(--oa-color-text-tertiary, rgba(0, 0, 0, 0.45));
|
|
56
56
|
|
|
57
57
|
&:hover {
|
|
58
|
-
background: rgba(0, 0, 0, 0.08);
|
|
59
|
-
color: rgba(0, 0, 0, 0.88);
|
|
58
|
+
background: var(--oa-color-fill, rgba(0, 0, 0, 0.08));
|
|
59
|
+
color: var(--oa-color-text, rgba(0, 0, 0, 0.88));
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
}
|
|
@@ -1,21 +1,19 @@
|
|
|
1
1
|
import {Avatar, Dropdown, Modal} from "antd";
|
|
2
|
+
import {BgColorsOutlined} from "@ant-design/icons";
|
|
2
3
|
import React from "react";
|
|
3
|
-
import {
|
|
4
|
+
import {HttpClient, PageUtils, GlobalData, getToken, history, EventBus, ThemeSettings, ARTICLE_HEADER_AVATAR_DROPDOWN, ARTICLE_HEADER_RIGHT} from "../../framework";
|
|
4
5
|
|
|
5
6
|
export class HeaderRight extends React.Component {
|
|
6
7
|
|
|
7
8
|
state = {
|
|
8
|
-
isMobileDevice: false,
|
|
9
9
|
dropdownArticles: [],
|
|
10
10
|
headerArticles: [],
|
|
11
11
|
alertVisible: false,
|
|
12
12
|
confirmVisible: false,
|
|
13
|
+
themeSettingsVisible: false,
|
|
13
14
|
};
|
|
14
15
|
|
|
15
16
|
componentDidMount() {
|
|
16
|
-
if (DeviceUtils.isMobileDevice()) {
|
|
17
|
-
this.setState({isMobileDevice: true})
|
|
18
|
-
}
|
|
19
17
|
this.loadArticles()
|
|
20
18
|
}
|
|
21
19
|
|
|
@@ -45,17 +43,26 @@ export class HeaderRight extends React.Component {
|
|
|
45
43
|
PageUtils.open('/article/' + code, title)
|
|
46
44
|
}
|
|
47
45
|
|
|
46
|
+
renderThemeSettings = () => {
|
|
47
|
+
return (
|
|
48
|
+
<button type="button" className='item' title="界面设置" aria-label="界面设置"
|
|
49
|
+
onClick={() => this.setState({themeSettingsVisible: true})}>
|
|
50
|
+
<BgColorsOutlined/>
|
|
51
|
+
</button>
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
renderThemeSettingsDrawer = () => {
|
|
56
|
+
return (
|
|
57
|
+
<ThemeSettings open={this.state.themeSettingsVisible}
|
|
58
|
+
onClose={() => this.setState({themeSettingsVisible: false})}/>
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
48
62
|
render() {
|
|
49
63
|
const info = GlobalData.getLoginInfo()
|
|
50
64
|
const {dropdownArticles, headerArticles} = this.state
|
|
51
65
|
|
|
52
|
-
if (this.state.isMobileDevice) {
|
|
53
|
-
return <div className='header-right'>
|
|
54
|
-
<button type="button" style={{background: 'none', border: 'none', padding: 0, font: 'inherit', cursor: 'pointer'}}
|
|
55
|
-
onClick={this.logout}>退出</button>
|
|
56
|
-
</div>
|
|
57
|
-
}
|
|
58
|
-
|
|
59
66
|
const articleItems = dropdownArticles.map(a => ({
|
|
60
67
|
key: 'article:' + a.code,
|
|
61
68
|
label: a.title,
|
|
@@ -75,6 +82,8 @@ export class HeaderRight extends React.Component {
|
|
|
75
82
|
</button>
|
|
76
83
|
))}
|
|
77
84
|
|
|
85
|
+
{this.renderThemeSettings()}
|
|
86
|
+
|
|
78
87
|
<Dropdown menu={{
|
|
79
88
|
onClick: ({key}) => {
|
|
80
89
|
if (key === 'userCenter') {
|
|
@@ -116,6 +125,8 @@ export class HeaderRight extends React.Component {
|
|
|
116
125
|
}}>
|
|
117
126
|
退出登录失败,是否清空缓存
|
|
118
127
|
</Modal>
|
|
128
|
+
|
|
129
|
+
{this.renderThemeSettingsDrawer()}
|
|
119
130
|
</div>
|
|
120
131
|
}
|
|
121
132
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import {history, PageFrame} from "../../framework";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 单页布局:不使用标签页,仅渲染当前路由对应页面。
|
|
6
|
+
* 路由变化时按 url 重新挂载 PageFrame,页面状态随之重置。
|
|
7
|
+
*/
|
|
8
|
+
export class SinglePageLayout extends React.Component {
|
|
9
|
+
state = {location: history.location};
|
|
10
|
+
|
|
11
|
+
unlisten = null;
|
|
12
|
+
|
|
13
|
+
componentDidMount() {
|
|
14
|
+
this.unlisten = history.listen(({location}) => {
|
|
15
|
+
this.setState({location});
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
componentWillUnmount() {
|
|
20
|
+
if (this.unlisten) {
|
|
21
|
+
this.unlisten();
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
render() {
|
|
26
|
+
const {pathname, search} = this.state.location;
|
|
27
|
+
const url = pathname + search;
|
|
28
|
+
return (
|
|
29
|
+
<div style={{height: '100%'}}>
|
|
30
|
+
<PageFrame key={url} url={url} show/>
|
|
31
|
+
</div>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export default SinglePageLayout;
|
|
@@ -83,8 +83,28 @@ export class TabLayout extends React.Component {
|
|
|
83
83
|
this.unsubscribeClosePage = EventBus.on('closePage', this.handleClosePageEvent);
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
componentDidUpdate() {
|
|
86
|
+
componentDidUpdate(prevProps) {
|
|
87
87
|
this.activeKey = this.state.activeKey;
|
|
88
|
+
// 菜单信息晚于 Tab 创建时才加载完成:路径/菜单映射变化后回填「未命名」Tab 标题
|
|
89
|
+
if (this.props.pathMenuMap !== prevProps.pathMenuMap) {
|
|
90
|
+
this.refreshTabLabels();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
refreshTabLabels() {
|
|
95
|
+
this.setState(prev => {
|
|
96
|
+
let changed = false;
|
|
97
|
+
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);
|
|
102
|
+
if (label === t.label) return t;
|
|
103
|
+
changed = true;
|
|
104
|
+
return {...t, label};
|
|
105
|
+
});
|
|
106
|
+
return changed ? {tabs} : null;
|
|
107
|
+
});
|
|
88
108
|
}
|
|
89
109
|
|
|
90
110
|
componentWillUnmount() {
|