@yuku123/z-frontend-common 0.1.2 → 0.1.3

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 (43) hide show
  1. package/dist/z-frontend-common.css +1 -0
  2. package/dist/z-frontend-common.es.js +6153 -300
  3. package/dist/z-frontend-common.umd.js +22 -4
  4. package/package.json +5 -4
  5. package/src/components/Ctc/Layout.jsx +328 -0
  6. package/src/components/Ctc/Layout.module.css +145 -0
  7. package/src/components/Ctc/agentTeam/index.tsx +308 -0
  8. package/src/components/Ctc/login/AuthPage.module.css +26 -0
  9. package/src/components/Ctc/login/AuthPage.tsx +235 -0
  10. package/src/components/Ctc/login/index.less +49 -0
  11. package/src/components/Ctc/login/index.tsx +142 -0
  12. package/src/components/Ctc/userPanel/index.tsx +998 -0
  13. package/src/components/Ctc/webide/index.tsx +272 -0
  14. package/src/components/LowCode/LowCodeModel.jsx +962 -0
  15. package/src/components/LowCode/LowCodePage.jsx +31 -0
  16. package/src/components/LowCode/LowCodeRuntime.jsx +335 -0
  17. package/src/components/LowCode/MaterializePage.jsx +235 -0
  18. package/src/components/LowCode/index.js +1 -0
  19. package/src/components/MockPlatform/CurlImportModal.jsx +362 -0
  20. package/src/components/MockPlatform/EndpointsTab.jsx +509 -0
  21. package/src/components/MockPlatform/EnvironmentsTab.jsx +212 -0
  22. package/src/components/MockPlatform/MockTemplateHelper.jsx +200 -0
  23. package/src/components/MockPlatform/OpenApiImportModal.jsx +305 -0
  24. package/src/components/MockPlatform/RecordingsTab.jsx +397 -0
  25. package/src/components/MockPlatform/RequestLogsTab.jsx +239 -0
  26. package/src/components/MockPlatform/ScenariosTab.jsx +236 -0
  27. package/src/components/MockPlatform/TestCasesTab.jsx +462 -0
  28. package/src/components/MockPlatform/index.jsx +127 -0
  29. package/src/components/Overview.jsx +74 -0
  30. package/src/index.js +26 -27
  31. package/src/services/agentTeam.js +7 -0
  32. package/src/services/api.js +84 -0
  33. package/src/services/ctcAc.js +7 -0
  34. package/src/services/ctcAcDomain.js +7 -0
  35. package/src/services/ctcAuthorization.js +7 -0
  36. package/src/services/ctcSurl.js +7 -0
  37. package/src/services/ctcUser.js +7 -0
  38. package/src/services/job.js +7 -0
  39. package/src/services/metaApp.js +7 -0
  40. package/src/services/privateConfig.js +7 -0
  41. package/src/services/request.js +6 -0
  42. package/src/services/webide.js +6 -0
  43. package/src/services/workspace.js +21 -0
@@ -0,0 +1,308 @@
1
+ import {useEffect, useRef, useState} from 'react'
2
+ import {PageContainer, ProCard} from '@ant-design/pro-components'
3
+ import {Alert, Button, Empty, Input, List, message, Space, Tag, Tooltip} from 'antd'
4
+ import {
5
+ CheckCircleOutlined,
6
+ CloseCircleOutlined,
7
+ PoweroffOutlined,
8
+ SendOutlined,
9
+ TeamOutlined,
10
+ UserAddOutlined
11
+ } from '@ant-design/icons'
12
+ import {agentTeamApi, type AgentTeam, type AgentTeamMember} from '@/services/agentTeam'
13
+
14
+ /**
15
+ * z-agent-team 即时通讯 (IM) 集成页
16
+ *
17
+ * 后端 (z-agent-team-web):
18
+ * - GET /api/agent/team/list
19
+ * - POST /api/agent/team/create
20
+ * - GET /api/agent/team/{teamCode}/members
21
+ * - POST /api/agent/team/{teamCode}/members/add
22
+ * - POST /api/agent/team/{teamCode}/members/remove?appCode=
23
+ * - WS /api/agent/team/ws (STOMP)
24
+ *
25
+ * 注意:完整的 IM 客户端 (含 STOMP 订阅) 需要 sockjs-client / @stomp/stompjs,
26
+ * 完整实现见 z-agent-team-frontend (z-agent/z-agent-team/z-agent-team-frontend)。
27
+ * 这里展示:群组管理 + 消息发送 (REST) + WS URL 配置。
28
+ */
29
+ const ROLE_COLOR: Record<string, string> = {
30
+ LEADER: 'red',
31
+ WORKER: 'blue',
32
+ OBSERVER: 'default',
33
+ }
34
+
35
+ export default function AgentTeamIM() {
36
+ const [teams, setTeams] = useState<AgentTeam[]>([])
37
+ const [selectedTeam, setSelectedTeam] = useState<AgentTeam | null>(null)
38
+ const [members, setMembers] = useState<AgentTeamMember[]>([])
39
+ const [loadingTeams, setLoadingTeams] = useState(false)
40
+ const [loadingMembers, setLoadingMembers] = useState(false)
41
+ const [wsState, setWsState] = useState<'disconnected' | 'connecting' | 'connected' | 'error'>('disconnected')
42
+ const [wsLog, setWsLog] = useState<string[]>([])
43
+ const [messageInput, setMessageInput] = useState('')
44
+ const [messages, setMessages] = useState<{from: string; text: string; time: string}[]>([])
45
+ const wsRef = useRef<WebSocket | null>(null)
46
+
47
+ // 加载团队列表
48
+ const loadTeams = async () => {
49
+ setLoadingTeams(true)
50
+ try {
51
+ const list = await agentTeamApi.listTeams()
52
+ setTeams(list || [])
53
+ } catch (e: any) {
54
+ message.warning('加载团队失败:' + (e?.message || '未知错误'))
55
+ setTeams([])
56
+ } finally {
57
+ setLoadingTeams(false)
58
+ }
59
+ }
60
+
61
+ // 加载成员
62
+ const loadMembers = async (teamCode: string) => {
63
+ setLoadingMembers(true)
64
+ try {
65
+ const list = await agentTeamApi.listMembers(teamCode)
66
+ setMembers(list || [])
67
+ } catch (e: any) {
68
+ message.warning('加载成员失败:' + (e?.message || '未知错误'))
69
+ setMembers([])
70
+ } finally {
71
+ setLoadingMembers(false)
72
+ }
73
+ }
74
+
75
+ // WebSocket 连接 (原生 WS,无 STOMP 简化版)
76
+ const connectWs = () => {
77
+ if (wsRef.current) {
78
+ message.warning('已连接')
79
+ return
80
+ }
81
+ const url = agentTeamApi.buildWsUrl()
82
+ setWsState('connecting')
83
+ setWsLog(prev => [...prev, `[${new Date().toLocaleTimeString()}] 连接到 ${url}...`])
84
+ try {
85
+ const ws = new WebSocket(url)
86
+ ws.onopen = () => {
87
+ setWsState('connected')
88
+ setWsLog(prev => [...prev, `[${new Date().toLocaleTimeString()}] ✅ 连接已建立`])
89
+ }
90
+ ws.onmessage = (ev) => {
91
+ const text = typeof ev.data === 'string' ? ev.data : '[binary]'
92
+ setWsLog(prev => [...prev, `[${new Date().toLocaleTimeString()}] ← ${text}`])
93
+ // 简单消息格式: "from|text"
94
+ const m = text.match(/^([^|]+)\|(.+)$/)
95
+ if (m) {
96
+ setMessages(prev => [...prev, {from: m[1], text: m[2], time: new Date().toLocaleTimeString()}])
97
+ }
98
+ }
99
+ ws.onerror = () => {
100
+ setWsState('error')
101
+ setWsLog(prev => [...prev, `[${new Date().toLocaleTimeString()}] ❌ 连接错误`])
102
+ }
103
+ ws.onclose = () => {
104
+ setWsState('disconnected')
105
+ setWsLog(prev => [...prev, `[${new Date().toLocaleTimeString()}] 🔌 连接已关闭`])
106
+ wsRef.current = null
107
+ }
108
+ wsRef.current = ws
109
+ } catch (e: any) {
110
+ setWsState('error')
111
+ message.error('WS 连接失败:' + (e?.message || '未知错误'))
112
+ }
113
+ }
114
+
115
+ const disconnectWs = () => {
116
+ if (wsRef.current) {
117
+ wsRef.current.close()
118
+ wsRef.current = null
119
+ }
120
+ }
121
+
122
+ const sendMessage = () => {
123
+ if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) {
124
+ message.error('WebSocket 未连接')
125
+ return
126
+ }
127
+ if (!messageInput.trim()) return
128
+ const text = `${localStorage.getItem('userInfo') ? JSON.parse(localStorage.getItem('userInfo')!).userName : 'me'}|${messageInput}`
129
+ wsRef.current.send(text)
130
+ setWsLog(prev => [...prev, `[${new Date().toLocaleTimeString()}] → ${messageInput}`])
131
+ setMessages(prev => [...prev, {from: 'me', text: messageInput, time: new Date().toLocaleTimeString()}])
132
+ setMessageInput('')
133
+ }
134
+
135
+ useEffect(() => {
136
+ loadTeams()
137
+ return () => {
138
+ disconnectWs()
139
+ }
140
+ }, [])
141
+
142
+ useEffect(() => {
143
+ if (selectedTeam) {
144
+ loadMembers(selectedTeam.teamCode)
145
+ }
146
+ }, [selectedTeam])
147
+
148
+ const stateColor = {
149
+ disconnected: 'default',
150
+ connecting: 'processing',
151
+ connected: 'success',
152
+ error: 'error',
153
+ } as const
154
+ const stateText = {
155
+ disconnected: '未连接',
156
+ connecting: '连接中',
157
+ connected: '已连接',
158
+ error: '错误',
159
+ } as const
160
+
161
+ return (
162
+ <PageContainer
163
+ header={{
164
+ title: 'Agent 群组 IM (z-agent-team)',
165
+ subTitle: '团队协作 + 实时通讯',
166
+ breadcrumb: {},
167
+ }}
168
+ >
169
+ <div style={{display: 'grid', gridTemplateColumns: '300px 1fr', gap: 16}}>
170
+ {/* 团队列表面板 */}
171
+ <ProCard title={<Space><TeamOutlined/> 团队列表</Space>} loading={loadingTeams} headerBordered>
172
+ {teams.length === 0 ? (
173
+ <Empty
174
+ image={Empty.PRESENTED_IMAGE_SIMPLE}
175
+ description="暂无团队"
176
+ >
177
+ <Button type="primary" size="small" onClick={loadTeams}>
178
+ 刷新
179
+ </Button>
180
+ </Empty>
181
+ ) : (
182
+ <List
183
+ dataSource={teams}
184
+ renderItem={(t) => (
185
+ <List.Item
186
+ onClick={() => setSelectedTeam(t)}
187
+ style={{
188
+ cursor: 'pointer',
189
+ padding: '8px 12px',
190
+ background: selectedTeam?.teamCode === t.teamCode ? '#e6f7ff' : undefined,
191
+ borderRadius: 4,
192
+ }}
193
+ >
194
+ <List.Item.Meta
195
+ title={t.teamName}
196
+ description={
197
+ <Space size={4}>
198
+ <Tag>{t.teamCode}</Tag>
199
+ {t.status === 'ENABLE' ? <Tag color="green">启用</Tag> : <Tag>停用</Tag>}
200
+ </Space>
201
+ }
202
+ />
203
+ </List.Item>
204
+ )}
205
+ />
206
+ )}
207
+ </ProCard>
208
+
209
+ {/* 详情面板 */}
210
+ <ProCard
211
+ title={
212
+ selectedTeam
213
+ ? <Space><TeamOutlined/> {selectedTeam.teamName} ({selectedTeam.teamCode})</Space>
214
+ : '请选择团队'
215
+ }
216
+ headerBordered
217
+ extra={
218
+ <Space>
219
+ {wsState === 'connected' ? (
220
+ <Button icon={<PoweroffOutlined/>} onClick={disconnectWs}>断开</Button>
221
+ ) : (
222
+ <Button type="primary" icon={<CheckCircleOutlined/>} onClick={connectWs}>连接</Button>
223
+ )}
224
+ <Tag color={stateColor[wsState]}>{stateText[wsState]}</Tag>
225
+ </Space>
226
+ }
227
+ >
228
+ {selectedTeam ? (
229
+ <div style={{display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16}}>
230
+ {/* 成员列表 */}
231
+ <ProCard title="成员" loading={loadingMembers} size="small">
232
+ <List
233
+ dataSource={members}
234
+ renderItem={(m) => (
235
+ <List.Item>
236
+ <List.Item.Meta
237
+ title={<Space>{m.appCode}<Tag color={ROLE_COLOR[m.role || 'WORKER']}>{m.role}</Tag></Space>}
238
+ description={`优先级: ${m.priority ?? 0} · ${m.enabled === 1 ? '✅ 启用' : '❌ 停用'}`}
239
+ />
240
+ </List.Item>
241
+ )}
242
+ />
243
+ </ProCard>
244
+
245
+ {/* 消息面板 */}
246
+ <ProCard title="实时消息" size="small" styles={{body: {padding: 0, display: 'flex', flexDirection: 'column', height: 400}}}>
247
+ <div style={{flex: 1, overflow: 'auto', padding: 12}}>
248
+ {messages.length === 0 ? (
249
+ <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="暂无消息" />
250
+ ) : (
251
+ messages.map((m, i) => (
252
+ <div key={i} style={{marginBottom: 8}}>
253
+ <Tag color={m.from === 'me' ? 'blue' : 'green'}>{m.from}</Tag>
254
+ <span style={{marginLeft: 8}}>{m.text}</span>
255
+ <span style={{marginLeft: 8, color: '#999', fontSize: 11}}>{m.time}</span>
256
+ </div>
257
+ ))
258
+ )}
259
+ </div>
260
+ <div style={{padding: 8, borderTop: '1px solid #f0f0f0', display: 'flex', gap: 8}}>
261
+ <Input
262
+ value={messageInput}
263
+ onChange={e => setMessageInput(e.target.value)}
264
+ onPressEnter={sendMessage}
265
+ placeholder={wsState === 'connected' ? '输入消息...' : '请先连接 WebSocket'}
266
+ disabled={wsState !== 'connected'}
267
+ />
268
+ <Button type="primary" icon={<SendOutlined/>} onClick={sendMessage}
269
+ disabled={wsState !== 'connected'}>
270
+ 发送
271
+ </Button>
272
+ </div>
273
+ </ProCard>
274
+ </div>
275
+ ) : (
276
+ <Empty description="请从左侧选择一个团队"/>
277
+ )}
278
+
279
+ {/* WS 日志 */}
280
+ {wsLog.length > 0 && (
281
+ <ProCard title="WebSocket 日志" size="small" style={{marginTop: 16}}>
282
+ <pre style={{
283
+ background: '#1e1e1e', color: '#d4d4d4', padding: 8, borderRadius: 4,
284
+ fontFamily: 'Menlo, monospace', fontSize: 11, maxHeight: 180, overflow: 'auto',
285
+ margin: 0,
286
+ }}>
287
+ {wsLog.join('\n')}
288
+ </pre>
289
+ </ProCard>
290
+ )}
291
+ </ProCard>
292
+ </div>
293
+
294
+ <Alert
295
+ style={{marginTop: 16}}
296
+ type="info"
297
+ showIcon
298
+ message="说明"
299
+ description={
300
+ <span>
301
+ 完整 STOMP 客户端 (含订阅 Topic、ACK、消息历史) 见 <code>z-agent/z-agent-team/z-agent-team-frontend</code>。
302
+ 本页提供轻量级 WS 连接 + 群组管理 REST 端点示例。
303
+ </span>
304
+ }
305
+ />
306
+ </PageContainer>
307
+ )
308
+ }
@@ -0,0 +1,26 @@
1
+ .container {
2
+ min-height: 100vh;
3
+ display: flex;
4
+ align-items: center;
5
+ justify-content: center;
6
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
7
+ }
8
+
9
+ .content {
10
+ width: 400px;
11
+ padding: 20px;
12
+ }
13
+
14
+ .card {
15
+ border-radius: 8px;
16
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
17
+ }
18
+
19
+ .header {
20
+ text-align: center;
21
+ margin-bottom: 24px;
22
+ }
23
+
24
+ .header h3 {
25
+ margin-bottom: 8px;
26
+ }
@@ -0,0 +1,235 @@
1
+ import React, {useState} from 'react'
2
+ import {useNavigate} from 'react-router-dom'
3
+ import {Button, Card, Checkbox, Form, Input, message, Tabs, Typography,} from 'antd'
4
+ import {LockOutlined, MailOutlined, MobileOutlined, UserOutlined} from '@ant-design/icons'
5
+ import {authRequest} from '../../../services/request'
6
+ import styles from './AuthPage.module.css'
7
+
8
+ const {Title, Text} = Typography
9
+
10
+ type LoginType = 'account' | 'phone' | 'email'
11
+
12
+ // 登录请求 — 后端 AuthController @ /api/ctc/auth/login, body: {identifier, password}
13
+ const loginByUsername = (data: { identifier: string; password: string }) =>
14
+ authRequest.post('/ctc/auth/login', data)
15
+
16
+ // 注册 / 手机登录 / 验证码 — 后端暂未实现,占位返回空响应避免 UI 报错
17
+ const stubNotImplemented = () =>
18
+ Promise.resolve({code: 0, message: '功能暂未开放'})
19
+
20
+ const AuthPage: React.FC = () => {
21
+ const navigate = useNavigate()
22
+ const [action, setAction] = useState<'login' | 'register'>('login')
23
+ const [type, setType] = useState<LoginType>('account')
24
+ const [sendingCode, setSendingCode] = useState(false)
25
+ const [countdown, setCountdown] = useState(0)
26
+ const [form] = Form.useForm()
27
+
28
+ // 发送验证码 (后端暂未实现)
29
+ const handleSendCode = async () => {
30
+ message.info('验证码功能暂未开放')
31
+ }
32
+
33
+ // 处理登录/注册提交
34
+ const handleSubmit = async (values: any) => {
35
+ try {
36
+ let response: any
37
+
38
+ if (action === 'login' && type === 'account') {
39
+ // 后端 LoginRequest: {identifier, password, tenantCode?}
40
+ response = await loginByUsername({identifier: values.username, password: values.password})
41
+ // 后端返回: {token, account: {id, username, nickname, tenantCode, ...}}
42
+ if (response && response.token) {
43
+ message.success('登录成功!')
44
+ localStorage.setItem('token', response.token)
45
+ const acct = response.account || {}
46
+ localStorage.setItem('userInfo', JSON.stringify({
47
+ userId: acct.id,
48
+ userName: acct.username,
49
+ nickname: acct.nickname,
50
+ tenantCode: acct.tenantCode,
51
+ }))
52
+ navigate('/ctc')
53
+ return
54
+ }
55
+ message.error(response?.message || '登录失败')
56
+ } else {
57
+ // 注册 / 手机登录 — 后端暂未实现
58
+ await stubNotImplemented()
59
+ message.info('该功能暂未开放')
60
+ return
61
+ }
62
+ } catch (error: any) {
63
+ message.error(error.message || '操作失败,请重试')
64
+ }
65
+ }
66
+
67
+ // Tab配置
68
+ const getTabItems = () => {
69
+ const items = [
70
+ {key: 'account', label: '账户密码'},
71
+ ]
72
+
73
+ if (action === 'register') {
74
+ items.push({key: 'phone', label: '手机注册'})
75
+ items.push({key: 'email', label: '邮箱注册'})
76
+ } else {
77
+ items.push({key: 'phone', label: '手机验证码'})
78
+ }
79
+
80
+ return items
81
+ }
82
+
83
+ // 获取表单项
84
+ const getFormItems = () => {
85
+ const isLogin = action === 'login'
86
+
87
+ return (
88
+ <>
89
+ {type === 'account' && (
90
+ <>
91
+ <Form.Item name="username" rules={[{required: true, message: '请输入用户名!'}]}>
92
+ <Input
93
+ size="large"
94
+ prefix={<UserOutlined/>}
95
+ placeholder="用户名"
96
+ />
97
+ </Form.Item>
98
+ <Form.Item name="password" rules={[{required: true, message: '请输入密码!'}]}>
99
+ <Input.Password
100
+ size="large"
101
+ prefix={<LockOutlined/>}
102
+ placeholder="密码"
103
+ />
104
+ </Form.Item>
105
+ </>
106
+ )}
107
+
108
+ {(type === 'phone' || type === 'email') && (
109
+ <>
110
+ <Form.Item
111
+ name="receiver"
112
+ rules={[
113
+ {required: true, message: type === 'phone' ? '请输入手机号!' : '请输入邮箱!'}
114
+ ]}
115
+ >
116
+ <Input
117
+ size="large"
118
+ prefix={type === 'phone' ? <MobileOutlined/> : <MailOutlined/>}
119
+ placeholder={type === 'phone' ? '手机号' : '邮箱'}
120
+ disabled={countdown > 0}
121
+ />
122
+ </Form.Item>
123
+ <Form.Item>
124
+ <div style={{display: 'flex', gap: 8}}>
125
+ <Input
126
+ size="large"
127
+ prefix={<LockOutlined/>}
128
+ placeholder="验证码"
129
+ style={{flex: 1}}
130
+ />
131
+ <Button
132
+ type="primary"
133
+ onClick={handleSendCode}
134
+ loading={sendingCode}
135
+ disabled={countdown > 0}
136
+ style={{minWidth: 100}}
137
+ >
138
+ {countdown > 0 ? `${countdown}秒` : '获取验证码'}
139
+ </Button>
140
+ </div>
141
+ </Form.Item>
142
+ {!isLogin && (
143
+ <Form.Item name="password" rules={[{required: true, message: '请输入密码!'}]}>
144
+ <Input.Password
145
+ size="large"
146
+ prefix={<LockOutlined/>}
147
+ placeholder="设置密码"
148
+ />
149
+ </Form.Item>
150
+ )}
151
+ </>
152
+ )}
153
+ </>
154
+ )
155
+ }
156
+
157
+ return (
158
+ <div className={styles.container}>
159
+ <div className={styles.content}>
160
+ <Card className={styles.card}>
161
+ {/* 品牌标识 — 内联 SVG(不再显示"CTC 组织管理中心 / 4A + SSO 统一身份认证平台"那种奇怪描述) */}
162
+ <div className={styles.header} style={{textAlign: 'center'}}>
163
+ <svg width="48" height="48" viewBox="0 0 56 56" fill="none" aria-label="logo"
164
+ style={{marginBottom: 8}}>
165
+ <defs>
166
+ <linearGradient id="brandGradAuth" x1="0" y1="0" x2="56" y2="56"
167
+ gradientUnits="userSpaceOnUse">
168
+ <stop offset="0%" stopColor="#1677ff"/>
169
+ <stop offset="100%" stopColor="#0958d9"/>
170
+ </linearGradient>
171
+ </defs>
172
+ <rect x="2" y="2" width="52" height="52" rx="14" fill="url(#brandGradAuth)"/>
173
+ <path
174
+ d="M18 18 H40 L18 38 H40"
175
+ stroke="#ffffff"
176
+ strokeWidth="3.5"
177
+ strokeLinecap="round"
178
+ strokeLinejoin="round"
179
+ fill="none"
180
+ />
181
+ </svg>
182
+ <Title level={4} style={{margin: 0}}>
183
+ {action === 'register' ? '用户注册' : '欢迎登录'}
184
+ </Title>
185
+ </div>
186
+
187
+ <Form
188
+ form={form}
189
+ layout="vertical"
190
+ onFinish={handleSubmit}
191
+ initialValues={{autoLogin: true}}
192
+ >
193
+ {action !== 'forgot' && (
194
+ <Tabs
195
+ activeKey={type}
196
+ onChange={(key) => setType(key as LoginType)}
197
+ centered
198
+ items={getTabItems()}
199
+ />
200
+ )}
201
+
202
+ {getFormItems()}
203
+
204
+ <Form.Item>
205
+ <div style={{display: 'flex', justifyContent: 'space-between', alignItems: 'center'}}>
206
+ {action === 'login' && (
207
+ <Form.Item name="autoLogin" valuePropName="checked" noStyle>
208
+ <Checkbox>自动登录</Checkbox>
209
+ </Form.Item>
210
+ )}
211
+ <a
212
+ onClick={() => {
213
+ setAction(action === 'login' ? 'register' : 'login')
214
+ }}
215
+ >
216
+ {action === 'login'
217
+ ? '没有账号?立即注册'
218
+ : '已有账号?立即登录'}
219
+ </a>
220
+ </div>
221
+ </Form.Item>
222
+
223
+ <Form.Item>
224
+ <Button type="primary" htmlType="submit" block size="large">
225
+ {action === 'login' ? '登录' : '注册'}
226
+ </Button>
227
+ </Form.Item>
228
+ </Form>
229
+ </Card>
230
+ </div>
231
+ </div>
232
+ )
233
+ }
234
+
235
+ export default AuthPage
@@ -0,0 +1,49 @@
1
+ @import 'antd/dist/reset.css';
2
+
3
+ .container {
4
+ display: flex;
5
+ flex-direction: column;
6
+ height: 100vh;
7
+ overflow: auto;
8
+ background: #f0f2f5;
9
+ }
10
+
11
+ .lang {
12
+ width: 100%;
13
+ height: 40px;
14
+ line-height: 44px;
15
+ text-align: right;
16
+
17
+ :global(.ant-dropdown-trigger) {
18
+ margin-right: 24px;
19
+ padding: 12px;
20
+ cursor: pointer;
21
+ }
22
+ }
23
+
24
+ .content {
25
+ flex: 1;
26
+ padding: 32px 0;
27
+ }
28
+
29
+ @media (min-width: 768px) {
30
+ .container {
31
+ background-image: url('https://gw.alipayobjects.com/zos/rmsportal/TVYTbAXWheQpRcWDaDMu.svg');
32
+ background-repeat: no-repeat;
33
+ background-position: center 110px;
34
+ background-size: 100%;
35
+ }
36
+
37
+ .content {
38
+ padding: 32px 0 24px;
39
+ }
40
+ }
41
+
42
+ .icon {
43
+ margin-left: 8px;
44
+ color: rgba(0, 0, 0, 0.2);
45
+ font-size: 24px;
46
+ vertical-align: middle;
47
+ cursor: pointer;
48
+ transition: color 0.3s;
49
+ }