# 计算机毕设: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:** (此处放你的仓库链接)

 

> 点赞 + 收藏 + 关注,后续更新更多毕设实战项目!

 

 

更多推荐