07. 开发学生信息管理功能

SQL
CREATE TABLE `student` (
`id` int NOT NULL AUTO_INCREMENT COMMENT 'ID',
`username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '账号',
`password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '密码',
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '名称',
`avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '头像',
`role` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '角色',
`clazz_id` int DEFAULT NULL COMMENT '班级',
`major_id` int DEFAULT NULL COMMENT '专业',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `username` (`username`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC COMMENT='学生信息';
后端模块
class Student(Model):
"""学生模块"""
id = fields.IntField(pk=True, null=False)
username = fields.CharField(max_length=255, null=True)
password = fields.CharField(max_length=255, null=True)
name = fields.CharField(max_length=255, null=True)
avatar = fields.CharField(max_length=255, null=True)
role = fields.CharField(max_length=255, null=True)
major = fields.ForeignKeyField('models.Major', null=True)
clazz = fields.ForeignKeyField('models.Clazz', null=True)
class Meta:
table = 'student'
from typing import Optional
from fastapi import APIRouter
from pydantic import create_model, Field
from tortoise.contrib.pydantic import pydantic_model_creator
from common.exception_handler import CustomException
from common.result import Result, PageInfo
from models import Student, Clazz
router = APIRouter(prefix="/student")
StudentPydantic = pydantic_model_creator(Student)
StudentCreatePydantic = create_model(
"StudentCreatePydantic",
**{
name: (Optional[field.annotation], None)
for name, field in StudentPydantic.model_fields.items()
},
clazz_id=(Optional[int], Field(None, alias="clazzId"))
)
# 新增
@router.post("/add")
async def add(student_create_pydantic: StudentCreatePydantic):
db_student = await Student.get_or_none(username=student_create_pydantic.username)
if db_student is not None:
raise CustomException("账号重复")
# 将参数转换成 字典数据
create_data = student_create_pydantic.model_dump(exclude_unset=True, exclude={"id"})
create_data['role'] = '学生' # 设置默认的角色
create_data['password'] = '123' # 设置默认的密码
if student_create_pydantic.clazz_id is not None:
"""插入对应的专业ID"""
clazz = await Clazz.get_or_none(id=student_create_pydantic.clazz_id).prefetch_related("major")
create_data['major_id'] = clazz.major.id if clazz.major else None
await Student.create(**create_data) # no=xxx,name=xxx,college=xxx
return Result.success()
# 更新
@router.put("/update")
async def add(student_create_pydantic: StudentCreatePydantic):
if student_create_pydantic.id is None:
raise CustomException("缺少参数ID")
# 将参数转换成 字典数据
update_data = student_create_pydantic.model_dump(exclude_unset=True, exclude={"id"})
await Student.filter(id=student_create_pydantic.id).update(**update_data) # no=xxx,name=xxx,college=xxx where id = xxx
return Result.success()
# 删除
@router.delete('/delete/{student_id}')
async def delete(student_id: int):
await Student.filter(id=student_id).delete()
return Result.success()
# 单个查询
@router.get('/selectById/{student_id}')
async def select_by_id(student_id: int):
student = await Student.get_or_none(id=student_id)
return Result.success(student)
# 查询所有数据
@router.get('/selectAll')
async def select_all(name: str = ""):
student_list = await Student.filter(name__contains=name) # name__contains表示根据name进行模糊查询
return Result.success(student_list)
# 分页查询数据
@router.get('/selectPage')
async def select_all(name: str = "", pageNum: int = 1, pageSize: int = 10):
# name__contains表示根据name进行模糊查询 prefetch_related 关联查询到 major模块的数据
query = Student.filter(name__contains=name).prefetch_related("clazz", "major")
student_list = await query.offset((pageNum - 1) * pageSize).limit(pageSize)
total = await query.count()
# student_list 转成字典数据
# majorName 怎么返回??
# {id=xxx, name=xxx, no=xxx}
student_dict_list = [
{
**StudentPydantic.model_validate(student).model_dump(), # id=xxx,no=xxx,name=xxx
"clazzId": student.clazz.id if student.clazz else None,
"clazzName": student.clazz.name if student.clazz else None,
"majorName": student.major.name if student.major else None
}
for student in student_list
]
page_info = PageInfo(list=student_dict_list, total=total)
return Result.success(page_info)
前端页面
Studnet.vue
<template>
<div>
<div class="card" style="margin-bottom: 5px;">
<el-input v-model="data.name" style="width: 300px; margin-right: 10px" placeholder="请输入名称查询"></el-input>
<el-button type="primary" @click="load">查询</el-button>
<el-button type="info" style="margin: 0 10px" @click="reset">重置</el-button>
</div>
<div class="card" style="margin-bottom: 5px">
<div style="margin-bottom: 10px">
<el-button type="primary" @click="handleAdd">新增</el-button>
</div>
<el-table :data="data.tableData" stripe>
<el-table-column label="用户名" prop="username"></el-table-column>
<el-table-column label="名称" prop="name"></el-table-column>
<el-table-column label="头像">
<template #default="scope">
<el-image v-if="scope.row.avatar" preview-teleported :src="scope.row.avatar" :preview-src-list="[scope.row.avatar]" style="width: 40px; height: 40px; border-radius: 50%"></el-image>
</template>
</el-table-column>
<el-table-column label="角色" prop="role"></el-table-column>
<el-table-column label="班级" prop="clazzName"></el-table-column>
<el-table-column label="专业" prop="majorName"></el-table-column>
<el-table-column label="操作" align="center" width="160">
<template #default="scope">
<el-button type="primary" @click="handleEdit(scope.row)">编辑</el-button>
<el-button type="danger" @click="handleDelete(scope.row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
<div class="card">
<el-pagination @current-change="load" background layout="total, prev, pager, next" v-model:page-size="data.pageSize" v-model:current-page="data.pageNum" :total="data.total"/>
</div>
<el-dialog title="学生信息" width="40%" v-model="data.formVisible" :close-on-click-modal="false" destroy-on-close>
<el-form ref="formRef" :model="data.form" :rules="data.rules" label-width="100px" style="padding-right: 50px">
<el-form-item label="账号" prop="username">
<el-input :disabled="data.form.id > 0" v-model="data.form.username" autocomplete="off" />
</el-form-item>
<el-form-item label="名称" prop="name">
<el-input v-model="data.form.name" autocomplete="off" />
</el-form-item>
<el-form-item label="头像" prop="avatar">
<el-upload :action="uploadUrl" list-type="picture" :on-success="handleImgSuccess">
<el-button type="primary">上传图片</el-button>
</el-upload>
</el-form-item>
<el-form-item label="所属班级" prop="clazzId">
<el-select placeholder="请选择班级" v-model="data.form.clazzId">
<el-option v-for="item in data.classList" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="data.formVisible = false">取 消</el-button>
<el-button type="primary" @click="save">保 存</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup>
import request from "@/utils/request";
import {reactive, ref} from "vue";
import {ElMessageBox, ElMessage} from "element-plus";
// 文件上传的接口地址
const uploadUrl = import.meta.env.VITE_BASE_URL + '/files/upload'
const formRef = ref()
const data = reactive({
user: JSON.parse(localStorage.getItem('system-user') || '{}'),
pageNum: 1,
pageSize: 10,
total: 0,
formVisible: false,
form: {},
tableData: [],
classList: [],
name: null,
rules: {
username: [
{ required: true, message: '请输入账号', trigger: 'blur' }
],
name: [
{ required: true, message: '请输入名称', trigger: 'blur' }
],
avatar: [
{ required: true, message: '请上传头像', trigger: 'blur' }
],
clazzId: [
{ required: true, message: '请选择班级', trigger: 'change' }
],
}
})
// 查询班级的信息list
request.get('/clazz/selectAll').then(res => {
data.classList = res.data
})
// 分页查询
const load = () => {
request.get('/student/selectPage', {
params: {
pageNum: data.pageNum,
pageSize: data.pageSize,
name: data.name
}
}).then(res => {
if (res.code === '200') {
data.tableData = res.data?.list
data.total = res.data?.total
} else {
ElMessage.error(res.msg)
}
})
}
load()
// 新增
const handleAdd = () => {
data.form = {}
data.formVisible = true
}
// 编辑
const handleEdit = (row) => {
data.form = JSON.parse(JSON.stringify(row))
data.formVisible = true
}
// 新增保存
const add = () => {
request.post('/student/add', data.form).then(res => {
if (res.code === '200') {
load()
ElMessage.success('操作成功')
data.formVisible = false
} else {
ElMessage.error(res.msg)
}
})
}
// 编辑保存
const update = () => {
request.put('/student/update', data.form).then(res => {
if (res.code === '200') {
load()
ElMessage.success('操作成功')
data.formVisible = false
} else {
ElMessage.error(res.msg)
}
})
}
// 弹窗保存
const save = () => {
formRef.value.validate(valid => {
if (valid) {
// data.form有id就是更新,没有就是新增
data.form.id ? update() : add()
}
})
}
// 删除
const handleDelete = (id) => {
ElMessageBox.confirm('删除后数据无法恢复,您确定删除吗?', '删除确认', { type: 'warning' }).then(res => {
request.delete('/student/delete/' + id).then(res => {
if (res.code === '200') {
load()
ElMessage.success('操作成功')
} else {
ElMessage.error(res.msg)
}
})
}).catch(err => {})
}
// 重置
const reset = () => {
data.name = null
load()
}
// 处理文件上传的钩子
const handleImgSuccess = (res) => {
data.form.avatar = res.data // res.data就是文件上传返回的文件路径,获取到路径后赋值表单的属性
}
</script>