- 3 消息页 + 2 资料编辑页 + 3 设置页 + 2 资料展示页 - api.ts 新增 Profile/Messages/ChangePassword 等类型和方法 - SSEContext 事件映射修复 + 断线重连修复 - 剩余页面加 USE_MOCK 双模式,52/55 页面已完成 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
130 lines
3.9 KiB
TypeScript
130 lines
3.9 KiB
TypeScript
'use client'
|
||
|
||
import { createContext, useContext, useEffect, useRef, useCallback, ReactNode } from 'react'
|
||
import { useAuth } from './AuthContext'
|
||
import { USE_MOCK } from './AuthContext'
|
||
import { getAccessToken } from '@/lib/api'
|
||
|
||
type SSEEventType = 'task_updated' | 'review_progress' | 'review_completed' | 'new_task' | 'review_decision'
|
||
type SSEHandler = (data: Record<string, unknown>) => void
|
||
|
||
interface SSEContextType {
|
||
subscribe: (eventType: SSEEventType, handler: SSEHandler) => () => void
|
||
}
|
||
|
||
const SSEContext = createContext<SSEContextType | undefined>(undefined)
|
||
|
||
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:8000'
|
||
|
||
export function SSEProvider({ children }: { children: ReactNode }) {
|
||
const { isAuthenticated } = useAuth()
|
||
const listenersRef = useRef<Map<SSEEventType, Set<SSEHandler>>>(new Map())
|
||
const abortRef = useRef<AbortController | null>(null)
|
||
const reconnectTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||
|
||
const dispatch = useCallback((eventType: SSEEventType, data: Record<string, unknown>) => {
|
||
const handlers = listenersRef.current.get(eventType)
|
||
if (handlers) {
|
||
handlers.forEach(handler => handler(data))
|
||
}
|
||
}, [])
|
||
|
||
const connect = useCallback(async () => {
|
||
if (USE_MOCK || !isAuthenticated) return
|
||
|
||
// 清除旧连接
|
||
abortRef.current?.abort()
|
||
const controller = new AbortController()
|
||
abortRef.current = controller
|
||
|
||
try {
|
||
const token = getAccessToken()
|
||
if (!token) return
|
||
|
||
const response = await fetch(`${API_BASE_URL}/api/v1/sse/events`, {
|
||
headers: {
|
||
'Authorization': `Bearer ${token}`,
|
||
'Accept': 'text/event-stream',
|
||
},
|
||
signal: controller.signal,
|
||
})
|
||
|
||
if (!response.ok || !response.body) return
|
||
|
||
const reader = response.body.getReader()
|
||
const decoder = new TextDecoder()
|
||
let buffer = ''
|
||
|
||
while (true) {
|
||
const { done, value } = await reader.read()
|
||
if (done) break
|
||
|
||
buffer += decoder.decode(value, { stream: true })
|
||
const lines = buffer.split('\n')
|
||
buffer = lines.pop() || ''
|
||
|
||
let currentEvent = ''
|
||
let currentData = ''
|
||
|
||
for (const line of lines) {
|
||
if (line.startsWith('event:')) {
|
||
currentEvent = line.slice(6).trim()
|
||
} else if (line.startsWith('data:')) {
|
||
currentData = line.slice(5).trim()
|
||
} else if (line === '' && currentEvent && currentData) {
|
||
try {
|
||
const parsed = JSON.parse(currentData)
|
||
dispatch(currentEvent as SSEEventType, parsed)
|
||
} catch {
|
||
// 忽略解析错误
|
||
}
|
||
currentEvent = ''
|
||
currentData = ''
|
||
}
|
||
}
|
||
}
|
||
// 流正常结束(服务器关闭连接),5秒后重连
|
||
if (!controller.signal.aborted) {
|
||
reconnectTimerRef.current = setTimeout(connect, 5000)
|
||
}
|
||
} catch (err) {
|
||
if (err instanceof DOMException && err.name === 'AbortError') return
|
||
// 5秒后重连
|
||
reconnectTimerRef.current = setTimeout(connect, 5000)
|
||
}
|
||
}, [isAuthenticated, dispatch])
|
||
|
||
useEffect(() => {
|
||
connect()
|
||
return () => {
|
||
abortRef.current?.abort()
|
||
if (reconnectTimerRef.current) clearTimeout(reconnectTimerRef.current)
|
||
}
|
||
}, [connect])
|
||
|
||
const subscribe = useCallback((eventType: SSEEventType, handler: SSEHandler): (() => void) => {
|
||
if (!listenersRef.current.has(eventType)) {
|
||
listenersRef.current.set(eventType, new Set())
|
||
}
|
||
listenersRef.current.get(eventType)!.add(handler)
|
||
|
||
return () => {
|
||
listenersRef.current.get(eventType)?.delete(handler)
|
||
}
|
||
}, [])
|
||
|
||
return (
|
||
<SSEContext.Provider value={{ subscribe }}>
|
||
{children}
|
||
</SSEContext.Provider>
|
||
)
|
||
}
|
||
|
||
export function useSSE() {
|
||
const context = useContext(SSEContext)
|
||
if (!context) {
|
||
throw new Error('useSSE must be used within SSEProvider')
|
||
}
|
||
return context
|
||
}
|