# 计算机毕设:Vue3 + Flask 前后端分离项目实战(完整源码)
# 计算机毕设:Vue3 + Flask 前后端分离项目实战(完整源码)
> **关键词:** 计算机毕设、Vue3毕设、Flask后端、前后端分离、Web课程设计
>
> **适用场景:** 毕业设计、Web开发课程设计、软件工程实践
## 一、项目概述
很多同学毕设想做**前后端分离**的Web项目,但不知道怎么把前端Vue和后端Flask串起来。本文手把手搭建一个完整的**「在线问卷调查系统」**,涵盖用户注册登录、问卷创建、填写提交、数据统计四个核心模块。
**技术栈:**
- 前端:Vue 3 + Vite + Element Plus + Axios + ECharts
- 后端:Flask + Flask-CORS + SQLAlchemy + JWT
- 数据库:SQLite(可替换为MySQL)
## 二、项目结构
```
survey-system/
├── backend/ # 后端
│ ├── app.py # Flask主入口
│ ├── config.py # 配置文件
│ ├── models.py # 数据库模型
│ ├── routes/
│ │ ├── auth.py # 认证接口
│ │ ├── survey.py # 问卷接口
│ │ └── response.py # 回答接口
│ ├── requirements.txt
│ └── survey.db # SQLite数据库
│
└── frontend/ # 前端
├── src/
│ ├── views/
│ │ ├── Login.vue
│ │ ├── Register.vue
│ │ ├── Dashboard.vue
│ │ ├── CreateSurvey.vue
│ │ ├── FillSurvey.vue
│ │ └── Statistics.vue
│ ├── router/index.js
│ ├── api/index.js
│ ├── App.vue
│ └── main.js
├── package.json
└── vite.config.js
```
## 三、后端代码
### 3.1 config.py — 配置
```python
import os
class Config:
SECRET_KEY = os.environ.get('SECRET_KEY', 'your-secret-key-here')
SQLALCHEMY_DATABASE_URI = 'sqlite:///survey.db'
SQLALCHEMY_TRACK_MODIFICATIONS = False
JWT_EXPIRATION_HOURS = 24
```
### 3.2 models.py — 数据模型
```python
from flask_sqlalchemy import SQLAlchemy
from datetime import datetime
db = SQLAlchemy()
class User(db.Model):
__tablename__ = 'user'
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(50), unique=True, nullable=False)
password_hash = db.Column(db.String(128), nullable=False)
created_at = db.Column(db.DateTime, default=datetime.now)
surveys = db.relationship('Survey', backref='creator', lazy=True)
class Survey(db.Model):
__tablename__ = 'survey'
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(200), nullable=False)
description = db.Column(db.Text)
creator_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
is_active = db.Column(db.Boolean, default=True)
created_at = db.Column(db.DateTime, default=datetime.now)
questions = db.relationship('Question', backref='survey', lazy=True, cascade='all, delete-orphan')
responses = db.relationship('Response', backref='survey', lazy=True)
class Question(db.Model):
__tablename__ = 'question'
id = db.Column(db.Integer, primary_key=True)
survey_id = db.Column(db.Integer, db.ForeignKey('survey.id'), nullable=False)
content = db.Column(db.String(500), nullable=False)
qtype = db.Column(db.String(20), nullable=False) # single / multiple / text
options = db.Column(db.Text) # JSON格式存储选项
order_num = db.Column(db.Integer, default=0)
answers = db.relationship('Answer', backref='question', lazy=True)
class Response(db.Model):
__tablename__ = 'response'
id = db.Column(db.Integer, primary_key=True)
survey_id = db.Column(db.Integer, db.ForeignKey('survey.id'), nullable=False)
respondent_name = db.Column(db.String(50))
submitted_at = db.Column(db.DateTime, default=datetime.now)
answers = db.relationship('Answer', backref='response', lazy=True, cascade='all, delete-orphan')
class Answer(db.Model):
__tablename__ = 'answer'
id = db.Column(db.Integer, primary_key=True)
response_id = db.Column(db.Integer, db.ForeignKey('response.id'), nullable=False)
question_id = db.Column(db.Integer, db.ForeignKey('question.id'), nullable=False)
content = db.Column(db.Text) # 回答内容
```
### 3.3 routes/auth.py — 认证接口
```python
from flask import Blueprint, request, jsonify
from werkzeug.security import generate_password_hash, check_password_hash
import jwt, datetime
from models import db, User
from config import Config
auth_bp = Blueprint('auth', __name__, url_prefix='/api/auth')
@auth_bp.route('/register', methods=['POST'])
def register():
data = request.get_json()
username = data.get('username', '').strip()
password = data.get('password', '').strip()
if not username or not password:
return jsonify({'code': 400, 'msg': '用户名和密码不能为空'})
if User.query.filter_by(username=username).first():
return jsonify({'code': 400, 'msg': '用户名已存在'})
user = User(
username=username,
password_hash=generate_password_hash(password)
)
db.session.add(user)
db.session.commit()
return jsonify({'code': 200, 'msg': '注册成功'})
@auth_bp.route('/login', methods=['POST'])
def login():
data = request.get_json()
user = User.query.filter_by(username=data.get('username')).first()
if not user or not check_password_hash(user.password_hash, data.get('password')):
return jsonify({'code': 401, 'msg': '用户名或密码错误'})
token = jwt.encode({
'user_id': user.id,
'exp': datetime.datetime.now() + datetime.timedelta(hours=Config.JWT_EXPIRATION_HOURS)
}, Config.SECRET_KEY, algorithm='HS256')
return jsonify({
'code': 200,
'msg': '登录成功',
'data': {'token': token, 'username': user.username}
})
```
### 3.4 routes/survey.py — 问卷接口
```python
from flask import Blueprint, request, jsonify
import jwt, json
from functools import wraps
from models import db, Survey, Question
from config import Config
survey_bp = Blueprint('survey', __name__, url_prefix='/api/survey')
def login_required(f):
"""JWT鉴权装饰器"""
@wraps(f)
def decorated(*args, **kwargs):
token = request.headers.get('Authorization', '').replace('Bearer ', '')
if not token:
return jsonify({'code': 401, 'msg': '未登录'}), 401
try:
data = jwt.decode(token, Config.SECRET_KEY, algorithms=['HS256'])
request.user_id = data['user_id']
except jwt.ExpiredSignatureError:
return jsonify({'code': 401, 'msg': '登录已过期'}), 401
except jwt.InvalidTokenError:
return jsonify({'code': 401, 'msg': '无效token'}), 401
return f(*args, **kwargs)
return decorated
@survey_bp.route('', methods=['POST'])
@login_required
def create_survey():
"""创建问卷"""
data = request.get_json()
survey = Survey(
title=data['title'],
description=data.get('description', ''),
creator_id=request.user_id
)
db.session.add(survey)
db.session.flush()
for i, q in enumerate(data.get('questions', [])):
question = Question(
survey_id=survey.id,
content=q['content'],
qtype=q['qtype'],
options=json.dumps(q.get('options', []), ensure_ascii=False),
order_num=i
)
db.session.add(question)
db.session.commit()
return jsonify({'code': 200, 'msg': '创建成功', 'data': {'id': survey.id}})
@survey_bp.route('/my', methods=['GET'])
@login_required
def my_surveys():
"""获取我创建的问卷列表"""
surveys = Survey.query.filter_by(creator_id=request.user_id)\
.order_by(Survey.created_at.desc()).all()
result = [{
'id': s.id,
'title': s.title,
'is_active': s.is_active,
'created_at': s.created_at.strftime('%Y-%m-%d %H:%M'),
'response_count': len(s.responses)
} for s in surveys]
return jsonify({'code': 200, 'data': result})
@survey_bp.route('/<int:sid>', methods=['GET'])
def get_survey(sid):
"""获取问卷详情(填写用)"""
survey = Survey.query.get_or_404(sid)
if not survey.is_active:
return jsonify({'code': 400, 'msg': '该问卷已关闭'})
questions = [{
'id': q.id,
'content': q.content,
'qtype': q.qtype,
'options': json.loads(q.options) if q.options else []
} for q in sorted(survey.questions, key=lambda x: x.order_num)]
return jsonify({
'code': 200,
'data': {
'id': survey.id,
'title': survey.title,
'description': survey.description,
'questions': questions
}
})
@survey_bp.route('/<int:sid>/toggle', methods=['POST'])
@login_required
def toggle_survey(sid):
"""开启/关闭问卷"""
survey = Survey.query.get_or_404(sid)
if survey.creator_id != request.user_id:
return jsonify({'code': 403, 'msg': '无权限'})
survey.is_active = not survey.is_active
db.session.commit()
return jsonify({'code': 200, 'msg': '操作成功'})
```
### 3.5 routes/response.py — 回答与统计接口
```python
from flask import Blueprint, request, jsonify
import json
from models import db, Survey, Response, Answer, Question
response_bp = Blueprint('response', __name__, url_prefix='/api/response')
@response_bp.route('', methods=['POST'])
def submit_response():
"""提交问卷回答"""
data = request.get_json()
survey = Survey.query.get_or_404(data['survey_id'])
if not survey.is_active:
return jsonify({'code': 400, 'msg': '该问卷已关闭'})
response = Response(
survey_id=survey.id,
respondent_name=data.get('name', '匿名用户')
)
db.session.add(response)
db.session.flush()
for ans in data.get('answers', []):
answer = Answer(
response_id=response.id,
question_id=ans['question_id'],
content=json.dumps(ans['content'], ensure_ascii=False) if isinstance(ans['content'], list) else str(ans['content'])
)
db.session.add(answer)
db.session.commit()
return jsonify({'code': 200, 'msg': '提交成功'})
@response_bp.route('/survey/<int:sid>/stats', methods=['GET'])
def get_statistics(sid):
"""获取问卷统计数据"""
survey = Survey.query.get_or_404(sid)
questions = sorted(survey.questions, key=lambda x: x.order_num)
stats = []
for q in questions:
answers = Answer.query.filter_by(question_id=q.id).all()
if q.qtype in ('single', 'multiple'):
# 选择题:统计每个选项的计数
options = json.loads(q.options) if q.options else []
count_map = {opt: 0 for opt in options}
for a in answers:
content = json.loads(a.content) if a.content.startswith('[') else [a.content]
for c in content:
if c in count_map:
count_map[c] += 1
stats.append({
'question': q.content,
'type': q.qtype,
'data': count_map
})
else:
# 文本题:返回所有回答
texts = [a.content for a in answers]
stats.append({
'question': q.content,
'type': 'text',
'data': texts,
'total': len(texts)
})
return jsonify({
'code': 200,
'data': {
'title': survey.title,
'total_responses': len(survey.responses),
'questions': stats
}
})
```
### 3.6 app.py — 主入口
```python
from flask import Flask
from flask_cors import CORS
from config import Config
from models import db
from routes.auth import auth_bp
from routes.survey import survey_bp
from routes.response import response_bp
app = Flask(__name__)
app.config.from_object(Config)
CORS(app)
db.init_app(app)
app.register_blueprint(auth_bp)
app.register_blueprint(survey_bp)
app.register_blueprint(response_bp)
with app.app_context():
db.create_all()
if __name__ == '__main__':
app.run(debug=True, port=5000)
```
## 四、前端核心代码
### 4.1 api/index.js — 请求封装
```javascript
import axios from 'axios'
const api = axios.create({
baseURL: 'http://localhost:5000/api',
timeout: 10000
})
// 请求拦截器:自动携带token
api.interceptors.request.use(config => {
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
})
// 响应拦截器
api.interceptors.response.use(
res => res.data,
err => {
if (err.response?.status === 401) {
localStorage.removeItem('token')
window.location.href = '/login'
}
return Promise.reject(err)
}
)
export default api
```
### 4.2 CreateSurvey.vue — 创建问卷
```vue
<template>
<div class="create-container">
<el-card>
<h2>创建问卷</h2>
<el-form :model="form" label-width="80px">
<el-form-item label="标题">
<el-input v-model="form.title" placeholder="请输入问卷标题" />
</el-form-item>
<el-form-item label="描述">
<el-input v-model="form.description" type="textarea" :rows="2" />
</el-form-item>
</el-form>
<div v-for="(q, index) in form.questions" :key="index" class="question-card">
<div class="question-header">
<span>第 {{ index + 1 }} 题</span>
<el-button type="danger" text @click="removeQuestion(index)">删除</el-button>
</div>
<el-input v-model="q.content" placeholder="请输入题目" class="mb-10" />
<el-select v-model="q.qtype" placeholder="题型" class="mb-10">
<el-option label="单选" value="single" />
<el-option label="多选" value="multiple" />
<el-option label="文本" value="text" />
</el-select>
<div v-if="q.qtype !== 'text'">
<div v-for="(opt, oi) in q.options" :key="oi" class="option-row">
<el-input v-model="q.options[oi]" :placeholder="'选项' + (oi + 1)" />
<el-button @click="q.options.splice(oi, 1)" text>×</el-button>
</div>
<el-button @click="q.options.push('')" text type="primary">+ 添加选项</el-button>
</div>
</div>
<el-button @click="addQuestion" style="width: 100%; margin: 16px 0;">
+ 添加题目
</el-button>
<el-button type="primary" @click="submit" style="width: 100%;">
创建问卷
</el-button>
</el-card>
</div>
</template>
<script setup>
import { reactive } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import api from '../api'
const router = useRouter()
const form = reactive({
title: '',
description: '',
questions: [{ content: '', qtype: 'single', options: ['', ''] }]
})
const addQuestion = () => {
form.questions.push({ content: '', qtype: 'single', options: ['', ''] })
}
const removeQuestion = (i) => {
form.questions.splice(i, 1)
}
const submit = async () => {
if (!form.title) return ElMessage.warning('请输入标题')
const { code, msg } = await api.post('/survey', form)
if (code === 200) {
ElMessage.success('创建成功')
router.push('/dashboard')
} else {
ElMessage.error(msg)
}
}
</script>
```
### 4.3 FillSurvey.vue — 填写问卷
```vue
<template>
<div class="fill-container">
<el-card v-if="survey">
<h2>{{ survey.title }}</h2>
<p class="desc">{{ survey.description }}</p>
<div v-for="(q, index) in survey.questions" :key="q.id" class="question-item">
<p class="q-title">{{ index + 1 }}. {{ q.content }}</p>
<el-radio-group v-if="q.qtype === 'single'" v-model="answers[q.id]">
<el-radio v-for="opt in q.options" :key="opt" :value="opt">
{{ opt }}
</el-radio>
</el-radio-group>
<el-checkbox-group v-else-if="q.qtype === 'multiple'" v-model="answers[q.id]">
<el-checkbox v-for="opt in q.options" :key="opt" :value="opt">
{{ opt }}
</el-checkbox>
</el-checkbox-group>
<el-input v-else v-model="answers[q.id]" type="textarea" :rows="3"
placeholder="请输入你的回答" />
</div>
<el-button type="primary" @click="submit" style="width: 100%; margin-top: 20px;">
提交问卷
</el-button>
</el-card>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import api from '../api'
const route = useRoute()
const router = useRouter()
const survey = ref(null)
const answers = reactive({})
onMounted(async () => {
const { code, data } = await api.get(`/survey/${route.params.id}`)
if (code === 200) {
survey.value = data
data.questions.forEach(q => {
answers[q.id] = q.qtype === 'multiple' ? [] : ''
})
}
})
const submit = async () => {
const answerList = Object.entries(answers).map(([qid, content]) => ({
question_id: parseInt(qid),
content
}))
const { code, msg } = await api.post('/response', {
survey_id: survey.value.id,
answers: answerList
})
if (code === 200) {
ElMessage.success('提交成功!')
router.push('/')
} else {
ElMessage.error(msg)
}
}
</script>
```
### 4.4 Statistics.vue — 数据统计(含图表)
```vue
<template>
<div class="stats-container">
<el-card v-if="stats">
<h2>{{ stats.title }} - 数据统计</h2>
<p>共收到 <strong>{{ stats.total_responses }}</strong> 份回答</p>
<div v-for="(q, index) in stats.questions" :key="index" class="chart-card">
<h3>{{ q.question }}</h3>
<!-- 选择题:显示柱状图 -->
<div v-if="q.type !== 'text'" ref="chartRefs" :id="'chart-' + index"
style="width: 100%; height: 300px;"></div>
<!-- 文本题:显示回答列表 -->
<div v-else>
<p class="text-total">共 {{ q.total }} 条回答</p>
<div v-for="(text, ti) in q.data.slice(0, 10)" :key="ti" class="text-item">
{{ ti + 1 }}. {{ text }}
</div>
</div>
</div>
</el-card>
</div>
</template>
<script setup>
import { ref, onMounted, nextTick } from 'vue'
import { useRoute } from 'vue-router'
import * as echarts from 'echarts'
import api from '../api'
const route = useRoute()
const stats = ref(null)
onMounted(async () => {
const { code, data } = await api.get(`/response/survey/${route.params.id}/stats`)
if (code === 200) {
stats.value = data
await nextTick()
renderCharts()
}
})
const renderCharts = () => {
stats.value.questions.forEach((q, index) => {
if (q.type === 'text') return
const el = document.getElementById('chart-' + index)
if (!el) return
const chart = echarts.init(el)
chart.setOption({
title: { text: '' },
tooltip: {},
xAxis: { type: 'category', data: Object.keys(q.data) },
yAxis: { type: 'value' },
series: [{
type: 'bar',
data: Object.values(q.data),
itemStyle: { color: '#4A90D9' }
}]
})
})
}
</script>
```
## 五、项目启动
### 后端
```bash
cd backend
pip install flask flask-cors flask-sqlalchemy pyjwt
python app.py
```
### 前端
```bash
cd frontend
npm install
npm run dev
```
前端默认运行在 `http://localhost:5173`,后端在 `http://localhost:5000`。
## 六、毕设报告撰写要点
1. **系统需求分析** — 用例图、功能需求、非功能需求
2. **系统设计** — 架构图(前后端分离架构)、数据库ER图、类图
3. **系统实现** — 关键代码说明、接口文档
4. **系统测试** — 功能测试用例、截图
5. **总结** — 项目亮点、不足、改进方向
> **加分项:** 加入响应式设计、数据导出(Excel/PDF)、微信小程序端。
---
**完整源码已上传GitHub:** (此处放你的仓库链接)
> 点赞 + 收藏 + 关注,后续更新更多毕设实战项目!
更多推荐

所有评论(0)