@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
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import {
|
|
3
|
+
Alert,
|
|
4
|
+
Avatar,
|
|
5
|
+
Button,
|
|
6
|
+
Card,
|
|
7
|
+
Col,
|
|
8
|
+
Form,
|
|
9
|
+
Input,
|
|
10
|
+
Modal,
|
|
11
|
+
Popconfirm,
|
|
12
|
+
Row,
|
|
13
|
+
Space,
|
|
14
|
+
Table,
|
|
15
|
+
Tag,
|
|
16
|
+
Tooltip,
|
|
17
|
+
} from "antd";
|
|
18
|
+
import {EditOutlined, UserOutlined} from "@ant-design/icons";
|
|
19
|
+
import PermView from "./PermView";
|
|
20
|
+
import AvatarPicker from "./AvatarPicker";
|
|
21
|
+
import {
|
|
22
|
+
AvatarUtils,
|
|
23
|
+
EventBus,
|
|
24
|
+
GlobalData,
|
|
25
|
+
HttpClient,
|
|
26
|
+
history,
|
|
27
|
+
LOGIN_PATH,
|
|
28
|
+
Page,
|
|
29
|
+
} from "../../framework";
|
|
30
|
+
|
|
31
|
+
export const pageMeta = {
|
|
32
|
+
title: '个人中心',
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const metaLabelStyle = {
|
|
36
|
+
marginBottom: 2,
|
|
37
|
+
fontSize: 12,
|
|
38
|
+
color: 'var(--oa-color-text-secondary, rgba(0, 0, 0, 0.65))',
|
|
39
|
+
whiteSpace: 'nowrap',
|
|
40
|
+
};
|
|
41
|
+
const metaValueStyle = {fontSize: 14, wordBreak: 'break-word'};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 从 User-Agent 粗略识别浏览器 / 操作系统,识别不出时显示原始 UA 摘要。
|
|
45
|
+
* 仅用于个人中心展示,不追求完整设备库。
|
|
46
|
+
*/
|
|
47
|
+
const parseDevice = (ua) => {
|
|
48
|
+
if (!ua) {
|
|
49
|
+
return '未知设备';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const browser = /MicroMessenger/.test(ua) ? '微信'
|
|
53
|
+
: /Edg\//.test(ua) ? 'Edge'
|
|
54
|
+
: /OPR\/|Opera/.test(ua) ? 'Opera'
|
|
55
|
+
: /Firefox\//.test(ua) ? 'Firefox'
|
|
56
|
+
: /Chrome\/|Chromium/.test(ua) ? 'Chrome'
|
|
57
|
+
: /Safari\//.test(ua) ? 'Safari'
|
|
58
|
+
: '浏览器';
|
|
59
|
+
|
|
60
|
+
const os = /Windows NT/.test(ua) ? 'Windows'
|
|
61
|
+
: /Android/.test(ua) ? 'Android'
|
|
62
|
+
: /(iPhone|iPad|iPod)/.test(ua) ? 'iOS'
|
|
63
|
+
: /Mac OS X/.test(ua) ? 'macOS'
|
|
64
|
+
: /Linux/.test(ua) ? 'Linux'
|
|
65
|
+
: '';
|
|
66
|
+
|
|
67
|
+
return os ? `${browser} / ${os}` : browser;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export default class UserCenterPage extends React.Component {
|
|
71
|
+
|
|
72
|
+
formRef = React.createRef();
|
|
73
|
+
editFormRef = React.createRef();
|
|
74
|
+
|
|
75
|
+
state = {
|
|
76
|
+
info: {},
|
|
77
|
+
sessions: [],
|
|
78
|
+
sessionModalOpen: false,
|
|
79
|
+
sessionLoading: false,
|
|
80
|
+
changePwdOpen: false,
|
|
81
|
+
changePwdSuccess: false,
|
|
82
|
+
editOpen: false,
|
|
83
|
+
editSaving: false,
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
componentDidMount() {
|
|
87
|
+
HttpClient.get('admin/user-center/info', null).then(rs => {
|
|
88
|
+
this.setState({info: rs.data})
|
|
89
|
+
})
|
|
90
|
+
this.loadSessions();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
loadSessions = (showLoading = false) => {
|
|
94
|
+
if (showLoading) {
|
|
95
|
+
this.setState({sessionLoading: true});
|
|
96
|
+
}
|
|
97
|
+
return HttpClient.get('admin/user-center/sessions', null, {toastError: false})
|
|
98
|
+
.then(rs => {
|
|
99
|
+
const sessions = rs.data || [];
|
|
100
|
+
this.setState({sessions});
|
|
101
|
+
return sessions;
|
|
102
|
+
})
|
|
103
|
+
.catch(() => {
|
|
104
|
+
// 会话查询失败不打扰用户,个人中心其它信息照常展示
|
|
105
|
+
this.setState({sessions: []});
|
|
106
|
+
return [];
|
|
107
|
+
})
|
|
108
|
+
.finally(() => {
|
|
109
|
+
if (showLoading) {
|
|
110
|
+
this.setState({sessionLoading: false});
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
openSessions = () => {
|
|
116
|
+
this.setState({sessionModalOpen: true});
|
|
117
|
+
this.loadSessions(true);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
closeSessions = () => {
|
|
121
|
+
this.setState({sessionModalOpen: false});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
kickSession = (id) => {
|
|
125
|
+
HttpClient.post('admin/user-center/sessions/kick', {id})
|
|
126
|
+
.then(() => this.loadSessions(true))
|
|
127
|
+
.then(sessions => {
|
|
128
|
+
// 只剩当前设备时没有必要继续展示详情
|
|
129
|
+
this.setState({sessionModalOpen: sessions.length > 1});
|
|
130
|
+
})
|
|
131
|
+
.catch(() => {
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
onPwdFinish = (values) => {
|
|
136
|
+
HttpClient.post('admin/user-center/update-pwd', values, null).then(() => {
|
|
137
|
+
this.setState({changePwdOpen: false, changePwdSuccess: true});
|
|
138
|
+
})
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
pwdValidator = (rule, value) => HttpClient.get("admin/sys-user/pwd-strength", {password: value}, {toastError: false})
|
|
142
|
+
.catch(e => {
|
|
143
|
+
throw new Error(e.message)
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
openEdit = () => {
|
|
147
|
+
this.setState({editOpen: true});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
onEditFinish = (values) => {
|
|
151
|
+
const data = {
|
|
152
|
+
phone: values.phone?.trim() || '',
|
|
153
|
+
email: values.email || '',
|
|
154
|
+
avatar: values.avatar || '',
|
|
155
|
+
};
|
|
156
|
+
this.setState({editSaving: true});
|
|
157
|
+
HttpClient.post('admin/user-center/update-info', data, null).then(() => {
|
|
158
|
+
this.setState(prevState => ({
|
|
159
|
+
info: {...prevState.info, ...data},
|
|
160
|
+
editOpen: false,
|
|
161
|
+
}));
|
|
162
|
+
// 同步顶栏头像
|
|
163
|
+
GlobalData.setLoginInfo({...GlobalData.getLoginInfo(), avatar: data.avatar});
|
|
164
|
+
EventBus.emit('loginInfoChange');
|
|
165
|
+
}).finally(() => {
|
|
166
|
+
this.setState({editSaving: false});
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
renderInfoItem = (label, value, cellStyle, valueStyle) => (
|
|
171
|
+
<div style={{minWidth: 0, ...cellStyle}}>
|
|
172
|
+
<div style={metaLabelStyle}>{label}</div>
|
|
173
|
+
<div style={{...metaValueStyle, ...valueStyle}}>{value || '-'}</div>
|
|
174
|
+
</div>
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
sessionColumns = [
|
|
178
|
+
{
|
|
179
|
+
title: '设备',
|
|
180
|
+
dataIndex: 'userAgent',
|
|
181
|
+
render: (ua) => {
|
|
182
|
+
const label = parseDevice(ua);
|
|
183
|
+
return ua ? <Tooltip title={ua}><span>{label}</span></Tooltip> : label;
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
title: 'IP',
|
|
188
|
+
dataIndex: 'ip',
|
|
189
|
+
width: 140,
|
|
190
|
+
render: v => v || '-',
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
title: '登录时间',
|
|
194
|
+
dataIndex: 'loginTime',
|
|
195
|
+
width: 170,
|
|
196
|
+
render: v => v || '-',
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
title: '最后活跃',
|
|
200
|
+
dataIndex: 'lastActiveTime',
|
|
201
|
+
width: 170,
|
|
202
|
+
render: v => v || '-',
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
title: '操作',
|
|
206
|
+
dataIndex: 'option',
|
|
207
|
+
width: 110,
|
|
208
|
+
render: (_, record) => record.current
|
|
209
|
+
? <Tag color="green" style={{marginInlineEnd: 0}}>当前设备</Tag>
|
|
210
|
+
: <Popconfirm title="确定下线该设备吗?"
|
|
211
|
+
okText="下线"
|
|
212
|
+
cancelText="取消"
|
|
213
|
+
onConfirm={() => this.kickSession(record.id)}>
|
|
214
|
+
<Button size="small" danger type="link" style={{padding: 0}}>下线</Button>
|
|
215
|
+
</Popconfirm>,
|
|
216
|
+
},
|
|
217
|
+
];
|
|
218
|
+
|
|
219
|
+
render() {
|
|
220
|
+
const {
|
|
221
|
+
info,
|
|
222
|
+
sessions,
|
|
223
|
+
sessionModalOpen,
|
|
224
|
+
sessionLoading,
|
|
225
|
+
changePwdOpen,
|
|
226
|
+
changePwdSuccess,
|
|
227
|
+
editOpen,
|
|
228
|
+
editSaving,
|
|
229
|
+
} = this.state;
|
|
230
|
+
const avatarSrc = AvatarUtils.resolve(info.avatar);
|
|
231
|
+
const unitOrg = [info.unit, info.org].filter(Boolean).join(' / ') || '-';
|
|
232
|
+
|
|
233
|
+
return <Page>
|
|
234
|
+
|
|
235
|
+
{sessions.length > 1 && <Alert
|
|
236
|
+
type="warning"
|
|
237
|
+
showIcon
|
|
238
|
+
style={{marginBottom: 16}}
|
|
239
|
+
title={`检测到你的账号当前在 ${sessions.length} 处登录`}
|
|
240
|
+
description="如非本人操作,请查看详情并下线其他设备,必要时修改密码。"
|
|
241
|
+
action={<Button size="small" onClick={this.openSessions}>查看详情</Button>}
|
|
242
|
+
/>}
|
|
243
|
+
|
|
244
|
+
<Card title="个人信息"
|
|
245
|
+
style={{marginBottom: 16}}
|
|
246
|
+
extra={<Space>
|
|
247
|
+
<Button type="primary" icon={<EditOutlined/>} onClick={this.openEdit}>编辑资料</Button>
|
|
248
|
+
<Button onClick={() => this.setState({changePwdOpen: true})}>修改密码</Button>
|
|
249
|
+
</Space>}>
|
|
250
|
+
<div style={{
|
|
251
|
+
display: 'grid',
|
|
252
|
+
gridTemplateColumns: 'minmax(0, 1fr) minmax(0, 1fr)',
|
|
253
|
+
columnGap: 32,
|
|
254
|
+
rowGap: 20,
|
|
255
|
+
alignItems: 'start',
|
|
256
|
+
}}>
|
|
257
|
+
<div style={{display: 'flex', alignItems: 'center', gap: 16, minWidth: 0}}>
|
|
258
|
+
<Avatar size={72} src={avatarSrc || undefined} icon={<UserOutlined/>}/>
|
|
259
|
+
<div style={{minWidth: 0}}>
|
|
260
|
+
<div style={{display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap', minWidth: 0}}>
|
|
261
|
+
<span style={{
|
|
262
|
+
fontSize: 18,
|
|
263
|
+
fontWeight: 600,
|
|
264
|
+
lineHeight: 1.4,
|
|
265
|
+
wordBreak: 'break-word',
|
|
266
|
+
}}>{info.name || '-'}</span>
|
|
267
|
+
{(info.roles || []).map(role =>
|
|
268
|
+
<Tag key={role} style={{marginInlineEnd: 0}}>{role}</Tag>
|
|
269
|
+
)}
|
|
270
|
+
</div>
|
|
271
|
+
<div style={{
|
|
272
|
+
marginTop: 4,
|
|
273
|
+
fontSize: 13,
|
|
274
|
+
color: 'var(--oa-color-text-secondary, rgba(0, 0, 0, 0.65))',
|
|
275
|
+
wordBreak: 'break-all',
|
|
276
|
+
}}>
|
|
277
|
+
账号:{info.account || '-'}
|
|
278
|
+
</div>
|
|
279
|
+
</div>
|
|
280
|
+
</div>
|
|
281
|
+
|
|
282
|
+
{this.renderInfoItem('所属单位/部门', unitOrg, {alignSelf: 'center'})}
|
|
283
|
+
{this.renderInfoItem('手机', info.phone)}
|
|
284
|
+
{this.renderInfoItem('邮箱', info.email, null, {wordBreak: 'break-all'})}
|
|
285
|
+
{this.renderInfoItem('上次登录', info.lastLoginTime)}
|
|
286
|
+
{this.renderInfoItem('注册时间', info.createTime)}
|
|
287
|
+
</div>
|
|
288
|
+
</Card>
|
|
289
|
+
|
|
290
|
+
<Card>
|
|
291
|
+
<PermView/>
|
|
292
|
+
</Card>
|
|
293
|
+
|
|
294
|
+
<Modal open={sessionModalOpen} title="登录设备" width={720}
|
|
295
|
+
footer={<Button onClick={this.closeSessions}>关闭</Button>}
|
|
296
|
+
onCancel={this.closeSessions}>
|
|
297
|
+
<div style={{
|
|
298
|
+
marginBottom: 12,
|
|
299
|
+
fontSize: 13,
|
|
300
|
+
color: 'var(--oa-color-text-secondary, rgba(0, 0, 0, 0.65))',
|
|
301
|
+
}}>
|
|
302
|
+
当前账号共 {sessions.length} 处登录,可下线非当前设备。
|
|
303
|
+
</div>
|
|
304
|
+
<Table rowKey="id"
|
|
305
|
+
size="middle"
|
|
306
|
+
loading={sessionLoading}
|
|
307
|
+
columns={this.sessionColumns}
|
|
308
|
+
dataSource={sessions}
|
|
309
|
+
pagination={false}
|
|
310
|
+
locale={{emptyText: '暂无登录设备'}}/>
|
|
311
|
+
</Modal>
|
|
312
|
+
|
|
313
|
+
{/* destroyOnHidden:每次打开重建表单,配合 initialValues 回填当前资料(头像/手机/邮箱) */}
|
|
314
|
+
<Modal open={editOpen} title="编辑资料" okText="保存" cancelText="取消"
|
|
315
|
+
width={560}
|
|
316
|
+
destroyOnHidden
|
|
317
|
+
confirmLoading={editSaving}
|
|
318
|
+
onCancel={() => this.setState({editOpen: false})}
|
|
319
|
+
onOk={() => this.editFormRef.current?.submit()}>
|
|
320
|
+
<Form ref={this.editFormRef} onFinish={this.onEditFinish} layout="vertical"
|
|
321
|
+
initialValues={{phone: info.phone, email: info.email, avatar: info.avatar}}
|
|
322
|
+
style={{paddingTop: 4}}>
|
|
323
|
+
<Form.Item label="头像" name="avatar"
|
|
324
|
+
rules={[{required: true, message: '请选择或上传头像'}]}>
|
|
325
|
+
<AvatarPicker/>
|
|
326
|
+
</Form.Item>
|
|
327
|
+
<Row gutter={16}>
|
|
328
|
+
<Col span={12}>
|
|
329
|
+
<Form.Item name="phone"
|
|
330
|
+
label="手机号码"
|
|
331
|
+
rules={[
|
|
332
|
+
{pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号'},
|
|
333
|
+
{max: 11, message: '手机号长度不能超过11位'},
|
|
334
|
+
]}
|
|
335
|
+
>
|
|
336
|
+
<Input placeholder="请输入手机号" maxLength={11} allowClear/>
|
|
337
|
+
</Form.Item>
|
|
338
|
+
</Col>
|
|
339
|
+
<Col span={12}>
|
|
340
|
+
<Form.Item name="email"
|
|
341
|
+
label="用户邮箱"
|
|
342
|
+
rules={[
|
|
343
|
+
{type: 'email', message: '请输入正确的邮箱地址'},
|
|
344
|
+
{max: 30, message: '邮箱长度不能超过30位'},
|
|
345
|
+
]}
|
|
346
|
+
>
|
|
347
|
+
<Input placeholder="请输入邮箱" maxLength={30} allowClear/>
|
|
348
|
+
</Form.Item>
|
|
349
|
+
</Col>
|
|
350
|
+
</Row>
|
|
351
|
+
</Form>
|
|
352
|
+
</Modal>
|
|
353
|
+
|
|
354
|
+
<Modal open={changePwdOpen} title="修改密码" okText="确定" cancelText="取消"
|
|
355
|
+
width={480}
|
|
356
|
+
onCancel={() => this.setState({changePwdOpen: false})}
|
|
357
|
+
onOk={() => this.formRef.current?.submit()}>
|
|
358
|
+
<Form ref={this.formRef} onFinish={this.onPwdFinish} layout="vertical"
|
|
359
|
+
style={{paddingTop: 4}}>
|
|
360
|
+
<Form.Item name='oldPassword'
|
|
361
|
+
label='原密码'
|
|
362
|
+
rules={[{required: true, message: '请输入原密码'}]}
|
|
363
|
+
>
|
|
364
|
+
<Input.Password></Input.Password>
|
|
365
|
+
</Form.Item>
|
|
366
|
+
<Form.Item name='newPassword'
|
|
367
|
+
label='新密码'
|
|
368
|
+
extra={'请输入字母、数字、特殊字符'}
|
|
369
|
+
rules={[
|
|
370
|
+
{required: true, message: '请输入新密码'},
|
|
371
|
+
{pattern: /^[\x20-\x7E]{1,64}$/, message: '密码仅支持英文、数字与常见符号,长度不超过64位'},
|
|
372
|
+
{
|
|
373
|
+
validator: this.pwdValidator
|
|
374
|
+
}
|
|
375
|
+
]}
|
|
376
|
+
>
|
|
377
|
+
<Input.Password maxLength={64}></Input.Password>
|
|
378
|
+
</Form.Item>
|
|
379
|
+
</Form>
|
|
380
|
+
</Modal>
|
|
381
|
+
|
|
382
|
+
<Modal open={changePwdSuccess} title="提示" okText="确定"
|
|
383
|
+
onCancel={() => this.setState({changePwdSuccess: false})}
|
|
384
|
+
onOk={() => {
|
|
385
|
+
this.setState({changePwdSuccess: false});
|
|
386
|
+
history.push(LOGIN_PATH);
|
|
387
|
+
}}>
|
|
388
|
+
修改密码成功
|
|
389
|
+
</Modal>
|
|
390
|
+
|
|
391
|
+
</Page>
|
|
392
|
+
}
|
|
393
|
+
}
|
package/vite-plugin/index.js
CHANGED
|
@@ -45,6 +45,47 @@ function fileToRoutePath(file) {
|
|
|
45
45
|
return routePath || '/';
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
/** 按页面目录推断 pageMeta 默认值 */
|
|
49
|
+
function defaultPageMeta(relPath) {
|
|
50
|
+
const top = relPath.split('/')[0];
|
|
51
|
+
// /login 是框架约定的登录页路径:业务项目覆盖该页面时无需再声明 pageMeta
|
|
52
|
+
if (relPath === 'login' || relPath === 'login/index') return {requiresAuth: false, layout: 'blank'};
|
|
53
|
+
if (top === 'public') return {requiresAuth: false, layout: 'blank'};
|
|
54
|
+
if (top === 'standalone') return {requiresAuth: true, layout: 'blank'};
|
|
55
|
+
return {requiresAuth: true, layout: 'admin'};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** 页面 pageMeta 源码缓存:dev 下只有元信息变化时才重建路由表 */
|
|
59
|
+
const pageMetaCache = new Map();
|
|
60
|
+
|
|
61
|
+
/** 提取 pageMeta 导出源码,用于判断页面元信息是否变化 */
|
|
62
|
+
function readPageMetaSource(file) {
|
|
63
|
+
try {
|
|
64
|
+
const source = fs.readFileSync(file, 'utf8');
|
|
65
|
+
const matched = source.match(/export\s+const\s+pageMeta(?:\s*:\s*[^=]+)?\s*=\s*\{[\s\S]*?\};?/);
|
|
66
|
+
return matched ? matched[0].replace(/\s+/g, '') : '';
|
|
67
|
+
} catch {
|
|
68
|
+
return '';
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** 生成浏览器端 pageMeta 归一化函数:补默认值、修正非法组合 */
|
|
73
|
+
const PAGE_META_HELPER = `function __resolvePageMeta(mod, defaults) {
|
|
74
|
+
let meta = mod?.pageMeta;
|
|
75
|
+
if (meta != null && typeof meta !== 'object') {
|
|
76
|
+
console.warn('[open-admin] 页面 pageMeta 必须是对象,已忽略');
|
|
77
|
+
meta = null;
|
|
78
|
+
}
|
|
79
|
+
const requiresAuth = typeof meta?.requiresAuth === 'boolean' ? meta.requiresAuth : defaults.requiresAuth;
|
|
80
|
+
let layout = meta?.layout === 'admin' || meta?.layout === 'blank' ? meta.layout : defaults.layout;
|
|
81
|
+
if (!requiresAuth && layout === 'admin') {
|
|
82
|
+
console.warn('[open-admin] pageMeta:requiresAuth=false 时必须使用 blank 布局,已自动修正');
|
|
83
|
+
layout = 'blank';
|
|
84
|
+
}
|
|
85
|
+
const title = typeof meta?.title === 'string' && meta.title.trim() ? meta.title.trim() : undefined;
|
|
86
|
+
return {requiresAuth, layout, title};
|
|
87
|
+
}`;
|
|
88
|
+
|
|
48
89
|
/** 生成可被 Vite dev/build 解析的 import 路径 */
|
|
49
90
|
function toImportPath(root, file) {
|
|
50
91
|
const nm = file.indexOf('/node_modules/');
|
|
@@ -61,17 +102,20 @@ function generateRoutesModule(root) {
|
|
|
61
102
|
const imports = [];
|
|
62
103
|
const entries = [];
|
|
63
104
|
collectPageFiles(root).forEach((file, i) => {
|
|
105
|
+
const pageMatch = file.match(PAGE_PATH_RE);
|
|
64
106
|
const routePath = fileToRoutePath(file);
|
|
65
|
-
if (!routePath) return;
|
|
107
|
+
if (!routePath || !pageMatch) return;
|
|
108
|
+
pageMetaCache.set(file, readPageMetaSource(file));
|
|
66
109
|
if (seen.has(routePath)) {
|
|
67
110
|
console.warn(`[open-admin] 路由 ${routePath} 被覆盖(来自 ${file})`);
|
|
68
111
|
return;
|
|
69
112
|
}
|
|
70
113
|
seen.add(routePath);
|
|
71
|
-
|
|
72
|
-
|
|
114
|
+
const defaults = defaultPageMeta(pageMatch[1]);
|
|
115
|
+
imports.push(`import P${i}, * as M${i} from ${JSON.stringify(toImportPath(root, file))};`);
|
|
116
|
+
entries.push(` {path: ${JSON.stringify(routePath)}, component: P${i}, meta: __resolvePageMeta(M${i}, ${JSON.stringify(defaults)})},`);
|
|
73
117
|
});
|
|
74
|
-
return `${imports.join('\n')}\n\nexport default [\n${entries.join('\n')}\n];\n`;
|
|
118
|
+
return `${imports.join('\n')}\n\n${PAGE_META_HELPER}\n\nexport default [\n${entries.join('\n')}\n];\n`;
|
|
75
119
|
}
|
|
76
120
|
|
|
77
121
|
export default function openAdmin() {
|
|
@@ -92,16 +136,30 @@ export default function openAdmin() {
|
|
|
92
136
|
},
|
|
93
137
|
|
|
94
138
|
configureServer(server) {
|
|
95
|
-
const
|
|
96
|
-
if (!PAGE_PATH_RE.test(file.split(path.sep).join('/'))) return;
|
|
139
|
+
const reloadRoutes = () => {
|
|
97
140
|
const mod = server.moduleGraph.getModuleById(RESOLVED_ID);
|
|
98
141
|
if (mod) {
|
|
99
142
|
server.moduleGraph.invalidateModule(mod);
|
|
100
143
|
server.ws.send({type: 'full-reload'});
|
|
101
144
|
}
|
|
102
145
|
};
|
|
146
|
+
const onPagesFileChange = (file) => {
|
|
147
|
+
const normalized = file.split(path.sep).join('/');
|
|
148
|
+
if (!PAGE_PATH_RE.test(normalized)) return;
|
|
149
|
+
reloadRoutes();
|
|
150
|
+
};
|
|
151
|
+
// 只有 pageMeta 变化才重建路由表;普通页面代码交由 React Fast Refresh 处理
|
|
152
|
+
const onPageFileChange = (file) => {
|
|
153
|
+
const normalized = file.split(path.sep).join('/');
|
|
154
|
+
if (!PAGE_PATH_RE.test(normalized)) return;
|
|
155
|
+
const next = readPageMetaSource(normalized);
|
|
156
|
+
if (pageMetaCache.get(normalized) === next) return;
|
|
157
|
+
pageMetaCache.set(normalized, next);
|
|
158
|
+
reloadRoutes();
|
|
159
|
+
};
|
|
103
160
|
server.watcher.on('add', onPagesFileChange);
|
|
104
161
|
server.watcher.on('unlink', onPagesFileChange);
|
|
162
|
+
server.watcher.on('change', onPageFileChange);
|
|
105
163
|
},
|
|
106
164
|
};
|
|
107
165
|
}
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import {Button, Card, Descriptions, Form, Input, Modal} from "antd";
|
|
3
|
-
import PermView from "./permView";
|
|
4
|
-
import {HttpClient, history, Page} from "../../framework";
|
|
5
|
-
|
|
6
|
-
export default class UserCenterPage extends React.Component {
|
|
7
|
-
|
|
8
|
-
formRef = React.createRef();
|
|
9
|
-
|
|
10
|
-
state = {
|
|
11
|
-
info: {},
|
|
12
|
-
changePwdOpen: false,
|
|
13
|
-
changePwdSuccess: false,
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
componentDidMount() {
|
|
17
|
-
HttpClient.get('admin/userCenter/info', null).then(rs => {
|
|
18
|
-
this.setState({info: rs.data})
|
|
19
|
-
})
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
onPwdFinish = (values) => {
|
|
23
|
-
HttpClient.post('admin/userCenter/update-pwd', values, null).then(() => {
|
|
24
|
-
this.setState({changePwdOpen: false, changePwdSuccess: true});
|
|
25
|
-
})
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
pwdValidator = (rule, value) => HttpClient.get("admin/sysUser/pwd-strength", {password: value}, {toastError: false})
|
|
29
|
-
.catch(e => {
|
|
30
|
-
throw new Error(e.message)
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
render() {
|
|
34
|
-
const {info, changePwdOpen, changePwdSuccess} = this.state;
|
|
35
|
-
return <Page>
|
|
36
|
-
|
|
37
|
-
<Card title="个人信息"
|
|
38
|
-
style={{marginBottom: 16}}
|
|
39
|
-
extra={<Button type="primary" danger onClick={() => this.setState({changePwdOpen: true})}>修改密码</Button>}>
|
|
40
|
-
<Descriptions column={2} size="middle">
|
|
41
|
-
<Descriptions.Item label="用户名称">{info.name}</Descriptions.Item>
|
|
42
|
-
<Descriptions.Item label="账号">{info.account}</Descriptions.Item>
|
|
43
|
-
<Descriptions.Item label="手机号码">{info.phone}</Descriptions.Item>
|
|
44
|
-
<Descriptions.Item label="用户邮箱">{info.email}</Descriptions.Item>
|
|
45
|
-
<Descriptions.Item label="所属单位">{info.unit}</Descriptions.Item>
|
|
46
|
-
<Descriptions.Item label="所属机构">{info.org}</Descriptions.Item>
|
|
47
|
-
<Descriptions.Item label="所属角色">{(info.roles || []).join('、') || '-'}</Descriptions.Item>
|
|
48
|
-
<Descriptions.Item label="创建日期">{info.createTime}</Descriptions.Item>
|
|
49
|
-
</Descriptions>
|
|
50
|
-
</Card>
|
|
51
|
-
|
|
52
|
-
<Card title="我的权限">
|
|
53
|
-
<PermView/>
|
|
54
|
-
</Card>
|
|
55
|
-
|
|
56
|
-
<Modal open={changePwdOpen} title="修改密码" okText="确定" cancelText="取消"
|
|
57
|
-
onCancel={() => this.setState({changePwdOpen: false})}
|
|
58
|
-
onOk={() => this.formRef.current?.submit()}>
|
|
59
|
-
<Form ref={this.formRef} onFinish={this.onPwdFinish} style={{maxWidth: 400}}>
|
|
60
|
-
<Form.Item name='oldPassword'
|
|
61
|
-
label='原密码'
|
|
62
|
-
rules={[{required: true, message: '请输入原密码'}]}
|
|
63
|
-
>
|
|
64
|
-
<Input.Password></Input.Password>
|
|
65
|
-
</Form.Item>
|
|
66
|
-
<Form.Item name='newPassword'
|
|
67
|
-
label='新密码'
|
|
68
|
-
extra={'请输入字母、数字、特殊字符'}
|
|
69
|
-
rules={[
|
|
70
|
-
{required: true, message: '请输入新密码'},
|
|
71
|
-
{pattern: /^[\x20-\x7E]{1,64}$/, message: '密码仅支持英文、数字与常见符号,长度不超过64位'},
|
|
72
|
-
{
|
|
73
|
-
validator: this.pwdValidator
|
|
74
|
-
}
|
|
75
|
-
]}
|
|
76
|
-
>
|
|
77
|
-
<Input.Password maxLength={64}></Input.Password>
|
|
78
|
-
</Form.Item>
|
|
79
|
-
</Form>
|
|
80
|
-
</Modal>
|
|
81
|
-
|
|
82
|
-
<Modal open={changePwdSuccess} title="提示" okText="确定"
|
|
83
|
-
onCancel={() => this.setState({changePwdSuccess: false})}
|
|
84
|
-
onOk={() => {
|
|
85
|
-
this.setState({changePwdSuccess: false});
|
|
86
|
-
history.push('/public/login');
|
|
87
|
-
}}>
|
|
88
|
-
修改密码成功
|
|
89
|
-
</Modal>
|
|
90
|
-
|
|
91
|
-
</Page>
|
|
92
|
-
}
|
|
93
|
-
}
|