@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.
Files changed (60) hide show
  1. package/package.json +2 -5
  2. package/src/framework/biz/PermActions.tsx +1 -1
  3. package/src/framework/biz/UserPicker.less +17 -0
  4. package/src/framework/biz/UserPicker.tsx +296 -0
  5. package/src/framework/biz/index.tsx +2 -0
  6. package/src/framework/biz/pickerUser.ts +19 -0
  7. package/src/framework/components/ContextMenu/index.tsx +11 -8
  8. package/src/framework/components/DownloadModal/index.tsx +7 -7
  9. package/src/framework/components/ErrorBoundary/index.tsx +1 -1
  10. package/src/framework/components/FormModal/index.tsx +4 -4
  11. package/src/framework/components/NamedIcon/index.tsx +1 -1
  12. package/src/framework/components/OrgSwitcher/index.tsx +1 -1
  13. package/src/framework/components/Page/index.tsx +18 -4
  14. package/src/framework/components/PageLoading/index.tsx +2 -2
  15. package/src/framework/components/ProTable/index.less +70 -8
  16. package/src/framework/components/ProTable/index.tsx +63 -16
  17. package/src/framework/components/ThemeSettings/index.tsx +173 -0
  18. package/src/framework/components/index.ts +1 -0
  19. package/src/framework/config/index.ts +44 -2
  20. package/src/framework/config/layoutConfig.ts +52 -0
  21. package/src/framework/config/themeConfig.ts +449 -14
  22. package/src/framework/fields/BaseRemoteSelect/index.tsx +4 -4
  23. package/src/framework/fields/FieldDateRange/index.tsx +1 -1
  24. package/src/framework/fields/FieldDictSelect/index.tsx +1 -1
  25. package/src/framework/fields/FieldEditor/index.tsx +2 -2
  26. package/src/framework/fields/FieldNumberRange/index.tsx +1 -1
  27. package/src/framework/fields/FieldUploadFile/index.tsx +3 -3
  28. package/src/framework/fields/FieldUploadImage/index.tsx +19 -19
  29. package/src/framework/fields/FieldUserPicker/index.tsx +124 -0
  30. package/src/framework/fields/index.ts +1 -0
  31. package/src/framework/utils/HttpClient.ts +15 -4
  32. package/src/framework/utils/MessageApi.ts +16 -3
  33. package/src/framework/utils/UrlUtils.ts +1 -1
  34. package/src/framework/views/ViewText/index.tsx +1 -1
  35. package/src/layouts/admin/AdminTabs.less +11 -11
  36. package/src/layouts/admin/HeaderRight.jsx +23 -12
  37. package/src/layouts/admin/SinglePageLayout.jsx +36 -0
  38. package/src/layouts/admin/TabLayout.jsx +21 -1
  39. package/src/layouts/admin/index.jsx +46 -26
  40. package/src/layouts/admin/index.less +68 -24
  41. package/src/layouts/index.tsx +29 -4
  42. package/src/pages/article/$code.jsx +2 -2
  43. package/src/pages/article/article.less +13 -13
  44. package/src/pages/index.jsx +1 -1
  45. package/src/pages/public/login.jsx +29 -15
  46. package/src/pages/public/login.less +243 -22
  47. package/src/pages/standalone/forceUpdatePwd.jsx +8 -7
  48. package/src/pages/system/article/index.jsx +13 -15
  49. package/src/pages/system/codegen/index.jsx +54 -51
  50. package/src/pages/system/dict/index.jsx +2 -2
  51. package/src/pages/system/file/index.jsx +3 -3
  52. package/src/pages/system/job/index.jsx +57 -20
  53. package/src/pages/system/log/index.jsx +2 -2
  54. package/src/pages/system/org/index.jsx +5 -5
  55. package/src/pages/system/role/index.jsx +28 -55
  56. package/src/pages/system/role/rolePerm.jsx +1 -1
  57. package/src/pages/system/user/index.jsx +7 -11
  58. package/src/pages/system/user/userPerm.jsx +5 -5
  59. package/src/pages/userCenter/permView.jsx +2 -2
  60. package/src/style/global.less +6 -6
@@ -2,18 +2,17 @@ import React from 'react'
2
2
  import {
3
3
  Alert,
4
4
  AutoComplete,
5
- Button,
6
5
  Divider,
7
6
  Form,
8
7
  Input,
9
- message,
10
8
  Modal,
11
9
  Select,
10
+ Spin,
12
11
  Switch,
13
12
  Tag
14
13
  } from 'antd'
15
- import {PlusOutlined} from "@ant-design/icons";
16
- import {FormModal, HttpClient, Page, PermActions, ProTable, UrlUtils, ValueType} from "../../../framework";
14
+ import {PlusOutlined, SearchOutlined} from "@ant-design/icons";
15
+ import {FormModal, getMessageApi, HttpClient, Page, PermActions, ProTable, UrlUtils, ValueType} from "../../../framework";
17
16
 
18
17
 
19
18
  const cronOptions = [
@@ -53,6 +52,10 @@ export default class JobPage extends React.Component {
53
52
  status: null,
54
53
 
55
54
  executeRecordOpen: false,
55
+
56
+ logOpen: false,
57
+ logLoading: false,
58
+ logContent: '',
56
59
  }
57
60
  tableRef = React.createRef()
58
61
  modalRef = React.createRef()
@@ -91,7 +94,7 @@ export default class JobPage extends React.Component {
91
94
  }
92
95
 
93
96
  handleDelete = row => {
94
- const hide = message.loading("删除任务中...")
97
+ const hide = getMessageApi().loading("删除任务中...")
95
98
  HttpClient.post('admin/job/delete', {id: row.id}, null, {toastError: false}).then(() => {
96
99
  hide();
97
100
  this.tableRef.current.reload();
@@ -178,26 +181,40 @@ export default class JobPage extends React.Component {
178
181
  this.setState({executeRecordOpen: true, selectedRecord: record})
179
182
  }
180
183
 
184
+ showLog = (record) => {
185
+ this.setState({logOpen: true, logLoading: true, logContent: '', selectedLogRecord: record})
186
+ HttpClient.get('admin/sys/log/job/' + record.id, null, {toastError: false}).then(content => {
187
+ this.setState({
188
+ logContent: typeof content === 'string' ? content : JSON.stringify(content),
189
+ logLoading: false
190
+ })
191
+ }).catch(e => {
192
+ console.error('[Job] 加载任务日志失败:', e)
193
+ this.setState({logContent: '日志加载失败:' + (e?.message || ''), logLoading: false})
194
+ })
195
+ }
196
+
181
197
 
182
198
  render() {
183
199
  return <Page>
184
200
  <ProTable
185
201
  actionRef={this.tableRef}
186
- toolBarRender={() => ( // NOSONAR: AntD 渲染函数惯例
187
- <PermActions>
188
- <Button type='primary' perm='job:create' icon={<PlusOutlined/>} onClick={() => this.handleAdd()}>新增</Button>
189
- <Button perm='job:read' onClick={this.showStatus}>查看状态</Button>
190
- </PermActions>
202
+ searchInline
203
+ toolBarRightRender={() => (
204
+ <PermActions actions={[
205
+ {label: '新增', type: 'primary', icon: <PlusOutlined/>, perm: 'job:create', onClick: () => this.handleAdd()},
206
+ {label: '查看状态', perm: 'job:read', onClick: this.showStatus},
207
+ ]}/>
191
208
  )}
192
209
  request={(params) => HttpClient.get('admin/job/page', params)}
193
210
  columns={this.columns}
194
- searchFormRender={() => ( // NOSONAR: AntD 渲染函数惯例
211
+ searchFormRender={() => (
195
212
  <>
196
- <Form.Item label='名称' name='name'>
197
- <Input/>
213
+ <Form.Item name='name' noStyle>
214
+ <Input prefix={<SearchOutlined/>} placeholder='任务名称' allowClear/>
198
215
  </Form.Item>
199
- <Form.Item label='执行类' name='jobClass'>
200
- <Input/>
216
+ <Form.Item name='jobClass' noStyle>
217
+ <Input placeholder='执行类' allowClear/>
201
218
  </Form.Item>
202
219
  </>
203
220
  )}
@@ -291,10 +308,7 @@ export default class JobPage extends React.Component {
291
308
  {
292
309
  title: '操作',
293
310
  dataIndex: 'option',
294
- render: (_, record) => {
295
- const url = UrlUtils.contextPath('/admin/sys/log/job/' + record.id);
296
- return <a href={url} target='_blank'>日志</a>;
297
- },
311
+ render: (_, record) => <a onClick={() => this.showLog(record)}>日志</a>,
298
312
  }
299
313
  ]} request={(params) => {
300
314
  params.jobId = this.state.selectedRecord.id
@@ -302,6 +316,29 @@ export default class JobPage extends React.Component {
302
316
  }}></ProTable>
303
317
 
304
318
  </Modal>
319
+
320
+ <Modal title='任务日志'
321
+ open={this.state.logOpen}
322
+ onCancel={() => this.setState({logOpen: false})}
323
+ width={1024}
324
+ destroyOnHidden
325
+ footer={this.state.selectedLogRecord ? (
326
+ <a href={UrlUtils.contextPath('/admin/sys/log/job/' + this.state.selectedLogRecord.id)}
327
+ target='_blank' rel='noreferrer'>在新窗口打开</a>
328
+ ) : null}
329
+ >
330
+ <Spin spinning={this.state.logLoading}>
331
+ <pre style={{
332
+ margin: 0,
333
+ maxHeight: '60vh',
334
+ overflow: 'auto',
335
+ whiteSpace: 'pre-wrap',
336
+ wordBreak: 'break-all',
337
+ fontSize: 12,
338
+ lineHeight: 1.6
339
+ }}>{this.state.logContent}</pre>
340
+ </Spin>
341
+ </Modal>
305
342
  </Page>
306
343
  }
307
344
 
@@ -311,7 +348,7 @@ export default class JobPage extends React.Component {
311
348
  const option = this.state.jobClassOptions.find(o => o.value === changed.jobClass)
312
349
  if (option) {
313
350
  const {label} = option;
314
- const match = label.match(/[((](.*?)[))]/); // NOSONAR: 匹配中英文括号内内容,已最简
351
+ const match = label.match(/[((](.*?)[))]/);
315
352
  if (match) { // 取括号内中文描述设置为name
316
353
  this.modalRef.current.formInstance.setFieldValue("name", match[1])
317
354
  }
@@ -53,11 +53,11 @@ export default class LogPage extends React.Component {
53
53
 
54
54
 
55
55
  render() {
56
- return <Page title="操作日志" description="查看系统操作日志">
56
+ return <Page>
57
57
  <ProTable
58
58
  request={(params) => HttpClient.get('admin/sysLog/page', params)}
59
59
  columns={this.columns}
60
- searchFormRender={() => ( // NOSONAR: AntD 渲染函数惯例
60
+ searchFormRender={() => (
61
61
  <>
62
62
  <Form.Item label='操作' name='operation'>
63
63
  <Input/>
@@ -94,13 +94,13 @@ export default class OrgPage extends React.Component {
94
94
  const params = this.state.params
95
95
  const canSort = PermUtils.hasPermission('sys-org:update')
96
96
 
97
- return <Page title="组织机构" description="管理组织机构树" actions={
98
- <Button type='primary' perm='sys-org:create' icon={<PlusOutlined/>} onClick={this.handleAdd}>新增</Button>
99
- }>
97
+ return <Page>
100
98
  <Splitter>
101
99
  <Splitter.Panel defaultSize={400} style={{paddingRight: 8}}>
102
100
 
103
- <Card size='small' >
101
+
102
+ <Card title="机构树" extra={<Button type='primary' perm='sys-org:create'
103
+ icon={<PlusOutlined/>} onClick={this.handleAdd}>新增</Button>}>
104
104
  <div style={{display: 'flex', alignItems: 'center', gap: 4}}>
105
105
  <Input.Search placeholder='搜索' value={params.searchText} onChange={e => {
106
106
  params.searchText = e.target.value
@@ -140,7 +140,7 @@ export default class OrgPage extends React.Component {
140
140
  onSelect={this.onSelect}
141
141
  showIcon
142
142
  blockNode
143
- icon={item => <NamedIcon name={item.data.iconName}/>} // NOSONAR: AntD 渲染函数惯例
143
+ icon={item => <NamedIcon name={item.data.iconName}/>}
144
144
  draggable={this.state.draggable}
145
145
  onDrop={this.onDrop}
146
146
  showLine
@@ -1,19 +1,17 @@
1
- import {PlusOutlined} from '@ant-design/icons'
2
- import {Button, Form, Input, InputNumber, Modal, Transfer} from 'antd'
1
+ import {PlusOutlined, SearchOutlined} from '@ant-design/icons'
2
+ import {Form, Input, InputNumber} from 'antd'
3
3
  import React from 'react'
4
- import {FieldBoolean, FormModal, HttpClient, Page, PageUtils, PermActions, ProTable, ViewText} from "../../../framework";
4
+ import {FieldBoolean, FormModal, HttpClient, Page, PageUtils, PermActions, ProTable, UserPicker, ViewText} from "../../../framework";
5
5
 
6
6
 
7
7
  export default class RolePage extends React.Component {
8
8
 
9
9
  state = {
10
- usersModalOpen: false,
10
+ pickerOpen: false,
11
11
  usersModalLoading: false,
12
12
  selectedRecord: null,
13
13
 
14
- userList: [],
15
14
  targetKeys: [],
16
- selectedKeys: [],
17
15
  }
18
16
 
19
17
  modalRef = React.createRef()
@@ -28,9 +26,8 @@ export default class RolePage extends React.Component {
28
26
  }
29
27
 
30
28
  handleEditUser = record => {
31
- this.setState({usersModalOpen: true, selectedRecord: record})
32
29
  HttpClient.get('admin/sysRole/user-list', {id: record.id}).then(rs => {
33
- this.setState({userList: rs.data.list, targetKeys: rs.data.selectedKeys})
30
+ this.setState({pickerOpen: true, selectedRecord: record, targetKeys: rs.data?.selectedKeys || []})
34
31
  })
35
32
  }
36
33
 
@@ -121,14 +118,14 @@ export default class RolePage extends React.Component {
121
118
  },
122
119
  ]
123
120
 
124
- handleSaveUsers = () => {
121
+ handleSaveUsers = userIdList => {
125
122
  this.setState({usersModalLoading: true})
126
123
  const params = {
127
124
  id: this.state.selectedRecord.id,
128
- userIdList: this.state.targetKeys
125
+ userIdList
129
126
  }
130
127
  HttpClient.post('admin/sysRole/grant-users', params, null, {toastError: false}).then(() => {
131
- this.setState({usersModalOpen: false, usersModalLoading: false})
128
+ this.setState({pickerOpen: false, usersModalLoading: false})
132
129
  }).catch(() => {
133
130
  this.setState({usersModalLoading: false})
134
131
  })
@@ -136,25 +133,24 @@ export default class RolePage extends React.Component {
136
133
 
137
134
  render() {
138
135
  return <Page
139
- title="角色管理"
140
- description="管理系统角色,包括角色权限分配、用户设置等"
141
136
  >
142
137
  <ProTable
143
138
  actionRef={this.tableRef}
144
- toolBarRender={() => ( // NOSONAR: AntD 渲染函数惯例
145
- <PermActions>
146
- <Button perm='sys-role:create' type='primary' icon={<PlusOutlined/>} onClick={this.handleAdd}>新增</Button>
147
- </PermActions>
139
+ toolBarRightRender={() => (
140
+ <PermActions actions={[
141
+ {label: '新增', type: 'primary', icon: <PlusOutlined/>, perm: 'sys-role:create', onClick: this.handleAdd},
142
+ ]}/>
148
143
  )}
149
144
  request={(params) => HttpClient.get('admin/sysRole/page', params)}
150
145
  columns={this.columns}
151
- searchFormRender={() => ( // NOSONAR: AntD 渲染函数惯例
146
+ searchInline
147
+ searchFormRender={() => (
152
148
  <>
153
- <Form.Item label='角色名称' name='name'>
154
- <Input/>
149
+ <Form.Item name='name' noStyle>
150
+ <Input prefix={<SearchOutlined/>} placeholder='角色名称' allowClear/>
155
151
  </Form.Item>
156
- <Form.Item label='角色编码' name='code'>
157
- <Input/>
152
+ <Form.Item name='code' noStyle>
153
+ <Input placeholder='角色编码' allowClear/>
158
154
  </Form.Item>
159
155
  </>
160
156
  )}
@@ -185,39 +181,16 @@ export default class RolePage extends React.Component {
185
181
  </FormModal>
186
182
 
187
183
 
188
- <Modal title={'角色用户' + "【" + this.state.selectedRecord?.name + '】'}
189
- open={this.state.usersModalOpen}
190
- confirmLoading={this.state.usersModalLoading}
191
- destroyOnHidden
192
- mask={{ closable: false }}
193
- width={800}
194
- onCancel={() => this.setState({usersModalOpen: false})}
195
- onOk={this.handleSaveUsers}
196
- >
197
-
198
-
199
- <Transfer
200
- styles={{ section: { height: '60vh', width: 300 } }}
201
-
202
- dataSource={this.state.userList} titles={["未选择", "已选择"]}
203
- targetKeys={this.state.targetKeys}
204
- selectedKeys={this.state.selectedKeys}
205
- render={item => item.title}
206
- onChange={(nextTargetKeys, _direction, _moveKeys) => {
207
- this.setState({
208
- targetKeys: nextTargetKeys
209
- })
210
- }}
211
- onSelectChange={(sourceSelectedKeys, targetSelectedKeys) => {
212
- this.setState({
213
- selectedKeys: [...sourceSelectedKeys, ...targetSelectedKeys]
214
- })
215
- }}
216
- showSearch
217
- />
218
-
219
-
220
- </Modal>
184
+ <UserPicker
185
+ open={this.state.pickerOpen}
186
+ title={'为「' + (this.state.selectedRecord?.name || '') + '」角色分配用户'}
187
+ value={this.state.targetKeys}
188
+ confirmLoading={this.state.usersModalLoading}
189
+ requestUrl='admin/sysRole/user-page'
190
+ usersUrl='admin/sysRole/user-by-ids'
191
+ onOk={this.handleSaveUsers}
192
+ onCancel={() => this.setState({pickerOpen: false})}
193
+ />
221
194
  </Page>
222
195
 
223
196
 
@@ -93,7 +93,7 @@ export default class RolePermPage extends React.Component {
93
93
 
94
94
 
95
95
  render() {
96
- return <Page title="角色权限设置" description="设置角色的菜单和权限">
96
+ return <Page>
97
97
  <Card title='角色权限设置' loading={this.state.loading}
98
98
  variant={"borderless"}
99
99
  extra={<Button type='primary' icon={<SaveOutlined/>} onClick={this.savePerms}>保存权限</Button>}>
@@ -80,10 +80,6 @@ export default class UserPage extends React.Component {
80
80
  },
81
81
 
82
82
 
83
- {
84
- title: '手机',
85
- dataIndex: 'phone'
86
- },
87
83
  {
88
84
  title: '邮箱',
89
85
  dataIndex: 'email'
@@ -156,9 +152,9 @@ export default class UserPage extends React.Component {
156
152
 
157
153
  render() {
158
154
 
159
- return <Page title="用户管理" description="管理系统用户">
155
+ return <Page >
160
156
  <Splitter>
161
- <Splitter.Panel defaultSize={240} style={{paddingRight: 8}}>
157
+ <Splitter.Panel defaultSize={340} style={{paddingRight: 8}}>
162
158
  <Card size='small'>
163
159
  <OrgTree onChange={this.onSelectOrg}/>
164
160
  </Card>
@@ -167,17 +163,17 @@ export default class UserPage extends React.Component {
167
163
  <ProTable
168
164
  searchFormCols={3}
169
165
  actionRef={this.tableRef}
170
- toolBarRender={() => ( // NOSONAR: AntD 渲染函数惯例
171
- <PermActions>
172
- <Button perm='sys-user:create' type='primary' icon={<PlusOutlined/>} onClick={this.handleAdd}>新增</Button>
173
- </PermActions>
166
+ toolBarRightRender={() => (
167
+ <PermActions actions={[
168
+ {label: '新增', type: 'primary', icon: <PlusOutlined/>, perm: 'sys-user:create', onClick: this.handleAdd},
169
+ ]}/>
174
170
  )}
175
171
  request={(params) => {
176
172
  params.orgId = this.state.currentOrgId
177
173
  return HttpClient.get('admin/sysUser/page', params)
178
174
  }}
179
175
  columns={this.columns}
180
- searchFormRender={() => ( // NOSONAR: AntD 渲染函数惯例
176
+ searchFormRender={() => (
181
177
  <>
182
178
  <Form.Item label='姓名' name='name'>
183
179
  <Input/>
@@ -1,6 +1,6 @@
1
- import {Form, Modal, message} from 'antd';
1
+ import {Form, Modal} from 'antd';
2
2
  import React from 'react';
3
- import {FieldDictSelect, FieldRemoteSelect, FieldSysOrgTree, HttpClient} from "../../../framework";
3
+ import {FieldDictSelect, FieldRemoteSelect, FieldSysOrgTree, getMessageApi, HttpClient} from "../../../framework";
4
4
 
5
5
 
6
6
  export default class UserPerm extends React.Component {
@@ -15,9 +15,9 @@ export default class UserPerm extends React.Component {
15
15
  dataPermType: null
16
16
  },
17
17
  }
18
- formRef = React.createRef() // NOSONAR: React ref,内部通过 this.formRef 使用
18
+ formRef = React.createRef()
19
19
 
20
- show(item) { // NOSONAR: ref 暴露给父组件调用的公共 API
20
+ show(item) {
21
21
  this.setState({visible: true})
22
22
 
23
23
  HttpClient.get('admin/sysUser/get-perm-info', {id: item.id}).then(rs => {
@@ -41,7 +41,7 @@ export default class UserPerm extends React.Component {
41
41
  })
42
42
  this.props.onOk()
43
43
  }).catch(e => {
44
- message.error(HttpClient.errToMsg(e))
44
+ getMessageApi().error(HttpClient.errToMsg(e))
45
45
  this.setState({
46
46
  confirmLoading: false
47
47
  })
@@ -26,7 +26,7 @@ export default class PermView extends React.Component {
26
26
  },
27
27
  {
28
28
  title: '状态', dataIndex: 'status', key: 'status', width: 140,
29
- render: (status) => { // NOSONAR: 返回元素或占位文本,类型不同属预期
29
+ render: (status) => {
30
30
  if (status === 'mine') {
31
31
  return <Tag color='blue'>我的机构</Tag>;
32
32
  }
@@ -48,7 +48,7 @@ export default class PermView extends React.Component {
48
48
  },
49
49
  {
50
50
  title: '状态', dataIndex: 'status', key: 'status', width: 140,
51
- render: (status) => { // NOSONAR: 返回元素或占位文本,类型不同属预期
51
+ render: (status) => {
52
52
  if (status === 'all') {
53
53
  return <Tag color='green'>已授权</Tag>;
54
54
  }
@@ -6,29 +6,29 @@ table.oa-table {
6
6
  table-layout: fixed;
7
7
 
8
8
  th {
9
- background-color: #f2f2f2; /* 背景色 */
10
- color: #333; /* 文字颜色 */
9
+ background-color: var(--oa-color-fill, #fafafa); /* 背景色(跟随明暗主题) */
10
+ color: var(--oa-color-text, #333); /* 文字颜色 */
11
11
  font-weight: bold; /* 加粗 */
12
12
  padding: 12px 15px; /* 内边距 */
13
13
  text-align: left; /* 文字对齐 */
14
- border-bottom: 2px solid #ddd; /* 底部边框 */
14
+ border-bottom: 2px solid var(--oa-color-border, #ddd); /* 底部边框 */
15
15
  }
16
16
 
17
17
  td {
18
18
  padding: 10px 15px; /* 内边距 */
19
- border-bottom: 1px solid #ddd; /* 底部边框 */
19
+ border-bottom: 1px solid var(--oa-color-border, #ddd); /* 底部边框 */
20
20
  text-align: left; /* 文字对齐 */
21
21
  }
22
22
 
23
23
  /* 表格行悬停效果 */
24
24
 
25
25
  tr:hover {
26
- background-color: #f5f5f5; /* 悬停背景色 */
26
+ background-color: var(--oa-color-fill, #f5f5f5); /* 悬停背景色 */
27
27
  }
28
28
 
29
29
  /* 斑马纹效果 - 交替行颜色 */
30
30
  tr:nth-child(even) {
31
- background-color: #f9f9f9;
31
+ background-color: var(--oa-color-bg-layout, #f9f9f9);
32
32
  }
33
33
 
34
34
  /* 响应式表格 - 在小屏幕上添加水平滚动 */