feat(frontend): Align admin and user API integration

This commit is contained in:
2026-04-18 18:21:31 +08:00
parent 3adb088eca
commit 2247d235ec
74 changed files with 8626 additions and 647 deletions
@@ -0,0 +1,603 @@
/**
* Mock 拦截器 - 拦截所有 API 请求并返回模拟数据
* 用于无后端情况下的前端界面展示
*/
import {
mockUsers,
mockVideos,
mockCategories,
mockComments,
mockFollowing,
mockFollowers,
mockFavorites,
mockDanmaku,
mockCaptcha,
mockCurrentUser,
mockMessages,
mockNotifications,
mockSettings,
paginate
} from './data'
// Mock 存储状态
const mockStore = {
isLoggedIn: true,
currentUserId: 1,
videos: [...mockVideos],
comments: [...mockComments],
userVideos: { 1: [...mockVideos.filter(v => v.userId === 1)], 2: [...mockVideos.filter(v => v.userId === 2)], 3: [...mockVideos.filter(v => v.userId === 3)] },
messages: [...mockMessages],
notifications: [...mockNotifications],
settings: { ...mockSettings }
}
// 延迟函数,模拟网络延迟
const delay = (ms: number = 100) => new Promise(resolve => setTimeout(resolve, ms))
// Mock API 处理器映射
const mockHandlers: Record<string, (url: string, options: any) => Promise<any>> = {
// ==================== 认证相关 ====================
'GET /auth/captcha': async () => {
await delay()
return mockCaptcha
},
'POST /auth/login': async () => {
await delay(200)
return {
accessToken: 'mock-access-token-' + Date.now(),
refreshToken: 'mock-refresh-token-' + Date.now(),
tokenType: 'Bearer',
expiresIn: 7200,
userId: 1,
username: 'testuser'
}
},
'POST /auth/register': async () => {
await delay(200)
return '注册成功'
},
'POST /auth/logout': async () => {
await delay()
return null
},
'POST /auth/refresh': async () => {
await delay()
return {
accessToken: 'mock-access-token-refreshed-' + Date.now(),
refreshToken: 'mock-refresh-token-refreshed-' + Date.now(),
tokenType: 'Bearer',
expiresIn: 7200,
userId: 1,
username: 'testuser'
}
},
'GET /auth/current': async () => {
await delay()
return mockCurrentUser
},
'GET /auth/username/:id': async (url: string) => {
await delay()
const id = parseInt(url.split('/').pop() || '0')
const user = mockUsers.find(u => u.id === id)
return user?.nickname || '未知用户'
},
// ==================== 用户相关 ====================
'GET /user/profile/current': async () => {
await delay()
return mockUsers.find(u => u.id === mockStore.currentUserId) || mockUsers[0]
},
'GET /user/profile/public/:id': async (url: string) => {
await delay()
const id = parseInt(url.split('/').pop() || '0')
return mockUsers.find(u => u.id === id) || mockUsers[0]
},
'PUT /user/profile': async (_url: string, options: any) => {
await delay(200)
const user = mockUsers.find(u => u.id === mockStore.currentUserId)
if (user && options.body) {
Object.assign(user, options.body)
}
return null
},
'POST /user/profile/avatar': async () => {
await delay(500)
return 'https://picsum.photos/100/100?random=' + Date.now()
},
'POST /user/follow': async (_url: string) => {
await delay(100)
return null
},
'DELETE /user/follow': async (_url: string) => {
await delay(100)
return null
},
'GET /user/follow/check/:id': async (url: string) => {
await delay()
const id = parseInt(url.split('/').pop() || '0')
return mockFollowing.some(f => f.id === id)
},
'GET /user/follow/following': async (_url: string) => {
await delay()
return mockFollowing
},
'GET /user/follow/followers': async (_url: string) => {
await delay()
return mockFollowers
},
'GET /user/message/list': async () => {
await delay()
return mockStore.messages
.filter(item => item.senderId !== mockStore.currentUserId)
.sort((a, b) => new Date(b.createTime).getTime() - new Date(a.createTime).getTime())
},
'GET /user/message/detail/:id': async (url: string) => {
await delay()
const targetId = parseInt(url.split('/').pop() || '0')
return mockStore.messages
.filter(item =>
(item.senderId === targetId && item.receiverId === mockStore.currentUserId) ||
(item.senderId === mockStore.currentUserId && item.receiverId === targetId)
)
.sort((a, b) => new Date(a.createTime).getTime() - new Date(b.createTime).getTime())
},
'POST /user/message': async (_url: string, options: any) => {
await delay(120)
const body = options.body || {}
const newMessage = {
id: mockStore.messages.length + 1,
senderId: mockStore.currentUserId,
senderNickname: mockUsers.find(u => u.id === mockStore.currentUserId)?.nickname || '用户',
senderAvatar: mockUsers.find(u => u.id === mockStore.currentUserId)?.avatar || '',
receiverId: body.receiverId,
receiverNickname: mockUsers.find(u => u.id === body.receiverId)?.nickname || '用户',
receiverAvatar: mockUsers.find(u => u.id === body.receiverId)?.avatar || '',
content: body.content,
isRead: 1,
createTime: new Date().toISOString()
}
mockStore.messages.push(newMessage)
return newMessage
},
'PUT /user/message/read/:id': async () => {
await delay()
return null
},
'GET /user/message/unread/count': async () => {
await delay()
return mockStore.messages.filter(item => item.receiverId === mockStore.currentUserId && item.isRead === 0).length
},
'GET /user/notification/list': async (_url: string, options: any) => {
await delay()
const { type } = options.params || {}
if (!type) return mockStore.notifications
return mockStore.notifications.filter(item => item.type === type)
},
'PUT /user/notification/read': async () => {
await delay()
mockStore.notifications.forEach(item => {
item.isRead = 1
})
return null
},
'PUT /user/notification/read/:id': async (url: string) => {
await delay()
const id = parseInt(url.split('/').pop() || '0')
const notification = mockStore.notifications.find(item => item.id === id)
if (notification) notification.isRead = 1
return null
},
'GET /user/notification/unread/count': async () => {
await delay()
return mockStore.notifications.filter(item => item.isRead === 0).length
},
'GET /user/setting': async () => {
await delay()
return mockStore.settings
},
'PUT /user/setting': async (_url: string, options: any) => {
await delay()
Object.assign(mockStore.settings, options.body || {})
return null
},
// ==================== 视频相关 ====================
'GET /content/video/public/:id': async (url: string) => {
await delay()
const id = parseInt(url.split('/').pop() || '0')
return mockVideos.find(v => v.id === id) || mockVideos[0]
},
'GET /content/video/detail': async (url: string) => {
await delay()
const id = parseInt(url.split('/').pop() || '0')
return mockVideos.find(v => v.id === id) || mockVideos[0]
},
'GET /content/video/public/list': async (_url: string, options: any) => {
await delay()
const params = options.params || {}
let videos = [...mockVideos]
// 按分类筛选
if (params.categoryId) {
videos = videos.filter(v => v.categoryId === params.categoryId)
}
// 按关键词搜索
if (params.keyword) {
const keyword = params.keyword.toLowerCase()
videos = videos.filter(v =>
v.title.toLowerCase().includes(keyword) ||
v.description.toLowerCase().includes(keyword)
)
}
return paginate(videos, params.pageNum || 1, params.pageSize || 20)
},
'GET /content/video/list': async (_url: string, options: any) => {
await delay()
const params = options.params || {}
return paginate(mockVideos, params.pageNum || 1, params.pageSize || 20)
},
'GET /content/video/user/:id': async (url: string, options: any) => {
await delay()
const userId = parseInt(url.split('/').pop() || '0')
const params = options.params || {}
const videos = mockVideos.filter(v => v.userId === userId)
return paginate(videos, params.pageNum || params.page || 1, params.pageSize || params.size || 20)
},
'POST /content/video': async (_url: string, options: any) => {
await delay(300)
const body = options.body || {}
const newVideo = {
id: mockVideos.length + 1,
userId: mockStore.currentUserId,
title: body.title || '新视频',
description: body.description || '',
coverUrl: body.coverUrl || 'https://picsum.photos/320/180?random=' + Date.now(),
videoUrl: body.videoUrl || '',
duration: body.duration || 0,
width: body.width || 1920,
height: body.height || 1080,
format: body.format || 'mp4',
size: body.size || 0,
categoryId: body.categoryId || 1,
status: 0,
likeCount: 0,
coinCount: 0,
favoriteCount: 0,
commentCount: 0,
viewCount: 0,
shareCount: 0,
danmakuCount: 0,
isRecommend: 0,
isTop: 0,
publishTime: new Date().toISOString(),
createTime: new Date().toISOString(),
updateTime: new Date().toISOString(),
tagIds: body.tagIds || [],
tagNames: [],
categoryName: '科技',
userNickname: mockUsers.find(u => u.id === mockStore.currentUserId)?.nickname || '用户',
userAvatar: mockUsers.find(u => u.id === mockStore.currentUserId)?.avatar || '',
isLiked: false,
isFavorited: false,
isCoined: false
}
mockVideos.unshift(newVideo)
return newVideo
},
'POST /content/video/upload': async () => {
await delay(1000)
return {
videoUrl: 'https://www.w3schools.com/html/mov_bbb.mp4',
coverUrl: 'https://picsum.photos/320/180?random=' + Date.now()
}
},
'POST /content/video/like': async () => {
await delay(100)
return null
},
'DELETE /content/video/like': async () => {
await delay(100)
return null
},
'POST /content/video/coin': async () => {
await delay(100)
return null
},
'POST /content/video/favorite': async () => {
await delay(100)
return null
},
'DELETE /content/video/favorite': async () => {
await delay(100)
return null
},
'POST /content/video/share': async () => {
await delay(100)
return null
},
'POST /content/video/play': async () => {
await delay(50)
return null
},
// ==================== 分类相关 ====================
'GET /content/category/tree': async () => {
await delay()
return mockCategories
},
'GET /content/category/list': async () => {
await delay()
return mockCategories
},
// ==================== 评论相关 ====================
'GET /comment/public/list': async (_url: string, options: any) => {
await delay()
const params = options.params || {}
let comments = mockComments.filter(c => c.videoId === params.videoId)
if (params.sortBy === 'hot') {
comments.sort((a, b) => b.likeCount - a.likeCount)
} else {
comments.sort((a, b) => new Date(b.createTime).getTime() - new Date(a.createTime).getTime())
}
return paginate(comments, params.pageNum || 1, params.pageSize || 20)
},
'POST /comment': async (_url: string, options: any) => {
await delay(200)
const body = options.body || {}
const newComment = {
id: mockComments.length + 1,
videoId: body.videoId,
userId: mockStore.currentUserId,
parentId: body.parentId || 0,
rootId: body.rootId || 0,
content: body.content,
likeCount: 0,
replyCount: 0,
isTop: 0,
createTime: new Date().toISOString(),
userNickname: mockUsers.find(u => u.id === mockStore.currentUserId)?.nickname || '用户',
userAvatar: mockUsers.find(u => u.id === mockStore.currentUserId)?.avatar || '',
isLiked: false
}
mockComments.unshift(newComment)
return newComment
},
'POST /comment/like': async () => {
await delay(100)
return null
},
'DELETE /comment/like': async () => {
await delay(100)
return null
},
// ==================== 弹幕相关 ====================
'GET /content/danmaku/all/:id': async (url: string) => {
await delay()
const videoId = parseInt(url.split('/').pop() || '0')
return mockDanmaku.filter(d => d.videoId === videoId || videoId === 0)
},
'POST /content/danmaku': async (_url: string, _options: any) => {
await delay(100)
return null
},
'GET /content/danmaku/time': async (_url: string, options: any) => {
await delay()
const params = options.params || {}
return mockDanmaku.filter(d => d.videoId === params.videoId)
},
// ==================== 收藏相关 ====================
'GET /content/favorite/list': async () => {
await delay()
return mockFavorites
},
'POST /content/favorite': async (_url: string, options: any) => {
await delay(100)
const body = options.body || {}
const newFavorite = {
id: mockFavorites.length + 1,
userId: mockStore.currentUserId,
name: body.name || '新收藏夹',
description: body.description || '',
coverUrl: '',
visibility: body.visibility || 1,
videoCount: 0,
createTime: new Date().toISOString()
}
mockFavorites.push(newFavorite)
return newFavorite
},
'GET /content/favorite': async (_url: string, options: any) => {
await delay()
const params = options.params || {}
return paginate(mockVideos.slice(0, 5), params.pageNum || 1, params.pageSize || 20)
},
'GET /content/favorite/:id/videos': async (url: string, options: any) => {
await delay()
const favoriteId = Number(url.split('/').slice(-2, -1)[0] || '1')
const params = options.params || {}
const base = favoriteId === 1 ? mockVideos.slice(0, 4) : mockVideos.slice(4, 9)
return paginate(base, params.pageNum || 1, params.pageSize || 20)
},
// ==================== 推荐相关 ====================
'GET /recommend/home': async (_url: string, options: any) => {
await delay()
const params = options.params || {}
const videos = mockVideos.filter(v => v.isRecommend === 1)
const page = params.page || 1
const size = params.size || 20
const start = (page - 1) * size
return videos.slice(start, start + size)
},
'GET /recommend/hot': async (_url: string, options: any) => {
await delay()
const params = options.params || {}
const videos = [...mockVideos].sort((a, b) => b.viewCount - a.viewCount)
const size = params.size || 20
return videos.slice(0, size)
},
'GET /recommend/similar/:id': async (_url: string, options: any) => {
await delay()
const params = options.params || {}
const size = params.size || params.limit || 10
return mockVideos.slice(0, size)
},
// ==================== 搜索相关 ====================
'GET /search/video': async (_url: string, options: any) => {
await delay()
const params = options.params || {}
let videos = [...mockVideos]
if (params.keyword) {
const keyword = params.keyword.toLowerCase()
videos = videos.filter(v =>
v.title.toLowerCase().includes(keyword) ||
v.description.toLowerCase().includes(keyword)
)
}
return paginate(videos, params.pageNum || 1, params.pageSize || 20)
},
// ==================== 用户视频 ====================
'GET /user/video': async (_url: string, options: any) => {
await delay()
const userId = mockStore.currentUserId
const params = options.params || {}
const videos = mockVideos.filter(v => v.userId === userId)
return paginate(videos, params.pageNum || 1, params.pageSize || 20)
}
}
// 匹配 Mock 处理器
function matchHandler(url: string, method: string): ((url: string, options: any) => Promise<any>) | null {
// 标准化 URL(移除查询参数和 /api 前缀)
let normalizedUrl = url.replace(/^\/api/, '').split('?')[0]
// 构建匹配模式
const patterns = Object.keys(mockHandlers)
for (const pattern of patterns) {
const [patternMethod, patternPath] = pattern.split(' ')
// 检查方法是否匹配
if (patternMethod !== method.toUpperCase()) continue
// 检查路径是否匹配
const patternParts = patternPath.split('/')
const urlParts = normalizedUrl.split('/')
if (patternParts.length !== urlParts.length) continue
let matched = true
for (let i = 0; i < patternParts.length; i++) {
if (patternParts[i] !== urlParts[i] && !patternParts[i].startsWith(':')) {
matched = false
break
}
}
if (matched) {
return mockHandlers[pattern]
}
}
return null
}
// Mock 请求处理函数
export async function mockRequest(url: string, options: any = {}): Promise<any> {
const method = options.method || 'GET'
// 查找匹配的处理器
const handler = matchHandler(url, method)
if (handler) {
console.log(`[Mock] ${method} ${url}`)
try {
const result = await handler(url, options)
return {
code: 200,
message: 'success',
data: result
}
} catch (error) {
console.error(`[Mock Error] ${method} ${url}`, error)
return {
code: 500,
message: 'Mock 处理错误',
data: null
}
}
}
// 未找到处理器,返回默认响应
console.warn(`[Mock] No handler for ${method} ${url}`)
return {
code: 404,
message: '未找到对应的 Mock 处理器',
data: null
}
}
// 判断是否启用 Mock
export function isMockEnabled(): boolean {
return import.meta.env.VITE_MOCK === 'true' || import.meta.env.VITE_MOCK === true
}
// 导出 Mock 开关
export const mockEnabled = isMockEnabled()