1+ "use client" ;
2+
3+ import {
4+ Card ,
5+ CardHeader ,
6+ CardBody ,
7+ Badge ,
8+ Button ,
9+ Chip ,
10+ Divider ,
11+ Dropdown ,
12+ DropdownTrigger ,
13+ DropdownMenu ,
14+ DropdownItem ,
15+ Tooltip ,
16+ } from "@heroui/react" ;
17+ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" ;
18+ import {
19+ faServer ,
20+ faDesktop ,
21+ faCog ,
22+ faEdit ,
23+ faTrash ,
24+ faEye ,
25+ faArrowRight ,
26+ faPlus ,
27+ } from "@fortawesome/free-solid-svg-icons" ;
28+ import React from 'react' ;
29+ import { Flex , Box } from "@/components" ;
30+ import { TunnelGroup } from "@/lib/types" ;
31+
32+ interface Tunnel {
33+ id : string ;
34+ name : string ;
35+ type : string ;
36+ tunnelAddress : string ;
37+ tunnelPort : string ;
38+ targetAddress : string ;
39+ targetPort : string ;
40+ status : {
41+ type : "success" | "danger" | "warning" ;
42+ text : string ;
43+ } ;
44+ endpoint : string ;
45+ }
46+
47+ interface GroupCardProps {
48+ group : TunnelGroup ;
49+ tunnels : Tunnel [ ] ;
50+ onEdit ?: ( group : TunnelGroup ) => void ;
51+ onDelete ?: ( group : TunnelGroup ) => void ;
52+ onAddTunnels ?: ( group : TunnelGroup ) => void ;
53+ onViewDetails ?: ( group : TunnelGroup ) => void ;
54+ }
55+
56+ export const GroupCard : React . FC < GroupCardProps > = ( {
57+ group,
58+ tunnels,
59+ onEdit,
60+ onDelete,
61+ onAddTunnels,
62+ onViewDetails,
63+ } ) => {
64+ // 获取分组中的隧道
65+ const groupTunnels = tunnels . filter ( tunnel =>
66+ group . tunnelIds . includes ( tunnel . id )
67+ ) ;
68+
69+ // 获取隧道类型标签
70+ const getTunnelTypeChip = ( type : string ) => {
71+ return (
72+ < Chip
73+ size = "sm"
74+ variant = "flat"
75+ color = { type === '服务端' ? 'primary' : 'secondary' }
76+ >
77+ { type }
78+ </ Chip >
79+ ) ;
80+ } ;
81+
82+ // 获取状态颜色
83+ const getStatusDot = ( status : { type : string } ) => {
84+ const colorMap = {
85+ success : 'bg-success' ,
86+ danger : 'bg-danger' ,
87+ warning : 'bg-warning' ,
88+ } ;
89+ return colorMap [ status . type as keyof typeof colorMap ] || 'bg-default' ;
90+ } ;
91+
92+ // 统计不同类型的隧道数量
93+ const tunnelStats = groupTunnels . reduce ( ( acc , tunnel ) => {
94+ const type = tunnel . type ;
95+ acc [ type ] = ( acc [ type ] || 0 ) + 1 ;
96+ return acc ;
97+ } , { } as Record < string , number > ) ;
98+
99+ // 统计运行中的隧道数量
100+ const runningCount = groupTunnels . filter ( t => t . status . type === 'success' ) . length ;
101+
102+ return (
103+ < Card className = "hover:shadow-lg transition-all duration-200 border-l-4"
104+ style = { { borderLeftColor : group . color } } >
105+ < CardHeader >
106+ < Flex className = "items-center justify-between w-full" >
107+ < Flex className = "items-center gap-3" >
108+ < div
109+ className = "w-4 h-4 rounded-full shadow-sm"
110+ style = { { backgroundColor : group . color } }
111+ />
112+ < Box >
113+ < h3 className = "font-semibold text-lg" > { group . name } </ h3 >
114+ { group . description && (
115+ < p className = "text-sm text-default-500 mt-1" > { group . description } </ p >
116+ ) }
117+ </ Box >
118+ </ Flex >
119+
120+ < Dropdown >
121+ < DropdownTrigger >
122+ < Button
123+ isIconOnly
124+ size = "sm"
125+ variant = "light"
126+ className = "text-default-400 hover:text-default-600"
127+ >
128+ < FontAwesomeIcon icon = { faCog } />
129+ </ Button >
130+ </ DropdownTrigger >
131+ < DropdownMenu >
132+ < DropdownItem
133+ key = "view"
134+ startContent = { < FontAwesomeIcon icon = { faEye } /> }
135+ onPress = { ( ) => onViewDetails ?.( group ) }
136+ >
137+ 查看详情
138+ </ DropdownItem >
139+ < DropdownItem
140+ key = "add"
141+ startContent = { < FontAwesomeIcon icon = { faPlus } /> }
142+ onPress = { ( ) => onAddTunnels ?.( group ) }
143+ >
144+ 添加隧道
145+ </ DropdownItem >
146+ < DropdownItem
147+ key = "edit"
148+ startContent = { < FontAwesomeIcon icon = { faEdit } /> }
149+ onPress = { ( ) => onEdit ?.( group ) }
150+ >
151+ 编辑分组
152+ </ DropdownItem >
153+ < DropdownItem
154+ key = "delete"
155+ className = "text-danger"
156+ color = "danger"
157+ startContent = { < FontAwesomeIcon icon = { faTrash } /> }
158+ onPress = { ( ) => onDelete ?.( group ) }
159+ >
160+ 删除分组
161+ </ DropdownItem >
162+ </ DropdownMenu >
163+ </ Dropdown >
164+ </ Flex >
165+ </ CardHeader >
166+
167+ < Divider />
168+
169+ < CardBody >
170+ < Box className = "space-y-4" >
171+ { /* 统计信息 */ }
172+ < Flex className = "items-center gap-4" >
173+ < Badge content = { group . tunnelIds . length } color = "primary" >
174+ < FontAwesomeIcon icon = { faServer } className = "text-default-400" />
175+ </ Badge >
176+ < span className = "text-sm text-default-500" >
177+ { group . tunnelIds . length } 个隧道
178+ </ span >
179+
180+ { runningCount > 0 && (
181+ < >
182+ < div className = "w-1 h-1 bg-default-300 rounded-full" />
183+ < span className = "text-sm text-success" >
184+ { runningCount } 个运行中
185+ </ span >
186+ </ >
187+ ) }
188+ </ Flex >
189+
190+ { /* 隧道类型统计 */ }
191+ { Object . keys ( tunnelStats ) . length > 0 && (
192+ < Flex className = "gap-2 flex-wrap" >
193+ { Object . entries ( tunnelStats ) . map ( ( [ type , count ] ) => (
194+ < Chip key = { type } size = "sm" variant = "flat" color = "default" >
195+ { type } : { count }
196+ </ Chip >
197+ ) ) }
198+ </ Flex >
199+ ) }
200+
201+ { /* 隧道列表预览 */ }
202+ { groupTunnels . length > 0 ? (
203+ < Box className = "space-y-2" >
204+ < h4 className = "text-sm font-medium text-default-700" > 隧道列表</ h4 >
205+ < Box className = "space-y-2 max-h-40 overflow-y-auto" >
206+ { groupTunnels . slice ( 0 , 5 ) . map ( tunnel => (
207+ < Flex
208+ key = { tunnel . id }
209+ className = "items-center gap-3 p-2 bg-default-50 rounded-lg hover:bg-default-100 transition-colors"
210+ >
211+ < div className = { `w-2 h-2 rounded-full ${ getStatusDot ( tunnel . status ) } ` } />
212+ < FontAwesomeIcon
213+ icon = { tunnel . type === '服务端' ? faServer : faDesktop }
214+ className = "text-default-400 text-sm"
215+ />
216+ < span className = "text-sm font-medium flex-1 truncate" >
217+ { tunnel . name }
218+ </ span >
219+ { getTunnelTypeChip ( tunnel . type ) }
220+ </ Flex >
221+ ) ) }
222+
223+ { groupTunnels . length > 5 && (
224+ < div className = "text-center py-2" >
225+ < span className = "text-xs text-default-400" >
226+ 还有 { groupTunnels . length - 5 } 个隧道...
227+ </ span >
228+ </ div >
229+ ) }
230+ </ Box >
231+ </ Box >
232+ ) : (
233+ < Box className = "text-center py-6" >
234+ < FontAwesomeIcon icon = { faServer } className = "text-3xl text-default-300 mb-2" />
235+ < p className = "text-sm text-default-500" > 该分组暂无隧道</ p >
236+ < Button
237+ size = "sm"
238+ color = "primary"
239+ variant = "flat"
240+ className = "mt-2"
241+ startContent = { < FontAwesomeIcon icon = { faPlus } /> }
242+ onPress = { ( ) => onAddTunnels ?.( group ) }
243+ >
244+ 添加隧道
245+ </ Button >
246+ </ Box >
247+ ) }
248+
249+ { /* 连接关系示例(针对双端/穿透类型) */ }
250+ { groupTunnels . length >= 2 && (
251+ < Box className = "border-t pt-3" >
252+ < h4 className = "text-sm font-medium text-default-700 mb-2" > 连接关系</ h4 >
253+ < Box className = "bg-gradient-to-r from-primary-50 to-secondary-50 p-3 rounded-lg" >
254+ < Flex className = "items-center gap-2 text-sm" >
255+ < span className = "font-medium" > { groupTunnels [ 0 ] ?. name } </ span >
256+ < FontAwesomeIcon icon = { faArrowRight } className = "text-default-400" />
257+ < span className = "font-medium" > { groupTunnels [ 1 ] ?. name } </ span >
258+ </ Flex >
259+ < p className = "text-xs text-default-500 mt-1" >
260+ 双端连接示例
261+ </ p >
262+ </ Box >
263+ </ Box >
264+ ) }
265+ </ Box >
266+ </ CardBody >
267+ </ Card >
268+ ) ;
269+ } ;
0 commit comments