feat: 前端全面对接后端 API(Phase 1 完成)
- 新增基础设施:useOSSUpload Hook、SSEContext Provider、taskStageMapper 工具 - 达人端4页面:任务列表/详情/脚本上传/视频上传对接真实 API - 代理商端3页面:工作台/审核队列/审核详情对接真实 API - 品牌方端4页面:项目列表/创建项目/项目详情/Brief配置对接真实 API - 保留 USE_MOCK 开关,mock 模式下使用类型安全的 mock 数据 - 所有页面添加 loading 骨架屏、SSE 实时更新、错误处理 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
4a3c7e7923
commit
54eaa54966
@@ -21,7 +21,7 @@ const AuthContext = createContext<AuthContextType | undefined>(undefined)
|
||||
const USER_STORAGE_KEY = 'miaosi_user'
|
||||
|
||||
// 开发模式:使用 mock 数据
|
||||
const USE_MOCK = process.env.NEXT_PUBLIC_USE_MOCK === 'true' || process.env.NODE_ENV === 'development'
|
||||
export const USE_MOCK = process.env.NEXT_PUBLIC_USE_MOCK === 'true' || process.env.NODE_ENV === 'development'
|
||||
|
||||
// Mock 用户数据
|
||||
const MOCK_USERS: Record<string, User & { password: string }> = {
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
'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 = ''
|
||||
}
|
||||
}
|
||||
}
|
||||
} 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
|
||||
}
|
||||
Reference in New Issue
Block a user