11import { useState , useEffect , useRef } from 'react' ;
2- import { Button , Card , Form , Input , List , Modal , Popconfirm , Select , Tooltip , Space , Skeleton } from 'antd' ;
3- import { DeleteOutlined , FormOutlined , PlusOutlined , InfoCircleOutlined } from '@ant-design/icons' ;
2+ import { Button , Card , Form , Input , Modal , Select , Tooltip , Space , Skeleton , Avatar , Tag , Dropdown , MenuProps } from 'antd' ;
3+ import { DeleteOutlined , FormOutlined , PlusOutlined , InfoCircleOutlined , MoreOutlined , ApiOutlined , ThunderboltFilled } from '@ant-design/icons' ;
4+ import { ImSwitch } from 'react-icons/im' ;
45
56import Title from '@/components/Title' ;
67import useAssistant from '@/hooks/useAssistant' ;
@@ -47,6 +48,23 @@ const modelInfoMap: Record<string, { desc: string; label: string }> = {
4748 // 你可以继续添加更多模型
4849} ;
4950
51+ // 获取模型主题(颜色和图标)
52+ const getModelTheme = ( model : string ) : { color : string ; icon : string } => {
53+ const themeMap : Record < string , { color : string ; icon : string } > = {
54+ 'deepseek-chat' : { color : '#1890ff' , icon : 'DS' } ,
55+ 'deepseek-reasoner' : { color : '#722ed1' , icon : 'DR' } ,
56+ 'moonshot-v1-128k' : { color : '#13c2c2' , icon : 'M' } ,
57+ 'gpt-4o' : { color : '#52c41a' , icon : 'GPT4' } ,
58+ 'gpt-3.5-turbo' : { color : '#faad14' , icon : 'GPT3' } ,
59+ 'glm-4' : { color : '#eb2f96' , icon : 'GLM' } ,
60+ 'qwen-turbo' : { color : '#f5222d' , icon : 'QW' } ,
61+ 'ernie-bot' : { color : '#fa8c16' , icon : 'EB' } ,
62+ 'doubao-chat' : { color : '#2f54eb' , icon : 'DB' } ,
63+ } ;
64+
65+ return themeMap [ model ] || { color : '#8c8c8c' , icon : 'AI' } ;
66+ } ;
67+
5068export default ( ) => {
5169 const [ form ] = Form . useForm ( ) ;
5270 const [ isModalOpen , setIsModalOpen ] = useState ( false ) ;
@@ -107,90 +125,173 @@ export default () => {
107125 </ div >
108126 </ Card >
109127
110- { /* 列表卡片骨架屏 */ }
111- < Card className = "border-stroke" >
112- { [ 1 , 2 , 3 , 4 , 5 ] . map ( ( item ) => (
113- < div key = { item } className = "py-4 border-b border-gray-100 last:border-b-0" >
114- < div className = "flex items-center justify-between" >
115- < div className = "flex-1 space-x-4" >
116- < Skeleton . Input active size = "default" style = { { width : 200 , height : 24 , marginBottom : 8 } } />
117- < Skeleton . Input active size = "small" style = { { width : 150 , height : 24 } } />
128+ { /* 卡片网格骨架屏 */ }
129+ < div className = "grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-3 mt-3" >
130+ { [ 1 , 2 , 3 , 4 , 5 , 6 ] . map ( ( item ) => (
131+ < Card key = { item } className = "border-stroke" >
132+ < div className = "flex items-start justify-between mb-4" >
133+ < div className = "flex items-center gap-3" >
134+ < Skeleton . Avatar active size = { 48 } shape = "square" />
135+ < div >
136+ < Skeleton . Input active size = "small" style = { { width : 80 , height : 16 } } />
137+ </ div >
118138 </ div >
139+ < Skeleton . Button active size = "small" style = { { width : 32 , height : 32 } } />
140+ </ div >
141+ < div className = "bg-gray-100 rounded-md p-3 py-4 mb-4" >
119142
120- < div className = "flex space-x-2" >
121- < Skeleton . Button active size = "small" style = { { width : 80 , height : 28 } } />
122- < Skeleton . Button active size = "small" style = { { width : 28 , height : 28 } } />
123- < Skeleton . Button active size = "small" style = { { width : 28 , height : 28 } } />
124- < Skeleton . Button active size = "small" style = { { width : 80 , height : 28 } } />
125- </ div >
126143 </ div >
127- </ div >
144+ < div className = "pt-2 border-t border-gray-100" >
145+ < Skeleton . Button active size = "default" style = { { width : '100%' , height : 32 } } />
146+ </ div >
147+ </ Card >
128148 ) ) }
129- </ Card >
149+ </ div >
130150 </ div >
131151 ) ;
132152 }
133153
134154 return (
135155 < div >
136156 < Title value = "助手管理" >
137- < Button type = "primary" icon = { < PlusOutlined /> } onClick = { ( ) => setIsModalOpen ( true ) } >
157+ < Button type = "primary" onClick = { ( ) => setIsModalOpen ( true ) } >
138158 添加助手
139159 </ Button >
140160 </ Title >
141161
142- < Card className = "border-stroke" >
143- < List
144- dataSource = { list }
145- renderItem = { ( item ) => {
146- const info = modelInfoMap [ item . model ] ;
147- return (
148- < List . Item
149- actions = { [
150- < Button key = "test" type = "link" onClick = { ( ) => testConnection ( item ) } loading = { testingMap [ item . id ] } >
151- { testingMap [ item . id ] ? '测试中...' : '测试连接' }
152- </ Button > ,
153- < Button
154- key = "edit"
155- type = "text"
156- onClick = { ( ) => {
157- form . setFieldsValue ( item ) ;
158- setInputModelValue ( item . model ) ;
159- setAssistant ( item ) ;
160- setIsModalOpen ( true ) ;
161- } }
162- icon = { < FormOutlined className = "text-primary" /> }
163- /> ,
164- < Popconfirm key = "del" title = "您确定要删除这个助手吗?" onConfirm = { ( ) => delAssistantData ( + item . id ) } okText = "确定" cancelText = "取消" >
165- < Button type = "text" danger icon = { < DeleteOutlined /> } />
166- </ Popconfirm > ,
167- < Button key = "default" type = "text" onClick = { ( ) => setDefaultAssistant ( + item . id ) } >
168- { item . isDefault ? '默认助手' : '设为默认' }
169- </ Button > ,
170- ] }
171- >
172- < List . Item . Meta
173- title = {
174- < Space >
175- < span > { item . name } </ span >
176- </ Space >
177- }
178- description = {
179- < span >
180- 模型: { info ? info . label : item . model }
162+ { /* 卡片网格区域 */ }
163+ < div className = "grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-3 mt-3" >
164+ { list . map ( ( item ) => {
165+ const info = modelInfoMap [ item . model ] ;
166+ const theme = getModelTheme ( item . model ) ;
167+ const isTesting = testingMap [ item . id ] ;
168+ const isDefault = ! ! item . isDefault ;
169+
170+ // 下拉菜单项
171+ const menuItems : MenuProps [ 'items' ] = [
172+ {
173+ key : 'edit' ,
174+ label : '编辑配置' ,
175+ icon : < FormOutlined /> ,
176+ onClick : ( ) => {
177+ form . setFieldsValue ( item ) ;
178+ setInputModelValue ( item . model ) ;
179+ setAssistant ( item ) ;
180+ setIsModalOpen ( true ) ;
181+ }
182+ } ,
183+ {
184+ key : 'default' ,
185+ label : isDefault ? '已开启' : '开启助手' ,
186+ icon : < ImSwitch /> ,
187+ disabled : isDefault ,
188+ onClick : ( ) => setDefaultAssistant ( + item . id )
189+ } ,
190+ {
191+ type : 'divider' ,
192+ } ,
193+ {
194+ key : 'delete' ,
195+ label : '删除助手' ,
196+ danger : true ,
197+ icon : < DeleteOutlined /> ,
198+ onClick : ( ) => {
199+ Modal . confirm ( {
200+ title : '确认删除' ,
201+ content : `您确定要删除助手 "${ item . name } " 吗?此操作无法撤销。` ,
202+ okText : '删除' ,
203+ okType : 'danger' ,
204+ cancelText : '取消' ,
205+ onOk : ( ) => delAssistantData ( + item . id ) ,
206+ } ) ;
207+ }
208+ }
209+ ] ;
210+
211+ return (
212+ < Card
213+ key = { item . id }
214+ className = { `relative p-5 rounded-xl shadow-sm hover:shadow-md transition-all duration-300 overflow-hidden ${ item . isDefault
215+ ? 'border-2 border-blue-500 bg-gradient-to-br from-blue-50 via-white to-blue-50'
216+ : 'border border-gray-200 bg-gradient-to-br from-gray-50 via-white to-slate-50'
217+ } `}
218+ styles = { { body : { padding : '10px' , flex : 1 , display : 'flex' , flexDirection : 'column' } } }
219+ >
220+ { /* 卡片头部:图标与名称 */ }
221+ < div className = "flex items-start justify-between mb-4" >
222+ < div className = "flex items-center gap-3" >
223+ < Avatar
224+ shape = "square"
225+ size = { 48 }
226+ style = { { backgroundColor : theme . color , verticalAlign : 'middle' } }
227+ className = "shadow-md text-lg font-bold"
228+ >
229+ { theme . icon }
230+ </ Avatar >
231+
232+ < div >
233+ < div className = "font-bold text-lg text-gray-800 leading-tight mb-1 truncate max-w-[160px] ml-[5px]" >
234+ { item . name }
235+ </ div >
236+
237+ < Space size = { 4 } >
238+ < Tag bordered = { false } className = "text-xs bg-gray-100 text-gray-500 mr-0" >
239+ { info ? info . label : item . model }
240+ </ Tag >
241+
181242 { info && (
182243 < Tooltip title = { info . desc } >
183- < InfoCircleOutlined style = { { marginLeft : 8 } } />
244+ < InfoCircleOutlined className = "text-gray-400 cursor-pointer hover:text-primary" />
184245 </ Tooltip >
185246 ) }
186- </ span >
187- }
188- />
189- </ List . Item >
190- ) ;
191- } }
192- />
193- </ Card >
247+ </ Space >
248+ </ div >
249+ </ div >
250+
251+ { /* 更多操作菜单 */ }
252+ < Dropdown menu = { { items : menuItems } } placement = "bottomRight" arrow className = "bg-gray-50" >
253+ < Button type = "text" icon = { < MoreOutlined className = "text-xl text-gray-400" /> } />
254+ </ Dropdown >
255+ </ div >
256+
257+ { /* 卡片内容:URL显示 */ }
258+ < div className = "bg-gray-50 rounded-md px-3 py-2 mb-2 flex-1 border border-gray-100" >
259+ < div className = "flex items-center text-gray-400 text-xs uppercase font-bold mb-1" >
260+ < ApiOutlined className = "mr-1" /> API Endpoint
261+ </ div >
262+ < div
263+ className = "text-gray-600 font-mono text-sm m-0 break-all"
264+ >
265+ { item . url }
266+ </ div >
267+ </ div >
268+
269+ { /* 卡片底部:主要操作 */ }
270+ < div className = "mt-auto pt-2 border-t border-gray-100 flex justify-end" >
271+ < Button
272+ type = { isTesting ? 'default' : 'dashed' }
273+ className = { `${ isTesting ? '' : 'text-primary border-primary bg-blue-50/50' } w-full` }
274+ icon = { isTesting ? < ThunderboltFilled spin /> : < ThunderboltFilled /> }
275+ loading = { isTesting }
276+ onClick = { ( ) => testConnection ( item ) }
277+ >
278+ { isTesting ? '连接测试中...' : '测试连接' }
279+ </ Button >
280+ </ div >
281+ </ Card >
282+ ) ;
283+ } ) }
284+
285+ { /* 空状态下的添加按钮(如果没有数据或者作为最后一个Card) */ }
286+ < Button
287+ type = "dashed"
288+ className = "h-auto min-h-[200px] border-2 flex flex-col items-center justify-center gap-2 !bg-white text-gray-400 hover:text-primary hover:border-primary transition-colors rounded-lg bg-transparent"
289+ onClick = { ( ) => setIsModalOpen ( true ) }
290+ >
291+ < PlusOutlined style = { { fontSize : '24px' } } />
292+ < span className = "font-medium" > 添加新助手</ span >
293+ </ Button >
294+ </ div >
194295
195296 < Modal
196297 title = { assistant . id ? '编辑助手' : '添加助手' }
@@ -204,16 +305,27 @@ export default () => {
204305 } }
205306 >
206307 < Form form = { form } layout = "vertical" size = "large" >
207- < Form . Item name = "name" label = "助手名称 " rules = { [ { required : true , message : '请输入助手名称' } ] } >
308+ < Form . Item name = "name" label = "名称 " rules = { [ { required : true , message : '请输入助手名称' } ] } >
208309 < Input placeholder = "例如:DeepSeek、OpenAI 等" />
209310 </ Form . Item >
210311
211- < Form . Item name = "url" label = "API 地址" tooltip = "填写完整的 API 接口地址,如 https://api.deepseek.com/v1" rules = { [ { required : true , message : '请输入 API 地址' } ] } >
212- < Input placeholder = "https://api.deepseek.com/v1" />
312+ < Form . Item
313+ name = "url"
314+ label = "API 地址"
315+ tooltip = "填写完整的 API 接口地址,如 https://api.deepseek.com/v1"
316+ rules = { [
317+ { required : true , message : '请输入 API 地址' } ,
318+ {
319+ pattern : / ^ h t t p s ? : \/ \/ / ,
320+ message : '请输入正确的 API 地址'
321+ }
322+ ] }
323+ >
324+ < Input placeholder = "https://api.deepseek.com/v1" autoComplete = "off" />
213325 </ Form . Item >
214326
215327 < Form . Item name = "key" label = "API 密钥" rules = { [ { required : true , message : '请输入 API 密钥' } ] } >
216- < Input . Password placeholder = "请输入 API 密钥" />
328+ < Input . Password placeholder = "请输入 API 密钥" autoComplete = "new-password" />
217329 </ Form . Item >
218330
219331 < Form . Item name = "model" label = "模型" rules = { [ { required : true , message : '请选择或输入模型' } ] } >
0 commit comments