# 2026毕设季|Vue 3 + Flask 前后端分离项目:从零搭建到部署完整指南(附通用源码)
> 🎯 **适合人群:** 做毕设的大学生、想学前后端分离的 Python 开发者、需要快速出项目的同学
> ⏱️ **阅读时间:** 约 20 分钟
> 💡 **学完能做:** 一个带登录、CRUD、权限管理、文件上传的完整毕设项目
---
## 一、前言
又到毕设季了。每年 5-6 月,CSDN 上搜索量暴涨的关键字一定有这些:
- "毕设源码"
- "前后端分离项目"
- "Vue + Python 毕设"
- "管理系统 毕业设计"
很多同学选了 SpringBoot + Vue,但说实话——**如果你是 Python 方向的,Flask 才是最优解。**
为什么?
| 对比项 | SpringBoot + Vue | Flask + Vue |
|--------|-----------------|-------------|
| 后端语言 | Java | Python |
| 上手难度 | 中等 | **简单** |
| 代码量 | 多 | **少 40%** |
| 毕设答辩优势 | 普通 | **Python 热门加分** |
| 部署复杂度 | 需要 JDK + Maven | **pip install 就行** |
**一个 Flask 后端 + Vue 3 前端的项目,代码量小、逻辑清晰、答辩好讲,性价比极高。**
今天这篇文章,给你一套 **通用项目模板**,换皮就能适配各种毕设题目:
- 📚 图书管理系统
- 👨🎓 学生信息管理系统
- 🛒 在线商城
- 📝 博客系统
- 🏥 医院预约系统
- ... 任何需要 CRUD 的系统
---
## 二、技术栈总览
```
前端:Vue 3 + Vite + Element Plus + Vue Router + Pinia + Axios
后端:Flask + Flask-SQLAlchemy + Flask-JWT-Extended + Flask-CORS
数据库:SQLite(开发)/ MySQL(部署)
部署:Nginx + Gunicorn
```
---
## 三、项目结构
```
graduation-project/
├── frontend/ # 前端项目
│ ├── src/
│ │ ├── api/ # 接口请求
│ │ │ └── index.js
│ │ ├── router/ # 路由配置
│ │ │ └── index.js
│ │ ├── stores/ # 状态管理(Pinia)
│ │ │ └── user.js
│ │ ├── views/ # 页面
│ │ │ ├── Login.vue
│ │ │ ├── Layout.vue
│ │ │ ├── Dashboard.vue
│ │ │ └── UserManage.vue
│ │ ├── App.vue
│ │ └── main.js
│ ├── index.html
│ ├── vite.config.js
│ └── package.json
│
├── backend/ # 后端项目
│ ├── app/
│ │ ├── __init__.py # Flask 应用工厂
│ │ ├── models.py # 数据模型
│ │ ├── auth.py # 认证模块
│ │ ├── routes/ # 路由
│ │ │ ├── user.py
│ │ │ └── upload.py
│ │ └── utils.py # 工具函数
│ ├── config.py # 配置文件
│ ├── run.py # 启动入口
│ └── requirements.txt
│
└── README.md
```
---
## 四、后端实现(Flask)
### 4.1 安装依赖
```bash
mkdir backend && cd backend
pip install flask flask-sqlalchemy flask-jwt-extended flask-cors werkzeug
```
**requirements.txt:**
```
Flask==3.1.1
Flask-SQLAlchemy==3.1.1
Flask-JWT-Extended==4.7.1
Flask-Cors==5.0.1
Werkzeug==3.1.3
PyMySQL==1.1.1
```
### 4.2 配置文件 `config.py`
```python
import os
class Config:
SECRET_KEY = os.environ.get('SECRET_KEY', 'dev-secret-key-change-in-production')
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL', 'sqlite:///app.db')
SQLALCHEMY_TRACK_MODIFICATIONS = False
JWT_SECRET_KEY = os.environ.get('JWT_SECRET_KEY', 'jwt-secret-key-change-me')
JWT_ACCESS_TOKEN_EXPIRES = 86400 # 24小时
UPLOAD_FOLDER = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'uploads')
MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # 16MB
```
### 4.3 应用工厂 `app/__init__.py`
```python
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_jwt_extended import JWTManager
from flask_cors import CORS
from config import Config
db = SQLAlchemy()
jwt = JWTManager()
def create_app():
app = Flask(__name__)
app.config.from_object(Config)
# 初始化扩展
db.init_app(app)
jwt.init_app(app)
CORS(app, supports_credentials=True)
# 注册蓝图
from app.auth import auth_bp
from app.routes.user import user_bp
from app.routes.upload import upload_bp
app.register_blueprint(auth_bp, url_prefix='/api/auth')
app.register_blueprint(user_bp, url_prefix='/api/users')
app.register_blueprint(upload_bp, url_prefix='/api/upload')
# 创建数据库表
with app.app_context():
db.create_all()
_init_admin()
return app
def _init_admin():
"""初始化管理员账号"""
from app.models import User
if not User.query.filter_by(username='admin').first():
admin = User(username='admin', role='admin')
admin.set_password('admin123')
db.session.add(admin)
db.session.commit()
print("✅ 管理员账号已创建: admin / admin123")
```
### 4.4 数据模型 `app/models.py`
```python
from datetime import datetime
from werkzeug.security import generate_password_hash, check_password_hash
from app import db
class User(db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
password_hash = db.Column(db.String(256), nullable=False)
role = db.Column(db.String(20), default='user') # admin / user
email = db.Column(db.String(120))
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
def set_password(self, password):
self.password_hash = generate_password_hash(password)
def check_password(self, password):
return check_password_hash(self.password_hash, password)
def to_dict(self):
return {
'id': self.id,
'username': self.username,
'role': self.role,
'email': self.email,
'created_at': self.created_at.strftime('%Y-%m-%d %H:%M:%S'),
'updated_at': self.updated_at.strftime('%Y-%m-%d %H:%M:%S'),
}
# ==========================================
# 在这里添加你自己的业务模型
# 例如:图书、订单、文章等
# ==========================================
class Book(db.Model):
"""示例:图书管理系统"""
__tablename__ = 'books'
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(200), nullable=False)
author = db.Column(db.String(100))
isbn = db.Column(db.String(20), unique=True)
category = db.Column(db.String(50))
price = db.Column(db.Float, default=0.0)
stock = db.Column(db.Integer, default=0)
description = db.Column(db.Text)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
def to_dict(self):
return {
'id': self.id,
'title': self.title,
'author': self.author,
'isbn': self.isbn,
'category': self.category,
'price': self.price,
'stock': self.stock,
'description': self.description,
'created_at': self.created_at.strftime('%Y-%m-%d %H:%M:%S'),
}
```
### 4.5 认证模块 `app/auth.py`
```python
from flask import Blueprint, request, jsonify
from flask_jwt_extended import create_access_token, jwt_required, get_jwt_identity
from app.models import User
from app import db
auth_bp = Blueprint('auth', __name__)
@auth_bp.route('/login', methods=['POST'])
def login():
"""用户登录"""
data = request.get_json()
username = data.get('username')
password = data.get('password')
if not username or not password:
return jsonify({'code': 400, 'message': '用户名和密码不能为空'}), 400
user = User.query.filter_by(username=username).first()
if not user or not user.check_password(password):
return jsonify({'code': 401, 'message': '用户名或密码错误'}), 401
token = create_access_token(identity=str(user.id))
return jsonify({
'code': 200,
'message': '登录成功',
'data': {
'token': token,
'user': user.to_dict()
}
})
@auth_bp.route('/register', methods=['POST'])
def register():
"""用户注册"""
data = request.get_json()
username = data.get('username')
password = data.get('password')
if not username or not password:
return jsonify({'code': 400, 'message': '用户名和密码不能为空'}), 400
if User.query.filter_by(username=username).first():
return jsonify({'code': 400, 'message': '用户名已存在'}), 400
user = User(username=username, email=data.get('email'))
user.set_password(password)
db.session.add(user)
db.session.commit()
return jsonify({'code': 200, 'message': '注册成功'})
@auth_bp.route('/profile', methods=['GET'])
@jwt_required()
def profile():
"""获取当前用户信息"""
user_id = get_jwt_identity()
user = User.query.get(int(user_id))
if not user:
return jsonify({'code': 404, 'message': '用户不存在'}), 404
return jsonify({'code': 200, 'data': user.to_dict()})
```
### 4.6 用户管理路由 `app/routes/user.py`
```python
from flask import Blueprint, request, jsonify
from flask_jwt_extended import jwt_required, get_jwt_identity
from app.models import User
from app import db
user_bp = Blueprint('user', __name__)
@user_bp.route('/', methods=['GET'])
@jwt_required()
def get_users():
"""获取用户列表(分页 + 搜索)"""
page = request.args.get('page', 1, type=int)
size = request.args.get('size', 10, type=int)
keyword = request.args.get('keyword', '')
query = User.query
if keyword:
query = query.filter(User.username.like(f'%{keyword}%'))
pagination = query.order_by(User.created_at.desc()).paginate(
page=page, per_page=size, error_out=False
)
return jsonify({
'code': 200,
'data': {
'list': [u.to_dict() for u in pagination.items],
'total': pagination.total,
'page': page,
'size': size
}
})
@user_bp.route('/', methods=['POST'])
@jwt_required()
def create_user():
"""创建用户"""
data = request.get_json()
if User.query.filter_by(username=data.get('username')).first():
return jsonify({'code': 400, 'message': '用户名已存在'}), 400
user = User(
username=data['username'],
email=data.get('email'),
role=data.get('role', 'user')
)
user.set_password(data.get('password', '123456'))
db.session.add(user)
db.session.commit()
return jsonify({'code': 200, 'message': '创建成功', 'data': user.to_dict()})
@user_bp.route('/<int:user_id>', methods=['PUT'])
@jwt_required()
def update_user(user_id):
"""更新用户"""
user = User.query.get_or_404(user_id)
data = request.get_json()
if 'username' in data:
existing = User.query.filter(User.username == data['username'], User.id != user_id).first()
if existing:
return jsonify({'code': 400, 'message': '用户名已存在'}), 400
user.username = data['username']
if 'email' in data:
user.email = data['email']
if 'role' in data:
user.role = data['role']
if 'password' in data and data['password']:
user.set_password(data['password'])
db.session.commit()
return jsonify({'code': 200, 'message': '更新成功', 'data': user.to_dict()})
@user_bp.route('/<int:user_id>', methods=['DELETE'])
@jwt_required()
def delete_user(user_id):
"""删除用户"""
user = User.query.get_or_404(user_id)
# 不能删除自己
current_user_id = get_jwt_identity()
if str(user_id) == current_user_id:
return jsonify({'code': 400, 'message': '不能删除自己'}), 400
db.session.delete(user)
db.session.commit()
return jsonify({'code': 200, 'message': '删除成功'})
```
### 4.7 文件上传 `app/routes/upload.py`
```python
import os
import uuid
from datetime import datetime
from flask import Blueprint, request, jsonify, current_app, send_from_directory
from flask_jwt_extended import jwt_required
from werkzeug.utils import secure_filename
upload_bp = Blueprint('upload', __name__)
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'pdf', 'doc', 'docx', 'xls', 'xlsx'}
def allowed_file(filename):
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
@upload_bp.route('/', methods=['POST'])
@jwt_required()
def upload_file():
"""文件上传"""
if 'file' not in request.files:
return jsonify({'code': 400, 'message': '没有文件'}), 400
file = request.files['file']
if file.filename == '':
return jsonify({'code': 400, 'message': '未选择文件'}), 400
if not allowed_file(file.filename):
return jsonify({'code': 400, 'message': '不支持的文件类型'}), 400
# 生成唯一文件名
ext = file.filename.rsplit('.', 1)[1].lower()
date_dir = datetime.now().strftime('%Y%m%d')
upload_dir = os.path.join(current_app.config['UPLOAD_FOLDER'], date_dir)
os.makedirs(upload_dir, exist_ok=True)
filename = f"{uuid.uuid4().hex}.{ext}"
filepath = os.path.join(upload_dir, filename)
file.save(filepath)
return jsonify({
'code': 200,
'message': '上传成功',
'data': {
'url': f'/api/upload/file/{date_dir}/{filename}',
'filename': file.filename
}
})
@upload_bp.route('/file/<date_dir>/<filename>', methods=['GET'])
def get_file(date_dir, filename):
"""访问上传的文件"""
directory = os.path.join(current_app.config['UPLOAD_FOLDER'], date_dir)
return send_from_directory(directory, filename)
```
### 4.8 启动入口 `run.py`
```python
from app import create_app
app = create_app()
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=True)
```
---
## 五、前端实现(Vue 3)
### 5.1 创建项目
```bash
npm create vite@latest frontend -- --template vue
cd frontend
npm install
npm install element-plus @element-plus/icons-vue
npm install vue-router@4 pinia axios
```
### 5.2 `vite.config.js`
```javascript
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
server: {
port: 3000,
proxy: {
'/api': {
target: 'http://localhost:5000',
changeOrigin: true
}
}
}
})
```
### 5.3 `src/main.js`
```javascript
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import App from './App.vue'
import router from './router'
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.use(ElementPlus, { locale: zhCn })
// 注册所有图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
app.mount('#app')
```
### 5.4 Axios 封装 `src/api/index.js`
```javascript
import axios from 'axios'
import { ElMessage } from 'element-plus'
import router from '../router'
const api = axios.create({
baseURL: '/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(
response => response.data,
error => {
const msg = error.response?.data?.message || '请求失败'
if (error.response?.status === 401) {
localStorage.removeItem('token')
router.push('/login')
ElMessage.error('登录已过期,请重新登录')
} else {
ElMessage.error(msg)
}
return Promise.reject(error)
}
)
// ======== 接口定义 ========
// 认证
export const login = (data) => api.post('/auth/login', data)
export const register = (data) => api.post('/auth/register', data)
export const getProfile = () => api.get('/auth/profile')
// 用户管理
export const getUsers = (params) => api.get('/users/', { params })
export const createUser = (data) => api.post('/users/', data)
export const updateUser = (id, data) => api.put(`/users/${id}`, data)
export const deleteUser = (id) => api.delete(`/users/${id}`)
// 文件上传
export const uploadFile = (formData) => api.post('/upload/', formData, {
headers: { 'Content-Type': 'multipart/form-data' }
})
export default api
```
### 5.5 路由配置 `src/router/index.js`
```javascript
import { createRouter, createWebHistory } from 'vue-router'
const routes = [
{
path: '/login',
name: 'Login',
component: () => import('../views/Login.vue')
},
{
path: '/',
component: () => import('../views/Layout.vue'),
redirect: '/dashboard',
children: [
{
path: 'dashboard',
name: 'Dashboard',
component: () => import('../views/Dashboard.vue'),
meta: { title: '首页' }
},
{
path: 'users',
name: 'UserManage',
component: () => import('../views/UserManage.vue'),
meta: { title: '用户管理' }
}
// 在这里添加更多路由
]
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
// 路由守卫
router.beforeEach((to, from, next) => {
const token = localStorage.getItem('token')
if (to.path !== '/login' && !token) {
next('/login')
} else {
next()
}
})
export default router
```
### 5.6 用户状态管理 `src/stores/user.js`
```javascript
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { login as loginApi, getProfile } from '../api'
export const useUserStore = defineStore('user', () => {
const token = ref(localStorage.getItem('token') || '')
const userInfo = ref(null)
async function login(username, password) {
const res = await loginApi({ username, password })
if (res.code === 200) {
token.value = res.data.token
userInfo.value = res.data.user
localStorage.setItem('token', res.data.token)
}
return res
}
async function fetchProfile() {
const res = await getProfile()
if (res.code === 200) {
userInfo.value = res.data
}
}
function logout() {
token.value = ''
userInfo.value = null
localStorage.removeItem('token')
}
return { token, userInfo, login, fetchProfile, logout }
})
```
### 5.7 登录页 `src/views/Login.vue`
```vue
<template>
<div class="login-container">
<el-card class="login-card">
<h2 class="login-title">毕设管理系统</h2>
<el-form :model="form" :rules="rules" ref="formRef">
<el-form-item prop="username">
<el-input v-model="form.username" prefix-icon="User" placeholder="用户名" />
</el-form-item>
<el-form-item prop="password">
<el-input v-model="form.password" prefix-icon="Lock" placeholder="密码"
type="password" @keyup.enter="handleLogin" />
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="loading" style="width: 100%" @click="handleLogin">
登 录
</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { useUserStore } from '../stores/user'
const router = useRouter()
const userStore = useUserStore()
const formRef = ref(null)
const loading = ref(false)
const form = reactive({ username: '', password: '' })
const rules = {
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
password: [{ required: true, message: '请输入密码', trigger: 'blur' }]
}
const handleLogin = async () => {
await formRef.value.validate()
loading.value = true
try {
const res = await userStore.login(form.username, form.password)
if (res.code === 200) {
ElMessage.success('登录成功')
router.push('/')
}
} finally {
loading.value = false
}
}
</script>
<style scoped>
.login-container {
display: flex; justify-content: center; align-items: center;
height: 100vh; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.login-card { width: 400px; padding: 20px; }
.login-title { text-align: center; margin-bottom: 30px; color: #303133; }
</style>
```
### 5.8 布局页 `src/views/Layout.vue`
```vue
<template>
<el-container style="height: 100vh">
<!-- 侧边栏 -->
<el-aside width="220px" style="background-color: #304156">
<div class="logo">管理系统</div>
<el-menu :default-active="route.path" router background-color="#304156"
text-color="#bfcbd9" active-text-color="#409EFF">
<el-menu-item index="/dashboard">
<el-icon><HomeFilled /></el-icon>
<span>首页</span>
</el-menu-item>
<el-menu-item index="/users">
<el-icon><User /></el-icon>
<span>用户管理</span>
</el-menu-item>
<!-- 在这里添加更多菜单项 -->
</el-menu>
</el-aside>
<!-- 主内容区 -->
<el-container>
<el-header style="display: flex; justify-content: flex-end; align-items: center; border-bottom: 1px solid #eee">
<span style="margin-right: 15px">{{ userStore.userInfo?.username }}</span>
<el-button type="danger" link @click="handleLogout">退出登录</el-button>
</el-header>
<el-main>
<router-view />
</el-main>
</el-container>
</el-container>
</template>
<script setup>
import { onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useUserStore } from '../stores/user'
const route = useRoute()
const router = useRouter()
const userStore = useUserStore()
onMounted(() => {
userStore.fetchProfile()
})
const handleLogout = () => {
userStore.logout()
router.push('/login')
}
</script>
<style scoped>
.logo {
height: 60px; display: flex; align-items: center; justify-content: center;
color: #fff; font-size: 18px; font-weight: bold;
}
</style>
```
### 5.9 首页 `src/views/Dashboard.vue`
```vue
<template>
<div>
<h2>欢迎使用管理系统</h2>
<el-row :gutter="20" style="margin-top: 20px">
<el-col :span="6">
<el-card shadow="hover">
<template #header>用户总数</template>
<div class="stat-value">{{ stats.userCount }}</div>
</el-card>
</el-col>
<!-- 添加更多统计卡片 -->
</el-row>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { getUsers } from '../api'
const stats = ref({ userCount: 0 })
onMounted(async () => {
const res = await getUsers({ page: 1, size: 1 })
if (res.code === 200) {
stats.value.userCount = res.data.total
}
})
</script>
<style scoped>
.stat-value { font-size: 36px; font-weight: bold; color: #409EFF; text-align: center; }
</style>
```
### 5.10 用户管理页 `src/views/UserManage.vue`
```vue
<template>
<div>
<!-- 搜索栏 -->
<el-row :gutter="10" style="margin-bottom: 20px">
<el-col :span="6">
<el-input v-model="keyword" placeholder="搜索用户名" clearable @clear="fetchData" />
</el-col>
<el-col :span="4">
<el-button type="primary" @click="fetchData">搜索</el-button>
</el-col>
<el-col :span="4">
<el-button type="success" @click="showDialog()">新增用户</el-button>
</el-col>
</el-row>
<!-- 表格 -->
<el-table :data="tableData" border stripe>
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="username" label="用户名" />
<el-table-column prop="email" label="邮箱" />
<el-table-column prop="role" label="角色">
<template #default="{ row }">
<el-tag :type="row.role === 'admin' ? 'danger' : 'info'">
{{ row.role === 'admin' ? '管理员' : '普通用户' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="created_at" label="创建时间" width="180" />
<el-table-column label="操作" width="200">
<template #default="{ row }">
<el-button type="primary" link @click="showDialog(row)">编辑</el-button>
<el-popconfirm title="确定删除?" @confirm="handleDelete(row.id)">
<template #reference>
<el-button type="danger" link>删除</el-button>
</template>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination style="margin-top: 20px; justify-content: center"
v-model:current-page="page" v-model:page-size="size"
:total="total" :page-sizes="[10, 20, 50]"
layout="total, sizes, prev, pager, next" @change="fetchData" />
<!-- 弹窗 -->
<el-dialog v-model="dialogVisible" :title="isEdit ? '编辑用户' : '新增用户'" width="500px">
<el-form :model="form" :rules="rules" ref="formRef" label-width="80px">
<el-form-item label="用户名" prop="username">
<el-input v-model="form.username" />
</el-form-item>
<el-form-item label="邮箱">
<el-input v-model="form.email" />
</el-form-item>
<el-form-item label="角色">
<el-select v-model="form.role" style="width: 100%">
<el-option label="管理员" value="admin" />
<el-option label="普通用户" value="user" />
</el-select>
</el-form-item>
<el-form-item :label="isEdit ? '新密码' : '密码'">
<el-input v-model="form.password" type="password"
:placeholder="isEdit ? '留空则不修改' : '请输入密码'" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { getUsers, createUser, updateUser, deleteUser } from '../api'
const tableData = ref([])
const total = ref(0)
const page = ref(1)
const size = ref(10)
const keyword = ref('')
const dialogVisible = ref(false)
const isEdit = ref(false)
const editId = ref(null)
const formRef = ref(null)
const form = reactive({ username: '', email: '', role: 'user', password: '' })
const rules = {
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }]
}
const fetchData = async () => {
const res = await getUsers({ page: page.value, size: size.value, keyword: keyword.value })
if (res.code === 200) {
tableData.value = res.data.list
total.value = res.data.total
}
}
const showDialog = (row) => {
isEdit.value = !!row
editId.value = row?.id
Object.assign(form, row || { username: '', email: '', role: 'user', password: '' })
dialogVisible.value = true
}
const handleSubmit = async () => {
await formRef.value.validate()
const res = isEdit.value
? await updateUser(editId.value, form)
: await createUser(form)
if (res.code === 200) {
ElMessage.success(isEdit.value ? '更新成功' : '创建成功')
dialogVisible.value = false
fetchData()
}
}
const handleDelete = async (id) => {
const res = await deleteUser(id)
if (res.code === 200) {
ElMessage.success('删除成功')
fetchData()
}
}
onMounted(fetchData)
</script>
```
---
## 六、运行项目
### 后端
```bash
cd backend
python run.py
# 启动在 http://localhost:5000
```
### 前端
```bash
cd frontend
npm run dev
# 启动在 http://localhost:3000
```
### 默认账号
- 管理员:`admin` / `admin123`
---
## 七、适配其他毕设题目
这套模板是 **通用的**,你只需要:
1. **改模型** — 在 `models.py` 里把 `Book` 模型改成你需要的(如 Student、Order、Article)
2. **加路由** — 在 `routes/` 下新建对应的路由文件
3. **加页面** — 在 `views/` 下新建对应的 Vue 页面
4. **加菜单** — 在 `Layout.vue` 的菜单里加一项
举个例子,改成「学生信息管理系统」:
```python
# models.py - 把 Book 改成 Student
class Student(db.Model):
__tablename__ = 'students'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(50), nullable=False)
student_id = db.Column(db.String(20), unique=True) # 学号
gender = db.Column(db.String(10))
major = db.Column(db.String(100)) # 专业
class_name = db.Column(db.String(50)) # 班级
phone = db.Column(db.String(20))
created_at = db.Column(db.DateTime, default=datetime.utcnow)
```
**5 分钟就能换皮完成。**
---
## 八、部署上线
### 8.1 后端部署(Gunicorn)
```bash
pip install gunicorn
cd backend
gunicorn -w 4 -b 0.0.0.0:5000 "app:create_app()"
```
### 8.2 前端打包
```bash
cd frontend
npm run build
# 生成 dist/ 目录
```
### 8.3 Nginx 配置
```nginx
server {
listen 80;
server_name your-domain.com;
# 前端静态文件
location / {
root /path/to/frontend/dist;
try_files $uri $uri/ /index.html;
}
# 后端 API 代理
location /api/ {
proxy_pass http://127.0.0.1:5000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
```
---
## 九、答辩 Tips
1. **架构图一定要画** — 前后端分离架构图,评委最爱看
2. **数据库 ER 图** — 用 Navicat 或 draw.io 画
3. **演示流程** — 登录 → 首页 → 增删改查 → 退出,跑通就行
4. **重点讲难点** — JWT 认证、文件上传、分页查询,挑 1-2 个讲
5. **准备常见问题:**
- 为什么选 Flask 不选 Django?→ 轻量、灵活、适合中小型项目
- 前后端怎么通信?→ RESTful API + JSON + Axios
- 怎么保证安全?→ JWT Token + 密码哈希 + CORS
---
## 十、总结
本文提供了一套 **完整的 Vue 3 + Flask 前后端分离项目模板**:
| 功能 | 后端 | 前端 |
|------|------|------|
| 登录注册 | Flask-JWT-Extended | Pinia + 路由守卫 |
| 用户管理 | CRUD + 分页 + 搜索 | Element Plus 表格 |
| 文件上传 | Flask 文件处理 | Axios multipart |
| 权限控制 | JWT 中间件 | 路由守卫 |
**拿到源码后,改模型 + 改页面 = 你的毕设。**
---
> 📌 **如果这篇文章对你有帮助,请点赞 👍 + 收藏 ⭐ + 关注!**
>
> 毕设季持续更新更多项目模板,关注不迷路!
---
**标签:** Vue3、Flask、毕设、前后端分离、毕业设计、Python毕设、管理系统、Element Plus、JWT、2026毕设
更多推荐

所有评论(0)