学生管理

数据库

先在数据库中创建一张学生信息表

Student 实体类定义

/model的包下创建一个student.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# -*- coding:utf-8 -*-
# Author: Zachary
from sqlalchemy import Integer, String
from sqlalchemy.orm import Mapped, mapped_column

from model import Base


class Student(Base):
__tablename__ = "student"
id: Mapped[int] = mapped_column(Integer, primary_key=True, nullable=False)
username: Mapped[str] = mapped_column(String(255), nullable=False)
password: Mapped[str] = mapped_column(String(255), nullable=False)
name: Mapped[str] = mapped_column(String(255), nullable=False)
phone: Mapped[str] = mapped_column(String(255), nullable=False)
gender: Mapped[str] = mapped_column(String(255), nullable=False)
birthday: Mapped[str] = mapped_column(String(255), nullable=False)
avatar: Mapped[str] = mapped_column(String(255), nullable=False

学生登录

前端-登录页面

学生登录和 admin 的登录,我们复用同一个前端页面和后端接口,所以需要加一个role用以区分是 admin 登录了还是普通的 student 登录了;同时,不同用户进入页面之后。看到的内容应该是不同的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
<template>
<div>
<div class="login-container">
<div style="width: 420px" class="login-box">
<div class="title">学生信息管理系统 - 登录</div>
<el-form :model="data.form" ref="formRef" :rules="rules">
<el-form-item prop="username">
<el-input
style="height: 40px;font-size: 18px"
prefix-icon="Avatar"
v-model="data.form.username"
placeholder="请输入账号"
/>
</el-form-item>
<el-form-item prop="password">
<el-input
show-password
style="height: 40px;font-size: 18px"
prefix-icon="Lock"
v-model="data.form.password"
placeholder="请输入密码"
/>
</el-form-item>
<el-form-item prop="role">
<el-radio-group size="large" v-model="data.form.role">
<el-radio-button
style="margin-left: 70px"
label="管理员"
value="ADMIN"
/>
<el-radio-button
style="margin-left: 50px"
label="学生"
value="STUDENT"
/>
</el-radio-group>
</el-form-item>
<el-form-item>
<el-button
type="primary"
style="width: 100%;font-size: 18px"
plain
@click="login"
>登 录</el-button
>
</el-form-item>
</el-form>
<div style="margin-top:35px;text-align: right;font-size: 15px">
还没有账号?请<a href="/register">注册</a>
</div>
</div>
</div>
</div>
</template>

<script setup>
import { reactive, ref } from "vue";
import request from "@/utils/request";
import { ElMessage } from "element-plus";
import router from "@/router";

const data = reactive({
form: { role: "ADMIN" },
});

const rules = reactive({
username: [{ required: true, message: "请输入账号", trigger: "blur" }],
password: [{ required: true, message: "请输入密码", trigger: "blur" }],
});

const formRef = ref();

const login = () => {
formRef.value.validate((valid) => {
if (valid) {
request
.post("/login", data.form)
.then((res) => {
if (res.code === "200") {
localStorage.setItem("student-user", JSON.stringify(res.data));
ElMessage.success("登录成功");
router.push("/home");
} else {
ElMessage.error(res.msg);
}
})
.catch((err) => {
ElMessage.error(err.response?.data?.msg || err.message);
});
}
});
};
</script>

<style scoped>
.login-container {
min-height: 100vh;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
background-image: url("@/assets/imgs/login_background.png");
background-size: cover;
}

.login-box {
background-color: rgb(255, 255, 255, 50%);
box-shadow: 0 0 10px rgba(84, 221, 245, 0.41);
padding: 30px;
}

.title {
font-weight: bold;
font-size: 30px;
text-align: center;
margin-bottom: 35px;
}
</style>

后端-管理员/学生登录的 api 接口

管理员和学生的登录复用同一个 api 接口

所以需要修改一下之前的登录逻辑

Account

/model下创建一个account.py,接收前端传递来的账号、密码、角色

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# -*- coding:utf-8 -*-
# Author: Zachary
from pydantic import BaseModel


class AccountLogin(BaseModel):
username: str
password: str
role: str


class AccountLoginResponse:
id: int
username: str
name: str
role: str
token: str

Student

给 student 表追加role字段

/model/student.py更新一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# -*- coding:utf-8 -*-
# Author: Zachary
from sqlalchemy import Integer, String
from sqlalchemy.orm import Mapped, mapped_column

from model import Base


class Student(Base):
__tablename__ = "student"
id: Mapped[int] = mapped_column(Integer, primary_key=True, nullable=False)
username: Mapped[str] = mapped_column(String(255), nullable=False)
password: Mapped[str] = mapped_column(String(255), nullable=False)
name: Mapped[str] = mapped_column(String(255), nullable=False)
phone: Mapped[str] = mapped_column(String(255), nullable=False)
gender: Mapped[str] = mapped_column(String(255), nullable=False)
birthday: Mapped[str] = mapped_column(String(255), nullable=False)
avatar: Mapped[str] = mapped_column(String(255), nullable=False)
role: Mapped[str] = mapped_column(String(255), nullable=False)

Admin

给 admin 表追加name字段和role字段

/model/admin.py更新一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# -*- coding:utf-8 -*-
# Author: Zachary
import bcrypt
from pydantic import BaseModel
from sqlalchemy import Integer, String
from sqlalchemy.orm import Mapped, mapped_column

from model import Base


class Admin(Base):
__tablename__ = "admin"
id: Mapped[int] = mapped_column(Integer, primary_key=True, nullable=False)
username: Mapped[str] = mapped_column(String(255), nullable=False)
password: Mapped[str] = mapped_column(String(255), nullable=False)
name: Mapped[str] = mapped_column(String(255), nullable=False)
role: Mapped[str] = mapped_column(String(255), nullable=False)


class AdminModel(BaseModel):
username: str
password: str


class AdminLoginResponse(BaseModel):
id: int
username: str
token: str

Service

在项目目录/service下创建一个studentService.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# -*- coding:utf-8 -*-
# Author: Zachary
from fastapi.encoders import jsonable_encoder
from sqlalchemy import select

from common.auth import auth_handler
from common.utils import set_attrs
from exception.customException import UserNotFoundException, PasswordNotMatchException
from model import Session
from model.account import AccountLogin, AccountLoginResponse
from model.student import Student


class StudentService:
@staticmethod
def login(account: AccountLogin, db_session: Session) -> AccountLoginResponse:
query = select(Student).where(Student.username == account.username)
exist_student: Student = db_session.execute(query).scalars().first()
if not exist_student:
raise UserNotFoundException("用户不存在")
if not auth_handler.verify_password(account.password, exist_student.password):
raise PasswordNotMatchException("身份验证未通过")
account_login_response = AccountLoginResponse()
set_attrs(account_login_response, jsonable_encoder(exist_student))
account_login_response.token = auth_handler.encode_token(exist_student.id)
return account_login_response

然后修改adminService.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# -*- coding:utf-8 -*-
# Author: Zachary
from fastapi.encoders import jsonable_encoder
from sqlalchemy import select

from common.auth import auth_handler
from common.utils import set_attrs
from exception.customException import UserNotFoundException, PasswordNotMatchException
from model import Session
from model.account import AccountLoginResponse, AccountLogin
from model.admin import Admin


class AdminService:
@staticmethod
def login(account: AccountLogin, db_session: Session) -> AccountLoginResponse:
query = select(Admin).where(Admin.username == account.username)
exist_admin: Admin = db_session.execute(query).scalars().first()
if not exist_admin:
raise UserNotFoundException("用户不存在")
if not auth_handler.verify_password(account.password, exist_admin.password):
raise PasswordNotMatchException("身份验证未通过")
account_login_response = AccountLoginResponse()
set_attrs(account_login_response, jsonable_encoder(exist_admin))
account_login_response.token = auth_handler.encode_token(exist_admin.id)
return account_login_response

Role

创建一个枚举类 Role,用以区分学生和管理员

/common下创建一个Enum.py

1
2
3
4
5
6
7
8
# -*- coding:utf-8 -*-
# Author: Zachary
from enum import Enum


class Role(str, Enum):
ADMIN = "管理员"
STUDENT = "学生"

API

然后修改一下登录的 api

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# -*- coding:utf-8 -*-
# Author: Zachary
from fastapi import Depends
from fastapi.encoders import jsonable_encoder

from api import app
from common.Enum import Role
from common.result import Result, ResultModel
from model import Session, get_db_session
from model.account import AccountLogin
from service.adminService import AdminService
from service.studentService import StudentService


@app.post("/login", response_model=ResultModel)
async def login(account: AccountLogin, db_session: Session = Depends(get_db_session)):
if Role.ADMIN.name.__eq__(account.role):
db_account = AdminService.login(account, db_session)
elif Role.STUDENT.name.__eq__(account.role):
db_account = StudentService.login(account, db_session)
else:
return Result.error("角色错误")
return Result.success(jsonable_encoder(db_account))

学生注册

前端-注册页面

写一下学生的注册页面,这里管理员就不参与注册了

在前端项目目录/views下创建一个Register.vue(这里我偷懒了,没有替换注册页的背景图,大家可以自行替换)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
<template>
<div>
<div class="register-container">
<div style="width: 400px" class="register-box">
<div class="title">学生信息管理系统 - 注册</div>
<el-form :model="data.form" ref="formRef" :rules="rules">
<el-form-item prop="username">
<el-input
style="height: 40px;font-size: 18px"
prefix-icon="User"
v-model="data.form.username"
placeholder="请输入账号"
/>
</el-form-item>
<el-form-item prop="password">
<el-input
show-password
style="height: 40px;font-size: 18px"
prefix-icon="Lock"
v-model="data.form.password"
placeholder="请输入密码"
/>
</el-form-item>
<el-form-item>
<el-button
type="primary"
style="width: 100%"
plain
@click="register"
>注 册</el-button
>
</el-form-item>
</el-form>
<div style="margin-top:30px;text-align: right">
已有账号?请直接<a href="/login">登录</a>
</div>
</div>
</div>
</div>
</template>

<script setup>
import { reactive, ref } from "vue";
import request from "@/utils/request";
import { ElMessage } from "element-plus";
import router from "@/router";

const data = reactive({
form: {},
});

const rules = reactive({
username: [{ required: true, message: "请输入账号", trigger: "blur" }],
password: [{ required: true, message: "请输入密码", trigger: "blur" }],
});

const formRef = ref();

const register = () => {
formRef.value.validate((valid) => {
if (valid) {
request
.post("/register", data.form)
.then((res) => {
if (res.code === "200") {
ElMessage.success("注册成功");
router.push("/login");
} else {
ElMessage.error(res.msg);
}
})
.catch((err) => {
ElMessage.error(err.response?.data?.msg || err.message);
});
}
});
};
</script>

<style scoped>
.register-container {
min-height: 100vh;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
background-image: url("@/assets/imgs/login_background.png");
background-size: cover;
}

.register-box {
background-color: rgb(255, 255, 255, 50%);
box-shadow: 0 0 10px rgba(84, 221, 245, 0.41);
padding: 30px;
}

.title {
font-weight: bold;
font-size: 30px;
text-align: center;
margin-bottom: 35px;
}
</style>

添加一下注册页面的路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import { createRouter, createWebHistory } from "vue-router";

const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: "/",
name: "Manager",
component: () => import("@/views/Manager.vue"),
redirect: "/home",
children: [
{
path: "home",
name: "Home",
component: () => import("@/views/manager/Home.vue"),
meta: { requiresAuth: true },
},
{
path: "course",
name: "Course",
component: () => import("@/views/manager/Course.vue"),
meta: { requiresAuth: true },
},
],
},
{
path: "/login",
name: "Login",
component: () => import("@/views/Login.vue"),
},
{
path: "/register",
name: "Register",
component: () => import("@/views/Register.vue"),
},
],
});

router.beforeEach((to, from, next) => {
const requiresAuth = to.matched.some((record) => record.meta.requiresAuth);
const user = JSON.parse(localStorage.getItem("student-user"));
if (requiresAuth && !user) {
// 如果目标路由需要认证,并且用户未登录
next("/login"); // 跳转到登录页面
} else {
next(); // 如果目标路由不需要认证,或者用户已登录,则正常导航到目标路由
}
});

export default router;

后端-注册的 api 接口

接下来实现一下,后端注册的逻辑

API

因为这个注册只是给学生使用的,在项目目录/api下新建一个studentApi.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# -*- coding:utf-8 -*-
# Author: Zachary
from fastapi import Depends

from api import app
from common.result import ResultModel, Result
from model import Session, get_db_session
from model.account import AccountRegister
from service.studentService import StudentService


@app.post("/register", response_model=ResultModel)
async def register(account: AccountRegister, db_session: Session = Depends(get_db_session)):
StudentService.register(account, db_session)
return Result.success()

添加一下 studentApi

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# -*- coding:utf-8 -*-
# Author: Zachary

from fastapi import FastAPI
from starlette.middleware.cors import CORSMiddleware

app = FastAPI()

# 跨域问题
origins = ["http://localhost:5173", "http://127.0.0.1:5173"] # 替换为你的前端应用的实际地址

app.add_middleware(
CORSMiddleware,
allow_origins=origins, # 允许跨域访问的来源域名列表
allow_credentials=True, # 是否允许携带cookie
allow_methods=["*"], # 允许的方法,默认包含常见的GET、POST等,"*"表示所有方法
allow_headers=["*"], # 允许的请求头,默认包含常见的Content-Type等,"*"表示所有请求头
)

from api import adminApi, exceptionHandler, courseApi, studentApi

account.py添加注册的请求体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# -*- coding:utf-8 -*-
# Author: Zachary
from pydantic import BaseModel


class AccountLogin(BaseModel):
username: str
password: str
role: str


class AccountLoginResponse:
id: int
username: str
name: str
role: str
token: str


class AccountRegister(BaseModel):
username: str
password: str

Service

实现一下注册的 service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# -*- coding:utf-8 -*-
# Author: Zachary
from fastapi.encoders import jsonable_encoder
from sqlalchemy import select

from common.Enum import Role
from common.auth import auth_handler
from common.utils import set_attrs
from exception.customException import UserNotFoundException, PasswordNotMatchException, UserExistException
from model import Session
from model.account import AccountLogin, AccountLoginResponse, AccountRegister
from model.student import Student


class StudentService:
@staticmethod
def login(account: AccountLogin, db_session: Session) -> AccountLoginResponse:
query = select(Student).where(Student.username == account.username)
exist_student: Student = db_session.execute(query).scalars().first()
if not exist_student:
raise UserNotFoundException("用户不存在")
if not auth_handler.verify_password(account.password, exist_student.password):
raise PasswordNotMatchException("身份验证未通过")
account_login_response = AccountLoginResponse()
set_attrs(account_login_response, jsonable_encoder(exist_student))
account_login_response.token = auth_handler.encode_token(exist_student.id)
return account_login_response

@staticmethod
def register(account: AccountRegister, db_session: Session):
query = select(Student).where(Student.username == account.username)
exist_student = db_session.execute(query).scalars().first()
if exist_student:
raise UserExistException("账号已存在")

new_student = Student()
account.password = auth_handler.get_password_hash(account.password)
set_attrs(new_student, jsonable_encoder(account))
if new_student.name is None:
new_student.name = account.username
new_student.role = Role.STUDENT.name

db_session.add(new_student)
db_session.commit()
return new_student

添加自定义异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# -*- coding:utf-8 -*-
# Author: Zachary

class UserNotFoundException(Exception):
def __init__(self, message: str):
self.message = message


class UserExistException(Exception):
def __init__(self, message: str):
self.message = message


class PasswordNotMatchException(Exception):
def __init__(self, message: str):
self.message = message


class TokenException(Exception):
def __init__(self, message: str):
self.message = message


class CourseExistException(Exception):
def __init__(self, message: str):
self.message = message


class CourseNotExistException(Exception):
def __init__(self, message: str):
self.message = message
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# -*- coding:utf-8 -*-
# Author: Zachary
from fastapi.encoders import jsonable_encoder
from starlette.responses import JSONResponse

from api import app
from fastapi import Request

from common.result import Result
from exception.customException import UserNotFoundException, PasswordNotMatchException, TokenException, \
CourseExistException, CourseNotExistException, UserExistException


@app.exception_handler(Exception)
async def exception_handler(request: Request, exc: Exception):
result = Result.error(code="500", msg=str(exc))
return JSONResponse(status_code=500, content=jsonable_encoder(result))


@app.exception_handler(UserNotFoundException)
async def user_not_fount_exception_handler(request: Request, exc: UserNotFoundException):
result = Result.error(code="404", msg=exc.message)
return JSONResponse(status_code=404, content=jsonable_encoder(result))


@app.exception_handler(PasswordNotMatchException)
async def password_not_fount_exception_handler(request: Request, exc: PasswordNotMatchException):
result = Result.error(code="401", msg=exc.message)
return JSONResponse(status_code=401, content=jsonable_encoder(result))


@app.exception_handler(TokenException)
async def token_exception_handler(request: Request, exc: TokenException):
result = Result.error(code="401", msg=exc.message)
return JSONResponse(status_code=401, content=jsonable_encoder(result))


@app.exception_handler(CourseExistException)
async def course_exist_exception_handler(request: Request, exc: CourseExistException):
result = Result.error(code="400", msg=exc.message)
return JSONResponse(status_code=400, content=jsonable_encoder(result))


@app.exception_handler(CourseNotExistException)
async def course_not_exist_exception_handler(request: Request, exc: CourseNotExistException):
result = Result.error(code="404", msg=exc.message)
return JSONResponse(status_code=404, content=jsonable_encoder(result))


@app.exception_handler(UserExistException)
async def user_exist_exception_handler(request: Request, exc: UserExistException):
result = Result.error(code="400", msg=exc.message)
return JSONResponse(status_code=400, content=jsonable_encoder(result))

登录/注册测试

现在测试一下学生登录和注册的逻辑

学生注册

学生登录

现在登录进来了,但是右上角的显示不太对,等下改改

管理员登录

学生管理

这块咱们做个设定,当管理员新增用户、替用户修改用户数据,密码必须重新设置为初始密码-123456

前端

复制粘贴一下之前的Course.vue,命名为Student.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
<template>
<div>
<div class="card" style="margin-bottom: 10px">
<el-input
@input="load"
style="width: 260px"
v-model="data.username"
placeholder="请输入要查询的学生学号"
:prefix-icon="Search"
/>
<el-input
@input="load"
style="margin-left:10px; width: 260px"
v-model="data.name"
placeholder="请输入要查询的学生姓名"
:prefix-icon="Search"
/>
<el-button type="info" plain style="margin: 0 0 0 10px" @click="reset"
>重置</el-button
>
</div>

<div class="card" style="margin-bottom: 10px">
<div>
<el-button
type="primary"
style="margin-bottom: 5px"
plain
@click="handleAdd"
>新增</el-button
>
</div>
<div>
<el-table :data="data.tableData" stripe style="width: 100%">
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="username" label="学生学号" />
<el-table-column prop="name" label="学生姓名" />
<el-table-column prop="gender" label="性别" />
<el-table-column prop="phone" label="手机号" />
<el-table-column prop="birthday" label="出生日期" />
<el-table-column prop="avatar" label="头像" />
<el-table-column label="操作" align="center">
<template #default="scope">
<el-button
type="primary"
size="small"
plain
@click="handleEdit(scope.row)"
>编辑</el-button
>
<el-button
type="danger"
size="small"
plain
@click="handleDelete(scope.row.id)"
>删除</el-button
>
</template>
</el-table-column>
</el-table>
</div>
</div>

<div class="card">
<el-pagination
v-model:current-page="data.pageNum"
v-model:page-size="data.pageSize"
@current-change="handleCurrentChange"
background
layout="prev, pager, next"
:total="data.total"
/>
</div>

<el-dialog width="35%" v-model="data.formVisible" title="学生信息">
<el-form
:model="data.form"
:rules="rules"
ref="formRef"
label-width="100px"
label-position="right"
style="padding-right: 45px"
>
<el-form-item label="学生学号" prop="username">
<el-input v-model="data.form.username" autocomplete="off" />
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input v-model="data.form.password" autocomplete="off" disabled />
</el-form-item>
<el-form-item label="学生姓名">
<el-input v-model="data.form.name" autocomplete="off" />
</el-form-item>
<el-form-item label="性别">
<el-radio-group v-model="data.form.gender">
<el-radio label="男"></el-radio>
<el-radio label="女"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="手机号">
<el-input v-model="data.form.phone" autocomplete="off" />
</el-form-item>
<el-form-item label="出生日期">
<el-date-picker
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
v-model="data.form.birthday"
></el-date-picker>
</el-form-item>
<el-form-item label="头像">
<el-input v-model="data.form.avatar" autocomplete="off" />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="data.formVisible = false" plain>取消</el-button>
<el-button type="primary" @click="save" plain>保存</el-button>
</span>
</template>
</el-dialog>
</div>
</template>

<script setup>
import { reactive, ref } from "vue";
import { Search } from "@element-plus/icons-vue";
import request from "@/utils/request";
import { ElMessage, ElMessageBox } from "element-plus";

const data = reactive({
username: "",
name: "",
tableData: [],
total: 0,
pageNum: 1,
pageSize: 5,
formVisible: false,
form: { password: "123456" },
});

const load = () => {
request
.get("/student/selectPage", {
params: {
pageNum: data.pageNum,
pageSize: data.pageSize,
username: data.username,
name: data.name,
},
})
.then((res) => {
if (res.code === "200") {
data.tableData = res.data?.list || [];
data.total = res.data?.total || 0;
} else {
ElMessage.error(res.msg);
}
})
.catch((err) => {
if (err.response?.data?.code === "401") {
localStorage.removeItem("student-user");
}
ElMessage.error(err.response?.data?.msg || err.message);
});
};

// 加载一次 获取课程数据
load();

const handleCurrentChange = () => {
// 当翻页的时候重新加载数据
load();
};

const reset = () => {
data.username = "";
data.name = "";
load();
};

const rules = reactive({
username: [{ required: true, message: "请输入学生学号", trigger: "blur" }],
password: [{ required: true, message: "请输入密码", trigger: "blur" }],
});

const formRef = ref();

const handleAdd = () => {
data.formVisible = true;
data.form = { password: "123456" };
};

const save = () => {
formRef.value.validate((valid) => {
if (valid) {
request
.request({
url: data.form.id ? "/student/update" : "/student/add",
method: data.form.id ? "put" : "post",
data: data.form,
})
.then((res) => {
if (res.code === "200") {
ElMessage.success("操作成功");
data.formVisible = false;
load();
} else {
ElMessage.error(res.msg);
}
})
.catch((err) => {
if (err.response?.data?.code === "401") {
localStorage.removeItem("student-user");
}
ElMessage.error(err.response?.data?.msg || err.message);
});
}
});
};

const handleEdit = (row) => {
data.form = JSON.parse(JSON.stringify(row));
data.formVisible = true;
data.form.password = "123456";
};

const handleDelete = (id) => {
ElMessageBox.confirm("删除后内容将无法恢复,您确认删除嘛?", "删除确认", {
type: "warning",
})
.then((res) => {
request.delete("/student/delete/" + id).then((res) => {
if (res.code === "200") {
ElMessage.success("删除成功");
load();
} else {
ElMessage.error(res.msg);
}
});
})
.catch((err) => {
if (err.response?.data?.code === "401") {
localStorage.removeItem("student-user");
}
ElMessage.error(err.response?.data?.msg || err.message);
});
};
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import { createRouter, createWebHistory } from "vue-router";

const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: "/",
name: "Manager",
component: () => import("@/views/Manager.vue"),
redirect: "/home",
children: [
{
path: "home",
name: "Home",
component: () => import("@/views/manager/Home.vue"),
meta: { requiresAuth: true },
},
{
path: "course",
name: "Course",
component: () => import("@/views/manager/Course.vue"),
meta: { requiresAuth: true },
},
{
path: "student",
name: "Student",
component: () => import("@/views/manager/Student.vue"),
meta: { requiresAuth: true },
},
],
},
{
path: "/login",
name: "Login",
component: () => import("@/views/Login.vue"),
},
{
path: "/register",
name: "Register",
component: () => import("@/views/Register.vue"),
},
],
});

router.beforeEach((to, from, next) => {
const requiresAuth = to.matched.some((record) => record.meta.requiresAuth);
const user = JSON.parse(localStorage.getItem("student-user"));
if (requiresAuth && !user) {
// 如果目标路由需要认证,并且用户未登录
next("/login"); // 跳转到登录页面
} else {
next(); // 如果目标路由不需要认证,或者用户已登录,则正常导航到目标路由
}
});

export default router;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
<template>
<div>
<div
style="height: 60px; background-color: #eae8e8; display: flex; align-items: center; border-bottom: 1px solid #c4c2c2"
>
<div style="flex: 1">
<div style="padding-left: 20px; display: flex; align-items: center">
<img src="@/assets/imgs/logo.png" alt="" style="width: 40px" />
<div style="font-weight: bold; font-size: 24px; margin-left: 5px">
学生信息管理系统
</div>
</div>
</div>
<div
style="width: fit-content; padding-right: 10px; display: flex; align-items: center;"
>
<img
src="https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png"
alt=""
style="width: 40px; height: 40px"
/>
<span style="margin-left: 5px">管理员</span>
</div>
</div>

<div style="display: flex">
<div
style="width: 200px; border-right: 1px solid #f3eeee; min-height: calc(100vh - 60px)"
>
<el-menu
router
style="border: none"
:default-active="$route.path"
:default-openeds="['/home', '2', '3']"
>
<el-menu-item index="/home">
<el-icon>
<HomeFilled />
</el-icon>
<span>系统首页</span>
</el-menu-item>

<el-sub-menu index="2">
<template #title>
<el-icon>
<Management />
</el-icon>
<span>学生管理</span>
</template>
<el-menu-item index="/student">
<el-icon>
<UserFilled />
</el-icon>
<span>学生信息</span>
</el-menu-item>
</el-sub-menu>

<el-sub-menu index="3">
<template #title>
<el-icon>
<Memo />
</el-icon>
<span>课程管理</span>
</template>
<el-menu-item index="/course">
<el-icon>
<Document />
</el-icon>
<span>课程信息</span>
</el-menu-item>
</el-sub-menu>

<el-menu-item index="/person">
<el-icon>
<User />
</el-icon>
<span>个人资料</span>
</el-menu-item>
<el-menu-item index="/login" @click="logout">
<el-icon>
<SwitchButton />
</el-icon>
<span>退出系统</span>
</el-menu-item>
</el-menu>
</div>

<div style="flex: 1; width: 0; background-color: #eaeaee; padding: 10px">
<router-view />
</div>
</div>
</div>
</template>

<script setup>
import { useRoute } from "vue-router";

const $route = useRoute();
console.log($route.path);

const logout = () => {
localStorage.removeItem("student-user");
};
</script>

<style scoped>
.el-menu-item.is-active {
background-color: #c3d7d3 !important;
}

.el-menu-item:hover {
color: #0c98d5;
}

:deep(th) {
color: #333;
}
</style>

后端

直接一次性实现学生管理的增删查改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# -*- coding:utf-8 -*-
# Author: Zachary
from typing import Optional

from fastapi import Depends, APIRouter, Query

from api import app
from common.auth import auth_handler
from common.pageHelper import PageHelper
from common.result import ResultModel, Result
from model import Session, get_db_session
from model.account import AccountRegister
from model.student import StudentSearch, StudentCreate, StudentUpdate
from service.studentService import StudentService


@app.post("/register", response_model=ResultModel)
async def register(account: AccountRegister, db_session: Session = Depends(get_db_session)):
StudentService.register(account, db_session)
return Result.success()


student_router = APIRouter(prefix="/student")


@student_router.get("/selectPage", response_model=ResultModel, dependencies=[Depends(auth_handler.auth_required)])
async def select_page(page: int = Query(1, ge=1, alias="pageNum", description="Page number"),
size: int = Query(5, gt=0, le=100, alias="pageSize", description="Page size"),
username: Optional[str] = Query(None, description="Student username"),
name: Optional[str] = Query(None, description="Student name"),
db_session: Session = Depends(get_db_session)):
pageInfo = PageHelper.startPage(page, size)
student_search = StudentSearch(username=username, name=name)
student_list = StudentService.select_page(student_search, db_session)
return Result.success(pageInfo.of(student_list))


@student_router.post("/add", response_model=ResultModel, dependencies=[Depends(auth_handler.auth_required)])
async def add(student: StudentCreate, db_session: Session = Depends(get_db_session)):
StudentService.add_student(student, db_session)
return Result.success()


@student_router.put("/update", response_model=ResultModel, dependencies=[Depends(auth_handler.auth_required)])
async def update(student: StudentUpdate, db_session: Session = Depends(get_db_session)):
StudentService.update_by_id(student, db_session)
return Result.success()


@student_router.delete("/delete/{id}", response_model=ResultModel, dependencies=[Depends(auth_handler.auth_required)])
async def delete(id: int, db_session: Session = Depends(get_db_session)):
StudentService.delete_by_id(id, db_session)
return Result.success()


app.include_router(student_router)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# -*- coding:utf-8 -*-
# Author: Zachary
from fastapi.encoders import jsonable_encoder
from sqlalchemy import select, asc

from common.Enum import Role
from common.auth import auth_handler
from common.utils import set_attrs
from exception.customException import UserNotFoundException, PasswordNotMatchException, UserExistException
from model import Session
from model.account import AccountLogin, AccountLoginResponse, AccountRegister
from model.student import Student, StudentCreate, StudentUpdate


class StudentService:
@staticmethod
def login(account: AccountLogin, db_session: Session) -> AccountLoginResponse:
query = select(Student).where(Student.username == account.username)
exist_student: Student = db_session.execute(query).scalars().first()
if not exist_student:
raise UserNotFoundException("用户不存在")
if not auth_handler.verify_password(account.password, exist_student.password):
raise PasswordNotMatchException("身份验证未通过")
account_login_response = AccountLoginResponse()
set_attrs(account_login_response, jsonable_encoder(exist_student))
account_login_response.token = auth_handler.encode_token(exist_student.id)
return account_login_response

@staticmethod
def register(account: AccountRegister, db_session: Session):
query = select(Student).where(Student.username == account.username)
exist_student = db_session.execute(query).scalars().first()
if exist_student:
raise UserExistException("账号已存在")

new_student = Student()
account.password = auth_handler.get_password_hash(account.password)
set_attrs(new_student, jsonable_encoder(account))
if new_student.name is None:
new_student.name = account.username
new_student.role = Role.STUDENT.name

db_session.add(new_student)
db_session.commit()
return new_student

@staticmethod
def select_page(student_search: Student, db_session: Session):
query = select(Student).order_by(asc(Student.id))
if student_search.username:
query = query.where(Student.username.like(f"%{student_search.username}%"))
if student_search.name:
query = query.where(Student.name.like(f"%{student_search.name}%"))

result = db_session.execute(query).scalars().all()
return result

@staticmethod
def add_student(student: StudentCreate, db_session: Session):
query = select(Student).where(Student.username == student.username)
exist_student = db_session.execute(query).scalars().all()
if exist_student:
raise UserExistException("账号已存在")

student.password = auth_handler.get_password_hash(student.password)
student = Student(**student.dict())
if student.name is None:
student.name = student.username
student.role = Role.STUDENT.name
db_session.add(student)
db_session.commit()
return student

@staticmethod
def update_by_id(student: StudentUpdate, db_session: Session):
exist_student: Student = check_student_exist(student.id, db_session)
student.password = auth_handler.get_password_hash(student.password)
set_attrs(exist_student, jsonable_encoder(student))
db_session.commit()
return exist_student

@staticmethod
def delete_by_id(id: int, db_session: Session):
exist_student: Student = check_student_exist(id, db_session)
db_session.delete(exist_student)
db_session.commit()
return exist_student


def check_student_exist(student_id: int, db_session: Session):
query = select(Student).where(Student.id == student_id)
exist_student: Student = db_session.execute(query).scalar()
if not exist_student:
raise UserNotFoundException("账号不存在")
return exist_student

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# -*- coding:utf-8 -*-
# Author: Zachary
from typing import Optional

from pydantic import BaseModel, Field
from sqlalchemy import Integer, String
from sqlalchemy.orm import Mapped, mapped_column

from model import Base


class Student(Base):
__tablename__ = "student"
id: Mapped[int] = mapped_column(Integer, primary_key=True, nullable=False)
username: Mapped[str] = mapped_column(String(255), nullable=False)
password: Mapped[str] = mapped_column(String(255), nullable=False)
name: Mapped[str] = mapped_column(String(255), nullable=False)
phone: Mapped[str] = mapped_column(String(255), nullable=False)
gender: Mapped[str] = mapped_column(String(255), nullable=False)
birthday: Mapped[str] = mapped_column(String(255), nullable=False)
avatar: Mapped[str] = mapped_column(String(255), nullable=False)
role: Mapped[str] = mapped_column(String(255), nullable=False)


class StudentBase(BaseModel):
username: str
password: str = Field(..., min_length=1)
name: Optional[str] = None
role: Optional[str] = None


class StudentSearch(BaseModel):
username: str | None
name: str | None


class StudentCreate(StudentBase):
phone: Optional[str] = None
gender: Optional[str] = None
birthday: Optional[str] = Field(None)
avatar: Optional[str] = Field(None)


class StudentUpdate(StudentCreate):
id: int

测试

但是修改过后,密码会被强制重置为 123456

限制权限

现在不管普通的学生还是管理员都能看到管理页面 这不符合

需要加入一个权限v-if="user.role === 'ADMIN'"

顺便修改一下右上角和主页显示的用户名称

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
<template>
<div>
<div
style="height: 60px; background-color: #eae8e8; display: flex; align-items: center; border-bottom: 1px solid #c4c2c2"
>
<div style="flex: 1">
<div style="padding-left: 20px; display: flex; align-items: center">
<img src="@/assets/imgs/logo.png" alt="" style="width: 40px" />
<div style="font-weight: bold; font-size: 24px; margin-left: 5px">
学生信息管理系统
</div>
</div>
</div>
<div
style="width: fit-content; padding-right: 10px; display: flex; align-items: center;"
>
<img
src="https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png"
alt=""
style="width: 40px; height: 40px"
/>
<span style="margin-left: 5px">{{ user.name }}</span>
</div>
</div>

<div style="display: flex">
<div
style="width: 200px; border-right: 1px solid #f3eeee; min-height: calc(100vh - 60px)"
>
<el-menu
router
style="border: none"
:default-active="$route.path"
:default-openeds="['/home', '2', '3']"
>
<el-menu-item index="/home">
<el-icon>
<HomeFilled />
</el-icon>
<span>系统首页</span>
</el-menu-item>

<el-sub-menu index="2" v-if="user.role === 'ADMIN'">
<template #title>
<el-icon>
<Management />
</el-icon>
<span>学生管理</span>
</template>
<el-menu-item index="/student">
<el-icon>
<UserFilled />
</el-icon>
<span>学生信息</span>
</el-menu-item>
</el-sub-menu>

<el-sub-menu index="3" v-if="user.role === 'ADMIN'">
<template #title>
<el-icon>
<Memo />
</el-icon>
<span>课程管理</span>
</template>
<el-menu-item index="/course">
<el-icon>
<Document />
</el-icon>
<span>课程信息</span>
</el-menu-item>
</el-sub-menu>

<el-menu-item index="/person">
<el-icon>
<User />
</el-icon>
<span>个人资料</span>
</el-menu-item>
<el-menu-item index="/login" @click="logout">
<el-icon>
<SwitchButton />
</el-icon>
<span>退出系统</span>
</el-menu-item>
</el-menu>
</div>

<div style="flex: 1; width: 0; background-color: #eaeaee; padding: 10px">
<router-view />
</div>
</div>
</div>
</template>

<script setup>
import { useRoute } from "vue-router";

const $route = useRoute();
console.log($route.path);

const logout = () => {
localStorage.removeItem("student-user");
};

const user = JSON.parse(localStorage.getItem("student-user") || "{}");
</script>

<style scoped>
.el-menu-item.is-active {
background-color: #c3d7d3 !important;
}

.el-menu-item:hover {
color: #0c98d5;
}

:deep(th) {
color: #333;
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<template>
<div class="welcome-container">
<div class="custom-card bg-white shadow rounded p-4 mb-5">
<h2 class="text-center text-primary">欢迎来到本系统</h2>
<p class="text-center mt-3">
您好 <span :style="{ color: '#116ca9' }">{{ user.name }}</span
>,祝您使用愉快!
</p>
</div>
</div>
</template>

<script setup>
import request from "@/utils/request";

const user = JSON.parse(localStorage.getItem("student-user") || "{}");
</script>

<style scoped>
.welcome-container {
display: flex;
justify-content: center;
align-items: center;
min-height: calc(100vh - 100px); /* 根据实际项目需要调整高度 */
}

.custom-card {
max-width: 400px;
border: none;
}
</style>

普通学生登录

管理员登录


更新: 2024-05-20 00:11:43
原文: https://www.yuque.com/zacharyblock/iacda/uuigf300461wndud