关于mysql+vue+fastapi的用户管理案例

目录

前期环境安装

前端页面设计

数据库表设计

后端设计

Databases.py封装代码

后端api方法编写

pinina数据共享的js编写

[前端页面的script steup的代码参考](#前端页面的script steup的代码参考)

运行效果演示

注:需要源代码参考可以私信博主


前期环境安装

安装 fastapi

bash 复制代码
pip install fastapi

安装 uvicorn

bash 复制代码
pip install  uvicorn

安装 mysql包模块

bash 复制代码
pip install mysql-connector-python

前端页面设计

页面效果

javascript 复制代码
<template>
  <div class="user-management">
    <h1>用户管理</h1>

    <!-- 搜索和添加按钮 -->
    <div class="actions-bar">
      <div class="search-box">
        <input
            type="text"
            v-model="searchQuery"
            placeholder="搜索用户名或邮箱..."
        />
      </div>
      <button @click="openAddDialog" class="add-btn">添加用户</button>
    </div>

    <!-- 提示消息 -->
    <div v-if="error" class="error">{{ error }}</div>
    <div v-if="successMessage" class="success">{{ successMessage }}</div>

    <!-- 加载状态 -->
    <div v-if="isLoading" class="loading">加载中...</div>

    <!-- 用户列表 -->
    <table v-else-if="filteredUsers.length > 0" class="user-table">
      <thead>
      <tr>
        <th>用户名</th>
        <th>年龄</th>
        <th>邮箱</th>
        <th>操作</th>
      </tr>
      </thead>
      <tbody>
      <tr v-for="user in filteredUsers" :key="user.id">
        <td>{{ user.username }}</td>
        <td>{{ user.age }}</td>
        <td>{{ user.email }}</td>
        <td class="actions">
          <button @click="openEditDialog(user.id)" class="edit-btn">编辑</button>
          <button @click="confirmDelete(user.id)" class="delete-btn">删除</button>
        </td>
      </tr>
      </tbody>
    </table>

    <!-- 空列表提示 -->
    <p v-else class="empty-message">暂无用户数据</p>

    <!-- 确认删除弹窗 -->
    <div v-if="showDeleteConfirm" class="dialog-overlay">
      <div class="dialog-content confirm-dialog">
        <h3>确认删除</h3>
        <p>确定要删除该用户吗?此操作不可撤销。</p>
        <div class="dialog-footer">
          <button @click="cancelDelete" class="cancel-btn">取消</button>
          <button @click="deleteUser" class="delete-btn">删除</button>
        </div>
      </div>
    </div>

    <!-- 添加/编辑用户弹窗 -->
    <UserFormDialog
        :is-open="isDialogOpen"
        :user-data="selectedUser"
        :mode="dialogMode"
        @close="closeDialog"
        @submit="handleFormSubmit"
    />
  </div>
</template>

<style scoped>
.user-management {
  max-width: 800px;
  margin: 0 auto;
  padding: 20px;
  font-family: Arial, sans-serif;
}

.actions-bar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 20px;
  flex-wrap: wrap;
  gap: 10px;
}

.search-box input {
  padding: 8px;
  border: 1px solid #ddd;
  border-radius: 4px;
  width: 250px;
  transition: border-color 0.3s;
}

.search-box input:focus {
  outline: none;
  border-color: #42b983;
}

.add-btn {
  padding: 8px 16px;
  background-color: #42b983;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  transition: background-color 0.3s;
}

.add-btn:hover {
  background-color: #3aa876;
}

.user-table {
  width: 100%;
  border-collapse: collapse;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
}

.user-table th, 
.user-table td {
  padding: 12px;
  text-align: left;
  border-bottom: 1px solid #eee;
}

.user-table th {
  background-color: #f5f5f5;
  font-weight: bold;
}

.user-table tr:hover {
  background-color: #f9f9f9;
}

.actions {
  display: flex;
  gap: 10px;
}

.edit-btn {
  padding: 5px 10px;
  background-color: #3498db;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  transition: background-color 0.3s;
}

.edit-btn:hover {
  background-color: #2980b9;
}

.delete-btn {
  padding: 5px 10px;
  background-color: #e74c3c;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  transition: background-color 0.3s;
}

.delete-btn:hover {
  background-color: #c0392b;
}

.error {
  text-align: center;
  color: #e74c3c;
  margin-bottom: 16px;
  padding: 10px;
  background-color: #fdecea;
  border-radius: 4px;
}

.success {
  text-align: center;
  color: #27ae60;
  margin-bottom: 16px;
  padding: 10px;
  background-color: #eafaf1;
  border-radius: 4px;
  animation: fadeInOut 3s ease-in-out;
}

.loading {
  text-align: center;
  padding: 20px;
  color: #7f8c8d;
}

.empty-message {
  text-align: center;
  padding: 20px;
  color: #7f8c8d;
}

/* 确认删除弹窗样式 */
.dialog-overlay {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: rgba(0, 0, 0, 0.5);
  display: flex;
  justify-content: center;
  align-items: center;
  z-index: 1000;
  animation: fadeIn 0.3s ease-out;
}

.dialog-content {
  background-color: white;
  border-radius: 12px;
  box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
  overflow: hidden;
  transform: translateY(-20px);
  animation: slideUp 0.3s ease-out forwards;
}

.confirm-dialog {
  width: 360px;
  max-width: 90%;
  padding: 24px;
  text-align: center;
}

.confirm-dialog h3 {
  font-size: 20px;
  color: #333;
  margin-top: 0;
  margin-bottom: 16px;
  font-weight: 600;
}

.confirm-dialog p {
  margin-bottom: 24px;
  color: #555;
  font-size: 14px;
  line-height: 1.5;
}

.dialog-footer {
  display: flex;
  justify-content: center;
  gap: 12px;
  margin-top: 16px;
}

.dialog-footer .cancel-btn {
  padding: 10px 20px;
  background-color: #f1f1f1;
  color: #333;
  border: none;
  border-radius: 6px;
  cursor: pointer;
  font-weight: 500;
  transition: background-color 0.3s;
}

.dialog-footer .cancel-btn:hover {
  background-color: #e0e0e0;
}

.dialog-footer .delete-btn {
  padding: 10px 20px;
  background-color: #e74c3c;
  color: white;
  border: none;
  border-radius: 6px;
  cursor: pointer;
  font-weight: 500;
  transition: background-color 0.3s, transform 0.2s;
}

.dialog-footer .delete-btn:hover {
  background-color: #c0392b;
  transform: translateY(-1px);
}

.dialog-footer .delete-btn:active {
  transform: translateY(0);
}

@keyframes fadeIn {
  from { opacity: 0; }
  to { opacity: 1; }
}

@keyframes slideUp {
  from { transform: translateY(-20px); opacity: 0; }
  to { transform: translateY(0); opacity: 1; }
}

@keyframes fadeInOut {
  0% { opacity: 0; }
  10% { opacity: 1; }
  90% { opacity: 1; }
  100% { opacity: 0; }
}

/* 响应式调整 */
@media (max-width: 600px) {
  .actions-bar {
    flex-direction: column;
    align-items: stretch;
  }

  .search-box input {
    width: 100%;
    margin-bottom: 10px;
  }

  .user-table {
    display: block;
    overflow-x: auto;
  }
}
</style>

数据库表设计

users表

sql 复制代码
create table users(
id int PRIMARY key auto_increment,
name varchar(50) not null,
age int
)

emails表

sql 复制代码
create table emails(
id int PRIMARY key auto_increment,
user_id int,
email varchar(50) unique,
FOREIGN key (user_id) REFERENCES users(id)
)

后端设计

需要封装数据库调用和方法的py文件

以及fastapi的接口方法编写

Databases.py封装代码

python 复制代码
class Database:
    def __init__(self, host, database, user, password, port=3306):
        self.host = host
        self.database = database
        self.user = user
        self.password = password
        self.port = port

    def get_connection(self):
        """获取数据库连接"""
        try:
            connection = mysql.connector.connect(
                host=self.host,
                database=self.database,
                user=self.user,
                password=self.password,
                port=self.port
            )
            if connection.is_connected():
                logging.info("成功获取到 MySQL 数据库连接")
                return connection
        except Error as e:
            logging.error(f"获取数据库连接时出错: {e}")
            raise

    def disconnect(self, connection):
        """关闭数据库连接"""
        if connection and connection.is_connected():
            connection.close()
            logging.info("MySQL 连接已关闭")

    def execute_query(self, query, params=None):
        """执行查询操作"""
        try:
            with self.get_connection() as connection:
                cursor = connection.cursor(dictionary=True)
                cursor.execute(query, params)
                result = cursor.fetchall()
                cursor.close()
                return result
        except Error as e:
            logging.error(f"执行查询时出错: {e}")
            raise

    def execute_update(self, query, params=None):
        """执行更新操作(INSERT, UPDATE, DELETE)"""
        try:
            with self.get_connection() as connection:
                cursor = connection.cursor()
                cursor.execute(query, params)
                connection.commit()
                rowcount = cursor.rowcount
                cursor.close()
                return rowcount
        except Error as e:
            connection.rollback()
            logging.error(f"执行更新时出错: {e}")
            raise

后端api方法编写

在创建完fast的app对象后

记得编写以下内容,实现数据的全局共享

python 复制代码
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

创建数据库对象

python 复制代码
db = Database(
    host='localhost',
    database='数据库名称',
    user='root',
    password='password'
)

在创建完对象,之后再编写fastapi接口

编写获取数据库内表的全部数据的api接口

python 复制代码
@app.get("/alldate")
async def get_all_date():
    try:
        with db.get_connection() as conn:
            with conn.cursor(dictionary=True) as cur:
                cur.execute("""
                    SELECT u.id, u.name, u.age, e.email
                    FROM users u
                    JOIN emails e ON u.id = e.user_id
                """)
                return cur.fetchall()
    except Exception as e:
        return {"error": str(e)}

添加用户adddate接口

python 复制代码
@app.post("/adddate")
async def add_date(
    name: str = Form(...),
    age: int = Form(...),
    email: str = Form(...)
):
    try:
        with db.get_connection() as conn:
            with conn.cursor() as cur:
                conn.start_transaction()
                cur.execute(
                    "INSERT INTO users (name, age) VALUES (%s, %s)",
                    (name, age)
                )
                user_id = cur.lastrowid
                cur.execute(
                    "INSERT INTO emails (email, user_id) VALUES (%s, %s)",
                    (email, user_id)
                )
                conn.commit()
                return {"message": "添加成功"}
    except Exception as e:
        return {"message": f"添加失败: {str(e)}"}

注:传参的时候采用form传参,因为页面组件采用的是form表单,如果只是name:str这样子去传的话,实际运行会出现错误代码422传参传不过去的。

在上面这个接口,我们只要修改语句调用database.py内的方法,修改sql语句就能实现完整的增删改查,为了篇幅,这个要自己去写。

pinina数据共享的js编写

需要导入pinia模块

详细参考:介绍 | Pinia 中文文档

UserStore.js

javascript 复制代码
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useUserStore = defineStore('user', () => {
  const users = ref([])
  const loading = ref(false)
  const error = ref(null)

  const allUsers = computed(() => users.value)

  const getUserById = (id) =>
    users.value.find(u => u.id == id) || null

  const fetchUsers = async () => {
    loading.value = true
    error.value = null
    try {
      const res = await fetch('http://127.0.0.1:8000/alldate')
      const data = await res.json()
      if (data.error) {
        error.value = data.error
      } else {
        users.value = data.map(u => ({
          id: u.id,
          username: u.name,
          age: u.age,
          email: u.email
        }))
      }
    } catch (err) {
      error.value = err.message || err
    } finally {
      loading.value = false
    }
  }

  const addUser = async ({ username, age, email }) => {
    loading.value = true
    error.value = null
    try {
      const res = await fetch('http://127.0.0.1:8000/adddate', {
        method: 'POST',
        headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
        body: new URLSearchParams({ name: username, age, email })
      })
      const result = await res.json()
      if (result.message && result.message.includes('失败')) throw new Error(result.message)
      await fetchUsers()
      return result
    } catch (err) {
      error.value = err.message
      throw err
    } finally {
      loading.value = false
    }
  }

  const updateUser = async (id, { username, age, email }) => {
    loading.value = true
    error.value = null
    try {
      const res = await fetch('http://127.0.0.1:8000/updatedate', {
        method: 'POST',
        headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
        body: new URLSearchParams({ id, name: username, age, email })
      })
      const result = await res.json()
      if (result.message && result.message.includes('失败')) throw new Error(result.message)
      await fetchUsers()
      return result
    } catch (err) {
      error.value = err.message
      throw err
    } finally {
      loading.value = false
    }
  }

  const deleteUser = async (id) => {
    loading.value = true
    error.value = null
    try {
      const res = await fetch('http://127.0.0.1:8000/deletedate', {
        method: 'POST',
        headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
        body: new URLSearchParams({ id })
      })
      const result = await res.json()
      if (result.message && result.message.includes('失败')) throw new Error(result.message)
      await fetchUsers()
      return result
    } catch (err) {
      error.value = err.message
      throw err
    } finally {
      loading.value = false
    }
  }

  return {
    users,
    loading,
    error,
    allUsers,
    fetchUsers,
    addUser,
    updateUser,
    deleteUser,
    getUserById
  }
})

篇幅有限,下篇文章会详细讲解编写者一个模块。

前端页面的script steup的代码参考

javascript 复制代码
import { ref, computed, watch, onMounted } from 'vue'
import { useUserStore } from '../stores/userStore'
import UserFormDialog from './UserFormDialog.vue'

/* ---------- 使用 Pinia ---------- */
const userStore = useUserStore()

/* ---------- 本地响应式变量 ---------- */
const searchQuery     = ref('')
const isDialogOpen    = ref(false)
const dialogMode      = ref('add')       // 'add' | 'edit'
const selectedUser    = ref(null)
const showDeleteConfirm = ref(false)
const userIdToDelete  = ref(null)

/* 用来在模板里展示的错误 / 成功提示 / 加载状态 */
const error         = computed(() => userStore.error)
const isLoading     = computed(() => userStore.loading)
const successMessage = ref('')

/* ---------- 搜索过滤 ---------- */
const filteredUsers = computed(() => {
  const q = searchQuery.value.toLowerCase().trim()
  if (!q) return userStore.allUsers
  return userStore.allUsers.filter(u =>
    u.username.toLowerCase().includes(q) ||
    u.email.toLowerCase().includes(q)
  )
})

/* ---------- 业务函数 ---------- */
const openAddDialog = () => {
  dialogMode.value = 'add'
  selectedUser.value = null
  isDialogOpen.value = true
}

const openEditDialog = (id) => {
  const user = userStore.getUserById(id)
  if (user) {
    dialogMode.value = 'edit'
    selectedUser.value = { ...user }
    isDialogOpen.value = true
  }
}

const closeDialog = () => {
  isDialogOpen.value = false
  selectedUser.value = null
}

const handleFormSubmit = async (formData) => {
  try {
    if (dialogMode.value === 'add') {
      await userStore.addUser(formData)
      successMessage.value = '用户添加成功'
    } else {
      await userStore.updateUser(selectedUser.value.id, formData)
      successMessage.value = '用户更新成功'
    }
  } catch (e) {
    /* error 已在 store 里设置,无需手动赋值 */
  }
}

const confirmDelete = (id) => {
  userIdToDelete.value = id
  showDeleteConfirm.value = true
}

const cancelDelete = () => {
  showDeleteConfirm.value = false
  userIdToDelete.value = null
}

const deleteUser = async () => {
  if (userIdToDelete.value) {
    try {
      await userStore.deleteUser(userIdToDelete.value)
      successMessage.value = '用户删除成功'
    } catch {}
    cancelDelete()
  }
}

/* 3 秒后自动清空成功提示 */
const clearSuccess = () => {
  setTimeout(() => (successMessage.value = ''), 3000)
}
watch(successMessage, () => successMessage.value && clearSuccess())

/* ---------- 初始化 ---------- */
onMounted(() => userStore.fetchUsers())

运行效果演示

注:需要源代码参考可以私信博主