11import { type DomainProjectTask } from "@/api/Api"
2- import { Badge } from "@/components/ui/badge"
32import { Button } from "@/components/ui/button"
43import { ScrollArea } from "@/components/ui/scroll-area"
54import { Spinner } from "@/components/ui/spinner"
65import { cn } from "@/lib/utils"
76import { stripMarkdown } from "@/utils/common"
87import { apiRequest } from "@/utils/requestUtils"
9- import { IconPlus } from "@tabler/icons-react"
8+ import { IconPlus , IconCircleCheck , IconAlertTriangle , IconLoader , IconLayoutSidebar } from "@tabler/icons-react"
109import dayjs from "dayjs"
10+ import relativeTime from "dayjs/plugin/relativeTime"
1111import { useCallback , useEffect , useState } from "react"
12+ import { useSearchParams } from "react-router-dom"
1213import { toast } from "sonner"
1314
15+ dayjs . extend ( relativeTime )
16+
1417const PAGE_SIZE = 50
1518
1619export default function TaskViewPage ( ) {
20+ const [ searchParams , setSearchParams ] = useSearchParams ( )
1721 const [ tasks , setTasks ] = useState < DomainProjectTask [ ] > ( [ ] )
1822 const [ loading , setLoading ] = useState ( false )
19- const [ selectedTaskId , setSelectedTaskId ] = useState < string | null > ( null )
23+ const [ selectedTaskId , setSelectedTaskId ] = useState < string | null > ( searchParams . get ( 'taskId' ) || null )
2024 const [ selectedTask , setSelectedTask ] = useState < DomainProjectTask | null > ( null )
25+ const [ sidebarWidth , setSidebarWidth ] = useState < 'wide' | 'narrow' > ( 'wide' )
26+ const [ , update ] = useState ( 0 )
27+
28+ useEffect ( ( ) => {
29+ const interval = setInterval ( ( ) => {
30+ update ( v => v + 1 )
31+ } , 30000 )
32+ return ( ) => clearInterval ( interval )
33+ } , [ ] )
2134
2235 const fetchTasks = useCallback ( async ( ) => {
2336 setLoading ( true )
@@ -29,13 +42,19 @@ export default function TaskViewPage() {
2942 const firstTask = fetchedTasks [ 0 ]
3043 setSelectedTaskId ( firstTask . id )
3144 setSelectedTask ( firstTask )
45+ setSearchParams ( { taskId : firstTask . id } , { replace : true } )
46+ } else if ( selectedTaskId ) {
47+ const foundTask = fetchedTasks . find ( t => t . id === selectedTaskId )
48+ if ( foundTask ) {
49+ setSelectedTask ( foundTask )
50+ }
3251 }
3352 } else {
3453 toast . error ( "获取任务列表失败: " + resp . message )
3554 }
3655 } )
3756 setLoading ( false )
38- } , [ selectedTaskId ] )
57+ } , [ selectedTaskId , setSearchParams ] )
3958
4059 useEffect ( ( ) => {
4160 fetchTasks ( )
@@ -45,20 +64,28 @@ export default function TaskViewPage() {
4564
4665 return (
4766 < div className = "flex h-screen overflow-hidden" >
48- < div className = "w-80 border-r flex flex-col h-full bg-background overflow-hidden" >
49- < div className = "p -2" >
67+ < div className = { cn ( " border-r flex flex-col h-full bg-muted/30 overflow-hidden transition-all duration-300" , sidebarWidth === 'wide' ? 'w-80' : 'w-40' ) } >
68+ < div className = "flex p-2 gap -2" >
5069 < Button
5170 variant = "outline"
5271 size = "sm"
53- className = "w-full "
72+ className = "flex-1 "
5473 onClick = { ( ) => window . location . href = '/console/tasks' }
5574 >
56- < IconPlus className = "w-4 h-4 mr-2" />
57- 启动新任务
75+ < IconPlus className = "w-4 h-4" />
76+ { sidebarWidth === 'wide' ? '启动新任务' : '新任务' }
77+ </ Button >
78+ < Button
79+ variant = "outline"
80+ size = "sm"
81+ className = "flex-shrink-0"
82+ onClick = { ( ) => setSidebarWidth ( sidebarWidth === 'wide' ? 'narrow' : 'wide' ) }
83+ >
84+ < IconLayoutSidebar className = "w-4 h-4" />
5885 </ Button >
5986 </ div >
6087 < ScrollArea className = "flex-1 h-0" >
61- < div className = "p-2 space-y-2 " >
88+ < div className = "p-2 space-y-1 " >
6289 { loading && tasks . length === 0 ? (
6390 < div className = "flex justify-center py-8" >
6491 < Spinner className = "size-6" />
@@ -67,32 +94,40 @@ export default function TaskViewPage() {
6794 < div className = "text-center py-8 text-muted-foreground text-sm" >
6895 暂无任务
6996 </ div >
70- ) : (
97+ ) : (
7198 tasks . map ( ( task ) => (
7299 < div
73100 key = { task . id }
74101 className = { cn (
75- "p-3 rounded-lg border cursor-pointer transition-all hover:bg-accent" ,
76- selectedTaskId === task . id && "bg-accent border-primary "
102+ "cursor-pointer transition-all hover:bg-accent rounded " ,
103+ selectedTaskId === task . id && "bg-accent"
77104 ) }
78105 onClick = { ( ) => {
79106 setSelectedTaskId ( task . id )
80107 setSelectedTask ( task )
108+ setSearchParams ( { taskId : task . id } )
81109 } }
82110 >
83- < div className = "font-medium text-sm line-clamp-1 break-all mb-2" >
84- { task . summary || stripMarkdown ( task . content ) }
85- </ div >
86- < div className = "flex items-center justify-between" >
87- < Badge variant = "outline" className = "text-xs" >
88- { task . status === "finished" && "已完成" }
89- { task . status === "error" && "失败" }
90- { task . status === "pending" && "等待中" }
91- { task . status === "processing" && "执行中" }
92- </ Badge >
93- < div className = "text-xs text-muted-foreground" >
94- { dayjs . unix ( task . created_at as number ) . fromNow ( ) }
95- </ div >
111+ < div className = { cn ( sidebarWidth === 'wide' ? "px-3 py-2" : "px-2 py-2" ) } >
112+ { sidebarWidth === 'wide' ? (
113+ < div className = "flex items-center gap-2 text-sm" >
114+ < div className = "flex-shrink-0" >
115+ { task . status === "finished" && < IconCircleCheck className = "w-4 h-4 text-muted-foreground/50" /> }
116+ { task . status === "error" && < IconAlertTriangle className = "w-4 h-4 text-muted-foreground/50" /> }
117+ { ( task . status === "pending" || task . status === "processing" ) && < IconLoader className = "w-4 h-4 text-muted-foreground animate-spin" style = { { animationDuration : '3s' } } /> }
118+ </ div >
119+ < div className = "flex-1 min-w-0 line-clamp-1" >
120+ { task . summary || stripMarkdown ( task . content ) }
121+ </ div >
122+ < div className = "flex-shrink-0 text-xs text-muted-foreground" >
123+ { dayjs . unix ( task . created_at as number ) . fromNow ( ) }
124+ </ div >
125+ </ div >
126+ ) : (
127+ < div className = "text-sm line-clamp-1" >
128+ { task . summary || stripMarkdown ( task . content ) }
129+ </ div >
130+ ) }
96131 </ div >
97132 </ div >
98133 ) )
0 commit comments