@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.
- package/dist/z-frontend-common.css +1 -0
- package/dist/z-frontend-common.es.js +6153 -300
- package/dist/z-frontend-common.umd.js +22 -4
- package/package.json +5 -4
- package/src/components/Ctc/Layout.jsx +328 -0
- package/src/components/Ctc/Layout.module.css +145 -0
- package/src/components/Ctc/agentTeam/index.tsx +308 -0
- package/src/components/Ctc/login/AuthPage.module.css +26 -0
- package/src/components/Ctc/login/AuthPage.tsx +235 -0
- package/src/components/Ctc/login/index.less +49 -0
- package/src/components/Ctc/login/index.tsx +142 -0
- package/src/components/Ctc/userPanel/index.tsx +998 -0
- package/src/components/Ctc/webide/index.tsx +272 -0
- package/src/components/LowCode/LowCodeModel.jsx +962 -0
- package/src/components/LowCode/LowCodePage.jsx +31 -0
- package/src/components/LowCode/LowCodeRuntime.jsx +335 -0
- package/src/components/LowCode/MaterializePage.jsx +235 -0
- package/src/components/LowCode/index.js +1 -0
- package/src/components/MockPlatform/CurlImportModal.jsx +362 -0
- package/src/components/MockPlatform/EndpointsTab.jsx +509 -0
- package/src/components/MockPlatform/EnvironmentsTab.jsx +212 -0
- package/src/components/MockPlatform/MockTemplateHelper.jsx +200 -0
- package/src/components/MockPlatform/OpenApiImportModal.jsx +305 -0
- package/src/components/MockPlatform/RecordingsTab.jsx +397 -0
- package/src/components/MockPlatform/RequestLogsTab.jsx +239 -0
- package/src/components/MockPlatform/ScenariosTab.jsx +236 -0
- package/src/components/MockPlatform/TestCasesTab.jsx +462 -0
- package/src/components/MockPlatform/index.jsx +127 -0
- package/src/components/Overview.jsx +74 -0
- package/src/index.js +26 -27
- package/src/services/agentTeam.js +7 -0
- package/src/services/api.js +84 -0
- package/src/services/ctcAc.js +7 -0
- package/src/services/ctcAcDomain.js +7 -0
- package/src/services/ctcAuthorization.js +7 -0
- package/src/services/ctcSurl.js +7 -0
- package/src/services/ctcUser.js +7 -0
- package/src/services/job.js +7 -0
- package/src/services/metaApp.js +7 -0
- package/src/services/privateConfig.js +7 -0
- package/src/services/request.js +6 -0
- package/src/services/webide.js +6 -0
- 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
|
+
}
|