Add HistoryContext and integrate it into relevant components for improved job management

This commit is contained in:
2026-01-26 17:23:04 +08:00
parent 8f7b6ec773
commit bb51b4e6c5
8 changed files with 163 additions and 10 deletions

View File

@@ -5,6 +5,7 @@ import { ThemeProvider } from '@/contexts/ThemeContext'
import { AuthProvider, useAuth } from '@/contexts/AuthContext' import { AuthProvider, useAuth } from '@/contexts/AuthContext'
import { AppProvider } from '@/contexts/AppContext' import { AppProvider } from '@/contexts/AppContext'
import { JobProvider } from '@/contexts/JobContext' import { JobProvider } from '@/contexts/JobContext'
import { HistoryProvider } from '@/contexts/HistoryContext'
import ErrorBoundary from '@/components/ErrorBoundary' import ErrorBoundary from '@/components/ErrorBoundary'
import LoadingScreen from '@/components/LoadingScreen' import LoadingScreen from '@/components/LoadingScreen'
import { SuperAdminRoute } from '@/components/SuperAdminRoute' import { SuperAdminRoute } from '@/components/SuperAdminRoute'
@@ -71,9 +72,11 @@ function App() {
element={ element={
<ProtectedRoute> <ProtectedRoute>
<AppProvider> <AppProvider>
<HistoryProvider>
<JobProvider> <JobProvider>
<Home /> <Home />
</JobProvider> </JobProvider>
</HistoryProvider>
</AppProvider> </AppProvider>
</ProtectedRoute> </ProtectedRoute>
} }

View File

@@ -1,5 +1,5 @@
import { useRef, useEffect } from 'react' import { useRef, useEffect } from 'react'
import { useHistory } from '@/hooks/useHistory' import { useHistoryContext } from '@/contexts/HistoryContext'
import { HistoryItem } from '@/components/HistoryItem' import { HistoryItem } from '@/components/HistoryItem'
import { ScrollArea } from '@/components/ui/scroll-area' import { ScrollArea } from '@/components/ui/scroll-area'
import { Sheet, SheetContent } from '@/components/ui/sheet' import { Sheet, SheetContent } from '@/components/ui/sheet'
@@ -15,7 +15,7 @@ interface HistorySidebarProps {
} }
function HistorySidebarContent({ onLoadParams }: Pick<HistorySidebarProps, 'onLoadParams'>) { function HistorySidebarContent({ onLoadParams }: Pick<HistorySidebarProps, 'onLoadParams'>) {
const { jobs, loading, loadingMore, hasMore, loadMore, deleteJob, error, retry } = useHistory() const { jobs, loading, loadingMore, hasMore, loadMore, deleteJob, error, retry } = useHistoryContext()
const observerTarget = useRef<HTMLDivElement>(null) const observerTarget = useRef<HTMLDivElement>(null)
useEffect(() => { useEffect(() => {
@@ -99,7 +99,7 @@ function HistorySidebarContent({ onLoadParams }: Pick<HistorySidebarProps, 'onLo
export function HistorySidebar({ open, onOpenChange, onLoadParams }: HistorySidebarProps) { export function HistorySidebar({ open, onOpenChange, onLoadParams }: HistorySidebarProps) {
return ( return (
<> <>
<aside className="hidden lg:block w-[320px] border-r h-[calc(100vh-64px)]"> <aside className="hidden lg:block w-[320px] border-r h-full">
<HistorySidebarContent onLoadParams={onLoadParams} /> <HistorySidebarContent onLoadParams={onLoadParams} />
</aside> </aside>

View File

@@ -14,6 +14,7 @@ import { IconLabel } from '@/components/IconLabel'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip' import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import { ttsApi, jobApi } from '@/lib/api' import { ttsApi, jobApi } from '@/lib/api'
import { useJobPolling } from '@/hooks/useJobPolling' import { useJobPolling } from '@/hooks/useJobPolling'
import { useHistoryContext } from '@/contexts/HistoryContext'
import { LoadingState } from '@/components/LoadingState' import { LoadingState } from '@/components/LoadingState'
import { AudioPlayer } from '@/components/AudioPlayer' import { AudioPlayer } from '@/components/AudioPlayer'
import { PresetSelector } from '@/components/PresetSelector' import { PresetSelector } from '@/components/PresetSelector'
@@ -53,6 +54,7 @@ const CustomVoiceForm = forwardRef<CustomVoiceFormHandle>((_props, ref) => {
}) })
const { currentJob, isPolling, isCompleted, startPolling, elapsedTime } = useJobPolling() const { currentJob, isPolling, isCompleted, startPolling, elapsedTime } = useJobPolling()
const { refresh } = useHistoryContext()
const { const {
register, register,
@@ -112,6 +114,9 @@ const CustomVoiceForm = forwardRef<CustomVoiceFormHandle>((_props, ref) => {
const result = await ttsApi.createCustomVoiceJob(data) const result = await ttsApi.createCustomVoiceJob(data)
toast.success('任务已创建') toast.success('任务已创建')
startPolling(result.job_id) startPolling(result.job_id)
try {
await refresh()
} catch {}
} catch (error) { } catch (error) {
toast.error('创建任务失败') toast.error('创建任务失败')
} finally { } finally {

View File

@@ -15,6 +15,7 @@ import { IconLabel } from '@/components/IconLabel'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip' import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import { ttsApi, jobApi } from '@/lib/api' import { ttsApi, jobApi } from '@/lib/api'
import { useJobPolling } from '@/hooks/useJobPolling' import { useJobPolling } from '@/hooks/useJobPolling'
import { useHistoryContext } from '@/contexts/HistoryContext'
import { LoadingState } from '@/components/LoadingState' import { LoadingState } from '@/components/LoadingState'
import { AudioPlayer } from '@/components/AudioPlayer' import { AudioPlayer } from '@/components/AudioPlayer'
import { AudioInputSelector } from '@/components/AudioInputSelector' import { AudioInputSelector } from '@/components/AudioInputSelector'
@@ -48,6 +49,7 @@ function VoiceCloneForm() {
}) })
const { currentJob, isPolling, isCompleted, startPolling, elapsedTime } = useJobPolling() const { currentJob, isPolling, isCompleted, startPolling, elapsedTime } = useJobPolling()
const { refresh } = useHistoryContext()
const { const {
register, register,
@@ -93,6 +95,9 @@ function VoiceCloneForm() {
}) })
toast.success('任务已创建') toast.success('任务已创建')
startPolling(result.job_id) startPolling(result.job_id)
try {
await refresh()
} catch {}
} catch (error) { } catch (error) {
toast.error('创建任务失败') toast.error('创建任务失败')
} finally { } finally {

View File

@@ -14,6 +14,7 @@ import { IconLabel } from '@/components/IconLabel'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip' import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import { ttsApi, jobApi } from '@/lib/api' import { ttsApi, jobApi } from '@/lib/api'
import { useJobPolling } from '@/hooks/useJobPolling' import { useJobPolling } from '@/hooks/useJobPolling'
import { useHistoryContext } from '@/contexts/HistoryContext'
import { LoadingState } from '@/components/LoadingState' import { LoadingState } from '@/components/LoadingState'
import { AudioPlayer } from '@/components/AudioPlayer' import { AudioPlayer } from '@/components/AudioPlayer'
import { PresetSelector } from '@/components/PresetSelector' import { PresetSelector } from '@/components/PresetSelector'
@@ -51,6 +52,7 @@ const VoiceDesignForm = forwardRef<VoiceDesignFormHandle>((_props, ref) => {
}) })
const { currentJob, isPolling, isCompleted, startPolling, elapsedTime } = useJobPolling() const { currentJob, isPolling, isCompleted, startPolling, elapsedTime } = useJobPolling()
const { refresh } = useHistoryContext()
const { const {
register, register,
@@ -103,6 +105,9 @@ const VoiceDesignForm = forwardRef<VoiceDesignFormHandle>((_props, ref) => {
const result = await ttsApi.createVoiceDesignJob(data) const result = await ttsApi.createVoiceDesignJob(data)
toast.success('任务已创建') toast.success('任务已创建')
startPolling(result.job_id) startPolling(result.job_id)
try {
await refresh()
} catch {}
} catch (error) { } catch (error) {
toast.error('创建任务失败') toast.error('创建任务失败')
} finally { } finally {

View File

@@ -0,0 +1,126 @@
import { createContext, useContext, useState, useEffect, useCallback, useMemo, type ReactNode } from 'react'
import { jobApi } from '@/lib/api'
import type { Job } from '@/types/job'
import { toast } from 'sonner'
interface HistoryContextType {
jobs: Job[]
total: number
loading: boolean
loadingMore: boolean
hasMore: boolean
error: string | null
loadMore: () => Promise<void>
refresh: () => Promise<void>
retry: () => Promise<void>
deleteJob: (id: number) => Promise<void>
}
const HistoryContext = createContext<HistoryContextType | undefined>(undefined)
export function HistoryProvider({ children }: { children: ReactNode }) {
const [jobs, setJobs] = useState<Job[]>([])
const [total, setTotal] = useState(0)
const [loading, setLoading] = useState(true)
const [loadingMore, setLoadingMore] = useState(false)
const [error, setError] = useState<string | null>(null)
const [skip, setSkip] = useState(0)
const limit = 20
const hasMore = jobs.length < total
const loadJobs = useCallback(async (currentSkip: number, isLoadMore = false) => {
try {
if (isLoadMore) {
setLoadingMore(true)
} else {
setLoading(true)
}
setError(null)
const response = await jobApi.listJobs(currentSkip, limit)
if (isLoadMore) {
setJobs(prev => [...prev, ...response.jobs])
} else {
setJobs(response.jobs)
}
setTotal(response.total)
} catch (error: any) {
const errorMessage = error.message || '加载历史记录失败'
setError(errorMessage)
toast.error(errorMessage)
} finally {
setLoading(false)
setLoadingMore(false)
}
}, [])
const loadMore = useCallback(async () => {
if (loadingMore || !hasMore) return
const newSkip = skip + limit
setSkip(newSkip)
await loadJobs(newSkip, true)
}, [skip, loadingMore, hasMore, loadJobs])
const refresh = useCallback(async () => {
setSkip(0)
await loadJobs(0, false)
}, [loadJobs])
const retry = useCallback(async () => {
setSkip(0)
await loadJobs(0, false)
}, [loadJobs])
const deleteJob = useCallback(async (id: number) => {
const previousJobs = [...jobs]
const previousTotal = total
setJobs(prev => prev.filter(job => job.id !== id))
setTotal(prev => prev - 1)
try {
await jobApi.deleteJob(id)
toast.success('删除成功')
} catch (error) {
setJobs(previousJobs)
setTotal(previousTotal)
toast.error('删除失败')
}
}, [jobs, total])
useEffect(() => {
loadJobs(0, false)
}, [loadJobs])
const value = useMemo(
() => ({
jobs,
total,
loading,
loadingMore,
hasMore,
error,
loadMore,
refresh,
retry,
deleteJob,
}),
[jobs, total, loading, loadingMore, hasMore, error, loadMore, refresh, retry, deleteJob]
)
return (
<HistoryContext.Provider value={value}>
{children}
</HistoryContext.Provider>
)
}
export function useHistoryContext() {
const context = useContext(HistoryContext)
if (!context) {
throw new Error('useHistoryContext must be used within HistoryProvider')
}
return context
}

View File

@@ -3,6 +3,7 @@ import { toast } from 'sonner'
import { jobApi } from '@/lib/api' import { jobApi } from '@/lib/api'
import type { Job, JobStatus } from '@/types/job' import type { Job, JobStatus } from '@/types/job'
import { POLL_INTERVAL } from '@/lib/constants' import { POLL_INTERVAL } from '@/lib/constants'
import { useHistoryContext } from '@/contexts/HistoryContext'
interface JobContextType { interface JobContextType {
currentJob: Job | null currentJob: Job | null
@@ -23,6 +24,8 @@ export function JobProvider({ children }: { children: ReactNode }) {
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
const [elapsedTime, setElapsedTime] = useState(0) const [elapsedTime, setElapsedTime] = useState(0)
const { refresh: historyRefresh } = useHistoryContext()
const stopJob = useCallback(() => { const stopJob = useCallback(() => {
setCurrentJob(null) setCurrentJob(null)
setStatus(null) setStatus(null)
@@ -61,11 +64,17 @@ export function JobProvider({ children }: { children: ReactNode }) {
if (pollInterval) clearInterval(pollInterval) if (pollInterval) clearInterval(pollInterval)
if (timeInterval) clearInterval(timeInterval) if (timeInterval) clearInterval(timeInterval)
toast.success('任务完成!') toast.success('任务完成!')
try {
historyRefresh()
} catch {}
} else if (job.status === 'failed') { } else if (job.status === 'failed') {
if (pollInterval) clearInterval(pollInterval) if (pollInterval) clearInterval(pollInterval)
if (timeInterval) clearInterval(timeInterval) if (timeInterval) clearInterval(timeInterval)
setError(job.error_message || '任务失败') setError(job.error_message || '任务失败')
toast.error(job.error_message || '任务失败') toast.error(job.error_message || '任务失败')
try {
historyRefresh()
} catch {}
} }
} catch (error: any) { } catch (error: any) {
if (pollInterval) clearInterval(pollInterval) if (pollInterval) clearInterval(pollInterval)
@@ -86,7 +95,7 @@ export function JobProvider({ children }: { children: ReactNode }) {
if (pollInterval) clearInterval(pollInterval) if (pollInterval) clearInterval(pollInterval)
if (timeInterval) clearInterval(timeInterval) if (timeInterval) clearInterval(timeInterval)
} }
}, []) }, [historyRefresh])
const value = useMemo( const value = useMemo(
() => ({ () => ({

View File

@@ -50,17 +50,17 @@ function Home() {
} }
return ( return (
<div className="min-h-screen bg-background"> <div className="h-screen overflow-hidden flex flex-col bg-background">
<Navbar onToggleSidebar={() => setSidebarOpen(!sidebarOpen)} /> <Navbar onToggleSidebar={() => setSidebarOpen(!sidebarOpen)} />
<div className="flex"> <div className="flex-1 flex overflow-hidden">
<HistorySidebar <HistorySidebar
open={sidebarOpen} open={sidebarOpen}
onOpenChange={setSidebarOpen} onOpenChange={setSidebarOpen}
onLoadParams={handleLoadParams} onLoadParams={handleLoadParams}
/> />
<main className="flex-1 container mx-auto p-3 md:p-6 max-w-[800px] md:max-w-[700px]"> <main className="flex-1 overflow-y-auto container mx-auto p-3 md:p-6 max-w-[800px] md:max-w-[700px]">
<Card> <Card>
<CardHeader> <CardHeader>
<Tabs value={currentTab} onValueChange={setCurrentTab}> <Tabs value={currentTab} onValueChange={setCurrentTab}>