学生成绩管理

成绩表设计

管理员课程打分

前端

添加课程打分弹窗

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
<template>
<div>
<div class="card" style="margin-bottom: 10px">
<el-input
@input="load"
style="width: 260px"
v-model="data.name"
placeholder="请输入要查询的课程名称"
:prefix-icon="Search"
/>
<el-input
@input="load"
style="margin-left:10px; width: 260px"
v-model="data.number"
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-table :data="data.tableData" stripe style="width: 100%">
<el-table-column prop="id" label="ID" />
<el-table-column prop="name" label="课程名称" />
<el-table-column prop="number" label="课程编号" />
<el-table-column prop="student.name" label="学生名称" />
<el-table-column label="操作" align="center" width="180px">
<template #default="scope">
<el-button
type="primary"
size="small"
plain
@click="addGrade(scope.row)"
v-if="data.user.role === 'ADMIN'"
>评分
</el-button>
<el-button
type="danger"
size="small"
plain
@click="del(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 v-model="data.formVisible" title="成绩信息" width="35%">
<el-form
:model="data.gradeForm"
label-width="100px"
label-position="right"
style="padding-right: 40px"
>
<el-form-item label="课程名称">
<el-input v-model="data.gradeForm.name" autocomplete="off" disabled />
</el-form-item>
<el-form-item label="分数">
<el-input v-model="data.gradeForm.score" autocomplete="off" />
</el-form-item>
<el-form-item label="评语">
<el-input
type="textarea"
v-model="data.gradeForm.comment"
autocomplete="off"
/>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button plain @click="data.formVisible = false">取消</el-button>
<el-button type="primary" plain @click="save">保存</el-button>
</span>
</template>
</el-dialog>
</div>
</template>

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

const data = reactive({
name: "",
number: "",
tableData: [],
total: 0,
pageNum: 1,
pageSize: 10,
user: JSON.parse(localStorage.getItem("student-user") || "{}"),
gradeForm: {},
formVisible: false,
});

const load = () => {
let params = {
pageNum: data.pageNum,
pageSize: data.pageSize,
name: data.name,
number: data.number,
};
if (data.user.role === "STUDENT") {
params.studentId = data.user.id;
}

request
.get("/studentCourse/selectPage", {
params: params,
})
.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.name = "";
data.number = "";
load();
};

const del = (id) => {
ElMessageBox.confirm("删除后内容将无法恢复,您确认删除嘛?", "删除确认", {
type: "warning",
})
.then((res) => {
request.delete("/studentCourse/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);
});
};

// 打分函数,弹窗
const addGrade = (row) => {
// 弹窗
data.formVisible = true;
data.gradeForm.name = row.name;
data.gradeForm.studentId = row.studentId;
data.gradeForm.courseId = row.courseId;
data.gradeForm.score = "";
data.gradeForm.comment = "";
};

const save = () => {
request
.post("/grade/add", data.gradeForm)
.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);
});
};
</script>

后端

依次创建上:gradeApi.pygradeService.pygrade.py

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

from api import app
from common.result import ResultModel, Result
from model import Session, get_db_session
from model.grade import GradeCreate
from service.gradeService import GradeService

grade_router = APIRouter(prefix="/grade")


@grade_router.post("/add", response_model=ResultModel)
async def add(grade: GradeCreate, db_session: Session = Depends(get_db_session)):
GradeService.add_grade(grade, db_session)
return Result.success()


app.include_router(grade_router)

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, fileApi, studentCourseApi, gradeApi
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
# -*- coding:utf-8 -*-
# Author: Zachary
from fastapi.encoders import jsonable_encoder
from sqlalchemy import select, and_

from common.utils import set_attrs
from exception.customException import GradeExistException
from model import Session
from model.grade import GradeCreate, Grade


class GradeService:

@staticmethod
def add_grade(grade: GradeCreate, db_session: Session):
query = select(Grade).where(
and_(Grade.studentId == grade.studentId,
Grade.courseId == grade.courseId))
exist_grade: Grade = db_session.execute(query).scalar()
if exist_grade:
raise GradeExistException("课程已打分")
new_grade = Grade()
set_attrs(new_grade, jsonable_encoder(grade))
db_session.add(new_grade)
db_session.commit()
return new_grade

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
# -*- coding:utf-8 -*-
# Author: Zachary
from typing import Optional

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

from model import Base
from model.course import Course
from model.student import Student


class Grade(Base):
__tablename__ = "grade"
id: Mapped[int] = mapped_column(Integer, primary_key=True, nullable=False)
courseId: Mapped[int] = mapped_column("course_id", Integer, nullable=False)
studentId: Mapped[int] = mapped_column("student_id", Integer, nullable=False)
score: Mapped[str] = mapped_column(Double, nullable=False)
comment: Mapped[str] = mapped_column(String(255), nullable=False)
feedback: Mapped[str] = mapped_column(String(255), nullable=False)



class GradeBase(BaseModel):
courseId: int
studentId: int
score: Optional[float] = None
comment: Optional[str] = None
feedback: Optional[str] = None


class GradeCreate(GradeBase):
pass

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
# -*- 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


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


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


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


class GradeExistException(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
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
# -*- 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, FileNotFoundException, \
StudentCourseExistException, StudentCourseNotExistException, GradeExistException


@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))


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


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


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


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

测试

管理员评分

显示课程分数

前端

复制StudentCourse.vue重命名为Grade.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
<template>
<div>
<div class="card" style="margin-bottom: 10px">
<el-input
@input="load"
style="width: 260px"
v-model="data.courseName"
placeholder="请输入要查询的课程名称"
:prefix-icon="Search"
/>
<el-input
@input="load"
style="margin-left:10px; width: 260px"
v-model="data.studentName"
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-table :data="data.tableData" stripe style="width: 100%">
<el-table-column prop="id" label="ID" />
<el-table-column prop="course.name" label="课程名称" />
<el-table-column prop="student.name" label="学生名称" />
<el-table-column prop="score" label="成绩" />
<el-table-column prop="comment" label="教师评语" />
<el-table-column prop="feedback" label="学生评价" />
<el-table-column label="操作" align="center" width="180px">
<template #default="scope">
<el-button
type="danger"
size="small"
plain
@click="del(scope.row.id)"
v-if="data.user.role === 'ADMIN'"
>删除
</el-button>
<el-button
type="primary"
size="small"
plain
@click="setFeedback(scope.row)"
v-if="data.user.role === 'STUDENT'"
>评价
</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 v-model="data.formVisible" title="反馈信息" width="35%">
<el-form
:model="data.form"
label-width="100px"
label-position="right"
style="padding-right: 40px"
>
<el-form-item label="反馈">
<el-input
type="textarea"
v-model="data.form.feedback"
autocomplete="off"
/>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button plain @click="data.formVisible = false">取消</el-button>
<el-button type="primary" plain @click="save">保存</el-button>
</span>
</template>
</el-dialog>
</div>
</template>

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

const data = reactive({
form: {},
courseName: "",
studentName: "",
tableData: [],
total: 0,
pageNum: 1,
pageSize: 10,
user: JSON.parse(localStorage.getItem("student-user") || "{}"),
formVisible: false,
});

const load = () => {
let params = {
pageNum: data.pageNum,
pageSize: data.pageSize,
courseName: data.courseName,
studentName: data.studentName,
};
if (data.user.role === "STUDENT") {
params.studentId = data.user.id;
}

request
.get("/grade/selectPage", {
params: params,
})
.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.courseName = "";
data.studentName = "";
load();
};

const del = (id) => {
ElMessageBox.confirm("删除后内容将无法恢复,您确认删除嘛?", "删除确认", {
type: "warning",
})
.then((res) => {
request.delete("/studentCourse/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);
});
};

// 打分
const setFeedback = (row) => {
// 弹窗
data.formVisible = true;
data.form = JSON.parse(JSON.stringify(row));
};

const save = () => {
request
.post("/grade/update", 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);
});
};
</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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
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: "person",
name: "Person",
component: () => import("@/views/manager/Person.vue"),
meta: { requiresAuth: true },
},
{
path: "courseList",
name: "CourseList",
component: () => import("@/views/manager/CourseList.vue"),
meta: { requiresAuth: true },
},
{
path: "studentCourse",
name: "StudentCourse",
component: () => import("@/views/manager/StudentCourse.vue"),
meta: { requiresAuth: true },
},
{
path: "grade",
name: "Grade",
component: () => import("@/views/manager/Grade.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
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
<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="
user.avatar ||
'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png'
"
alt=""
style="width: 40px; height: 40px; border-radius: 50%"
/>
<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', '4']"
>
<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">
<template #title>
<el-icon>
<Memo />
</el-icon>
<span>课程管理</span>
</template>
<el-menu-item index="/course" v-if="user.role === 'ADMIN'">
<el-icon>
<Document />
</el-icon>
<span>课程信息</span>
</el-menu-item>
<el-menu-item index="/courseList" v-if="user.role === 'STUDENT'">
<el-icon>
<Document />
</el-icon>
<span>学生选课</span>
</el-menu-item>
<el-menu-item index="/studentCourse">
<el-icon>
<Document />
</el-icon>
<span>选课记录</span>
</el-menu-item>
</el-sub-menu>

<el-sub-menu index="4">
<template #title>
<el-icon>
<Management />
</el-icon>
<span>成绩信息</span>
</template>
<el-menu-item index="/grade">
<el-icon>
<List />
</el-icon>
<span>学生成绩</span>
</el-menu-item>
</el-sub-menu>

<el-menu-item index="/person" v-if="user.role === 'STUDENT'">
<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
32
33
34
35
36
37
38
39
40
41
42
43
# -*- coding:utf-8 -*-
# Author: Zachary
from typing import Optional

from fastapi import APIRouter, Depends, Query

from api import app
from common.pageHelper import PageHelper
from common.result import ResultModel, Result
from model import Session, get_db_session
from model.grade import GradeCreate, GradeSearch, GradeUpdate
from service.gradeService import GradeService

grade_router = APIRouter(prefix="/grade")


@grade_router.post("/add", response_model=ResultModel)
async def add(grade: GradeCreate, db_session: Session = Depends(get_db_session)):
GradeService.add_grade(grade, db_session)
return Result.success()


@grade_router.get("/selectPage", response_model=ResultModel)
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"),
studentName: Optional[str] = Query(None, description="Student name"),
courseName: Optional[str] = Query(None, description="Course name"),
studentId: Optional[str] = Query(None, description="Student id"),
db_session: Session = Depends(get_db_session)):
pageInfo = PageHelper.startPage(page, size)
grade_search: GradeSearch = GradeSearch(studentName=studentName, courseName=courseName, studentId=studentId)
grade_list = GradeService.select_page(grade_search, db_session)
return Result.success(pageInfo.of(grade_list))


@grade_router.put("/update", response_model=ResultModel)
async def update(grade: GradeUpdate, db_session: Session = Depends(get_db_session)):
GradeService.update_by_id(grade, db_session)
return Result.success()


app.include_router(grade_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
# -*- coding:utf-8 -*-
# Author: Zachary
from fastapi.encoders import jsonable_encoder
from sqlalchemy import select, and_, asc

from common.utils import set_attrs
from exception.customException import GradeExistException
from model import Session
from model.course import Course
from model.grade import GradeCreate, Grade, GradeSearch, GradeUpdate
from model.student import Student


class GradeService:

@staticmethod
def add_grade(grade: GradeCreate, db_session: Session):
query = select(Grade).where(
and_(Grade.studentId == grade.studentId,
Grade.courseId == grade.courseId))
exist_grade: Grade = db_session.execute(query).scalar()
if exist_grade:
raise GradeExistException("课程已打分")
new_grade = Grade()
set_attrs(new_grade, jsonable_encoder(grade))
db_session.add(new_grade)
db_session.commit()
return new_grade

@staticmethod
def select_page(grade_search: GradeSearch, db_session: Session):
query = select(Grade, Student, Course).outerjoin(Grade.student).outerjoin(Grade.course).order_by(asc(Grade.id))
if grade_search.courseName:
query = query.where(Course.name.like(f"%{grade_search.courseName}%"))
if grade_search.studentName:
query = query.where(Student.name.like(f"%{grade_search.studentName}%"))
if grade_search.studentId:
query = query.where(Grade.studentId == grade_search.studentId)
result = db_session.execute(query).scalars().all()
return result

@staticmethod
def update_by_id(grade: GradeUpdate, db_session: Session):
exist_grade: Grade = check_grade_exist(grade.id, db_session)
set_attrs(exist_grade, jsonable_encoder(grade))
db_session.commit()
return exist_grade


def check_grade_exist(grade_id: int, db_session: Session):
query = select(Grade).where(Grade.id == grade_id)
exist_grade: Grade = db_session.execute(query).scalar()
if not exist_grade:
raise GradeExistException("成绩记录不存在")
return exist_grade

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
# -*- coding:utf-8 -*-
# Author: Zachary
from typing import Optional

from pydantic import BaseModel
from sqlalchemy import Integer, Double, ForeignKey, String
from sqlalchemy.orm import Mapped, mapped_column, relationship

from model import Base
from model.course import Course
from model.student import Student


class Grade(Base):
__tablename__ = "grade"
id: Mapped[int] = mapped_column(Integer, primary_key=True, nullable=False)
courseId: Mapped[int] = mapped_column("course_id", Integer, ForeignKey('course.id'), nullable=False)
studentId: Mapped[int] = mapped_column("student_id", Integer, ForeignKey('student.id'), nullable=False)
score: Mapped[str] = mapped_column(Double, nullable=False)
comment: Mapped[str] = mapped_column(String(255), nullable=False)
feedback: Mapped[str] = mapped_column(String(255), nullable=False)

student: Mapped[Student] = relationship(lazy=False, backref="grade")
course: Mapped[Course] = relationship(lazy=False, backref="grade")


class GradeBase(BaseModel):
courseId: int
studentId: int
score: Optional[float] = None
comment: Optional[str] = None
feedback: Optional[str] = None


class GradeCreate(GradeBase):
pass


class GradeSearch(BaseModel):
courseName: str | None
studentName: str | None
studentId: str | None


class GradeUpdate(GradeBase):
id: int

测试

管理员

课程分数编辑

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
<template>
<div>
<div class="card" style="margin-bottom: 10px">
<el-input
@input="load"
style="width: 260px"
v-model="data.courseName"
placeholder="请输入要查询的课程名称"
:prefix-icon="Search"
/>
<el-input
@input="load"
style="margin-left:10px; width: 260px"
v-model="data.studentName"
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-table :data="data.tableData" stripe style="width: 100%">
<el-table-column prop="id" label="ID" />
<el-table-column prop="course.name" label="课程名称" />
<el-table-column prop="student.name" label="学生名称" />
<el-table-column prop="score" label="成绩" />
<el-table-column prop="comment" label="教师评语" />
<el-table-column prop="feedback" label="学生评价" />
<el-table-column label="操作" align="center" width="180px">
<template #default="scope">
<el-button
type="primary"
size="small"
plain
@click="handleEdit(scope.row)"
v-if="data.user.role === 'ADMIN'"
>编辑
</el-button>
<el-button
type="danger"
size="small"
plain
@click="del(scope.row.id)"
v-if="data.user.role === 'ADMIN'"
>删除
</el-button>
<el-button
type="primary"
size="small"
plain
@click="handleEdit(scope.row)"
v-if="data.user.role === 'STUDENT'"
>评价
</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 v-model="data.formVisible" title="反馈信息" width="35%">
<el-form
:model="data.form"
label-width="100px"
label-position="right"
style="padding-right: 40px"
>
<el-form-item label="分数" v-if="data.user.role === 'ADMIN'">
<el-input v-model="data.form.score" autocomplete="off" />
</el-form-item>
<el-form-item label="教师评语" v-if="data.user.role === 'ADMIN'">
<el-input
type="textarea"
v-model="data.form.comment"
autocomplete="off"
/>
</el-form-item>
<el-form-item label="学生评价" v-if="data.user.role === 'STUDENT'">
<el-input
type="textarea"
v-model="data.form.feedback"
autocomplete="off"
/>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button plain @click="data.formVisible = false">取消</el-button>
<el-button type="primary" plain @click="save">保存</el-button>
</span>
</template>
</el-dialog>
</div>
</template>

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

const data = reactive({
form: {},
courseName: "",
studentName: "",
tableData: [],
total: 0,
pageNum: 1,
pageSize: 10,
user: JSON.parse(localStorage.getItem("student-user") || "{}"),
formVisible: false,
});

const load = () => {
let params = {
pageNum: data.pageNum,
pageSize: data.pageSize,
courseName: data.courseName,
studentName: data.studentName,
};
if (data.user.role === "STUDENT") {
params.studentId = data.user.id;
}

request
.get("/grade/selectPage", {
params: params,
})
.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.courseName = "";
data.studentName = "";
load();
};

const del = (id) => {
ElMessageBox.confirm("删除后内容将无法恢复,您确认删除嘛?", "删除确认", {
type: "warning",
})
.then((res) => {
request.delete("/studentCourse/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);
});
};

// 打分
const handleEdit = (row) => {
// 弹窗
data.formVisible = true;
data.form = JSON.parse(JSON.stringify(row));
};

const save = () => {
request
.put("/grade/update", 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);
});
};
</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
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
<template>
<div>
<div class="card" style="margin-bottom: 10px">
<el-input
@input="load"
style="width: 260px"
v-model="data.courseName"
placeholder="请输入要查询的课程名称"
:prefix-icon="Search"
/>
<el-input
@input="load"
style="margin-left:10px; width: 260px"
v-model="data.studentName"
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-table :data="data.tableData" stripe style="width: 100%">
<el-table-column prop="id" label="ID" />
<el-table-column prop="course.name" label="课程名称" />
<el-table-column prop="student.name" label="学生名称" />
<el-table-column prop="score" label="成绩" />
<el-table-column prop="comment" label="教师评语" />
<el-table-column prop="feedback" label="学生评价" />
<el-table-column label="操作" align="center" width="180px">
<template #default="scope">
<el-button
type="primary"
size="small"
plain
@click="handleEdit(scope.row)"
v-if="data.user.role === 'ADMIN'"
>编辑
</el-button>
<el-button
type="danger"
size="small"
plain
@click="del(scope.row.id)"
v-if="data.user.role === 'ADMIN'"
>删除
</el-button>
<el-button
type="primary"
size="small"
plain
@click="handleEdit(scope.row)"
v-if="data.user.role === 'STUDENT'"
>评价
</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 v-model="data.formVisible" title="反馈信息" width="35%">
<el-form
:model="data.form"
label-width="100px"
label-position="right"
style="padding-right: 40px"
>
<el-form-item label="分数" v-if="data.user.role === 'ADMIN'">
<el-input v-model="data.form.score" autocomplete="off" />
</el-form-item>
<el-form-item label="教师评语" v-if="data.user.role === 'ADMIN'">
<el-input
type="textarea"
v-model="data.form.comment"
autocomplete="off"
/>
</el-form-item>
<el-form-item label="学生评价" v-if="data.user.role === 'STUDENT'">
<el-input
type="textarea"
v-model="data.form.feedback"
autocomplete="off"
/>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button plain @click="data.formVisible = false">取消</el-button>
<el-button type="primary" plain @click="save">保存</el-button>
</span>
</template>
</el-dialog>
</div>
</template>

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

const data = reactive({
form: {},
courseName: "",
studentName: "",
tableData: [],
total: 0,
pageNum: 1,
pageSize: 10,
user: JSON.parse(localStorage.getItem("student-user") || "{}"),
formVisible: false,
});

const load = () => {
let params = {
pageNum: data.pageNum,
pageSize: data.pageSize,
courseName: data.courseName,
studentName: data.studentName,
};
if (data.user.role === "STUDENT") {
params.studentId = data.user.id;
}

request
.get("/grade/selectPage", {
params: params,
})
.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.courseName = "";
data.studentName = "";
load();
};

const del = (id) => {
ElMessageBox.confirm("删除后内容将无法恢复,您确认删除嘛?", "删除确认", {
type: "warning",
})
.then((res) => {
request.delete("/grade/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);
});
};

// 打分
const handleEdit = (row) => {
// 弹窗
data.formVisible = true;
data.form = JSON.parse(JSON.stringify(row));
};

const save = () => {
request
.put("/grade/update", 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);
});
};
</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
# -*- coding:utf-8 -*-
# Author: Zachary
from typing import Optional

from fastapi import APIRouter, Depends, Query

from api import app
from common.pageHelper import PageHelper
from common.result import ResultModel, Result
from model import Session, get_db_session
from model.grade import GradeCreate, GradeSearch, GradeUpdate
from service.gradeService import GradeService

grade_router = APIRouter(prefix="/grade")


@grade_router.post("/add", response_model=ResultModel)
async def add(grade: GradeCreate, db_session: Session = Depends(get_db_session)):
GradeService.add_grade(grade, db_session)
return Result.success()


@grade_router.get("/selectPage", response_model=ResultModel)
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"),
studentName: Optional[str] = Query(None, description="Student name"),
courseName: Optional[str] = Query(None, description="Course name"),
studentId: Optional[str] = Query(None, description="Student id"),
db_session: Session = Depends(get_db_session)):
pageInfo = PageHelper.startPage(page, size)
grade_search: GradeSearch = GradeSearch(studentName=studentName, courseName=courseName, studentId=studentId)
grade_list = GradeService.select_page(grade_search, db_session)
return Result.success(pageInfo.of(grade_list))


@grade_router.put("/update", response_model=ResultModel)
async def update(grade: GradeUpdate, db_session: Session = Depends(get_db_session)):
GradeService.update_by_id(grade, db_session)
return Result.success()


@grade_router.delete("/delete/{id}", response_model=ResultModel)
async def delete(id: int, db_session: Session = Depends(get_db_session)):
GradeService.delete_by_id(id, db_session)
return Result.success()


app.include_router(grade_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
# -*- coding:utf-8 -*-
# Author: Zachary
from fastapi.encoders import jsonable_encoder
from sqlalchemy import select, and_, asc

from common.utils import set_attrs
from exception.customException import GradeExistException
from model import Session
from model.course import Course
from model.grade import GradeCreate, Grade, GradeSearch, GradeUpdate
from model.student import Student


class GradeService:

@staticmethod
def add_grade(grade: GradeCreate, db_session: Session):
query = select(Grade).where(
and_(Grade.studentId == grade.studentId,
Grade.courseId == grade.courseId))
exist_grade: Grade = db_session.execute(query).scalar()
if exist_grade:
raise GradeExistException("课程已打分")
new_grade = Grade()
set_attrs(new_grade, jsonable_encoder(grade))
db_session.add(new_grade)
db_session.commit()
return new_grade

@staticmethod
def select_page(grade_search: GradeSearch, db_session: Session):
query = select(Grade, Student, Course).outerjoin(Grade.student).outerjoin(Grade.course).order_by(asc(Grade.id))
if grade_search.courseName:
query = query.where(Course.name.like(f"%{grade_search.courseName}%"))
if grade_search.studentName:
query = query.where(Student.name.like(f"%{grade_search.studentName}%"))
if grade_search.studentId:
query = query.where(Grade.studentId == grade_search.studentId)
result = db_session.execute(query).scalars().all()
return result

@staticmethod
def update_by_id(grade: GradeUpdate, db_session: Session):
exist_grade: Grade = check_grade_exist(grade.id, db_session)
set_attrs(exist_grade, jsonable_encoder(grade))
db_session.commit()
return exist_grade

@staticmethod
def delete_by_id(id: int, db_session: Session):
exist_grade: Grade = check_grade_exist(id, db_session)
db_session.delete(exist_grade)
db_session.commit()
return exist_grade


def check_grade_exist(grade_id: int, db_session: Session):
query = select(Grade).where(Grade.id == grade_id)
exist_grade: Grade = db_session.execute(query).scalar()
if not exist_grade:
raise GradeExistException("成绩记录不存在")
return exist_grade

测试

更新: 2024-05-26 21:20:47
原文: https://www.yuque.com/zacharyblock/iacda/izxixxgu5aq8g2nw