@@ -52,6 +52,7 @@ type SortField =
5252 | "cost"
5353 | "isError" ;
5454type SortOrder = "asc" | "desc" ;
55+ type SortKey = { field : SortField ; order : SortOrder } ;
5556
5657type ColumnKey =
5758 | "occurredAt"
@@ -250,25 +251,32 @@ function SkeletonRow() {
250251
251252function SortHeader ( {
252253 label,
253- active ,
254+ priority ,
254255 order,
256+ showPriority,
255257 onClick
256258} : {
257259 label : string ;
258- active : boolean ;
259- order : SortOrder ;
260+ priority ?: number ;
261+ order ?: SortOrder ;
262+ showPriority ?: boolean ;
260263 onClick : ( ) => void ;
261264} ) {
265+ const active = priority !== undefined ;
262266 return (
263267 < button
264268 type = "button"
265269 onClick = { onClick }
270+ title = { active ? "" : undefined }
266271 className = { `inline-flex items-center gap-1 font-semibold transition ${ active ? "text-white" : "text-slate-300 hover:text-white" } ` }
267272 >
268273 < span > { label } </ span >
269- { active ? (
274+ { active && order ? (
270275 order === "asc" ? < ArrowUp className = "h-3.5 w-3.5" /> : < ArrowDown className = "h-3.5 w-3.5" />
271276 ) : null }
277+ { active && showPriority && priority !== undefined ? (
278+ < span className = "text-[10px] font-bold leading-none opacity-60" > { priority } </ span >
279+ ) : null }
272280 </ button >
273281 ) ;
274282}
@@ -308,8 +316,8 @@ export default function RecordsPage() {
308316 const [ appliedStart , setAppliedStart ] = useState < string > ( "" ) ;
309317 const [ appliedEnd , setAppliedEnd ] = useState < string > ( "" ) ;
310318
311- const [ sortField , setSortField ] = useState < SortField > ( "occurredAt" ) ;
312- const [ sortOrder , setSortOrder ] = useState < SortOrder > ( "desc" ) ;
319+ const [ sortKeys , setSortKeys ] = useState < SortKey [ ] > ( [ { field : "occurredAt" , order : "desc" } ] ) ;
320+ const [ hasLoaded , setHasLoaded ] = useState ( false ) ;
313321 const [ columnSettings , setColumnSettings ] = useState < ColumnSetting [ ] > (
314322 DEFAULT_COLUMN_ORDER . map ( ( key ) => ( {
315323 key,
@@ -509,8 +517,7 @@ export default function RecordsPage() {
509517 ( cursorValue ?: string | null , includeFilters ?: boolean ) => {
510518 const params = new URLSearchParams ( ) ;
511519 params . set ( "limit" , String ( PAGE_SIZE ) ) ;
512- params . set ( "sortField" , sortField ) ;
513- params . set ( "sortOrder" , sortOrder ) ;
520+ params . set ( "sort" , sortKeys . map ( k => `${ k . field } :${ k . order } ` ) . join ( "," ) ) ;
514521 if ( cursorValue ) params . set ( "cursor" , cursorValue ) ;
515522 if ( appliedModel ) params . set ( "model" , appliedModel ) ;
516523 if ( appliedRoute ) params . set ( "route" , appliedRoute ) ;
@@ -520,7 +527,7 @@ export default function RecordsPage() {
520527 if ( includeFilters ) params . set ( "includeFilters" , "1" ) ;
521528 return params ;
522529 } ,
523- [ sortField , sortOrder , appliedModel , appliedRoute , appliedSource , appliedStart , appliedEnd ]
530+ [ sortKeys , appliedModel , appliedRoute , appliedSource , appliedStart , appliedEnd ]
524531 ) ;
525532
526533 const fetchRecords = useCallback (
@@ -538,6 +545,7 @@ export default function RecordsPage() {
538545 setCursor ( data . nextCursor ?? null ) ;
539546 setHasMore ( Boolean ( data . nextCursor ) ) ;
540547 setRecords ( ( prev ) => ( opts . append ? [ ...prev , ...data . items ] : data . items ) ) ;
548+ setHasLoaded ( true ) ;
541549 if ( data . filters ?. models ?. length ) {
542550 setModels ( data . filters . models ) ;
543551 }
@@ -670,17 +678,27 @@ export default function RecordsPage() {
670678 } , [ cursor , fetchRecords , hasMore , loading ] ) ;
671679
672680 const handleSort = useCallback ( ( field : SortField ) => {
673- if ( field === sortField ) {
674- setSortOrder ( ( prev ) => ( prev === "asc" ? "desc" : "asc" ) ) ;
675- } else {
676- setSortField ( field ) ;
677- setSortOrder ( "desc" ) ;
678- }
679- } , [ sortField ] ) ;
681+ setSortKeys ( prev => {
682+ const idx = prev . findIndex ( k => k . field === field ) ;
683+ if ( idx !== - 1 ) {
684+ const current = prev [ idx ] ;
685+ if ( current . order === "asc" ) {
686+ // 第三次点击:移除(多键时)或循环回 desc(唯一键时)
687+ // occurredAt 列不允许移除,始终保留
688+ if ( prev . length > 1 && field !== "occurredAt" ) return prev . filter ( ( _ , i ) => i !== idx ) ;
689+ return prev . map ( ( k , i ) => i === idx ? { ...k , order : "desc" } : k ) ;
690+ }
691+ // 第二次点击: desc → asc
692+ return prev . map ( ( k , i ) => i === idx ? { ...k , order : "asc" } : k ) ;
693+ }
694+ // 第一次点击:插入头部为主键
695+ return [ { field, order : "desc" } , ...prev ] ;
696+ } ) ;
697+ } , [ ] ) ;
680698
681699 useEffect ( ( ) => {
682700 resetAndFetch ( false ) ;
683- } , [ sortField , sortOrder , resetAndFetch ] ) ;
701+ } , [ sortKeys , resetAndFetch ] ) ;
684702
685703 const applyFilters = ( overrides ?: { model ?: string ; route ?: string ; source ?: string ; start ?: string ; end ?: string } ) => {
686704 const nextModel = ( overrides ?. model ?? modelInput ) . trim ( ) ;
@@ -773,16 +791,20 @@ export default function RecordsPage() {
773791 return < span className = "font-semibold text-slate-300" > { COLUMN_LABELS [ columnKey ] } </ span > ;
774792 }
775793
794+ const keyIdx = sortKeys . findIndex ( k => k . field === sortTarget ) ;
795+ const priority = keyIdx !== - 1 ? keyIdx + 1 : undefined ;
796+ const order = keyIdx !== - 1 ? sortKeys [ keyIdx ] . order : undefined ;
776797 return (
777798 < SortHeader
778799 label = { COLUMN_LABELS [ columnKey ] }
779- active = { sortField === sortTarget }
780- order = { sortOrder }
800+ priority = { priority }
801+ order = { order }
802+ showPriority = { sortKeys . length > 1 }
781803 onClick = { ( ) => handleSort ( sortTarget ) }
782804 />
783805 ) ;
784806 } ,
785- [ sortField , sortOrder , handleSort ]
807+ [ sortKeys , handleSort ]
786808 ) ;
787809
788810 const renderCellByColumn = useCallback (
@@ -1229,8 +1251,8 @@ export default function RecordsPage() {
12291251 < p className = "mt-3 text-xs text-slate-500" > 当前筛选:{ filterSummary } </ p >
12301252 </ section >
12311253
1232- < section className = { `mt-5 rounded-2xl bg-slate-800/40 p-4 shadow-sm ring-1 ring-slate-700 ${ loadingEmpty ? "min-h-[100vh]" : "" } ` } >
1233- { ! loadingEmpty ? (
1254+ < section className = { `mt-5 rounded-2xl bg-slate-800/40 p-4 shadow-sm ring-1 ring-slate-700 ${ loadingEmpty && ! hasLoaded ? "min-h-[100vh]" : "" } ` } >
1255+ { ( ! loadingEmpty || hasLoaded ) ? (
12341256 < div ref = { tableWrapperRef } className = "overflow-auto" >
12351257 < table className = "min-w-full w-full table-fixed border-separate border-spacing-y-2" >
12361258 < thead className = "sticky top-0 z-10" >
@@ -1289,12 +1311,23 @@ export default function RecordsPage() {
12891311 } ) }
12901312 </ tr >
12911313 ) ) }
1314+ { loading && records . length === 0 && hasLoaded ? (
1315+ [ 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 , 11 , 12 ] . map ( i => (
1316+ < tr key = { `skel-${ i } ` } className = "h-13" >
1317+ { visibleColumns . map ( colKey => (
1318+ < td key = { colKey } className = "px-3 py-3" >
1319+ < div className = "h-3 w-4/5 animate-pulse rounded bg-slate-700/60" />
1320+ </ td >
1321+ ) ) }
1322+ </ tr >
1323+ ) )
1324+ ) : null }
12921325 </ tbody >
12931326 </ table >
12941327 </ div >
12951328 ) : null }
12961329
1297- { loadingEmpty ? (
1330+ { loadingEmpty && ! hasLoaded ? (
12981331 < div className = "mt-4 grid min-h-[55vh] gap-3" >
12991332 { [ 1 , 2 , 3 ] . map ( ( i ) => (
13001333 < SkeletonRow key = { i } />
@@ -1306,7 +1339,7 @@ export default function RecordsPage() {
13061339
13071340 { isEmpty ? < p className = "mt-4 text-sm text-slate-400" > 暂无记录</ p > : null }
13081341
1309- { records . length > 0 ? (
1342+ { ( records . length > 0 || ( hasLoaded && loading ) ) ? (
13101343 < div className = "mt-4 flex items-center justify-between text-xs text-slate-500" >
13111344 < span > 已加载 { records . length } 条</ span >
13121345 { loading ? < span > 加载中...</ span > : hasMore ? < span > 继续向下滚动加载</ span > : < span > 已到底</ span > }
0 commit comments