1+ "use client" ;
2+ import { Button , Input , Select , SelectItem , Table , TableHeader , TableColumn , TableBody , TableRow , TableCell , Spinner , DateRangePicker , Pagination , Badge } from "@heroui/react" ;
3+ import { useSearchParams , useRouter } from "next/navigation" ;
4+ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" ;
5+ import { faArrowLeft , faSearch , faTrash } from "@fortawesome/free-solid-svg-icons" ;
6+ import { useState , useEffect , useCallback } from "react" ;
7+ import { buildApiUrl } from "@/lib/utils" ;
8+
9+ interface LogItem {
10+ id :number ;
11+ createAt :string ;
12+ level :string ;
13+ instanceId :string ;
14+ message :string ;
15+ }
16+
17+ export default function LogQueryPage ( ) {
18+ const router = useRouter ( ) ;
19+ const searchParams = useSearchParams ( ) ;
20+ const endpointId = searchParams . get ( "id" ) ;
21+
22+ const [ level , setLevel ] = useState < string > ( "all" ) ;
23+ const [ instanceId , setInstanceId ] = useState < string > ( "" ) ;
24+ const [ range , setRange ] = useState < { start :any , end :any } > ( { start :undefined , end :undefined } ) ;
25+ const [ items , setItems ] = useState < LogItem [ ] > ( [ ] ) ;
26+ const [ total , setTotal ] = useState ( 0 ) ;
27+ const [ totalPages , setTotalPages ] = useState ( 0 ) ;
28+ const [ page , setPage ] = useState ( 1 ) ;
29+ const [ pageSize , setPageSize ] = useState ( 20 ) ;
30+ const [ loading , setLoading ] = useState ( false ) ;
31+ const [ recycleCount , setRecycleCount ] = useState ( 0 ) ;
32+
33+ const formatDate = ( d :any ) => {
34+ if ( ! d ) return "" ;
35+ const pad = ( n :number ) => n . toString ( ) . padStart ( 2 , "0" ) ;
36+ // 处理 CalendarDate / DateValue 对象
37+ if ( typeof d === "object" && "year" in d && "month" in d && "day" in d ) {
38+ return `${ d . year } -${ pad ( d . month ) } -${ pad ( d . day ) } ` ;
39+ }
40+ const date = d instanceof Date ? d : new Date ( d ) ;
41+ return `${ date . getFullYear ( ) } -${ pad ( date . getMonth ( ) + 1 ) } -${ pad ( date . getDate ( ) ) } ` ;
42+ } ;
43+
44+ const fetchData = useCallback ( async ( p :number = page ) => {
45+ if ( ! endpointId ) return ;
46+ try {
47+ setLoading ( true ) ;
48+ const params = new URLSearchParams ( ) ;
49+ if ( level !== "all" ) params . append ( "level" , level ) ;
50+ if ( instanceId ) params . append ( "instanceId" , instanceId ) ;
51+ if ( range . start ) params . append ( "start" , formatDate ( range . start ) ) ;
52+ if ( range . end ) params . append ( "end" , formatDate ( range . end ) ) ;
53+ params . append ( "page" , String ( p ) ) ;
54+ params . append ( "size" , String ( pageSize ) ) ;
55+ const res = await fetch ( buildApiUrl ( `/api/endpoints/${ endpointId } /logs/search?` + params . toString ( ) ) ) ;
56+ const data = await res . json ( ) ;
57+ const processed = ( data . logs || [ ] ) . map ( ( item :any ) => {
58+ const rawMsg = item . message || "" ;
59+ const idx = rawMsg . indexOf ( "[0m" ) ;
60+ const cleanMsg = idx !== - 1 ?rawMsg . slice ( idx + 3 ) :rawMsg ;
61+ return { ...item , message : cleanMsg } ;
62+ } ) ;
63+ setItems ( processed ) ;
64+ setTotal ( data . total || 0 ) ;
65+ if ( data . totalPages ) { setTotalPages ( data . totalPages ) ; }
66+ if ( data . size ) { setPageSize ( data . size ) ; }
67+ } catch ( e ) { console . error ( e ) ; } finally { setLoading ( false ) ; }
68+ } , [ endpointId , level , instanceId , range , pageSize , page ] ) ;
69+
70+ // 首次加载
71+ useEffect ( ( ) => { fetchData ( 1 ) ; setPage ( 1 ) ; } , [ fetchData ] ) ;
72+
73+ // 获取回收站数量
74+ useEffect ( ( ) => {
75+ const fetchRecycle = async ( ) => {
76+ if ( ! endpointId ) return ;
77+ try {
78+ const res = await fetch ( buildApiUrl ( `/api/endpoints/${ endpointId } /recycle/count` ) ) ;
79+ const data = await res . json ( ) ;
80+ setRecycleCount ( data . count || 0 ) ;
81+ } catch ( e ) { console . error ( e ) ; }
82+ } ;
83+ fetchRecycle ( ) ;
84+ } , [ endpointId ] ) ;
85+
86+ return (
87+ < div className = "p-4 space-y-4" >
88+ < div className = "flex items-center gap-3 justify-between" >
89+ < div className = "flex items-center gap-3" >
90+ < Button isIconOnly variant = "flat" size = "sm" onClick = { ( ) => router . back ( ) } className = "bg-default-100 hover:bg-default-200 dark:bg-default-100/10 dark:hover:bg-default-100/20" >
91+ < FontAwesomeIcon icon = { faArrowLeft } />
92+ </ Button >
93+ < h1 className = "text-lg md:text-2xl font-bold truncate" > 日志查询</ h1 >
94+ </ div >
95+ < Button isIconOnly color = "danger" variant = "light" className = "relative bg-default-100 hover:bg-default-200 dark:bg-default-100/10 dark:hover:bg-default-100/20" onPress = { ( ) => router . push ( `/endpoints/recycle?id=${ endpointId } ` ) } >
96+ < Badge color = "danger" size = "sm" content = { recycleCount } className = "absolute -top-1 -right-1 pointer-events-none" >
97+ < FontAwesomeIcon icon = { faTrash } />
98+ </ Badge >
99+ </ Button >
100+ </ div >
101+ { /* 查询表单 */ }
102+ < div className = "flex flex-wrap md:flex-nowrap items-end gap-2" >
103+ { /* 日期范围选择 */ }
104+ { /* @ts -ignore hero-ui DateRangePicker 类型未完善 */ }
105+ < DateRangePicker locale = "zh-CN" value = { range } onChange = { ( v :any ) => setRange ( v ) } />
106+ < Select selectedKeys = { [ level ] } onSelectionChange = { ( keys ) => setLevel ( Array . from ( keys ) [ 0 ] as string ) } >
107+ < SelectItem key = "all" > 全部级别</ SelectItem >
108+ < SelectItem key = "debug" > Debug</ SelectItem >
109+ < SelectItem key = "info" > Info</ SelectItem >
110+ < SelectItem key = "warn" > Warn</ SelectItem >
111+ < SelectItem key = "error" > Error</ SelectItem >
112+ </ Select >
113+ < Input value = { instanceId } onValueChange = { setInstanceId } placeholder = "实例ID" />
114+ < Button color = "primary" startContent = { < FontAwesomeIcon icon = { faSearch } /> } onPress = { ( ) => { setPage ( 1 ) ; fetchData ( 1 ) ; } } > 查询</ Button >
115+ < Button variant = "flat" onPress = { ( ) => {
116+ setLevel ( "all" ) ;
117+ setInstanceId ( "" ) ;
118+ setRange ( { start :undefined , end :undefined } ) ;
119+ setPage ( 1 ) ;
120+ fetchData ( 1 ) ;
121+ } } > 重置</ Button >
122+ </ div >
123+ { /* 结果表格 */ }
124+ < Table aria-label = "日志列表" >
125+ < TableHeader >
126+ < TableColumn > 时间</ TableColumn >
127+ < TableColumn > 级别</ TableColumn >
128+ < TableColumn > 实例ID</ TableColumn >
129+ < TableColumn > 日志</ TableColumn >
130+ </ TableHeader >
131+ < TableBody items = { items } loadingContent = { < Spinner /> } isLoading = { loading } emptyContent = "暂无数据" >
132+ { item => (
133+ < TableRow key = { item . id } >
134+ < TableCell className = "w-80" > { item . createAt } </ TableCell >
135+ < TableCell className = "w-24" > { item . level } </ TableCell >
136+ < TableCell className = "w-40" > { item . instanceId } </ TableCell >
137+ < TableCell className = "max-w-[600px] truncate" title = { item . message } > { item . message } </ TableCell >
138+ </ TableRow >
139+ ) }
140+ </ TableBody >
141+ </ Table >
142+ { /* 分页 */ }
143+ < div className = "flex flex-col md:flex-row md:justify-between items-center pt-4 gap-2" >
144+ < span className = "text-sm text-default-500" > 共 { total } 条</ span >
145+ < Pagination
146+ page = { page }
147+ total = { totalPages || Math . ceil ( total / pageSize ) }
148+ onChange = { ( p ) => { setPage ( p ) ; fetchData ( p ) ; } }
149+ showControls
150+ />
151+ </ div >
152+ </ div >
153+ ) ;
154+ }
0 commit comments