604 lines
17 KiB
TypeScript
604 lines
17 KiB
TypeScript
/**
|
|
* 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 || {}
|
|
const 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 前缀)
|
|
const 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()
|