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,423 @@
import type {
AdminUser,
Category,
DashboardData,
PageResult,
PlatformComment,
PlatformUser,
PlatformVideo,
StatisticsData,
Tag,
TrendData
} from '@/types'
const now = '2026-03-22 10:00:00'
export const mockAdminUsers: AdminUser[] = [
{
id: 1,
username: 'admin',
nickname: '平台总管理员',
avatar: 'https://picsum.photos/seed/admin1/120/120',
email: 'admin@haovp.com',
phone: '13800000001',
role: 'admin',
status: 1,
lastLoginTime: now,
lastLoginIp: '127.0.0.1',
createTime: '2025-12-01 09:00:00'
},
{
id: 2,
username: 'reviewer',
nickname: '内容审核员',
avatar: 'https://picsum.photos/seed/admin2/120/120',
email: 'review@haovp.com',
phone: '13800000002',
role: 'manager',
status: 1,
lastLoginTime: '2026-03-22 08:40:00',
lastLoginIp: '10.10.0.7',
createTime: '2025-12-08 14:20:00'
}
]
export const mockCategories: Category[] = [
{ id: 1, name: '科技', parentId: 0, sort: 1, icon: 'cpu' },
{ id: 2, name: '编程开发', parentId: 1, sort: 1, icon: 'code' },
{ id: 3, name: '微服务架构', parentId: 1, sort: 2, icon: 'share' },
{ id: 4, name: '美食', parentId: 0, sort: 2, icon: 'food' },
{ id: 5, name: '校园生活', parentId: 0, sort: 3, icon: 'school' }
]
export const mockTags: Tag[] = [
{ id: 1, name: 'Vue3', useCount: 48, createTime: '2026-03-01 09:00:00' },
{ id: 2, name: 'Spring Cloud', useCount: 31, createTime: '2026-03-02 09:00:00' },
{ id: 3, name: 'FFmpeg', useCount: 17, createTime: '2026-03-03 09:00:00' },
{ id: 4, name: 'Redis', useCount: 22, createTime: '2026-03-04 09:00:00' },
{ id: 5, name: '微服务', useCount: 36, createTime: '2026-03-05 09:00:00' }
]
export const mockPlatformUsers: PlatformUser[] = [
{
id: 101,
username: 'xm_dev',
nickname: '视频达人小明',
avatar: 'https://picsum.photos/seed/user101/120/120',
email: 'xm@haovp.com',
phone: '13911112222',
gender: 1,
birthday: '1999-06-18',
signature: '专注前端工程化与系统设计',
status: 1,
followerCount: 12580,
followingCount: 256,
videoCount: 32,
likeCount: 45600,
level: 5,
vipLevel: 2,
createTime: '2025-11-18 12:00:00',
updateTime: '2026-03-21 18:20:00'
},
{
id: 102,
username: 'cloud_lab',
nickname: '科技爱好者',
avatar: 'https://picsum.photos/seed/user102/120/120',
email: 'cloud@haovp.com',
phone: '13911113333',
gender: 1,
birthday: '1998-04-22',
signature: '关注后端架构、容器化与云原生',
status: 1,
followerCount: 8920,
followingCount: 128,
videoCount: 24,
likeCount: 23400,
level: 4,
vipLevel: 1,
createTime: '2025-11-23 09:30:00',
updateTime: '2026-03-22 07:55:00'
},
{
id: 103,
username: 'food_girl',
nickname: '美食博主小美',
avatar: 'https://picsum.photos/seed/user103/120/120',
email: 'food@haovp.com',
phone: '13911114444',
gender: 2,
birthday: '2000-03-10',
signature: '记录温暖的烟火气与家常味道',
status: 1,
followerCount: 23450,
followingCount: 389,
videoCount: 56,
likeCount: 89000,
level: 6,
vipLevel: 3,
createTime: '2025-10-09 15:20:00',
updateTime: '2026-03-20 19:00:00'
},
{
id: 104,
username: 'campus_vision',
nickname: '校园影像社',
avatar: 'https://picsum.photos/seed/user104/120/120',
email: 'campus@haovp.com',
phone: '13911115555',
gender: 0,
birthday: '2001-09-01',
signature: '分享校园活动、毕业纪念与Vlog',
status: 0,
followerCount: 3120,
followingCount: 87,
videoCount: 12,
likeCount: 9520,
level: 3,
vipLevel: 0,
createTime: '2025-12-20 10:10:00',
updateTime: '2026-03-18 16:42:00'
}
]
export const mockPlatformVideos: PlatformVideo[] = [
{
id: 201,
userId: 101,
username: 'xm_dev',
userNickname: '视频达人小明',
userAvatar: 'https://picsum.photos/seed/user101/120/120',
title: 'Vue3 + TypeScript 企业级项目实战教程',
description: '从项目初始化、组件封装到状态管理与部署,完整展示前端工程化实践。',
coverUrl: 'https://picsum.photos/seed/video201/640/360',
videoUrl: 'https://www.w3schools.com/html/mov_bbb.mp4',
duration: 1845,
width: 1920,
height: 1080,
format: 'mp4',
size: 156000000,
categoryId: 2,
categoryName: '编程开发',
status: 1,
auditStatus: 1,
auditReason: '',
likeCount: 1256,
coinCount: 89,
favoriteCount: 456,
commentCount: 123,
viewCount: 15680,
shareCount: 45,
danmakuCount: 567,
isRecommend: 1,
isTop: 1,
publishTime: '2026-03-10 10:30:00',
createTime: '2026-03-10 09:00:00',
updateTime: '2026-03-10 10:30:00'
},
{
id: 202,
userId: 102,
username: 'cloud_lab',
userNickname: '科技爱好者',
userAvatar: 'https://picsum.photos/seed/user102/120/120',
title: 'Spring Cloud Gateway 与 JWT 鉴权流程演示',
description: '讲解统一网关入口、令牌校验、路由转发与限流治理的核心思路。',
coverUrl: 'https://picsum.photos/seed/video202/640/360',
videoUrl: 'https://www.w3schools.com/html/mov_bbb.mp4',
duration: 1320,
width: 1920,
height: 1080,
format: 'mp4',
size: 128000000,
categoryId: 3,
categoryName: '微服务架构',
status: 0,
auditStatus: 0,
auditReason: '',
likeCount: 665,
coinCount: 54,
favoriteCount: 212,
commentCount: 47,
viewCount: 9821,
shareCount: 28,
danmakuCount: 193,
isRecommend: 1,
isTop: 0,
publishTime: '',
createTime: '2026-03-21 21:15:00',
updateTime: '2026-03-21 21:15:00'
},
{
id: 203,
userId: 103,
username: 'food_girl',
userNickname: '美食博主小美',
userAvatar: 'https://picsum.photos/seed/user103/120/120',
title: '家常红烧肉做法 | 肥而不腻入口即化',
description: '展示食材准备、火候控制与成品摆盘过程。',
coverUrl: 'https://picsum.photos/seed/video203/640/360',
videoUrl: 'https://www.w3schools.com/html/mov_bbb.mp4',
duration: 580,
width: 1920,
height: 1080,
format: 'mp4',
size: 78000000,
categoryId: 4,
categoryName: '美食',
status: 1,
auditStatus: 1,
auditReason: '',
likeCount: 2345,
coinCount: 156,
favoriteCount: 789,
commentCount: 234,
viewCount: 45600,
shareCount: 123,
danmakuCount: 890,
isRecommend: 1,
isTop: 1,
publishTime: '2026-03-15 18:20:00',
createTime: '2026-03-15 17:40:00',
updateTime: '2026-03-15 18:20:00'
},
{
id: 204,
userId: 104,
username: 'campus_vision',
userNickname: '校园影像社',
userAvatar: 'https://picsum.photos/seed/user104/120/120',
title: '毕业季校园纪念短片',
description: '包含操场、教学楼、毕业合影等镜头,配合轻音乐剪辑。',
coverUrl: 'https://picsum.photos/seed/video204/640/360',
videoUrl: 'https://www.w3schools.com/html/mov_bbb.mp4',
duration: 420,
width: 1920,
height: 1080,
format: 'mp4',
size: 66000000,
categoryId: 5,
categoryName: '校园生活',
status: 2,
auditStatus: 2,
auditReason: '封面含未授权人物肖像,需重新提交',
likeCount: 118,
coinCount: 8,
favoriteCount: 24,
commentCount: 5,
viewCount: 1302,
shareCount: 3,
danmakuCount: 16,
isRecommend: 0,
isTop: 0,
publishTime: '',
createTime: '2026-03-19 20:20:00',
updateTime: '2026-03-20 09:30:00'
},
{
id: 205,
userId: 102,
username: 'cloud_lab',
userNickname: '科技爱好者',
userAvatar: 'https://picsum.photos/seed/user102/120/120',
title: '内容服务中的 FFmpeg 转码与 MinIO 存储实践',
description: '结合上传流程、封面提取与对象存储实现完整媒体处理链路。',
coverUrl: 'https://picsum.photos/seed/video205/640/360',
videoUrl: 'https://www.w3schools.com/html/mov_bbb.mp4',
duration: 1680,
width: 1920,
height: 1080,
format: 'mp4',
size: 189000000,
categoryId: 3,
categoryName: '微服务架构',
status: 3,
auditStatus: 1,
auditReason: '',
likeCount: 902,
coinCount: 77,
favoriteCount: 338,
commentCount: 62,
viewCount: 14220,
shareCount: 41,
danmakuCount: 288,
isRecommend: 0,
isTop: 0,
publishTime: '2026-03-08 13:00:00',
createTime: '2026-03-08 11:40:00',
updateTime: '2026-03-17 12:00:00'
}
]
export const mockPlatformComments: PlatformComment[] = [
{
id: 301,
videoId: 201,
videoTitle: 'Vue3 + TypeScript 企业级项目实战教程',
userId: 102,
userNickname: '科技爱好者',
userAvatar: 'https://picsum.photos/seed/user102/120/120',
parentId: 0,
rootId: 0,
content: '网关鉴权和前端权限路由那一段讲得很清楚,适合论文演示。',
likeCount: 156,
replyCount: 5,
isTop: 1,
status: 1,
createTime: '2026-03-20 09:30:00'
},
{
id: 302,
videoId: 202,
videoTitle: 'Spring Cloud Gateway 与 JWT 鉴权流程演示',
userId: 101,
userNickname: '视频达人小明',
userAvatar: 'https://picsum.photos/seed/user101/120/120',
parentId: 0,
rootId: 0,
content: '建议补一段 Sentinel 限流效果的说明,会更完整。',
likeCount: 48,
replyCount: 1,
isTop: 0,
status: 1,
createTime: '2026-03-21 10:12:00'
},
{
id: 303,
videoId: 203,
videoTitle: '家常红烧肉做法 | 肥而不腻入口即化',
userId: 104,
userNickname: '校园影像社',
userAvatar: 'https://picsum.photos/seed/user104/120/120',
parentId: 0,
rootId: 0,
content: '成品看起来很不错,步骤也很清晰。',
likeCount: 27,
replyCount: 0,
isTop: 0,
status: 1,
createTime: '2026-03-16 19:12:00'
},
{
id: 304,
videoId: 204,
videoTitle: '毕业季校园纪念短片',
userId: 103,
userNickname: '美食博主小美',
userAvatar: 'https://picsum.photos/seed/user103/120/120',
parentId: 0,
rootId: 0,
content: '该评论因包含联系方式已被系统删除。',
likeCount: 3,
replyCount: 0,
isTop: 0,
status: 0,
createTime: '2026-03-20 08:20:00'
}
]
export const mockStatistics: StatisticsData = {
totalUsers: 18324,
newUsersToday: 126,
activeUsersToday: 4280,
userGrowthRate: 8.6,
totalVideos: 2648,
newVideosToday: 43,
pendingReview: 7,
videoGrowthRate: 6.2,
totalViews: 5823400,
viewsToday: 128600,
viewGrowthRate: 12.4,
totalLikes: 986300,
totalComments: 138500,
totalDanmakus: 94200,
userTrend: createTrend([58, 72, 69, 88, 95, 110, 126]),
videoTrend: createTrend([18, 22, 25, 31, 29, 37, 43]),
viewTrend: createTrend([86200, 94500, 90800, 103200, 118600, 121000, 128600])
}
export const mockDashboardData: DashboardData = {
statistics: mockStatistics,
hotVideos: [...mockPlatformVideos].sort((a, b) => b.viewCount - a.viewCount).slice(0, 4),
recentComments: [...mockPlatformComments].sort((a, b) => b.createTime.localeCompare(a.createTime)).slice(0, 4),
recentUsers: [...mockPlatformUsers].sort((a, b) => b.createTime.localeCompare(a.createTime)).slice(0, 4)
}
export function paginate<T>(list: T[], page = 1, size = 10): PageResult<T> {
const start = (page - 1) * size
const records = list.slice(start, start + size)
return {
records,
total: list.length,
size,
current: page,
pages: Math.max(1, Math.ceil(list.length / size))
}
}
function createTrend(values: number[]): TrendData[] {
return ['03-16', '03-17', '03-18', '03-19', '03-20', '03-21', '03-22'].map((date, index) => ({
date,
value: values[index]
}))
}
@@ -0,0 +1,308 @@
import {
mockAdminUsers,
mockCategories,
mockDashboardData,
mockPlatformComments,
mockPlatformUsers,
mockPlatformVideos,
mockStatistics,
mockTags,
paginate
} from './data'
const delay = (ms = 120) => new Promise((resolve) => setTimeout(resolve, ms))
const mockStore = {
adminUsers: [...mockAdminUsers],
users: [...mockPlatformUsers],
videos: [...mockPlatformVideos],
comments: [...mockPlatformComments],
categories: [...mockCategories],
tags: [...mockTags]
}
function normalize(url: string) {
return url.replace(/^\/api\/admin/, '')
}
function matchPath(path: string, pattern: string) {
const pathParts = path.split('/').filter(Boolean)
const patternParts = pattern.split('/').filter(Boolean)
if (pathParts.length !== patternParts.length) return false
return patternParts.every((part, index) => part.startsWith(':') || part === pathParts[index])
}
function pathParam(path: string, indexFromEnd = 1) {
const parts = path.split('/').filter(Boolean)
return Number(parts[parts.length - indexFromEnd])
}
function filterByKeyword<T>(list: T[], getter: (item: T) => string, keyword?: string) {
if (!keyword) return list
const needle = keyword.toLowerCase()
return list.filter((item) => getter(item).toLowerCase().includes(needle))
}
export async function mockRequest(url: string, options: any = {}) {
const method = (options.method || 'GET').toUpperCase()
const path = normalize(url).split('?')[0]
const params = options.params || {}
const body = options.body || {}
await delay()
if (method === 'GET' && path === '/auth/captcha') {
return ok({
captchaKey: `admin-captcha-${Date.now()}`,
captchaImage: `data:image/svg+xml;utf8,${encodeURIComponent(`
<svg xmlns="http://www.w3.org/2000/svg" width="120" height="40" viewBox="0 0 120 40">
<rect width="120" height="40" rx="8" fill="#eef2ff"/>
<path d="M8 27 C22 9, 40 34, 56 12 S88 34, 112 10" stroke="#c7d2fe" stroke-width="2" fill="none"/>
<text x="16" y="28" fill="#1d4ed8" font-size="24" font-family="Georgia" transform="rotate(-8 16 28)">2</text>
<text x="40" y="28" fill="#0f766e" font-size="24" font-family="Georgia" transform="rotate(7 40 28)">4</text>
<text x="66" y="28" fill="#be185d" font-size="24" font-family="Georgia" transform="rotate(-6 66 28)">6</text>
<text x="92" y="28" fill="#7c3aed" font-size="24" font-family="Georgia" transform="rotate(8 92 28)">8</text>
</svg>
`)}`
})
}
if (method === 'POST' && path === '/auth/login') {
return ok({
accessToken: `admin-token-${Date.now()}`,
refreshToken: `admin-refresh-${Date.now()}`,
tokenType: 'Bearer',
expiresIn: 7200,
adminId: 1,
username: body.username || 'admin'
})
}
if (method === 'POST' && path === '/auth/logout') {
return ok(null)
}
if (method === 'GET' && path === '/auth/info') {
return ok(mockStore.adminUsers[0])
}
if (method === 'GET' && path === '/statistics/dashboard') {
return ok(mockDashboardData)
}
if (method === 'GET' && path === '/statistics/overview') {
return ok(mockStatistics)
}
if (method === 'GET' && path === '/statistics/user/trend') {
return ok(mockStatistics.userTrend)
}
if (method === 'GET' && path === '/statistics/video/trend') {
return ok(mockStatistics.videoTrend)
}
if (method === 'GET' && path === '/statistics/view/trend') {
return ok(mockStatistics.viewTrend)
}
if (method === 'GET' && path === '/platform/user/stats') {
const { totalUsers, newUsersToday, activeUsersToday, userGrowthRate } = mockStatistics
return ok({ totalUsers, newUsersToday, activeUsersToday, userGrowthRate })
}
if (method === 'GET' && path === '/platform/content/video/stats') {
const { totalVideos, newVideosToday, pendingReview, videoGrowthRate } = mockStatistics
return ok({ totalVideos, newVideosToday, pendingReview, videoGrowthRate })
}
if (method === 'GET' && path === '/admin/stats') {
return ok({
totalComments: mockStatistics.totalComments,
commentsToday: 84,
commentGrowthRate: 5.8
})
}
if (method === 'GET' && path === '/platform/user/list') {
let list = [...mockStore.users]
list = filterByKeyword(list, (item) => `${item.username} ${item.nickname} ${item.phone}`, params.keyword)
if (params.status !== undefined && params.status !== '') {
list = list.filter((item) => item.status === Number(params.status))
}
return ok(paginate(list, Number(params.page || 1), Number(params.size || 10)))
}
if (method === 'GET' && matchPath(path, '/platform/user/:id')) {
return ok(mockStore.users.find((item) => item.id === pathParam(path)) || mockStore.users[0])
}
if (method === 'PUT' && matchPath(path, '/platform/user/:id/status/:status')) {
const user = mockStore.users.find((item) => item.id === pathParam(path, 2))
if (user) user.status = pathParam(path)
return ok(null)
}
if (method === 'PUT' && matchPath(path, '/platform/user/:id/reset-password')) {
return ok(null)
}
if (method === 'GET' && path === '/platform/content/video/list') {
let list = [...mockStore.videos]
list = filterByKeyword(list, (item) => item.title, params.keyword)
if (params.status !== undefined && params.status !== '') {
list = list.filter((item) => item.status === Number(params.status))
}
if (params.auditStatus !== undefined && params.auditStatus !== '') {
list = list.filter((item) => item.auditStatus === Number(params.auditStatus))
}
if (params.categoryId) {
list = list.filter((item) => item.categoryId === Number(params.categoryId))
}
return ok(paginate(list, Number(params.page || 1), Number(params.size || 10)))
}
if (method === 'GET' && matchPath(path, '/platform/content/video/:id')) {
return ok(mockStore.videos.find((item) => item.id === pathParam(path)) || mockStore.videos[0])
}
if (method === 'POST' && matchPath(path, '/platform/content/video/review/:id')) {
const video = mockStore.videos.find((item) => item.id === pathParam(path))
if (video) {
video.auditStatus = Number(body.status)
video.auditReason = body.reason || ''
video.status = Number(body.status) === 1 ? 1 : 2
video.publishTime = Number(body.status) === 1 ? '2026-03-22 10:30:00' : ''
}
return ok(null)
}
if (method === 'PUT' && matchPath(path, '/platform/content/video/disable/:id')) {
const video = mockStore.videos.find((item) => item.id === pathParam(path))
if (video) video.status = 3
return ok(null)
}
if (method === 'GET' && path === '/admin/list') {
let list = [...mockStore.comments]
list = filterByKeyword(list, (item) => item.content, params.keyword)
if (params.status !== undefined && params.status !== '') {
list = list.filter((item) => item.status === Number(params.status))
}
return ok(paginate(list, Number(params.page || 1), Number(params.size || 10)))
}
if (method === 'DELETE' && matchPath(path, '/admin/:id')) {
const comment = mockStore.comments.find((item) => item.id === pathParam(path))
if (comment) comment.status = 0
return ok(null)
}
if (method === 'GET' && path === '/platform/content/category/list') {
return ok([...mockStore.categories].sort((a, b) => a.sort - b.sort))
}
if (method === 'POST' && path === '/platform/content/category') {
mockStore.categories.push({
id: mockStore.categories.length + 10,
name: body.name,
parentId: body.parentId || 0,
sort: body.sort || 0,
icon: body.icon || ''
})
return ok(null)
}
if (method === 'PUT' && path === '/platform/content/category') {
const category = mockStore.categories.find((item) => item.id === body.id)
if (category) Object.assign(category, body)
return ok(null)
}
if (method === 'DELETE' && matchPath(path, '/platform/content/category/:id')) {
mockStore.categories = mockStore.categories.filter((item) => item.id !== pathParam(path))
return ok(null)
}
if (method === 'GET' && path === '/content/tag/page') {
const list = filterByKeyword([...mockStore.tags], (item) => item.name, params.keyword)
return ok(paginate(list, Number(params.page || 1), Number(params.size || 10)))
}
if (method === 'POST' && path === '/content/tag') {
mockStore.tags.unshift({
id: mockStore.tags.length + 1,
name: body.name,
useCount: 0,
createTime: '2026-03-22 10:00:00'
})
return ok(null)
}
if (method === 'PUT' && path === '/content/tag') {
const tag = mockStore.tags.find((item) => item.id === body.id)
if (tag) tag.name = body.name
return ok(null)
}
if (method === 'DELETE' && matchPath(path, '/content/tag/:id')) {
mockStore.tags = mockStore.tags.filter((item) => item.id !== pathParam(path))
return ok(null)
}
if (method === 'GET' && path === '/user/list') {
const list = filterByKeyword([...mockStore.adminUsers], (item) => `${item.username} ${item.nickname}`, params.keyword)
return ok(paginate(list, Number(params.page || 1), Number(params.size || 10)))
}
if (method === 'POST' && path === '/user') {
mockStore.adminUsers.push({
id: mockStore.adminUsers.length + 1,
username: body.username,
nickname: body.nickname,
avatar: 'https://picsum.photos/seed/newadmin/120/120',
email: body.email || '',
phone: body.phone || '',
role: body.role || 'manager',
status: 1,
lastLoginTime: '',
lastLoginIp: '',
createTime: '2026-03-22 10:00:00'
})
return ok(null)
}
if (method === 'PUT' && path === '/user') {
const admin = mockStore.adminUsers.find((item) => item.id === body.id)
if (admin) Object.assign(admin, body)
return ok(null)
}
if (method === 'PUT' && matchPath(path, '/user/:id/status/:status')) {
const admin = mockStore.adminUsers.find((item) => item.id === pathParam(path, 2))
if (admin) admin.status = pathParam(path)
return ok(null)
}
if (method === 'PUT' && matchPath(path, '/user/:id/reset-password')) {
return ok(null)
}
return {
code: 404,
message: `未找到 Mock 处理器: ${method} ${path}`,
data: null
}
}
function ok(data: any) {
return {
code: 200,
message: 'success',
data
}
}
export function isMockEnabled() {
return import.meta.env.VITE_MOCK === 'true' || import.meta.env.VITE_MOCK === true
}