Your Name f02b3f4098 feat: 前端对接 Profile/Messages/Settings 页面 API
- 3 消息页 + 2 资料编辑页 + 3 设置页 + 2 资料展示页
- api.ts 新增 Profile/Messages/ChangePassword 等类型和方法
- SSEContext 事件映射修复 + 断线重连修复
- 剩余页面加 USE_MOCK 双模式,52/55 页面已完成

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 10:27:59 +08:00

130 lines
3.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'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
}