@@ -16,7 +16,11 @@ import {
1616 ModalHeader ,
1717 Skeleton ,
1818 cn ,
19- useDisclosure
19+ useDisclosure ,
20+ Dropdown ,
21+ DropdownTrigger ,
22+ DropdownMenu ,
23+ DropdownItem
2024} from "@heroui/react" ;
2125import { useState , useEffect } from "react" ;
2226
@@ -25,7 +29,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
2529import {
2630 faPlus ,
2731 faServer ,
28- faCheck ,
32+ faBullseye ,
2933 faEye ,
3034 faEdit ,
3135 faTrash ,
@@ -38,8 +42,8 @@ import {
3842 faPen ,
3943 faWifi ,
4044 faSpinner ,
41- faAdd ,
42- faLightbulb
45+ faCopy ,
46+ faEllipsisVertical
4347} from "@fortawesome/free-solid-svg-icons" ;
4448import AddEndpointModal from "./components/add-endpoint-modal" ;
4549import RenameEndpointModal from "./components/rename-endpoint-modal" ;
@@ -430,7 +434,7 @@ export default function EndpointsPage() {
430434 < div className = "flex items-center justify-between h-full w-full" >
431435 < div className = "flex items-center gap-2" >
432436 < FontAwesomeIcon
433- icon = { faCheck }
437+ icon = { faBullseye }
434438 className = {
435439 realTimeData . status === 'ONLINE' ? "text-success-600" :
436440 realTimeData . status === 'FAIL' ? "text-danger-600" : "text-warning-600"
@@ -440,37 +444,45 @@ export default function EndpointsPage() {
440444 { realTimeData . tunnelCount ? `${ realTimeData . tunnelCount } 个实例` : "0 个实例" }
441445 </ p >
442446 </ div >
443- < div className = "flex items-center gap-1" >
444- < div
445- className = { cn (
446- "inline-flex items-center justify-center w-8 h-8 rounded-lg cursor-pointer transition-colors" ,
447- realTimeData . status === 'ONLINE'
448- ? "text-warning hover:bg-warning/10"
449- : "text-success hover:bg-success/10"
450- ) }
451- onClick = { ( e ) => {
452- e . stopPropagation ( ) ;
453- if ( realTimeData . status === 'ONLINE' ) {
454- handleDisconnect ( endpoint . id ) ;
455- } else {
456- handleConnect ( endpoint . id ) ;
457- }
458- } }
459- >
460- < FontAwesomeIcon
461- icon = { realTimeData . status === 'ONLINE' ? faPlugCircleXmark : faPlug }
462- className = { realTimeData . status === 'ONLINE' ? "text-warning" : "text-success" }
463- />
464- </ div >
465- < div
466- className = "inline-flex items-center justify-center w-8 h-8 rounded-lg cursor-pointer text-danger hover:bg-danger/10 transition-colors"
467- onClick = { ( e ) => {
468- e . stopPropagation ( ) ;
469- handleDeleteClick ( endpoint ) ;
470- } }
471- >
472- < FontAwesomeIcon icon = { faTrash } />
473- </ div >
447+ < div className = "flex items-center" >
448+ < Dropdown placement = "bottom-end" >
449+ < DropdownTrigger >
450+ < Button isIconOnly variant = "light" size = "sm" onPress = { ( e ) => { ( e as any ) . stopPropagation ?.( ) ; } } >
451+ < FontAwesomeIcon icon = { faEllipsisVertical } />
452+ </ Button >
453+ </ DropdownTrigger >
454+ < DropdownMenu aria-label = "Actions" onAction = { ( key ) => {
455+ switch ( key ) {
456+ case 'toggle' :
457+ if ( realTimeData . status === 'ONLINE' ) handleDisconnect ( endpoint . id ) ; else handleConnect ( endpoint . id ) ;
458+ break ;
459+ case 'rename' :
460+ handleCardClick ( endpoint ) ;
461+ break ;
462+ case 'copy' :
463+ handleCopyConfig ( endpoint ) ;
464+ break ;
465+ case 'addTunnel' :
466+ handleAddTunnel ( endpoint ) ;
467+ break ;
468+ case 'delete' :
469+ handleDeleteClick ( endpoint ) ;
470+ break ;
471+ } } } >
472+ < DropdownItem key = "addTunnel" startContent = { < FontAwesomeIcon icon = { faPlus } /> } className = "text-primary" color = "primary" > 添加隧道</ DropdownItem >
473+ < DropdownItem key = "rename" startContent = { < FontAwesomeIcon icon = { faPen } /> } > 重命名</ DropdownItem >
474+ < DropdownItem key = "copy" startContent = { < FontAwesomeIcon icon = { faCopy } /> } > 复制配置</ DropdownItem >
475+ < DropdownItem
476+ key = "toggle"
477+ startContent = { < FontAwesomeIcon icon = { realTimeData . status === 'ONLINE' ?faPlugCircleXmark :faPlug } /> }
478+ color = { realTimeData . status === 'ONLINE' ? 'warning' : 'success' }
479+ className = { realTimeData . status === 'ONLINE' ? 'text-warning' : 'text-success' }
480+ >
481+ { realTimeData . status === 'ONLINE' ?'断开连接' :'连接主控' }
482+ </ DropdownItem >
483+ < DropdownItem key = "delete" className = "text-danger" color = "danger" startContent = { < FontAwesomeIcon icon = { faTrash } /> } > 删除主控</ DropdownItem >
484+ </ DropdownMenu >
485+ </ Dropdown >
474486 </ div >
475487 </ div >
476488 ) ;
@@ -552,10 +564,59 @@ export default function EndpointsPage() {
552564 }
553565 } ;
554566
567+ // 打开添加隧道弹窗
568+ const { isOpen : isAddTunnelOpen , onOpen : onAddTunnelOpen , onOpenChange : onAddTunnelOpenChange } = useDisclosure ( ) ;
569+ const [ tunnelUrl , setTunnelUrl ] = useState ( '' ) ;
570+
571+ function handleAddTunnel ( endpoint : FormattedEndpoint ) {
572+ setSelectedEndpoint ( endpoint ) ;
573+ setTunnelUrl ( '' ) ;
574+ onAddTunnelOpen ( ) ;
575+ }
576+
577+ // 提交添加隧道
578+ const handleSubmitAddTunnel = async ( ) => {
579+ if ( ! selectedEndpoint ) return ;
580+ if ( ! tunnelUrl . trim ( ) ) {
581+ addToast ( { title :'请输入 URL' , description :'隧道 URL 不能为空' , color :'warning' } ) ;
582+ return ;
583+ }
584+ try {
585+ const res = await fetch ( buildApiUrl ( '/api/tunnels/quick' ) , {
586+ method : 'POST' ,
587+ headers : { 'Content-Type' :'application/json' } ,
588+ body : JSON . stringify ( { endpointId : selectedEndpoint . id , url : tunnelUrl . trim ( ) } )
589+ } ) ;
590+ const data = await res . json ( ) ;
591+ if ( ! res . ok || ! data . success ) {
592+ throw new Error ( data . error || '创建隧道失败' ) ;
593+ }
594+ addToast ( { title :'创建成功' , description : data . message || '隧道已创建' , color :'success' } ) ;
595+ onAddTunnelOpenChange ( ) ;
596+ } catch ( err ) {
597+ addToast ( { title :'创建失败' , description : err instanceof Error ? err . message : '无法创建隧道' , color :'danger' } ) ;
598+ }
599+ } ;
600+
601+ // 复制配置到剪贴板
602+ function handleCopyConfig ( endpoint : FormattedEndpoint ) {
603+ const cfg = `API URL: ${ endpoint . url } ${ endpoint . apiPath } \nAPI KEY: ${ endpoint . apiKey } ` ;
604+ navigator . clipboard . writeText ( cfg ) . then ( ( ) => {
605+ addToast ( { title :'已复制' , description :'配置已复制到剪贴板' , color :'success' } ) ;
606+ } ) . catch ( ( ) => {
607+ addToast ( { title :'复制失败' , description :'无法复制到剪贴板' , color :'danger' } ) ;
608+ } ) ;
609+ }
610+
555611 return (
556612 < div className = "max-w-7xl mx-auto py-6 space-y-6" >
557613 < div className = "flex justify-between items-center" >
558- < h1 className = "text-2xl font-bold" > API 主控管理</ h1 >
614+ < div className = "flex items-center gap-4" >
615+ < h1 className = "text-2xl font-bold" > API 主控管理</ h1 >
616+ < Button isIconOnly variant = "light" onPress = { async ( ) => { await fetchEndpoints ( ) ; } } >
617+ < FontAwesomeIcon icon = { faRotateRight } />
618+ </ Button >
619+ </ div >
559620 </ div >
560621
561622 < div className = "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6" >
@@ -610,8 +671,6 @@ export default function EndpointsPage() {
610671 < Card
611672 key = { endpoint . id }
612673 className = "relative w-full h-[200px]"
613- isPressable
614- onPress = { ( ) => handleCardClick ( endpoint ) }
615674 >
616675 { /* 状态按钮 */ }
617676 < div
@@ -636,14 +695,14 @@ export default function EndpointsPage() {
636695 < h2 className = "inline bg-gradient-to-br from-foreground-800 to-foreground-500 bg-clip-text text-2xl font-semibold tracking-tight text-transparent dark:to-foreground-200" >
637696 { endpoint . name }
638697 </ h2 >
639- < span className = "inline-flex items-center px-2 py-1 text-xs font-normal rounded-md bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400" >
698+ { /* <span className="inline-flex items-center px-2 py-1 text-xs font-normal rounded-md bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400">
640699 {endpoint.apiPath}
641- </ span >
700+ </span> */ }
642701 </ div >
643702 < div className = "space-y-2" >
644703 < div className = "flex items-center gap-2 text-default-400" >
645704 < FontAwesomeIcon icon = { faServer } />
646- < span className = "text-small truncate" > { endpoint . url } </ span >
705+ < span className = "text-small truncate" > { endpoint . url } { endpoint . apiPath } </ span >
647706 </ div >
648707 < div className = "flex items-center gap-2 text-default-400" >
649708 < FontAwesomeIcon
@@ -712,11 +771,33 @@ export default function EndpointsPage() {
712771 < RenameEndpointModal
713772 isOpen = { isRenameOpen }
714773 onOpenChange = { onRenameOpenChange }
715- onRename = { handleRename }
716774 currentName = { selectedEndpoint . name }
775+ onRename = { handleRename }
717776 />
718777 ) }
719778
779+ { /* 添加隧道弹窗 */ }
780+ < Modal isOpen = { isAddTunnelOpen } onOpenChange = { onAddTunnelOpenChange } placement = "center" >
781+ < ModalContent >
782+ { ( onClose ) => (
783+ < >
784+ < ModalHeader > 添加隧道</ ModalHeader >
785+ < ModalBody >
786+ < Input
787+ placeholder = "<core>://<tunnel_addr>/<target_addr>"
788+ value = { tunnelUrl }
789+ onValueChange = { setTunnelUrl }
790+ />
791+ </ ModalBody >
792+ < ModalFooter >
793+ < Button variant = "light" onPress = { onClose } > 取消</ Button >
794+ < Button color = "primary" onPress = { handleSubmitAddTunnel } > 确定</ Button >
795+ </ ModalFooter >
796+ </ >
797+ ) }
798+ </ ModalContent >
799+ </ Modal >
800+
720801 { /* 删除确认模态框 */ }
721802 < Modal isOpen = { isDeleteOpen } onOpenChange = { onDeleteOpenChange } placement = "center" >
722803 < ModalContent >
0 commit comments