@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
package/package.json
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
"devDependencies": {
|
|
3
3
|
"@babel/core": "^7.29.7",
|
|
4
4
|
"@eslint/js": "^10.0.1",
|
|
5
|
-
"@playwright/test": "^1.62.0",
|
|
6
5
|
"@types/qs": "^6.15.1",
|
|
7
6
|
"@types/react": "^19.2.2",
|
|
8
7
|
"@types/react-dom": "^19.0.0",
|
|
@@ -54,11 +53,9 @@
|
|
|
54
53
|
"build": "vite build",
|
|
55
54
|
"preview": "vite preview",
|
|
56
55
|
"lint": "eslint src --max-warnings=0",
|
|
57
|
-
"lint:fix": "eslint src --fix"
|
|
58
|
-
"test:e2e": "playwright test",
|
|
59
|
-
"test:e2e:ui": "playwright test --ui"
|
|
56
|
+
"lint:fix": "eslint src --fix"
|
|
60
57
|
},
|
|
61
|
-
"version": "3.1.
|
|
58
|
+
"version": "3.1.8",
|
|
62
59
|
"overrides": {
|
|
63
60
|
"zod-validation-error": "^4.0.2"
|
|
64
61
|
}
|
|
@@ -101,7 +101,7 @@ export class PermActions extends React.Component<PermActionsProps> {
|
|
|
101
101
|
|
|
102
102
|
if (!more || visible.length <= 1) {
|
|
103
103
|
const buttons = visible.map((action, index) => (
|
|
104
|
-
<React.Fragment key={index}>{this.renderButton(action, size)}</React.Fragment>
|
|
104
|
+
<React.Fragment key={index}>{this.renderButton(action, size)}</React.Fragment>
|
|
105
105
|
));
|
|
106
106
|
return <Space>{buttons}</Space>;
|
|
107
107
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/* 选人器左侧机构树:机构名称过长时不换行,改为容器横向滚动 */
|
|
2
|
+
.oa-user-picker-tree {
|
|
3
|
+
height: 100%;
|
|
4
|
+
overflow: auto;
|
|
5
|
+
|
|
6
|
+
/* 让树按内容宽度撑开,超宽时由外层容器横向滚动 */
|
|
7
|
+
.ant-tree {
|
|
8
|
+
width: max-content;
|
|
9
|
+
min-width: 100%;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.ant-tree-treenode,
|
|
13
|
+
.ant-tree-node-content-wrapper,
|
|
14
|
+
.ant-tree-title {
|
|
15
|
+
white-space: nowrap;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {Button, Empty, Form, Input, Modal, Splitter, Table} from 'antd';
|
|
3
|
+
import type {FormInstance, TableColumnsType} from 'antd';
|
|
4
|
+
import {CloseOutlined} from '@ant-design/icons';
|
|
5
|
+
import {OrgTree} from './OrgTree';
|
|
6
|
+
import {formatUserLabel, type PickerUser} from './pickerUser';
|
|
7
|
+
import {HttpClient} from '../utils';
|
|
8
|
+
import './UserPicker.less';
|
|
9
|
+
|
|
10
|
+
export interface UserPickerProps {
|
|
11
|
+
/** 是否显示 */
|
|
12
|
+
open: boolean;
|
|
13
|
+
/** 弹窗标题 */
|
|
14
|
+
title?: string;
|
|
15
|
+
/** 是否多选,默认 true */
|
|
16
|
+
multiple?: boolean;
|
|
17
|
+
/** 已选用户 id(受控) */
|
|
18
|
+
value?: string[];
|
|
19
|
+
/** 弹窗宽度,默认 1080 */
|
|
20
|
+
width?: number;
|
|
21
|
+
/** 内容区高度,默认 530(刚好容纳默认每页 10 行,不出现表格内部纵向滚动) */
|
|
22
|
+
height?: number;
|
|
23
|
+
/** 分页数据源地址,默认 admin/sysUser/page */
|
|
24
|
+
requestUrl?: string;
|
|
25
|
+
/** 按 id 查询用户详情地址(用于回显),默认 admin/sysUser/by-ids */
|
|
26
|
+
usersUrl?: string;
|
|
27
|
+
/** 确定按钮 loading */
|
|
28
|
+
confirmLoading?: boolean;
|
|
29
|
+
/** 选择变化(点击确定时触发) */
|
|
30
|
+
onChange?: (ids: string[], users: PickerUser[]) => void;
|
|
31
|
+
/** 点击确定 */
|
|
32
|
+
onOk?: (ids: string[], users: PickerUser[]) => void;
|
|
33
|
+
/** 取消 */
|
|
34
|
+
onCancel?: () => void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface UserPickerState {
|
|
38
|
+
loading: boolean;
|
|
39
|
+
dataSource: PickerUser[];
|
|
40
|
+
total: number;
|
|
41
|
+
current: number;
|
|
42
|
+
pageSize: number;
|
|
43
|
+
filters: {name?: string; account?: string};
|
|
44
|
+
orgId: string | null;
|
|
45
|
+
selectedMap: Record<string, PickerUser>;
|
|
46
|
+
selectedKeyword: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 候选人表格列与状态无关,提到模块级,避免每次勾选都重建列定义
|
|
50
|
+
const USER_TABLE_COLUMNS: TableColumnsType<PickerUser> = [
|
|
51
|
+
{title: '姓名', dataIndex: 'name', width: 90},
|
|
52
|
+
{title: '登录账号', dataIndex: 'account', width: 120},
|
|
53
|
+
{title: '所属机构', dataIndex: 'orgLabel', ellipsis: true, render: (_, row) => row.orgLabel || row.unitLabel || ''},
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
// 机构树不随选择变化,memo 避免每次勾选都重渲染
|
|
57
|
+
const MemoOrgTree = React.memo(OrgTree);
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 通用选人器
|
|
61
|
+
*
|
|
62
|
+
* 机构树 + 服务端分页搜索 + 跨页多选,解决用户量大、同名难以区分的问题。
|
|
63
|
+
*/
|
|
64
|
+
export class UserPicker extends React.Component<UserPickerProps, UserPickerState> {
|
|
65
|
+
|
|
66
|
+
static readonly defaultProps = {
|
|
67
|
+
title: '选择用户',
|
|
68
|
+
multiple: true,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
formRef = React.createRef<FormInstance>();
|
|
72
|
+
|
|
73
|
+
state: UserPickerState = {
|
|
74
|
+
loading: false,
|
|
75
|
+
dataSource: [],
|
|
76
|
+
total: 0,
|
|
77
|
+
current: 1,
|
|
78
|
+
pageSize: 10,
|
|
79
|
+
filters: {},
|
|
80
|
+
orgId: null,
|
|
81
|
+
selectedMap: {},
|
|
82
|
+
selectedKeyword: '',
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
componentDidUpdate(prevProps: UserPickerProps) {
|
|
86
|
+
if (this.props.open && !prevProps.open) {
|
|
87
|
+
this.init();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
getRequestUrl = () => this.props.requestUrl || 'admin/sysUser/page';
|
|
92
|
+
|
|
93
|
+
getUsersUrl = () => this.props.usersUrl || 'admin/sysUser/by-ids';
|
|
94
|
+
|
|
95
|
+
init = () => {
|
|
96
|
+
this.formRef.current?.resetFields();
|
|
97
|
+
this.setState({current: 1, filters: {}, orgId: null, selectedMap: {}, selectedKeyword: ''}, () => {
|
|
98
|
+
this.loadData();
|
|
99
|
+
this.loadSelected(this.props.value || []);
|
|
100
|
+
});
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
loadSelected = (ids: string[]) => {
|
|
104
|
+
if (!ids.length) {
|
|
105
|
+
this.setState({selectedMap: {}});
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
HttpClient.get<PickerUser[]>(this.getUsersUrl(), {ids}, {toastError: false}).then(rs => {
|
|
109
|
+
const data = rs.data || [];
|
|
110
|
+
const map: Record<string, PickerUser> = {};
|
|
111
|
+
ids.forEach(id => {
|
|
112
|
+
const user = data.find(item => item.id === id);
|
|
113
|
+
if (user) {
|
|
114
|
+
map[id] = user;
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
this.setState({selectedMap: map});
|
|
118
|
+
});
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
loadData = () => {
|
|
122
|
+
const {filters, orgId, current, pageSize} = this.state;
|
|
123
|
+
this.setState({loading: true});
|
|
124
|
+
HttpClient.get<{ content: PickerUser[]; totalElements: number | string; size: number }>(this.getRequestUrl(), {
|
|
125
|
+
name: filters.name,
|
|
126
|
+
account: filters.account,
|
|
127
|
+
orgId: orgId || undefined,
|
|
128
|
+
page: current,
|
|
129
|
+
size: pageSize,
|
|
130
|
+
}).then(rs => {
|
|
131
|
+
const {content, totalElements, size} = rs.data;
|
|
132
|
+
this.setState({dataSource: content || [], total: Number(totalElements), pageSize: size || pageSize, loading: false});
|
|
133
|
+
}).catch(() => {
|
|
134
|
+
this.setState({loading: false});
|
|
135
|
+
});
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
onSearch = (values: UserPickerState['filters']) => {
|
|
139
|
+
this.setState({filters: values, current: 1}, this.loadData);
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
resetSearch = () => {
|
|
143
|
+
this.formRef.current?.resetFields();
|
|
144
|
+
this.setState({filters: {}, current: 1}, this.loadData);
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
onSelectOrg = (orgId: string | null) => {
|
|
148
|
+
this.setState({orgId, current: 1}, this.loadData);
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
// 跨页累计选择:保留已选行数据,去掉本次取消勾选的 key
|
|
152
|
+
// 注意:preserveSelectedRowKeys 下,跨页已选但不在当前页的行在 selectedRows 中为 undefined,需跳过
|
|
153
|
+
onSelectionChange = (keys: React.Key[], rows: PickerUser[]) => {
|
|
154
|
+
const map = {...this.state.selectedMap};
|
|
155
|
+
rows.forEach(row => {
|
|
156
|
+
if (row && row.id != null) {
|
|
157
|
+
map[row.id] = row;
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
const keySet = new Set(keys.map(String));
|
|
161
|
+
Object.keys(map).forEach(id => {
|
|
162
|
+
if (!keySet.has(id)) {
|
|
163
|
+
delete map[id];
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
this.setState({selectedMap: map});
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
removeSelected = (id: string) => {
|
|
170
|
+
const map = {...this.state.selectedMap};
|
|
171
|
+
delete map[id];
|
|
172
|
+
this.setState({selectedMap: map});
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
clearSelected = () => this.setState({selectedMap: {}, selectedKeyword: ''});
|
|
176
|
+
|
|
177
|
+
handleOk = () => {
|
|
178
|
+
const users = Object.values(this.state.selectedMap);
|
|
179
|
+
const ids = users.map(user => user.id);
|
|
180
|
+
this.props.onOk?.(ids, users);
|
|
181
|
+
this.props.onChange?.(ids, users);
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
render() {
|
|
185
|
+
const {open, title, multiple, width, height, confirmLoading, onCancel} = this.props;
|
|
186
|
+
const {loading, dataSource, total, current, pageSize, selectedMap, selectedKeyword} = this.state;
|
|
187
|
+
const selectedUsers = Object.values(selectedMap);
|
|
188
|
+
const keyword = selectedKeyword.trim().toLowerCase();
|
|
189
|
+
const filteredSelected = keyword
|
|
190
|
+
? selectedUsers.filter(user => formatUserLabel(user).toLowerCase().includes(keyword))
|
|
191
|
+
: selectedUsers;
|
|
192
|
+
|
|
193
|
+
return <Modal
|
|
194
|
+
open={open}
|
|
195
|
+
title={title}
|
|
196
|
+
width={width || 1120}
|
|
197
|
+
confirmLoading={confirmLoading}
|
|
198
|
+
okText='确定'
|
|
199
|
+
cancelText='取消'
|
|
200
|
+
mask={{closable: false}}
|
|
201
|
+
destroyOnHidden
|
|
202
|
+
onOk={this.handleOk}
|
|
203
|
+
onCancel={onCancel}
|
|
204
|
+
>
|
|
205
|
+
<Splitter style={{height: height || 530}}>
|
|
206
|
+
<Splitter.Panel defaultSize={320} min={180} max={560} style={{paddingRight: 8}}>
|
|
207
|
+
<div className="oa-user-picker-tree">
|
|
208
|
+
<MemoOrgTree onChange={this.onSelectOrg}/>
|
|
209
|
+
</div>
|
|
210
|
+
</Splitter.Panel>
|
|
211
|
+
<Splitter.Panel style={{padding: '0 12px'}}>
|
|
212
|
+
<div style={{height: '100%', display: 'flex', flexDirection: 'column'}}>
|
|
213
|
+
<Form layout='inline' ref={this.formRef} onFinish={this.onSearch} style={{marginBottom: 8}}>
|
|
214
|
+
<Form.Item name='name'><Input allowClear placeholder='姓名' style={{width: 100}}/></Form.Item>
|
|
215
|
+
<Form.Item name='account'><Input allowClear placeholder='账号' style={{width: 100}}/></Form.Item>
|
|
216
|
+
<Form.Item><Button type='primary' htmlType='submit'>查询</Button></Form.Item>
|
|
217
|
+
<Form.Item><Button onClick={this.resetSearch}>重置</Button></Form.Item>
|
|
218
|
+
</Form>
|
|
219
|
+
<div style={{flex: 1, minHeight: 0, overflow: 'auto'}}>
|
|
220
|
+
<Table<PickerUser>
|
|
221
|
+
rowKey='id'
|
|
222
|
+
size='small'
|
|
223
|
+
loading={loading}
|
|
224
|
+
columns={USER_TABLE_COLUMNS}
|
|
225
|
+
dataSource={dataSource}
|
|
226
|
+
rowSelection={{
|
|
227
|
+
type: multiple === false ? 'radio' : 'checkbox',
|
|
228
|
+
selectedRowKeys: selectedUsers.map(user => user.id),
|
|
229
|
+
preserveSelectedRowKeys: true,
|
|
230
|
+
onChange: this.onSelectionChange,
|
|
231
|
+
}}
|
|
232
|
+
pagination={{
|
|
233
|
+
current,
|
|
234
|
+
pageSize,
|
|
235
|
+
total,
|
|
236
|
+
size: 'small',
|
|
237
|
+
showSizeChanger: true,
|
|
238
|
+
pageSizeOptions: [10, 20, 50, 100],
|
|
239
|
+
showTotal: (count) => `共 ${count} 条`,
|
|
240
|
+
}}
|
|
241
|
+
onChange={(pagination) => this.setState({
|
|
242
|
+
current: pagination.current || 1,
|
|
243
|
+
pageSize: pagination.pageSize || pageSize,
|
|
244
|
+
}, this.loadData)}
|
|
245
|
+
locale={{emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description='暂无数据'/>}}
|
|
246
|
+
/>
|
|
247
|
+
</div>
|
|
248
|
+
</div>
|
|
249
|
+
</Splitter.Panel>
|
|
250
|
+
<Splitter.Panel defaultSize={260} min={180} max={520} style={{paddingLeft: 8}}>
|
|
251
|
+
<div style={{height: '100%', display: 'flex', flexDirection: 'column'}}>
|
|
252
|
+
<div style={{display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 8}}>
|
|
253
|
+
<span>已选 <b>{selectedUsers.length}</b> 人</span>
|
|
254
|
+
{selectedUsers.length > 0 && <a onClick={this.clearSelected}>清空</a>}
|
|
255
|
+
</div>
|
|
256
|
+
<Input
|
|
257
|
+
allowClear
|
|
258
|
+
placeholder='搜索已选'
|
|
259
|
+
value={selectedKeyword}
|
|
260
|
+
onChange={(e) => this.setState({selectedKeyword: e.target.value})}
|
|
261
|
+
style={{marginBottom: 8}}
|
|
262
|
+
/>
|
|
263
|
+
<div style={{flex: 1, minHeight: 0, overflow: 'auto'}}>
|
|
264
|
+
{selectedUsers.length === 0 && (
|
|
265
|
+
<div style={{color: 'var(--oa-color-text-tertiary, #999)', textAlign: 'center', marginTop: 24}}>暂未选择</div>
|
|
266
|
+
)}
|
|
267
|
+
{selectedUsers.length > 0 && filteredSelected.length === 0 && (
|
|
268
|
+
<div style={{color: 'var(--oa-color-text-tertiary, #999)', textAlign: 'center', marginTop: 24}}>无匹配人员</div>
|
|
269
|
+
)}
|
|
270
|
+
{filteredSelected.map(user => (
|
|
271
|
+
<div key={user.id} style={{
|
|
272
|
+
display: 'flex',
|
|
273
|
+
alignItems: 'center',
|
|
274
|
+
gap: 4,
|
|
275
|
+
padding: '3px 0',
|
|
276
|
+
borderBottom: '1px dashed var(--oa-color-border, #f0f0f0)',
|
|
277
|
+
}}>
|
|
278
|
+
<span
|
|
279
|
+
title={formatUserLabel(user)}
|
|
280
|
+
style={{flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap'}}
|
|
281
|
+
>
|
|
282
|
+
{formatUserLabel(user)}
|
|
283
|
+
</span>
|
|
284
|
+
<CloseOutlined
|
|
285
|
+
style={{cursor: 'pointer', color: 'var(--oa-color-text-tertiary, #999)', flexShrink: 0}}
|
|
286
|
+
onClick={() => this.removeSelected(user.id)}
|
|
287
|
+
/>
|
|
288
|
+
</div>
|
|
289
|
+
))}
|
|
290
|
+
</div>
|
|
291
|
+
</div>
|
|
292
|
+
</Splitter.Panel>
|
|
293
|
+
</Splitter>
|
|
294
|
+
</Modal>;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
@@ -6,6 +6,8 @@ export * from './PermActions'
|
|
|
6
6
|
export * from './Perm'
|
|
7
7
|
export * from './OrgTree'
|
|
8
8
|
export * from './RoleTree'
|
|
9
|
+
export * from './UserPicker'
|
|
10
|
+
export * from './pickerUser'
|
|
9
11
|
|
|
10
12
|
class InnerFieldUserSelect extends React.Component<ComponentProps<typeof FieldRemoteSelect>> {
|
|
11
13
|
render() {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** 选人器使用到的用户信息 */
|
|
2
|
+
export interface PickerUser {
|
|
3
|
+
id: string;
|
|
4
|
+
name?: string;
|
|
5
|
+
account?: string;
|
|
6
|
+
phone?: string;
|
|
7
|
+
orgLabel?: string;
|
|
8
|
+
unitLabel?: string;
|
|
9
|
+
[key: string]: unknown;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** 用户标签文案:姓名(账号 · 机构),用于区分同名用户 */
|
|
13
|
+
export function formatUserLabel(user: PickerUser): string {
|
|
14
|
+
const extras = [user.account, user.orgLabel || user.unitLabel].filter(Boolean) as string[];
|
|
15
|
+
if (extras.length === 0) {
|
|
16
|
+
return user.name || user.id;
|
|
17
|
+
}
|
|
18
|
+
return `${user.name || ''}(${extras.join(' · ')})`;
|
|
19
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
import {getToken} from '../../config';
|
|
2
3
|
|
|
3
4
|
interface ContextMenuItem {
|
|
4
5
|
key: string;
|
|
@@ -19,9 +20,10 @@ interface ContextMenuProps {
|
|
|
19
20
|
|
|
20
21
|
export class ContextMenu extends React.Component<ContextMenuProps> {
|
|
21
22
|
menuItemColor = (item): string => {
|
|
22
|
-
|
|
23
|
-
if (item.
|
|
24
|
-
return
|
|
23
|
+
const token = getToken();
|
|
24
|
+
if (item.danger) return token.colorError;
|
|
25
|
+
if (item.disabled) return String(token.colorTextDisabled);
|
|
26
|
+
return String(token.colorText);
|
|
25
27
|
};
|
|
26
28
|
menuRef = React.createRef<HTMLDivElement>();
|
|
27
29
|
timer: ReturnType<typeof setTimeout> | null = null;
|
|
@@ -73,6 +75,7 @@ export class ContextMenu extends React.Component<ContextMenuProps> {
|
|
|
73
75
|
|
|
74
76
|
render() {
|
|
75
77
|
const { x, y, items, onClick, onClose } = this.props;
|
|
78
|
+
const token = getToken();
|
|
76
79
|
|
|
77
80
|
return (
|
|
78
81
|
<div
|
|
@@ -82,16 +85,16 @@ export class ContextMenu extends React.Component<ContextMenuProps> {
|
|
|
82
85
|
left: x,
|
|
83
86
|
top: y,
|
|
84
87
|
zIndex: 1050,
|
|
85
|
-
background:
|
|
86
|
-
borderRadius:
|
|
88
|
+
background: String(token.colorBgElevated ?? token.colorBgContainer),
|
|
89
|
+
borderRadius: 'var(--oa-border-radius, 4px)',
|
|
87
90
|
padding: '4px 0',
|
|
88
91
|
minWidth: 120,
|
|
89
|
-
boxShadow: '0 6px 16px 0 rgba(0,0,0,0.08)
|
|
92
|
+
boxShadow: String(token.boxShadowSecondary ?? '0 6px 16px 0 rgba(0,0,0,0.08)'),
|
|
90
93
|
}}
|
|
91
94
|
>
|
|
92
95
|
{items.map(item => (
|
|
93
96
|
item.divider ? (
|
|
94
|
-
<div key={item.key} style={{ height: 1, background:
|
|
97
|
+
<div key={item.key} style={{ height: 1, background: String(token.colorSplit ?? token.colorBorderSecondary), margin: '4px 0' }} />
|
|
95
98
|
) : (
|
|
96
99
|
<div
|
|
97
100
|
key={item.key}
|
|
@@ -125,7 +128,7 @@ export class ContextMenu extends React.Component<ContextMenuProps> {
|
|
|
125
128
|
userSelect: 'none',
|
|
126
129
|
}}
|
|
127
130
|
onMouseEnter={(e) => {
|
|
128
|
-
if (!item.disabled) e.currentTarget.style.background =
|
|
131
|
+
if (!item.disabled) e.currentTarget.style.background = String(token.colorFillTertiary);
|
|
129
132
|
}}
|
|
130
133
|
onMouseLeave={(e) => {
|
|
131
134
|
e.currentTarget.style.background = 'transparent';
|
|
@@ -40,8 +40,8 @@ export interface DownloadModalProps {
|
|
|
40
40
|
onFinish?: () => void;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
export class DownloadModal extends React.Component<DownloadModalProps, ModalState> {
|
|
44
|
-
private abortController: AbortController | null = null;
|
|
43
|
+
export class DownloadModal extends React.Component<DownloadModalProps, ModalState> {
|
|
44
|
+
private abortController: AbortController | null = null;
|
|
45
45
|
private lastOptions: DownloadOptions | null = null;
|
|
46
46
|
private lastTime: number = 0;
|
|
47
47
|
private lastLoaded: number = 0;
|
|
@@ -61,7 +61,7 @@ export class DownloadModal extends React.Component<DownloadModalProps, ModalStat
|
|
|
61
61
|
};
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
download = (options: DownloadOptions) => {
|
|
64
|
+
download = (options: DownloadOptions) => {
|
|
65
65
|
this.startDownload(options);
|
|
66
66
|
};
|
|
67
67
|
|
|
@@ -287,7 +287,7 @@ export class DownloadModal extends React.Component<DownloadModalProps, ModalStat
|
|
|
287
287
|
>
|
|
288
288
|
<div style={{padding: '20px 0'}}>
|
|
289
289
|
{/* 文件名 */}
|
|
290
|
-
<div style={{marginBottom: 16, fontSize: 15, fontWeight: 500, color: '#333'}}>
|
|
290
|
+
<div style={{marginBottom: 16, fontSize: 15, fontWeight: 500, color: 'var(--oa-color-text, #333)'}}>
|
|
291
291
|
文件名:{fileName || '未知文件'}
|
|
292
292
|
</div>
|
|
293
293
|
|
|
@@ -295,7 +295,7 @@ export class DownloadModal extends React.Component<DownloadModalProps, ModalStat
|
|
|
295
295
|
{status === 'downloading' && (
|
|
296
296
|
<>
|
|
297
297
|
<Progress percent={progress} status="active" strokeColor="var(--ant-color-primary)"/>
|
|
298
|
-
<div style={{marginTop: 8, fontSize: 13, color: '#999'}}>
|
|
298
|
+
<div style={{marginTop: 8, fontSize: 13, color: 'var(--oa-color-text-tertiary, #999)'}}>
|
|
299
299
|
<span>{this.formatSize(loaded)}</span>
|
|
300
300
|
{total > 0 && <span> / {this.formatSize(total)}</span>}
|
|
301
301
|
{speed && <span style={{marginLeft: 12}}>{speed}</span>}
|
|
@@ -310,7 +310,7 @@ export class DownloadModal extends React.Component<DownloadModalProps, ModalStat
|
|
|
310
310
|
{status === 'completed' && (
|
|
311
311
|
<>
|
|
312
312
|
<Progress percent={100} status="success"/>
|
|
313
|
-
<div style={{marginTop: 8, fontSize: 13, color: '#999'}}>
|
|
313
|
+
<div style={{marginTop: 8, fontSize: 13, color: 'var(--oa-color-text-tertiary, #999)'}}>
|
|
314
314
|
{loaded > 0 && <span>文件大小:{this.formatSize(loaded)}</span>}
|
|
315
315
|
</div>
|
|
316
316
|
<div style={{marginTop: 8}}>
|
|
@@ -323,7 +323,7 @@ export class DownloadModal extends React.Component<DownloadModalProps, ModalStat
|
|
|
323
323
|
{status === 'failed' && (
|
|
324
324
|
<>
|
|
325
325
|
<Progress percent={progress} status="exception"/>
|
|
326
|
-
<div style={{marginTop: 8, fontSize: 13, color: '#ff4d4f'}}>
|
|
326
|
+
<div style={{marginTop: 8, fontSize: 13, color: 'var(--oa-color-error, #ff4d4f)'}}>
|
|
327
327
|
<CloseCircleOutlined style={{marginRight: 4}}/>
|
|
328
328
|
{errorMessage}
|
|
329
329
|
</div>
|
|
@@ -68,7 +68,7 @@ export default class ErrorBoundary extends React.Component<Props, State> {
|
|
|
68
68
|
maxHeight: 300, overflow: 'auto',
|
|
69
69
|
fontSize: 12, fontFamily: 'monospace',
|
|
70
70
|
whiteSpace: 'pre-wrap', wordBreak: 'break-all',
|
|
71
|
-
background: '#f5f5f5', padding: 12, borderRadius:
|
|
71
|
+
background: 'var(--oa-color-fill, #f5f5f5)', padding: 12, borderRadius: 'var(--oa-border-radius, 4px)',
|
|
72
72
|
color: getToken().colorError,
|
|
73
73
|
};
|
|
74
74
|
|
|
@@ -12,9 +12,9 @@ export interface FormModalProps {
|
|
|
12
12
|
children?: React.ReactNode
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
export default class FormModal extends React.Component<FormModalProps> {
|
|
15
|
+
export default class FormModal extends React.Component<FormModalProps> {
|
|
16
16
|
|
|
17
|
-
readonly state = {
|
|
17
|
+
readonly state = {
|
|
18
18
|
visible: false,
|
|
19
19
|
confirmLoading: false,
|
|
20
20
|
}
|
|
@@ -22,11 +22,11 @@ export default class FormModal extends React.Component<FormModalProps> { // NOSO
|
|
|
22
22
|
private pendingValues: Record<string, unknown> | null = null
|
|
23
23
|
private formRef = React.createRef<FormInstance>()
|
|
24
24
|
|
|
25
|
-
get formInstance() {
|
|
25
|
+
get formInstance() {
|
|
26
26
|
return this.formRef.current
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
open = (values?: Record<string, unknown>) => {
|
|
29
|
+
open = (values?: Record<string, unknown>) => {
|
|
30
30
|
this.pendingValues = values || {}
|
|
31
31
|
this.setState({visible: true})
|
|
32
32
|
}
|
|
@@ -12,7 +12,7 @@ const iconLoaders: Record<string, () => Promise<{ default: IconComponent }>> = {
|
|
|
12
12
|
for (const [path, loader] of Object.entries(
|
|
13
13
|
import.meta.glob('/node_modules/@ant-design/icons/es/icons/*.js')
|
|
14
14
|
)) {
|
|
15
|
-
const name = /([^/]+)\.js$/.exec(path)?.[1];
|
|
15
|
+
const name = /([^/]+)\.js$/.exec(path)?.[1];
|
|
16
16
|
if (name) {
|
|
17
17
|
iconLoaders[name] = loader as () => Promise<{ default: IconComponent }>;
|
|
18
18
|
}
|
|
@@ -35,7 +35,7 @@ export class OrgSwitcher extends React.Component<Record<string, never>, {tree: O
|
|
|
35
35
|
const {tree, currentOrgId} = this.state;
|
|
36
36
|
|
|
37
37
|
if (countUnits(tree) === 0) {
|
|
38
|
-
return <span style={{marginRight: 8, color:
|
|
38
|
+
return <span style={{marginRight: 8, color: 'var(--oa-color-text-tertiary, #999)'}}>暂无组织机构</span>;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
if (countUnits(tree) === 1) {
|
|
@@ -1,8 +1,19 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import { Typography } from "antd";
|
|
2
|
+
import { Space, Typography } from "antd";
|
|
3
3
|
import './index.less'
|
|
4
4
|
import {getToken} from "../../config";
|
|
5
|
-
import { PermActions } from "../../biz/PermActions";
|
|
5
|
+
import { PermActions, type PermAction } from "../../biz/PermActions";
|
|
6
|
+
import { PermUtils } from "../../utils";
|
|
7
|
+
|
|
8
|
+
/** 兼容旧写法:按子节点的 perm 属性过滤(新代码请用 PermAction[]) */
|
|
9
|
+
function renderActionNodes(nodes: React.ReactNode) {
|
|
10
|
+
const visible = React.Children.toArray(nodes).filter((node) => {
|
|
11
|
+
const perm = (node as React.ReactElement<{ perm?: string }>)?.props?.perm;
|
|
12
|
+
return !perm || PermUtils.hasPermission(perm);
|
|
13
|
+
});
|
|
14
|
+
if (visible.length === 0) return null;
|
|
15
|
+
return <Space>{visible}</Space>;
|
|
16
|
+
}
|
|
6
17
|
|
|
7
18
|
interface PageProps {
|
|
8
19
|
padding?: boolean;
|
|
@@ -10,7 +21,8 @@ interface PageProps {
|
|
|
10
21
|
debug?: boolean;
|
|
11
22
|
title?: React.ReactNode;
|
|
12
23
|
description?: React.ReactNode;
|
|
13
|
-
|
|
24
|
+
/** 右侧操作区:推荐传 PermAction[](数据驱动),兼容带 perm 属性的节点 */
|
|
25
|
+
actions?: PermAction[] | React.ReactNode;
|
|
14
26
|
children?: React.ReactNode;
|
|
15
27
|
}
|
|
16
28
|
|
|
@@ -44,7 +56,9 @@ export class Page extends React.Component<PageProps> {
|
|
|
44
56
|
{description && <div><Typography.Text type="secondary">{description}</Typography.Text></div>}
|
|
45
57
|
</div>
|
|
46
58
|
</div>
|
|
47
|
-
{actions &&
|
|
59
|
+
{actions != null && (Array.isArray(actions)
|
|
60
|
+
? <PermActions actions={actions}/>
|
|
61
|
+
: renderActionNodes(actions))}
|
|
48
62
|
</div>
|
|
49
63
|
</div>
|
|
50
64
|
)}
|
|
@@ -9,7 +9,7 @@ export interface PageLoadingProps {
|
|
|
9
9
|
messages?: string[];
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
export function PageLoading(props: PageLoadingProps) {
|
|
12
|
+
export function PageLoading(props: PageLoadingProps) {
|
|
13
13
|
const titles = props.messages || (props.message ? [props.message] : ['页面加载中...']);
|
|
14
14
|
const primaryColor = getToken().colorPrimary ?? "";
|
|
15
15
|
|
|
@@ -22,7 +22,7 @@ export function PageLoading(props: PageLoadingProps) { // NOSONAR: 仅读取 pro
|
|
|
22
22
|
<div className="oa-page-loading-messages">
|
|
23
23
|
{titles.map((msg, i) => (
|
|
24
24
|
<Alert
|
|
25
|
-
key={i}
|
|
25
|
+
key={i}
|
|
26
26
|
title={<span style={{color: primaryColor}}>{msg}</span>}
|
|
27
27
|
type="info"
|
|
28
28
|
showIcon
|