> 🎯 **适合人群:** 做毕设的大学生、想学前后端分离的 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毕设

更多推荐