@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
@@ -38,7 +38,7 @@ export default class OrgPage extends React.Component {
38
38
 
39
39
  loadTree = () => {
40
40
  this.setState({treeLoading: true})
41
- HttpClient.get('admin/sysOrg/tree', this.state.params, {toastError: false}).then(rs => {
41
+ HttpClient.get('admin/sys-org/tree', this.state.params, {toastError: false}).then(rs => {
42
42
  this.setState({treeData: rs.data})
43
43
  this.setState({treeLoading: false});
44
44
  }).catch(() => {
@@ -53,7 +53,7 @@ export default class OrgPage extends React.Component {
53
53
  handleDelete = () => {
54
54
  const {selectedOrg} = this.state
55
55
  if (!selectedOrg) return
56
- HttpClient.post('admin/sysOrg/delete', {id: selectedOrg.id}, null).then(() => {
56
+ HttpClient.post('admin/sys-org/delete', {id: selectedOrg.id}, null).then(() => {
57
57
  this.setState({selectedOrg: null, deleteModalOpen: false})
58
58
  this.loadTree()
59
59
  })
@@ -64,7 +64,7 @@ export default class OrgPage extends React.Component {
64
64
  this.setState({selectedOrg: null})
65
65
  return
66
66
  }
67
- HttpClient.get("admin/sysOrg/detail", {id: selectedKeys[0]}).then(rs => {
67
+ HttpClient.get("admin/sys-org/detail", {id: selectedKeys[0]}).then(rs => {
68
68
  this.setState({selectedOrg: rs.data})
69
69
  })
70
70
  }
@@ -80,7 +80,7 @@ export default class OrgPage extends React.Component {
80
80
 
81
81
  handleModalFinish = async values => {
82
82
  const isNew = !values.id
83
- const url = isNew ? 'admin/sysOrg/create' : 'admin/sysOrg/update'
83
+ const url = isNew ? 'admin/sys-org/create' : 'admin/sys-org/update'
84
84
  await HttpClient.post(url, values)
85
85
  this.loadTree()
86
86
  }
@@ -189,13 +189,13 @@ export default class OrgPage extends React.Component {
189
189
  onFinish={this.handleModalFinish}
190
190
  labelCol={{flex: '120px'}}>
191
191
  <Form.Item label='父节点' name='pid'>
192
- <FieldRemoteTreeSelect url='admin/sysOrg/tree'/>
192
+ <FieldRemoteTreeSelect url='admin/sys-org/tree'/>
193
193
  </Form.Item>
194
194
  <Form.Item label='名称' name='name' rules={[{required: true}]}>
195
195
  <Input/>
196
196
  </Form.Item>
197
197
  <Form.Item label='类型' name='type' rules={[{required: true}]}>
198
- <FieldRemoteSelect url='admin/sysOrg/type-options' placeholder='请选择类型'/>
198
+ <FieldRemoteSelect url='admin/sys-org/type-options' placeholder='请选择类型'/>
199
199
  </Form.Item>
200
200
  <Form.Item label='部门领导' name={['leader', 'id']}>
201
201
  <FieldUserSelect/>
@@ -233,7 +233,7 @@ export default class OrgPage extends React.Component {
233
233
  const dragKey = dragNode.key;
234
234
  const dropPos = e.node.pos.split('-');
235
235
  const dropPosition = e.dropPosition - Number(dropPos[dropPos.length - 1]);
236
- HttpClient.post('admin/sysOrg/sort', {dropPosition, dropToGap, dropKey, dragKey}, null).then(() => this.loadTree())
236
+ HttpClient.post('admin/sys-org/sort', {dropPosition, dropToGap, dropKey, dragKey}, null).then(() => this.loadTree())
237
237
  };
238
238
  }
239
239
 
@@ -26,20 +26,20 @@ export default class RolePage extends React.Component {
26
26
  }
27
27
 
28
28
  handleEditUser = record => {
29
- HttpClient.get('admin/sysRole/user-list', {id: record.id}).then(rs => {
29
+ HttpClient.get('admin/sys-role/user-list', {id: record.id}).then(rs => {
30
30
  this.setState({pickerOpen: true, selectedRecord: record, targetKeys: rs.data?.selectedKeys || []})
31
31
  })
32
32
  }
33
33
 
34
34
  onFinish = async values => {
35
35
  const isNew = !values.id;
36
- const url = isNew ? 'admin/sysRole/create' : 'admin/sysRole/update';
36
+ const url = isNew ? 'admin/sys-role/create' : 'admin/sys-role/update';
37
37
  await HttpClient.post(url, values)
38
38
  this.tableRef.current.reload()
39
39
  }
40
40
 
41
41
  handleDelete = record => {
42
- HttpClient.post('admin/sysRole/delete', {id: record.id}, null).then(() => {
42
+ HttpClient.post('admin/sys-role/delete', {id: record.id}, null).then(() => {
43
43
  this.tableRef.current.reload()
44
44
  })
45
45
  }
@@ -109,7 +109,7 @@ export default class RolePage extends React.Component {
109
109
  size="small"
110
110
  actions={[
111
111
  {label: '用户设置', perm: 'sys-role:grant-permission', onClick: () => this.handleEditUser(record)},
112
- {label: '权限设置', perm: 'sys-role:grant-permission', onClick: () => PageUtils.open('/system/role/rolePerm?id=' + record.id, '角色权限设置')},
112
+ {label: '权限设置', perm: 'sys-role:grant-permission', onClick: () => PageUtils.open('/system/role/role-perm?id=' + record.id)},
113
113
  {label: '编辑', perm: 'sys-role:update', onClick: () => this.handleEdit(record)},
114
114
  {label: '删除', perm: 'sys-role:delete', confirm: '是否确定删除系统角色', onClick: () => this.handleDelete(record)},
115
115
  ]}
@@ -124,7 +124,7 @@ export default class RolePage extends React.Component {
124
124
  id: this.state.selectedRecord.id,
125
125
  userIdList
126
126
  }
127
- HttpClient.post('admin/sysRole/grant-users', params, null, {toastError: false}).then(() => {
127
+ HttpClient.post('admin/sys-role/grant-users', params, null, {toastError: false}).then(() => {
128
128
  this.setState({pickerOpen: false, usersModalLoading: false})
129
129
  }).catch(() => {
130
130
  this.setState({usersModalLoading: false})
@@ -141,7 +141,7 @@ export default class RolePage extends React.Component {
141
141
  {label: '新增', type: 'primary', icon: <PlusOutlined/>, perm: 'sys-role:create', onClick: this.handleAdd},
142
142
  ]}/>
143
143
  )}
144
- request={(params) => HttpClient.get('admin/sysRole/page', params)}
144
+ request={(params) => HttpClient.get('admin/sys-role/page', params)}
145
145
  columns={this.columns}
146
146
  searchInline
147
147
  searchFormRender={() => (
@@ -186,8 +186,8 @@ export default class RolePage extends React.Component {
186
186
  title={'为「' + (this.state.selectedRecord?.name || '') + '」角色分配用户'}
187
187
  value={this.state.targetKeys}
188
188
  confirmLoading={this.state.usersModalLoading}
189
- requestUrl='admin/sysRole/user-page'
190
- usersUrl='admin/sysRole/user-by-ids'
189
+ requestUrl='admin/sys-role/user-page'
190
+ usersUrl='admin/sys-role/user-by-ids'
191
191
  onOk={this.handleSaveUsers}
192
192
  onCancel={() => this.setState({pickerOpen: false})}
193
193
  />
@@ -2,6 +2,11 @@ import React from "react";
2
2
  import {Button, Card, Checkbox, Table, Typography} from "antd";
3
3
  import {SaveOutlined} from "@ant-design/icons";
4
4
  import {HttpClient, Page} from "../../../framework";
5
+
6
+ export const pageMeta = {
7
+ title: '角色权限设置',
8
+ };
9
+
5
10
  export default class RolePermPage extends React.Component {
6
11
 
7
12
 
@@ -62,15 +67,15 @@ export default class RolePermPage extends React.Component {
62
67
  this.setState({loading: false})
63
68
  }
64
69
  };
65
- HttpClient.get('admin/sysRole/get', {id: this.roleId}).then(rs => {
70
+ HttpClient.get('admin/sys-role/get', {id: this.roleId}).then(rs => {
66
71
  this.setState({roleInfo: rs.data})
67
72
  onFinish()
68
73
  });
69
- HttpClient.get('admin/sysRole/perm-tree-table', {id: this.roleId}).then(rs => {
74
+ HttpClient.get('admin/sys-role/perm-tree-table', {id: this.roleId}).then(rs => {
70
75
  this.setState({dataSource: rs.data})
71
76
  onFinish()
72
77
  });
73
- HttpClient.get('admin/sysRole/own-perms', {id: this.roleId}).then(rs => {
78
+ HttpClient.get('admin/sys-role/own-perms', {id: this.roleId}).then(rs => {
74
79
  this.setState({rowSelectedKeys: rs.data})
75
80
  onFinish()
76
81
  });
@@ -88,7 +93,7 @@ export default class RolePermPage extends React.Component {
88
93
  menus.push(menuId)
89
94
  perms.push(...ks)
90
95
  }
91
- HttpClient.post('admin/sysRole/save-perms', {id: this.roleId, perms, menus})
96
+ HttpClient.post('admin/sys-role/save-perms', {id: this.roleId, perms, menus})
92
97
  };
93
98
 
94
99
 
@@ -20,7 +20,7 @@ export default class UserPerm extends React.Component {
20
20
  show(item) {
21
21
  this.setState({visible: true})
22
22
 
23
- HttpClient.get('admin/sysUser/get-perm-info', {id: item.id}).then(rs => {
23
+ HttpClient.get('admin/sys-user/get-perm-info', {id: item.id}).then(rs => {
24
24
  this.setState({formValues: rs.data})
25
25
  this.formRef.current.setFieldsValue(rs.data)
26
26
  })
@@ -34,7 +34,7 @@ export default class UserPerm extends React.Component {
34
34
  })
35
35
 
36
36
 
37
- HttpClient.post('admin/sysUser/grant-perm', values, null, {toastError: false}).then(() => {
37
+ HttpClient.post('admin/sys-user/grant-perm', values, null, {toastError: false}).then(() => {
38
38
  this.setState({
39
39
  visible: false,
40
40
  confirmLoading: false
@@ -70,7 +70,7 @@ export default class UserPerm extends React.Component {
70
70
  >
71
71
  <Form.Item name='id' noStyle></Form.Item>
72
72
  <Form.Item label='角色' name='roleIds' rules={[{required: true}]}>
73
- <FieldRemoteSelect url='admin/sysRole/options' multiple />
73
+ <FieldRemoteSelect url='admin/sys-role/options' multiple />
74
74
  </Form.Item>
75
75
  <Form.Item label='数据权限' name='dataPermType' rules={[{required: true}]}>
76
76
  <FieldDictSelect typeCode='dataPermType'/>
@@ -14,7 +14,7 @@ import {
14
14
  ProTable,
15
15
  ViewText,
16
16
  } from "../../../framework";
17
- import UserPerm from "./userPerm";
17
+ import UserPerm from "./UserPermModal";
18
18
 
19
19
  export default class UserPage extends React.Component {
20
20
 
@@ -36,13 +36,13 @@ export default class UserPage extends React.Component {
36
36
  }
37
37
 
38
38
  onFinishResetPwd = async values => {
39
- await HttpClient.post('admin/sysUser/reset-pwd', {id: this.state.resetPwdUser.id, password: values.password})
39
+ await HttpClient.post('admin/sys-user/reset-pwd', {id: this.state.resetPwdUser.id, password: values.password})
40
40
  this.setState({resetPwdUser: null})
41
41
  this.tableRef.current?.reload()
42
42
  }
43
43
 
44
44
  handleDelete = r => {
45
- HttpClient.post('admin/sysUser/delete', {id: r.id}, null).then(() => {
45
+ HttpClient.post('admin/sys-user/delete', {id: r.id}, null).then(() => {
46
46
  this.tableRef.current.reload();
47
47
  })
48
48
  }
@@ -135,7 +135,7 @@ export default class UserPage extends React.Component {
135
135
 
136
136
  onFinish = async values => {
137
137
  const isNew = !values.id;
138
- const url = isNew ? 'admin/sysUser/create' : 'admin/sysUser/update';
138
+ const url = isNew ? 'admin/sys-user/create' : 'admin/sys-user/update';
139
139
  const result = await HttpClient.post(url, values);
140
140
  if (result?.data?.password) {
141
141
  this.setState({
@@ -170,7 +170,7 @@ export default class UserPage extends React.Component {
170
170
  )}
171
171
  request={(params) => {
172
172
  params.orgId = this.state.currentOrgId
173
- return HttpClient.get('admin/sysUser/page', params)
173
+ return HttpClient.get('admin/sys-user/page', params)
174
174
  }}
175
175
  columns={this.columns}
176
176
  searchFormRender={() => (
@@ -185,7 +185,7 @@ export default class UserPage extends React.Component {
185
185
  <Input/>
186
186
  </Form.Item>
187
187
  <Form.Item label='角色' name='roleId'>
188
- <FieldRemoteSelect url='admin/sysRole/options' placeholder='请选择角色'/>
188
+ <FieldRemoteSelect url='admin/sys-role/options' placeholder='请选择角色'/>
189
189
  </Form.Item>
190
190
  <Form.Item label='状态' name='enabled'>
191
191
  <Select allowClear placeholder='全部' options={[{value: true, label: '启用'}, {value: false, label: '停用'}]}/>
@@ -0,0 +1,76 @@
1
+ import React from "react";
2
+ import {Avatar, Tooltip} from "antd";
3
+ import {
4
+ AvatarUtils,
5
+ FALLBACK_AVATAR_PRESETS,
6
+ FieldUploadImage,
7
+ getToken,
8
+ HttpClient,
9
+ } from "../../framework";
10
+
11
+ const ITEM_SIZE = 44;
12
+
13
+ /**
14
+ * 头像选择:内置头像 + 自定义上传
15
+ * <p>
16
+ * 内置头像从后端 admin/public/avatar-presets 获取,移动端可复用同一接口;
17
+ * 上传入口以虚线圆形按钮排在列表末尾,选中/上传后直接在同一排展示结果。
18
+ */
19
+ export default class AvatarPicker extends React.Component {
20
+
21
+ state = {
22
+ presets: FALLBACK_AVATAR_PRESETS,
23
+ }
24
+
25
+ componentDidMount() {
26
+ HttpClient.get('admin/public/avatar-presets', null, {toastError: false}).then(rs => {
27
+ const presets = rs.data?.presets;
28
+ if (Array.isArray(presets) && presets.length > 0) {
29
+ this.setState({presets});
30
+ }
31
+ }).catch(() => {
32
+ // 接口不可用时保留前端兜底列表
33
+ })
34
+ }
35
+
36
+ render() {
37
+ const {value, onChange} = this.props;
38
+ const {presets} = this.state;
39
+ const uploaded = value && !AvatarUtils.isPreset(value) ? value : '';
40
+ const primary = getToken().colorPrimary;
41
+
42
+ return <div style={{display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 10}}>
43
+ {presets.map(preset => {
44
+ const selected = (value || '') === preset.key;
45
+ return <Tooltip key={preset.key} title={preset.label}>
46
+ <button type="button"
47
+ aria-label={preset.label}
48
+ aria-pressed={selected}
49
+ onClick={() => onChange?.(preset.key)}
50
+ style={{
51
+ width: ITEM_SIZE,
52
+ height: ITEM_SIZE,
53
+ padding: 0,
54
+ border: 'none',
55
+ borderRadius: '50%',
56
+ background: 'transparent',
57
+ cursor: 'pointer',
58
+ lineHeight: 0,
59
+ boxShadow: selected ? `0 0 0 2px ${primary}` : 'none',
60
+ transition: 'box-shadow .2s',
61
+ }}>
62
+ <Avatar size={ITEM_SIZE} src={preset.url}/>
63
+ </button>
64
+ </Tooltip>;
65
+ })}
66
+
67
+ <FieldUploadImage value={uploaded}
68
+ onChange={(v) => onChange?.(v || '')}
69
+ maxCount={1}
70
+ shape="circle"
71
+ size={ITEM_SIZE}
72
+ triggerTitle="上传自定义头像"
73
+ defaultCropRatio={{width: 1, height: 1}}/>
74
+ </div>;
75
+ }
76
+ }
@@ -1,6 +1,7 @@
1
1
  import React from "react";
2
- import {Empty, Table, Tag} from "antd";
3
- import {HttpClient, Gap} from "../../framework";
2
+ import {Empty, Table, Tabs, Tag} from "antd";
3
+ import {HttpClient} from "../../framework";
4
+ import RecentOps from "./RecentOps";
4
5
 
5
6
  export default class PermView extends React.Component {
6
7
 
@@ -9,17 +10,12 @@ export default class PermView extends React.Component {
9
10
  }
10
11
 
11
12
  componentDidMount() {
12
- HttpClient.get('admin/userCenter/perms', null).then(rs => {
13
+ HttpClient.get('admin/user-center/perms', null).then(rs => {
13
14
  this.setState({data: rs.data})
14
15
  })
15
16
  }
16
17
 
17
- render() {
18
- const {data} = this.state;
19
- if (!data) {
20
- return <Empty description='加载中...'/>;
21
- }
22
-
18
+ renderOrgPerm = (data) => {
23
19
  const orgColumns = [
24
20
  {
25
21
  title: '机构名称', dataIndex: 'title', key: 'title',
@@ -38,6 +34,20 @@ export default class PermView extends React.Component {
38
34
  },
39
35
  ];
40
36
 
37
+ return <div>
38
+ <div style={{marginBottom: 16}}>
39
+ <span>数据权限:
40
+ <Tag color='processing'>{data.dataPermLabel || '-'}</Tag>
41
+ </span>
42
+ </div>
43
+ {data.orgRows?.length
44
+ ? <Table size='small' rowKey='key' columns={orgColumns} dataSource={data.orgRows}
45
+ pagination={false} expandable={{defaultExpandAllRows: true}}/>
46
+ : <Empty description='暂无机构数据'/>}
47
+ </div>;
48
+ }
49
+
50
+ renderFuncPerm = (data) => {
41
51
  const menuColumns = [
42
52
  {
43
53
  title: '菜单名称', dataIndex: 'title', key: 'title',
@@ -60,21 +70,34 @@ export default class PermView extends React.Component {
60
70
  },
61
71
  ];
62
72
 
63
- return <div>
64
- <div style={{marginBottom: 16}}>
65
- <span>数据权限:
66
- <Tag color='processing'>{data.dataPermLabel || '-'}</Tag>
67
- </span>
68
- </div>
69
- {data.orgRows?.length
70
- ? <Table size='small' rowKey='key' columns={orgColumns} dataSource={data.orgRows}
71
- pagination={false} expandable={{defaultExpandAllRows: true}}/>
72
- : <Empty description='暂无机构数据'/>}
73
- <Gap/>
74
- {data.menuRows?.length
75
- ? <Table size='small' rowKey='key' columns={menuColumns} dataSource={data.menuRows}
76
- pagination={false} expandable={{defaultExpandAllRows: true}}/>
77
- : <Empty description='暂无权限数据'/>}
78
- </div>;
73
+ return data.menuRows?.length
74
+ ? <Table size='small' rowKey='key' columns={menuColumns} dataSource={data.menuRows}
75
+ pagination={false} expandable={{defaultExpandAllRows: true}}/>
76
+ : <Empty description='暂无权限数据'/>;
77
+ }
78
+
79
+ render() {
80
+ const {data} = this.state;
81
+ if (!data) {
82
+ return <Empty description='加载中...'/>;
83
+ }
84
+
85
+ return <Tabs items={[
86
+ {
87
+ key: 'dataPerm',
88
+ label: '数据权限',
89
+ children: this.renderOrgPerm(data),
90
+ },
91
+ {
92
+ key: 'funcPerm',
93
+ label: '功能权限',
94
+ children: this.renderFuncPerm(data),
95
+ },
96
+ {
97
+ key: 'recentOps',
98
+ label: '最近操作',
99
+ children: <RecentOps/>,
100
+ },
101
+ ]}/>;
79
102
  }
80
103
  }
@@ -0,0 +1,36 @@
1
+ import React from "react";
2
+ import {Tag} from "antd";
3
+ import {HttpClient, ProTable} from "../../framework";
4
+
5
+ const columns = [
6
+ {
7
+ title: '时间', dataIndex: 'operationTime', sorter: true, width: 180,
8
+ },
9
+ {
10
+ title: '操作', dataIndex: 'operation',
11
+ },
12
+ {
13
+ title: '结果', dataIndex: 'success', width: 90,
14
+ render: (success) => <Tag color={success ? 'green' : 'red'}>{success ? '成功' : '失败'}</Tag>
15
+ },
16
+ {
17
+ title: '耗时', dataIndex: 'duration', width: 100,
18
+ render: (duration) => duration == null ? '-' : `${duration} ms`
19
+ },
20
+ {
21
+ title: 'IP', dataIndex: 'ip', width: 150,
22
+ },
23
+ ];
24
+
25
+ /**
26
+ * 个人中心"最近操作":仅展示当前登录用户的操作记录(后端按登录用户过滤)。
27
+ */
28
+ export default function RecentOps() {
29
+ return <ProTable
30
+ rowKey='id'
31
+ columns={columns}
32
+ defaultPageSize={10}
33
+ emptyText='暂无操作记录(仅记录系统关键操作)'
34
+ request={(params) => HttpClient.get('admin/user-center/logs', params)}
35
+ />
36
+ }