DevMentor - 智能面试与学习平台
文章说明
文章是为了给这个项目邀请开发者所著,考虑到本项目涉及到的功能较多,而且还蛮有难度的,所以邀请一些同学一起参与开发,共同交流技术;具体项目链接参见文章末尾的 gitee 地址
项目简介
DevMentor 是一个开源的智能编程学习与面试平台,融合 AI 技术,提供全面的技术课程学习和智能面试模拟功能。通过 AI 辅助和角色切换,帮助开发者提升技术实力,快速适应真实面试场景。
🎯 项目目标
- 打造优质的开源学习平台
- 提供智能化的学习体验
- 构建活跃的技术社区
- 帮助开发者快速成长
✨ 特性
-
📚 系统化的课程学习
- 前端、后端、算法等多个方向
- 循序渐进的课程体系
- 实战项目驱动学习
-
🤖 智能面试系统
- AI 模拟面试官
- 真实面试场景还原
- 智能反馈与建议
-
👥 活跃的技术社区
- 技术讨论与分享
- 经验交流与解答
- 开发者社交网络
🚀 快速开始
环境要求
- Node.js >= 14
- Java >= 8
- MySQL >= 5.7
- Redis >= 6.0
本地开发
- 克隆项目
git clone https://gitee.com/anxwefndu/DevMentor.git
cd DevMentor
- 前端启动
cd code/front
npm install
npm run serve
- Mock 服务启动
cd code/mock
npm install
npm run start
- 后端启动
cd code/end
mvn spring-boot:run
🔨 技术栈
前端
- Vue 3
- Vue Router
- TailwindCSS
- Axios
- ECharts
- Markdown 编辑器
后端
- Spring Boot
- MyBatis Plus
- MySQL
- Redis
- JWT
- Spring Mail
📋 开发进度
已完成
- ✅ 项目基础架构搭建
- ✅ 首页布局和基础组件
- ✅ 课程列表页面
- ✅ 课程详情页面
- ✅ 学习路径页面
- ✅ 社区基础页面
- ✅ Mock 数据服务
开发中
- 🚧 用户认证系统
- 🚧 课程视频系统
- 🚧 面试模拟系统
- 🚧 社区互动功能
项目链接
演示截图
1.系统首页
2.课程列表页面
3.面试题库页面
4.学习路径页面
5.技术社区页面
6.课程详情页面
7.关于我们页面
8.帮助中心页面
部分页面代码
code/front/src/views/Home.vue
<template>
<Layout>
<main class="pt-16 bg-gradient-to-b from-gray-50 to-white">
<section class="hero-section h-[600px] flex items-center bg-gradient-to-r from-primary/5 to-primary/10">
<div class="max-w-7xl mx-auto px-4 w-full">
<div class="max-w-2xl">
<h1 class="text-5xl font-bold text-gray-900 mb-6">打造你的技术能力,<br>开启职业新篇章</h1>
<p class="text-xl text-gray-600 mb-8">超过 10,000 名学员已经通过我们的平台提升技能,获得理想工作</p>
<router-link
to="/paths"
class="!rounded-button bg-primary text-white px-8 py-3 text-lg hover:bg-primary/90 whitespace-nowrap inline-block"
>
立即开始学习
</router-link>
</div>
</div>
</section>
<section class="py-16 bg-white">
<div class="max-w-7xl mx-auto px-4">
<h2 class="text-3xl font-bold text-center mb-12">热门课程推荐</h2>
<div class="grid grid-cols-4 gap-8">
<template v-for="course in courses" :key="course.id">
<div class="category-card bg-white rounded-lg shadow-sm overflow-hidden group">
<div class="image-wrapper overflow-hidden">
<img :src="course.imageUrl"
class="w-full h-[200px] object-cover transition-transform duration-500 group-hover:scale-110">
</div>
<div class="p-6">
<h3 class="text-xl font-bold mb-2">{{ course.title }}</h3>
<p class="text-gray-600 mb-4 line-clamp-2">{{ course.description }}</p>
<div class="flex items-center justify-between">
<span class="text-sm text-gray-500">{{ course.studentCount }} 人在学</span>
<router-link
:to="`/courses/${course.id}`"
class="!rounded-button bg-primary/10 text-primary px-4 py-2 hover:bg-primary/20 whitespace-nowrap"
>
查看详情
</router-link>
</div>
</div>
</div>
</template>
</div>
</div>
</section>
<section class="py-16">
<div class="max-w-7xl mx-auto px-4">
<h2 class="text-3xl font-bold text-center mb-12">精选面试题</h2>
<div class="flex gap-8">
<div class="w-64 bg-white rounded-lg p-6 shadow-sm">
<h3 class="text-lg font-bold mb-4">题目分类</h3>
<ul class="space-y-2">
<li v-for="category in categories" :key="category.id">
<a href="#" class="flex items-center text-gray-700 hover:text-primary">
<i :class="['fas', `fa-${category.icon}`, 'mr-2']"></i>
{{ category.name }}
</a>
</li>
</ul>
</div>
<div class="flex-1 bg-white rounded-lg p-6 shadow-sm">
<div class="flex items-center justify-between mb-6">
<h3 class="text-lg font-bold">最新题目</h3>
<div class="flex items-center space-x-2">
<button
class="!rounded-button px-4 py-2 bg-gray-100 text-gray-700 hover:bg-gray-200 whitespace-nowrap">难度</button>
<button
class="!rounded-button px-4 py-2 bg-gray-100 text-gray-700 hover:bg-gray-200 whitespace-nowrap">热度</button>
</div>
</div>
<div class="space-y-4">
<template v-for="question in questions" :key="question.id">
<div class="p-4 border border-gray-200 rounded-lg hover:border-primary/30 hover:bg-primary/5">
<router-link :to="`/questions/${question.id}`">
<div class="flex items-center justify-between mb-2">
<h4 class="font-medium">{{ question.title }}</h4>
<span :class="[
'px-2 py-1 text-xs rounded',
question.difficulty === '简单' ? 'bg-green-100 text-green-800' :
question.difficulty === '中等' ? 'bg-yellow-100 text-yellow-800' :
'bg-red-100 text-red-800'
]">
{{ question.difficulty }}
</span>
</div>
<div class="flex items-center text-sm text-gray-500">
<span class="mr-4">通过率:{{ question.passRate }}</span>
<span>提交次数:{{ question.submitCount }}</span>
</div>
</router-link>
</div>
</template>
</div>
</div>
</div>
</div>
</section>
<section class="py-16 bg-white">
<div class="max-w-7xl mx-auto px-4">
<h2 class="text-3xl font-bold text-center mb-12">学习数据</h2>
<div class="grid grid-cols-2 gap-8">
<div class="bg-white rounded-lg shadow-sm p-6">
<h3 class="text-lg font-bold mb-4">课程学习趋势</h3>
<div id="trendChart" class="w-full h-[300px]"></div>
</div>
<div class="bg-white rounded-lg shadow-sm p-6">
<h3 class="text-lg font-bold mb-4">技能分布</h3>
<div id="skillChart" class="w-full h-[300px]"></div>
</div>
</div>
</div>
</section>
<section class="py-16">
<div class="max-w-7xl mx-auto px-4">
<h2 class="text-3xl font-bold text-center mb-12">学习动态</h2>
<div class="grid grid-cols-3 gap-8">
<template v-for="activity in activities" :key="activity.id">
<div class="bg-white rounded-lg shadow-sm p-6">
<div class="flex items-center mb-4">
<img :src="activity.userAvatar" class="w-10 h-10 rounded-full mr-3">
<div>
<h4 class="font-medium">{{ activity.userName }}</h4>
<p class="text-sm text-gray-500">{{ activity.time }}</p>
</div>
</div>
<p class="text-gray-700">{{ activity.content }}</p>
</div>
</template>
</div>
</div>
</section>
</main>
</Layout>
</template>
<script setup>
import { onMounted, ref } from "vue";
import * as echarts from "echarts";
import request from '@/utils/request';
import Layout from '@/components/Layout.vue'
// 定义响应式数据
const courses = ref([]);
const categories = ref([]);
const questions = ref([]);
// 获取课程数据
const fetchCourses = async () => {
try {
const response = await request.get('/courses');
courses.value = response.data.data;
} catch (error) {
console.error('获取课程数据失败:', error);
}
};
// 获取题目分类
const fetchCategories = async () => {
try {
const response = await request.get('/questions/categories');
categories.value = response.data.data;
} catch (error) {
console.error('获取题目分类失败:', error);
}
};
// 获取题目列表
const fetchQuestions = async () => {
try {
const response = await request.get('/questions/list');
questions.value = response.data.data;
} catch (error) {
console.error('获取题目列表失败:', error);
}
};
// 获取趋势数据
const fetchTrendData = async () => {
try {
const response = await request.get('/statistics/trend');
return response.data.data;
} catch (error) {
console.error('获取趋势数据失败:', error);
return null;
}
};
// 获取技能分布数据
const fetchSkillData = async () => {
try {
const response = await request.get('/statistics/skills');
return response.data.data;
} catch (error) {
console.error('获取技能分布数据失败:', error);
return null;
}
};
// 添加响应式数据
const activities = ref([]);
// 添加获取学习动态的方法
const fetchActivities = async () => {
try {
const response = await request.get('/statistics/activities');
activities.value = response.data.data;
} catch (error) {
console.error('获取学习动态失败:', error);
}
};
onMounted(async () => {
// 获取所有数据
await Promise.all([
fetchCourses(),
fetchCategories(),
fetchQuestions(),
fetchActivities()
]);
// 初始化图表
const trendChart = echarts.init(document.getElementById('trendChart'));
const skillChart = echarts.init(document.getElementById('skillChart'));
// 获取图表数据
const trendData = await fetchTrendData();
const skillData = await fetchSkillData();
if (trendData) {
trendChart.setOption({
animation: false,
tooltip: {
trigger: 'axis'
},
xAxis: {
type: 'category',
data: trendData.dates
},
yAxis: {
type: 'value'
},
series: [{
data: trendData.data,
type: 'line',
smooth: true,
itemStyle: {
color: '#2D8CF0'
}
}]
});
}
if (skillData) {
skillChart.setOption({
animation: false,
tooltip: {
trigger: 'item'
},
series: [{
type: 'pie',
radius: ['40%', '70%'],
data: skillData,
itemStyle: {
borderRadius: 10,
borderColor: '#fff',
borderWidth: 2
},
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}]
});
}
window.addEventListener('resize', function () {
trendChart.resize();
skillChart.resize();
});
});
</script>
<style scoped>
.category-card {
transition: all 0.3s ease;
}
.category-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
}
.image-wrapper::after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 200px;
background: linear-gradient(to bottom, transparent 0%, rgba(0, 0, 0, 0.2) 100%);
opacity: 0;
transition: opacity 0.3s ease;
}
.category-card:hover .image-wrapper::after {
opacity: 1;
}
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
height: 3rem;
}
</style>
code/front/src/views/Courses.vue
<template>
<Layout>
<div class="max-w-7xl mx-auto px-4 py-8">
<!-- 页面标题 -->
<div class="mb-8">
<h1 class="text-3xl font-bold text-gray-900">全部课程</h1>
<p class="mt-2 text-gray-600">探索丰富的技术课程,提升你的专业技能</p>
</div>
<!-- 筛选区域 -->
<div class="bg-white rounded-lg shadow-sm p-6 mb-8">
<div class="space-y-4">
<!-- 课程方向 -->
<div>
<h3 class="font-medium mb-2">课程方向</h3>
<div class="flex flex-wrap gap-2">
<button
v-for="direction in directions"
:key="direction.id"
:class="[
'px-4 py-2 rounded-full text-sm',
selectedDirection === direction.id
? 'bg-primary text-white'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
]"
@click="selectedDirection = direction.id"
>
{{ direction.name }}
</button>
</div>
</div>
<!-- 难度级别 -->
<div>
<h3 class="font-medium mb-2">难度级别</h3>
<div class="flex flex-wrap gap-2">
<button
v-for="level in levels"
:key="level.id"
:class="[
'px-4 py-2 rounded-full text-sm',
selectedLevel === level.id
? 'bg-primary text-white'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
]"
@click="selectedLevel = level.id"
>
{{ level.name }}
</button>
</div>
</div>
</div>
</div>
<!-- 课程列表 -->
<div class="grid grid-cols-3 gap-6">
<div
v-for="course in filteredCourses"
:key="course.id"
class="category-card bg-white rounded-lg shadow-sm overflow-hidden group hover:shadow-md transition-shadow"
>
<div class="relative overflow-hidden image-wrapper">
<img
:src="course.imageUrl"
:alt="course.title"
class="w-full h-48 object-cover transition-transform duration-300 group-hover:scale-105"
>
<div class="absolute top-2 right-2">
<span
:class="[
'px-2 py-1 text-xs rounded',
course.level === '入门' ? 'bg-green-100 text-green-800' :
course.level === '进阶' ? 'bg-blue-100 text-blue-800' :
'bg-orange-100 text-orange-800'
]"
>
{{ course.level }}
</span>
</div>
</div>
<div class="p-4">
<router-link :to="`/courses/${course.id}`">
<h3 class="text-lg font-bold text-gray-900 mb-2 hover:text-primary">{{ course.title }}</h3>
</router-link>
<p class="text-gray-600 text-sm mb-4 line-clamp-2">{{ course.description }}</p>
<div class="flex items-center justify-between">
<div class="flex items-center space-x-2">
<span class="text-sm text-gray-500">
<i class="fas fa-user-graduate mr-1"></i>
{{ course.studentCount }} 人在学
</span>
<span class="text-sm text-gray-500">
<i class="fas fa-clock mr-1"></i>
{{ course.duration }}
</span>
</div>
<button class="text-primary hover:text-primary/80">
<router-link :to="`/courses/${course.id}`">
<i class="fas fa-arrow-right hover:text-primary"></i>
</router-link>
</button>
</div>
</div>
</div>
</div>
</div>
</Layout>
</template>
<script setup>
import Layout from '@/components/Layout.vue'
import {ref, computed, onMounted} from 'vue'
import request from '@/utils/request'
// 课程方向
const directions = [
{ id: 0, name: '全部' },
{ id: 1, name: '前端开发' },
{ id: 2, name: '后端开发' },
{ id: 3, name: '移动开发' },
{ id: 4, name: '算法与数据结构' },
{ id: 5, name: '人工智能' },
{ id: 6, name: '云计算与架构' }
]
// 难度级别
const levels = [
{ id: 0, name: '全部' },
{ id: 1, name: '入门' },
{ id: 2, name: '进阶' },
{ id: 3, name: '高级' }
]
const courses = ref([])
const selectedDirection = ref(0)
const selectedLevel = ref(0)
// 获取课程数据
const fetchCourses = async () => {
try {
const response = await request.get('/courses/list')
courses.value = response.data.data
} catch (error) {
console.error('获取课程数据失败:', error)
}
}
// 过滤后的课程列表
const filteredCourses = computed(() => {
return courses.value.filter(course => {
const directionMatch = selectedDirection.value === 0 || course.directionId === selectedDirection.value
const levelMatch = selectedLevel.value === 0 || course.levelId === selectedLevel.value
return directionMatch && levelMatch
})
})
onMounted(() => {
fetchCourses()
})
</script>
<style scoped>
.category-card {
transition: all 0.3s ease;
}
.category-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
}
.image-wrapper::after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 200px;
background: linear-gradient(to bottom, transparent 0%, rgba(0, 0, 0, 0.2) 100%);
opacity: 0;
transition: opacity 0.3s ease;
}
.category-card:hover .image-wrapper::after {
opacity: 1;
}
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
height: 2.5rem;
}
</style>
code/front/src/views/Community.vue
<template>
<Layout>
<div class="max-w-7xl mx-auto px-4 py-8">
<!-- 页面标题 -->
<div class="mb-8">
<h1 class="text-3xl font-bold text-gray-900">技术社区</h1>
<p class="mt-2 text-gray-600">分享经验,交流技术,一起成长</p>
</div>
<!-- 主要内容区 -->
<div class="flex gap-8">
<!-- 左侧主内容区 -->
<div class="flex-1">
<!-- 发帖按钮和筛选 -->
<div class="bg-white rounded-lg shadow-sm p-6 mb-6">
<div class="flex items-center justify-between mb-6">
<button class="!rounded-lg bg-primary text-white px-6 py-2 hover:bg-primary/90 flex items-center">
<i class="fas fa-pen-to-square mr-2"></i>
发布帖子
</button>
<div class="flex items-center space-x-4">
<button class="text-gray-600 hover:text-primary flex items-center">
<i class="fas fa-fire mr-1"></i>
热门
</button>
<button class="text-gray-600 hover:text-primary flex items-center">
<i class="fas fa-clock mr-1"></i>
最新
</button>
</div>
</div>
<!-- 标签筛选 -->
<div class="flex flex-wrap gap-2">
<span
v-for="tag in tags"
:key="tag.id"
:class="[
'px-3 py-1 rounded-full text-sm cursor-pointer transition-colors',
tag.active ? 'bg-primary text-white' : 'bg-gray-100 text-gray-600 hover:bg-gray-200'
]"
@click="toggleTag(tag)"
>
{{ tag.name }}
</span>
</div>
</div>
<!-- 帖子列表 -->
<div class="space-y-6">
<div
v-for="post in posts"
:key="post.id"
class="bg-white rounded-lg shadow-sm p-6 hover:shadow-md transition-shadow"
>
<!-- 作者信息 -->
<div class="flex items-center justify-between mb-4">
<div class="flex items-center">
<img :src="post.authorAvatar" :alt="post.authorName" class="w-10 h-10 rounded-full">
<div class="ml-3">
<h3 class="font-medium text-gray-900">{{ post.authorName }}</h3>
<p class="text-sm text-gray-500">{{ post.createTime }}</p>
</div>
</div>
<button class="text-gray-400 hover:text-gray-600">
<i class="fas fa-ellipsis-h"></i>
</button>
</div>
<!-- 帖子内容 -->
<h2 class="text-xl font-bold text-gray-900 mb-2 hover:text-primary" style="cursor: pointer">{{ post.title }}</h2>
<p class="text-gray-600 mb-4 line-clamp-3">{{ post.content }}</p>
<!-- 帖子标签 -->
<div class="flex flex-wrap gap-2 mb-4">
<span
v-for="tag in post.tags"
:key="tag"
class="px-2 py-1 bg-gray-100 text-gray-600 text-sm rounded"
>
{{ tag }}
</span>
</div>
<!-- 互动数据 -->
<div class="flex items-center space-x-6 text-gray-500">
<button class="hover:text-primary flex items-center">
<i class="far fa-thumbs-up mr-1"></i>
{{ post.likes }}
</button>
<button class="hover:text-primary flex items-center">
<i class="far fa-comment mr-1"></i>
{{ post.comments }}
</button>
<button class="hover:text-primary flex items-center">
<i class="far fa-bookmark mr-1"></i>
{{ post.collects }}
</button>
</div>
</div>
</div>
</div>
<!-- 右侧边栏 -->
<div class="w-80">
<!-- 活跃用户 -->
<div class="bg-white rounded-lg shadow-sm p-6 mb-6">
<h3 class="text-lg font-bold text-gray-900 mb-4">活跃用户</h3>
<div class="space-y-4">
<div
v-for="user in activeUsers"
:key="user.id"
class="flex items-center"
>
<img :src="user.avatar" :alt="user.name" class="w-10 h-10 rounded-full">
<div class="ml-3">
<h4 class="font-medium text-gray-900">{{ user.name }}</h4>
<p class="text-sm text-gray-500">{{ user.title }}</p>
</div>
</div>
</div>
</div>
<!-- 热门话题 -->
<div class="bg-white rounded-lg shadow-sm p-6">
<h3 class="text-lg font-bold text-gray-900 mb-4">热门话题</h3>
<div class="space-y-3">
<div
v-for="topic in hotTopics"
:key="topic.id"
class="flex items-center justify-between hover:bg-gray-50 p-2 rounded cursor-pointer"
>
<span class="text-gray-600"># {{ topic.name }}</span>
<span class="text-sm text-gray-500">{{ topic.count }}个讨论</span>
</div>
</div>
</div>
</div>
</div>
</div>
</Layout>
</template>
<script setup>
import { ref } from 'vue'
import Layout from '@/components/Layout.vue'
import request from '@/utils/request'
// 标签数据
const tags = ref([
{ id: 1, name: '全部', active: true },
{ id: 2, name: '前端开发', active: false },
{ id: 3, name: '后端开发', active: false },
{ id: 4, name: '算法', active: false },
{ id: 5, name: '面试经验', active: false },
{ id: 6, name: '职业发展', active: false }
])
// 切换标签
const toggleTag = (tag) => {
tags.value.forEach(t => t.active = t.id === tag.id)
fetchPosts() // 重新获取帖子数据
}
// 帖子数据
const posts = ref([])
const fetchPosts = async () => {
try {
const response = await request.get('/community/posts')
posts.value = response.data.data
} catch (error) {
console.error('获取帖子数据失败:', error)
}
}
// 活跃用户数据
const activeUsers = ref([])
const fetchActiveUsers = async () => {
try {
const response = await request.get('/community/active-users')
activeUsers.value = response.data.data
} catch (error) {
console.error('获取活跃用户数据失败:', error)
}
}
// 热门话题数据
const hotTopics = ref([])
const fetchHotTopics = async () => {
try {
const response = await request.get('/community/hot-topics')
hotTopics.value = response.data.data
} catch (error) {
console.error('获取热门话题数据失败:', error)
}
}
// 页面加载时获取数据
fetchPosts()
fetchActiveUsers()
fetchHotTopics()
</script>
附注
目前主要代码由 trae 编写,相关文档也由该工具生成;页面原型由mastergo和trae设计实现;后续开发中主要也会使用这两款工具