feat(frontend): Align admin and user API integration
This commit is contained in:
@@ -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()
|
||||
Reference in New Issue
Block a user