feat: 完整版 uni-app 前端 + 后端 API 改造
后端:
- 新增知识搜索 API GET /api/knowledge/search
- 成就 API 改造为 AI 主题(知识探索/收藏/AI问答/社交)
前端新增 7 个页面:
- favorites (我的收藏)
- achievements (成就)
- leaderboard (排行榜)
- search (搜索)
- progress (学习进度)
- feedback (意见反馈)
- settings (设置)
- news (AI资讯详情)
完善:
- 个人中心导航全部接真实页面,清除所有'开发中'stub
- index 搜索按钮 → search 页
- trend 卡片点击 → news 详情页
- api.js 新增 feedbackApi + knowledgeApi.search
- userApi.achievements 端点
构建验证: ✅ DONE Build complete (16 pages, 16 bundles)
This commit is contained in:
@@ -5,6 +5,45 @@ const ApiResponse = require('../utils/response')
|
||||
const { userAuthMiddleware } = require('../middleware/auth')
|
||||
const logger = require('../utils/logger')
|
||||
|
||||
/**
|
||||
* 搜索知识
|
||||
* GET /api/knowledge/search?q=xxx&page=1&limit=20
|
||||
*/
|
||||
router.get('/search', async (req, res) => {
|
||||
try {
|
||||
const { q, page = 1, limit = 20 } = req.query
|
||||
if (!q || q.trim().length === 0) {
|
||||
return ApiResponse.error(res, '请输入搜索关键词', 400)
|
||||
}
|
||||
|
||||
const query = {
|
||||
status: 'approved',
|
||||
$or: [
|
||||
{ title: { $regex: q, $options: 'i' } },
|
||||
{ content: { $regex: q, $options: 'i' } },
|
||||
{ tags: { $regex: q, $options: 'i' } }
|
||||
]
|
||||
}
|
||||
|
||||
const total = await Knowledge.countDocuments(query)
|
||||
const result = await Knowledge.find(query)
|
||||
.sort({ viewCount: -1, createdAt: -1 })
|
||||
.skip((parseInt(page) - 1) * parseInt(limit))
|
||||
.limit(parseInt(limit))
|
||||
.lean()
|
||||
|
||||
return ApiResponse.paginated(res, result, {
|
||||
page: parseInt(page),
|
||||
limit: parseInt(limit),
|
||||
total
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
logger.error('搜索知识失败:', error)
|
||||
return ApiResponse.serverError(res, error.message)
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 获取知识列表
|
||||
* GET /api/knowledge
|
||||
|
||||
@@ -202,133 +202,133 @@ router.get('/achievements', async (req, res) => {
|
||||
try {
|
||||
const user = req.user
|
||||
|
||||
// 生成成就数据
|
||||
const achievements = [
|
||||
{
|
||||
name: '探索成就',
|
||||
achievements: [
|
||||
// 生成成就数据(AI 维度主题)
|
||||
const achievements = [
|
||||
{
|
||||
id: 'dim1_complete',
|
||||
name: '一维探索者',
|
||||
description: '完成一维空间的探索',
|
||||
icon: '⭐',
|
||||
unlocked: user.exploreData.dim1?.completed || false,
|
||||
date: user.exploreData.dim1?.completed ? user.updatedAt.toISOString().split('T')[0] : null
|
||||
name: '知识探索',
|
||||
achievements: [
|
||||
{
|
||||
id: 'dim1_read',
|
||||
name: 'AI起源探索者',
|
||||
description: '完成 AI 起源维度的知识阅读',
|
||||
icon: '📜',
|
||||
unlocked: user.exploreData.dim1?.completed || false,
|
||||
date: user.exploreData.dim1?.completed ? user.updatedAt.toISOString().split('T')[0] : null
|
||||
},
|
||||
{
|
||||
id: 'dim2_read',
|
||||
name: 'AI发展探索者',
|
||||
description: '完成 AI 发展维度的知识阅读',
|
||||
icon: '🏛',
|
||||
unlocked: user.exploreData.dim2?.completed || false,
|
||||
date: user.exploreData.dim2?.completed ? user.updatedAt.toISOString().split('T')[0] : null
|
||||
},
|
||||
{
|
||||
id: 'dim3_read',
|
||||
name: 'AI当前探索者',
|
||||
description: '完成 AI 当前格局维度的知识阅读',
|
||||
icon: '♟',
|
||||
unlocked: user.exploreData.dim3?.completed || false,
|
||||
date: user.exploreData.dim3?.completed ? user.updatedAt.toISOString().split('T')[0] : null
|
||||
},
|
||||
{
|
||||
id: 'dim4_read',
|
||||
name: 'AI学习探索者',
|
||||
description: '完成 AI 学习与工具维度的知识阅读',
|
||||
icon: '🧠',
|
||||
unlocked: user.exploreData.dim4?.completed || false,
|
||||
date: user.exploreData.dim4?.completed ? user.updatedAt.toISOString().split('T')[0] : null
|
||||
},
|
||||
{
|
||||
id: 'dim5_read',
|
||||
name: 'AI趋势探索者',
|
||||
description: '完成 AI 趋势追踪维度的知识阅读',
|
||||
icon: '⚡',
|
||||
unlocked: user.exploreData.dim5?.completed || false,
|
||||
date: user.exploreData.dim5?.completed ? user.updatedAt.toISOString().split('T')[0] : null
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'dim2_complete',
|
||||
name: '二维探索者',
|
||||
description: '完成二维空间的探索',
|
||||
icon: '✨',
|
||||
unlocked: user.exploreData.dim2?.completed || false,
|
||||
date: user.exploreData.dim2?.completed ? user.updatedAt.toISOString().split('T')[0] : null
|
||||
name: '收藏成就',
|
||||
achievements: [
|
||||
{
|
||||
id: 'first_collection',
|
||||
name: '初次收藏',
|
||||
description: '收藏你的第一个 AI 知识',
|
||||
icon: '❤️',
|
||||
unlocked: user.collectedKnowledge?.length > 0,
|
||||
date: user.collectedKnowledge?.length > 0 ? user.updatedAt.toISOString().split('T')[0] : null
|
||||
},
|
||||
{
|
||||
id: 'five_collections',
|
||||
name: '收藏爱好者',
|
||||
description: '收藏5个 AI 知识',
|
||||
icon: '🧡',
|
||||
unlocked: user.collectedKnowledge?.length >= 5,
|
||||
date: user.collectedKnowledge?.length >= 5 ? user.updatedAt.toISOString().split('T')[0] : null
|
||||
},
|
||||
{
|
||||
id: 'ten_collections',
|
||||
name: '收藏大师',
|
||||
description: '收藏10个 AI 知识',
|
||||
icon: '💛',
|
||||
unlocked: user.collectedKnowledge?.length >= 10,
|
||||
date: user.collectedKnowledge?.length >= 10 ? user.updatedAt.toISOString().split('T')[0] : null
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'dim3_complete',
|
||||
name: '三维探索者',
|
||||
description: '完成三维空间的探索',
|
||||
icon: '🌟',
|
||||
unlocked: user.exploreData.dim3?.completed || false,
|
||||
date: user.exploreData.dim3?.completed ? user.updatedAt.toISOString().split('T')[0] : null
|
||||
name: 'AI 问答',
|
||||
achievements: [
|
||||
{
|
||||
id: 'first_ask',
|
||||
name: '初次提问',
|
||||
description: '向 AI 助手提出第一个问题',
|
||||
icon: '🤖',
|
||||
unlocked: user.achievements?.includes('first_ask') || false,
|
||||
date: user.achievements?.includes('first_ask') ? user.updatedAt.toISOString().split('T')[0] : null
|
||||
},
|
||||
{
|
||||
id: 'ten_asks',
|
||||
name: '勤学好问',
|
||||
description: '向 AI 助手提出 10 个问题',
|
||||
icon: '💡',
|
||||
unlocked: user.achievements?.includes('ten_asks') || false,
|
||||
date: user.achievements?.includes('ten_asks') ? user.updatedAt.toISOString().split('T')[0] : null
|
||||
},
|
||||
{
|
||||
id: 'fifty_asks',
|
||||
name: 'AI 达人',
|
||||
description: '向 AI 助手提出 50 个问题',
|
||||
icon: '🧠',
|
||||
unlocked: user.achievements?.includes('fifty_asks') || false,
|
||||
date: user.achievements?.includes('fifty_asks') ? user.updatedAt.toISOString().split('T')[0] : null
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'dim4_complete',
|
||||
name: '四维探索者',
|
||||
description: '完成四维空间的探索',
|
||||
icon: '💫',
|
||||
unlocked: user.exploreData.dim4?.completed || false,
|
||||
date: user.exploreData.dim4?.completed ? user.updatedAt.toISOString().split('T')[0] : null
|
||||
},
|
||||
{
|
||||
id: 'dim5_complete',
|
||||
name: '五维探索者',
|
||||
description: '完成五维空间的探索',
|
||||
icon: '🌌',
|
||||
unlocked: user.exploreData.dim5?.completed || false,
|
||||
date: user.exploreData.dim5?.completed ? user.updatedAt.toISOString().split('T')[0] : null
|
||||
name: '社交成就',
|
||||
achievements: [
|
||||
{
|
||||
id: 'first_share',
|
||||
name: '初次分享',
|
||||
description: '分享你的第一个知识给朋友',
|
||||
icon: '📤',
|
||||
unlocked: user.achievements?.includes('first_share') || false,
|
||||
date: user.achievements?.includes('first_share') ? user.updatedAt.toISOString().split('T')[0] : null
|
||||
},
|
||||
{
|
||||
id: 'five_shares',
|
||||
name: '分享达人',
|
||||
description: '分享 5 次知识给朋友',
|
||||
icon: '📣',
|
||||
unlocked: user.achievements?.includes('five_shares') || false,
|
||||
date: user.achievements?.includes('five_shares') ? user.updatedAt.toISOString().split('T')[0] : null
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: '创作成就',
|
||||
achievements: [
|
||||
{
|
||||
id: 'first_work',
|
||||
name: '初次创作',
|
||||
description: '创作你的第一个作品',
|
||||
icon: '🎨',
|
||||
unlocked: user.achievements?.includes('first_work') || false,
|
||||
date: user.achievements?.includes('first_work') ? user.updatedAt.toISOString().split('T')[0] : null
|
||||
},
|
||||
{
|
||||
id: 'five_works',
|
||||
name: '创作达人',
|
||||
description: '创作5个作品',
|
||||
icon: '🏆',
|
||||
unlocked: user.achievements?.includes('five_works') || false,
|
||||
date: user.achievements?.includes('five_works') ? user.updatedAt.toISOString().split('T')[0] : null
|
||||
},
|
||||
{
|
||||
id: 'ten_works',
|
||||
name: '创作大师',
|
||||
description: '创作10个作品',
|
||||
icon: '👑',
|
||||
unlocked: user.achievements?.includes('ten_works') || false,
|
||||
date: user.achievements?.includes('ten_works') ? user.updatedAt.toISOString().split('T')[0] : null
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: '收藏成就',
|
||||
achievements: [
|
||||
{
|
||||
id: 'first_collection',
|
||||
name: '初次收藏',
|
||||
description: '收藏你的第一个知识',
|
||||
icon: '❤️',
|
||||
unlocked: user.collectedKnowledge?.length > 0,
|
||||
date: user.collectedKnowledge?.length > 0 ? user.updatedAt.toISOString().split('T')[0] : null
|
||||
},
|
||||
{
|
||||
id: 'five_collections',
|
||||
name: '收藏爱好者',
|
||||
description: '收藏5个知识',
|
||||
icon: '🧡',
|
||||
unlocked: user.collectedKnowledge?.length >= 5,
|
||||
date: user.collectedKnowledge?.length >= 5 ? user.updatedAt.toISOString().split('T')[0] : null
|
||||
},
|
||||
{
|
||||
id: 'ten_collections',
|
||||
name: '收藏大师',
|
||||
description: '收藏10个知识',
|
||||
icon: '💛',
|
||||
unlocked: user.collectedKnowledge?.length >= 10,
|
||||
date: user.collectedKnowledge?.length >= 10 ? user.updatedAt.toISOString().split('T')[0] : null
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: '社交成就',
|
||||
achievements: [
|
||||
{
|
||||
id: 'first_share',
|
||||
name: '初次分享',
|
||||
description: '分享你的第一个作品',
|
||||
icon: '📤',
|
||||
unlocked: user.achievements?.includes('first_share') || false,
|
||||
date: user.achievements?.includes('first_share') ? user.updatedAt.toISOString().split('T')[0] : null
|
||||
},
|
||||
{
|
||||
id: 'five_shares',
|
||||
name: '分享达人',
|
||||
description: '分享5个作品',
|
||||
icon: '📣',
|
||||
unlocked: user.achievements?.includes('five_shares') || false,
|
||||
date: user.achievements?.includes('five_shares') ? user.updatedAt.toISOString().split('T')[0] : null
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
return ApiResponse.success(res, achievements)
|
||||
|
||||
@@ -338,87 +338,87 @@ router.get('/achievements', async (req, res) => {
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 获取用户统计数据
|
||||
* GET /api/user/stats
|
||||
*/
|
||||
router.get('/stats', async (req, res) => {
|
||||
try {
|
||||
const user = req.user
|
||||
const { Gallery } = require('../models')
|
||||
|
||||
// 获取用户作品数
|
||||
const worksCount = await Gallery.countDocuments({ userId: user._id })
|
||||
|
||||
// 获取等级信息
|
||||
const levelInfo = user.getLevelInfo()
|
||||
|
||||
// 计算探索时长(分钟转小时,保留1位小数)
|
||||
const playTimeHours = Math.round((user.totalPlayTime || 0) / 60 * 10) / 10
|
||||
|
||||
return ApiResponse.success(res, {
|
||||
level: levelInfo.level,
|
||||
levelName: levelInfo.name,
|
||||
levelPoints: levelInfo.points,
|
||||
levelProgress: levelInfo.progress,
|
||||
totalProgress: levelInfo.totalProgress,
|
||||
nextLevelPoints: levelInfo.nextLevelPoints,
|
||||
totalPlayTime: playTimeHours,
|
||||
totalScore: user.totalScore || 0,
|
||||
worksCount: worksCount,
|
||||
collectionCount: user.collectedKnowledge?.length || 0,
|
||||
achievementCount: user.achievements?.length || 0,
|
||||
unlockedDims: user.unlockedDims || [1]
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
logger.error('获取用户统计失败:', error)
|
||||
return ApiResponse.serverError(res, error.message)
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 获取用户作品列表
|
||||
* GET /api/user/works
|
||||
*/
|
||||
router.get('/works', async (req, res) => {
|
||||
try {
|
||||
const user = req.user
|
||||
const { page = 1, limit = 20 } = req.query
|
||||
const { Gallery } = require('../models')
|
||||
|
||||
const query = { userId: user._id }
|
||||
|
||||
const total = await Gallery.countDocuments(query)
|
||||
const works = await Gallery.find(query)
|
||||
.sort({ createdAt: -1 })
|
||||
.skip((parseInt(page) - 1) * parseInt(limit))
|
||||
.limit(parseInt(limit))
|
||||
.lean()
|
||||
|
||||
// 格式化作品数据
|
||||
const formattedWorks = works.map(work => ({
|
||||
id: work._id,
|
||||
title: work.title,
|
||||
description: work.description,
|
||||
imageUrl: work.imageUrl,
|
||||
dimension: work.dimension,
|
||||
status: work.status,
|
||||
likes: work.likes || 0,
|
||||
views: work.views || 0,
|
||||
createdAt: work.createdAt
|
||||
}))
|
||||
|
||||
return ApiResponse.paginated(res, formattedWorks, {
|
||||
page: parseInt(page),
|
||||
limit: parseInt(limit),
|
||||
total
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
logger.error('获取用户作品失败:', error)
|
||||
return ApiResponse.serverError(res, error.message)
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 获取用户统计数据
|
||||
* GET /api/user/stats
|
||||
*/
|
||||
router.get('/stats', async (req, res) => {
|
||||
try {
|
||||
const user = req.user
|
||||
const { Gallery } = require('../models')
|
||||
|
||||
// 获取用户作品数
|
||||
const worksCount = await Gallery.countDocuments({ userId: user._id })
|
||||
|
||||
// 获取等级信息
|
||||
const levelInfo = user.getLevelInfo()
|
||||
|
||||
// 计算探索时长(分钟转小时,保留1位小数)
|
||||
const playTimeHours = Math.round((user.totalPlayTime || 0) / 60 * 10) / 10
|
||||
|
||||
return ApiResponse.success(res, {
|
||||
level: levelInfo.level,
|
||||
levelName: levelInfo.name,
|
||||
levelPoints: levelInfo.points,
|
||||
levelProgress: levelInfo.progress,
|
||||
totalProgress: levelInfo.totalProgress,
|
||||
nextLevelPoints: levelInfo.nextLevelPoints,
|
||||
totalPlayTime: playTimeHours,
|
||||
totalScore: user.totalScore || 0,
|
||||
worksCount: worksCount,
|
||||
collectionCount: user.collectedKnowledge?.length || 0,
|
||||
achievementCount: user.achievements?.length || 0,
|
||||
unlockedDims: user.unlockedDims || [1]
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
logger.error('获取用户统计失败:', error)
|
||||
return ApiResponse.serverError(res, error.message)
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 获取用户作品列表
|
||||
* GET /api/user/works
|
||||
*/
|
||||
router.get('/works', async (req, res) => {
|
||||
try {
|
||||
const user = req.user
|
||||
const { page = 1, limit = 20 } = req.query
|
||||
const { Gallery } = require('../models')
|
||||
|
||||
const query = { userId: user._id }
|
||||
|
||||
const total = await Gallery.countDocuments(query)
|
||||
const works = await Gallery.find(query)
|
||||
.sort({ createdAt: -1 })
|
||||
.skip((parseInt(page) - 1) * parseInt(limit))
|
||||
.limit(parseInt(limit))
|
||||
.lean()
|
||||
|
||||
// 格式化作品数据
|
||||
const formattedWorks = works.map(work => ({
|
||||
id: work._id,
|
||||
title: work.title,
|
||||
description: work.description,
|
||||
imageUrl: work.imageUrl,
|
||||
dimension: work.dimension,
|
||||
status: work.status,
|
||||
likes: work.likes || 0,
|
||||
views: work.views || 0,
|
||||
createdAt: work.createdAt
|
||||
}))
|
||||
|
||||
return ApiResponse.paginated(res, formattedWorks, {
|
||||
page: parseInt(page),
|
||||
limit: parseInt(limit),
|
||||
total
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
logger.error('获取用户作品失败:', error)
|
||||
return ApiResponse.serverError(res, error.message)
|
||||
}
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
|
||||
Reference in New Issue
Block a user