refactor: 将 WebUI 源码纳入仓库,移除打包产物
- 新增 webui/ 前端源码目录(React+Vite+TypeScript+Tailwind) - 删除 api/webui/ 中旧的打包静态资源 - 更新 .gitignore 忽略 api/webui/ 构建输出和 webui/node_modules - 更新 README 增加前端构建说明
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { toast } from 'sonner'
|
||||
import { crawlerApi, configApi } from '@/lib/api'
|
||||
import { useCrawlerStore } from '@/store/crawlerStore'
|
||||
import type { CrawlerConfig } from '@/types/crawler'
|
||||
|
||||
export function useCrawlerStatus() {
|
||||
const setStatus = useCrawlerStore((state) => state.setStatus)
|
||||
const setRunningInfo = useCrawlerStore((state) => state.setRunningInfo)
|
||||
|
||||
return useQuery({
|
||||
queryKey: ['crawlerStatus'],
|
||||
queryFn: async () => {
|
||||
const { data } = await crawlerApi.getStatus()
|
||||
setStatus(data.status)
|
||||
setRunningInfo(data.platform, data.crawler_type, data.started_at)
|
||||
return data
|
||||
},
|
||||
refetchInterval: 2000,
|
||||
})
|
||||
}
|
||||
|
||||
export function useStartCrawler() {
|
||||
const queryClient = useQueryClient()
|
||||
const setStatus = useCrawlerStore((state) => state.setStatus)
|
||||
const clearLogs = useCrawlerStore((state) => state.clearLogs)
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (config: CrawlerConfig) => crawlerApi.start(config),
|
||||
onMutate: () => {
|
||||
clearLogs()
|
||||
setStatus('running')
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.success('Crawler started successfully')
|
||||
queryClient.invalidateQueries({ queryKey: ['crawlerStatus'] })
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
setStatus('idle')
|
||||
toast.error(`Failed to start crawler: ${error.message}`)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useStopCrawler() {
|
||||
const queryClient = useQueryClient()
|
||||
const setStatus = useCrawlerStore((state) => state.setStatus)
|
||||
|
||||
return useMutation({
|
||||
mutationFn: () => crawlerApi.stop(),
|
||||
onMutate: () => {
|
||||
setStatus('stopping')
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.success('Crawler stopped')
|
||||
setStatus('idle')
|
||||
queryClient.invalidateQueries({ queryKey: ['crawlerStatus'] })
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error(`Failed to stop crawler: ${error.message}`)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useCrawlerLogs() {
|
||||
const setLogs = useCrawlerStore((state) => state.setLogs)
|
||||
|
||||
return useQuery({
|
||||
queryKey: ['crawlerLogs'],
|
||||
queryFn: async () => {
|
||||
const { data } = await crawlerApi.getLogs(500)
|
||||
setLogs(data.logs)
|
||||
return data.logs
|
||||
},
|
||||
refetchInterval: false, // Use WebSocket instead
|
||||
})
|
||||
}
|
||||
|
||||
export function usePlatforms() {
|
||||
return useQuery({
|
||||
queryKey: ['platforms'],
|
||||
queryFn: async () => {
|
||||
const { data } = await configApi.getPlatforms()
|
||||
return data.platforms
|
||||
},
|
||||
staleTime: Infinity,
|
||||
})
|
||||
}
|
||||
|
||||
export function useConfigOptions() {
|
||||
return useQuery({
|
||||
queryKey: ['configOptions'],
|
||||
queryFn: async () => {
|
||||
const { data } = await configApi.getOptions()
|
||||
return data
|
||||
},
|
||||
staleTime: Infinity,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { useCrawlerStore } from '@/store/crawlerStore'
|
||||
import type { LogEntry } from '@/types/crawler'
|
||||
|
||||
// 模块级单例,确保全局只有一个 WebSocket 连接
|
||||
let globalWs: WebSocket | null = null
|
||||
let globalReconnectTimer: ReturnType<typeof setTimeout> | null = null
|
||||
let connectionCount = 0 // 跟踪连接使用者数量
|
||||
|
||||
export function useLogWebSocket() {
|
||||
const addLog = useCrawlerStore((state) => state.addLog)
|
||||
const addLogRef = useRef(addLog)
|
||||
|
||||
// 保持 addLog 引用最新
|
||||
useEffect(() => {
|
||||
addLogRef.current = addLog
|
||||
}, [addLog])
|
||||
|
||||
useEffect(() => {
|
||||
connectionCount++
|
||||
|
||||
const connect = () => {
|
||||
if (globalReconnectTimer) {
|
||||
clearTimeout(globalReconnectTimer)
|
||||
globalReconnectTimer = null
|
||||
}
|
||||
|
||||
// 如果已经连接或正在连接,不重复创建
|
||||
if (globalWs && (globalWs.readyState === WebSocket.OPEN || globalWs.readyState === WebSocket.CONNECTING)) {
|
||||
return
|
||||
}
|
||||
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
|
||||
const host = window.location.host
|
||||
const wsUrl = `${protocol}//${host}/api/ws/logs`
|
||||
|
||||
const ws = new WebSocket(wsUrl)
|
||||
globalWs = ws
|
||||
|
||||
ws.onopen = () => {
|
||||
if (globalWs !== ws) return
|
||||
console.log('WebSocket connected')
|
||||
}
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
if (globalWs !== ws) return
|
||||
if (event.data === 'ping') {
|
||||
ws.send('pong')
|
||||
return
|
||||
}
|
||||
if (event.data === 'pong') {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const log: LogEntry = JSON.parse(event.data)
|
||||
if (log.id && log.message) {
|
||||
addLogRef.current(log)
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse WebSocket message:', event.data)
|
||||
}
|
||||
}
|
||||
|
||||
ws.onclose = () => {
|
||||
if (globalWs !== ws) return
|
||||
console.log('WebSocket disconnected')
|
||||
globalWs = null
|
||||
// 只在还有使用者时才重连
|
||||
if (connectionCount > 0) {
|
||||
globalReconnectTimer = setTimeout(connect, 2000)
|
||||
}
|
||||
}
|
||||
|
||||
ws.onerror = (error) => {
|
||||
if (globalWs !== ws) return
|
||||
console.error('WebSocket error:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 首次连接
|
||||
connect()
|
||||
|
||||
// Heartbeat
|
||||
const heartbeat = setInterval(() => {
|
||||
if (globalWs && globalWs.readyState === WebSocket.OPEN) {
|
||||
globalWs.send('ping')
|
||||
}
|
||||
}, 30000)
|
||||
|
||||
return () => {
|
||||
connectionCount--
|
||||
clearInterval(heartbeat)
|
||||
|
||||
// 只在没有使用者时才断开连接
|
||||
if (connectionCount === 0) {
|
||||
if (globalReconnectTimer) {
|
||||
clearTimeout(globalReconnectTimer)
|
||||
globalReconnectTimer = null
|
||||
}
|
||||
if (globalWs) {
|
||||
const ws = globalWs
|
||||
globalWs = null
|
||||
ws.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}, []) // 空依赖数组
|
||||
|
||||
return { ws: globalWs }
|
||||
}
|
||||
Reference in New Issue
Block a user