first commit

This commit is contained in:
祀梦
2025-12-21 21:50:37 +08:00
commit 352698044b
30 changed files with 9893 additions and 0 deletions

31
.gitignore vendored Normal file
View File

@@ -0,0 +1,31 @@
# Node.js
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.env
# Python
__pycache__/
*.pyc
*.pyo
*.pyd
.Python
env/
venv/
pip-log.txt
pip-delete-this-directory.txt
.venv/
.env/
# OS
.DS_Store
Thumbs.db
# IDE
.vscode/
.idea/
# Build
dist/
build/

15
README.md Normal file
View File

@@ -0,0 +1,15 @@
# WebWork
这是一个基于 Node.js 后端和原生 HTML/CSS/JS 前端的 Web 项目。
## 项目结构
- `backend/`: Node.js 服务端代码
- `frontend/`: 前端静态资源
- `database/`: 数据库初始化脚本
## 快速开始
1. 进入 `backend` 目录,运行 `npm install` 安装依赖。
2. 配置 `.env` 文件(参考 `.env.example` 或根据需要创建)。
3. 启动服务器:`npm start`

View File

@@ -0,0 +1,64 @@
const mysql = require('mysql2/promise');
require('dotenv').config();
const pool = mysql.createPool({
host: process.env.DB_HOST || 'localhost',
user: process.env.DB_USER || 'root',
password: process.env.DB_PASSWORD || '123456',
database: process.env.DB_NAME || 'score_management',
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0
});
// 测试数据库连接
async function testConnection() {
try {
const connection = await pool.getConnection();
console.log('数据库连接成功');
connection.release();
return true;
} catch (error) {
console.error('数据库连接失败:', error.message);
return false;
}
}
// 执行查询
async function query(sql, params) {
try {
const [rows] = await pool.execute(sql, params);
return rows;
} catch (error) {
console.error('数据库查询错误:', error.message);
throw error;
}
}
// 执行事务
async function executeTransaction(operations) {
const connection = await pool.getConnection();
try {
await connection.beginTransaction();
for (const operation of operations) {
await connection.execute(operation.sql, operation.params);
}
await connection.commit();
console.log('事务执行成功');
} catch (error) {
await connection.rollback();
console.error('事务执行失败:', error.message);
throw error;
} finally {
connection.release();
}
}
module.exports = {
pool,
query,
executeTransaction,
testConnection
};

170
backend/middleware/auth.js Normal file
View File

@@ -0,0 +1,170 @@
const db = require('../config/database');
/**
* 认证中间件 - 检查用户是否已登录
*/
const requireAuth = (req, res, next) => {
if (!req.session.user) {
return res.status(401).json({
success: false,
message: '请先登录'
});
}
next();
};
/**
* 角色权限中间件 - 检查用户是否具有指定角色
* @param {Array} allowedRoles - 允许的角色数组
*/
const requireRole = (allowedRoles) => {
return (req, res, next) => {
if (!req.session.user) {
return res.status(401).json({
success: false,
message: '请先登录'
});
}
const userRole = req.session.user.role;
if (!allowedRoles.includes(userRole)) {
return res.status(403).json({
success: false,
message: '权限不足'
});
}
next();
};
};
/**
* 获取当前用户ID
*/
const getCurrentUserId = (req) => {
return req.session.user ? req.session.user.id : null;
};
/**
* 获取当前用户角色
*/
const getCurrentUserRole = (req) => {
return req.session.user ? req.session.user.role : null;
};
/**
* 检查是否是自己的数据(学生只能访问自己的数据)
*/
const checkOwnership = async (req, res, next) => {
try {
const userId = getCurrentUserId(req);
const userRole = getCurrentUserRole(req);
// 管理员和教师可以访问所有数据
if (userRole === 'admin' || userRole === 'teacher') {
return next();
}
// 学生只能访问自己的数据
if (userRole === 'student') {
const studentId = req.params.studentId || req.body.studentId;
// 检查学生ID是否属于当前用户
const [students] = await db.execute(
'SELECT id FROM students WHERE user_id = ?',
[userId]
);
if (students.length === 0) {
return res.status(403).json({
success: false,
message: '未找到学生信息'
});
}
const userStudentId = students[0].id;
// 如果请求中没有指定学生ID默认使用当前用户的学生ID
if (!studentId) {
req.studentId = userStudentId;
return next();
}
// 检查请求的学生ID是否与当前用户的学生ID匹配
if (parseInt(studentId) !== userStudentId) {
return res.status(403).json({
success: false,
message: '无权访问其他学生的数据'
});
}
req.studentId = userStudentId;
return next();
}
// 其他角色无权访问
return res.status(403).json({
success: false,
message: '权限不足'
});
} catch (error) {
console.error('所有权检查错误:', error);
res.status(500).json({
success: false,
message: '服务器错误'
});
}
};
/**
* 输入验证中间件
*/
const validateInput = (schema) => {
return (req, res, next) => {
const { error } = schema.validate(req.body);
if (error) {
return res.status(400).json({
success: false,
message: error.details[0].message
});
}
next();
};
};
/**
* 日志记录中间件
*/
const logOperation = (operation) => {
return async (req, res, next) => {
try {
const userId = getCurrentUserId(req);
const userRole = getCurrentUserRole(req);
const ip = req.ip || req.connection.remoteAddress;
// 记录操作日志
await db.execute(
'INSERT INTO operation_logs (user_id, operation, ip_address, user_role) VALUES (?, ?, ?, ?)',
[userId, operation, ip, userRole]
);
next();
} catch (error) {
console.error('日志记录错误:', error);
// 日志记录失败不影响主要操作
next();
}
};
};
module.exports = {
requireAuth,
requireRole,
getCurrentUserId,
getCurrentUserRole,
checkOwnership,
validateInput,
logOperation
};

1491
backend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

26
backend/package.json Normal file
View File

@@ -0,0 +1,26 @@
{
"name": "grade-management-system",
"version": "1.0.0",
"description": "学生成绩管理系统后端",
"main": "server.js",
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js"
},
"dependencies": {
"express": "^4.18.2",
"cors": "^2.8.5",
"express-session": "^1.17.3",
"express-mysql-session": "^3.0.0",
"mysql2": "^3.6.0",
"bcryptjs": "^2.4.3",
"dotenv": "^16.3.1",
"body-parser": "^1.20.2"
},
"devDependencies": {
"nodemon": "^3.0.1"
},
"engines": {
"node": ">=14.0.0"
}
}

311
backend/routes/admin.js Normal file
View File

@@ -0,0 +1,311 @@
const express = require('express');
const router = express.Router();
const db = require('../config/database');
const { requireAuth, requireRole } = require('../middleware/auth');
/**
* 获取所有用户
*/
router.get('/users', requireAuth, requireRole(['admin']), async (req, res) => {
try {
const { page = 1, limit = 10, search = '', role = '' } = req.query;
const offset = (page - 1) * limit;
let query = 'SELECT id, name, role, class, created_at FROM users WHERE 1=1';
let params = [];
if (search) {
query += ' AND (id LIKE ? OR name LIKE ? OR class LIKE ?)';
const searchTerm = `%${search}%`;
params.push(searchTerm, searchTerm, searchTerm);
}
if (role) {
query += ' AND role = ?';
params.push(role);
}
// 获取总数
const countQuery = query.replace('SELECT id, name, role, class, created_at', 'SELECT COUNT(*) as total');
const countResult = await db.pool.execute(countQuery, params);
const total = countResult[0][0].total;
// 获取分页数据
query += ' ORDER BY created_at DESC LIMIT ? OFFSET ?';
params.push(parseInt(limit), parseInt(offset));
const users = await db.pool.execute(query, params);
res.json({
success: true,
data: users,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total,
pages: Math.ceil(total / limit)
}
});
} catch (error) {
console.error('获取用户列表错误:', error);
res.status(500).json({
success: false,
message: '服务器错误'
});
}
});
/**
* 创建用户
*/
router.post('/users', requireAuth, requireRole(['admin']), async (req, res) => {
try {
const { id, name, password, role, className } = req.body;
// 输入验证
if (!id || !name || !password || !role) {
return res.status(400).json({
success: false,
message: '请填写所有必填字段'
});
}
// 检查用户ID是否存在
const existingUsers = await db.pool.execute(
'SELECT id FROM users WHERE id = ?',
[id]
);
if (existingUsers[0].length > 0) {
return res.status(400).json({
success: false,
message: '用户ID已存在'
});
}
// 哈希密码
const bcrypt = require('bcrypt');
const salt = await bcrypt.genSalt(10);
const passwordHash = await bcrypt.hash(password, salt);
// 创建用户
const result = await db.pool.execute(
'INSERT INTO users (id, name, password, role, class) VALUES (?, ?, ?, ?, ?)',
[id, name, passwordHash, role, className || null]
);
const userId = result[0].insertId;
// 根据角色创建相关记录
if (role === 'student') {
const studentId = 'STU' + Date.now().toString().slice(-6);
await db.pool.execute(
'INSERT INTO students (user_id, student_id, full_name, class_name) VALUES (?, ?, ?, ?)',
[userId, studentId, fullName, className || '未分配班级']
);
} else if (role === 'teacher') {
await db.pool.execute(
'INSERT INTO teachers (user_id, full_name) VALUES (?, ?)',
[userId, fullName]
);
}
res.json({
success: true,
message: '用户创建成功',
userId
});
} catch (error) {
console.error('创建用户错误:', error);
res.status(500).json({
success: false,
message: '服务器错误'
});
}
});
/**
* 更新用户
*/
router.put('/users/:id', requireAuth, requireRole(['admin']), async (req, res) => {
try {
const userId = req.params.id;
const { name, role, className } = req.body;
// 检查用户是否存在
const users = await db.pool.execute(
'SELECT * FROM users WHERE id = ?',
[userId]
);
if (users[0].length === 0) {
return res.status(404).json({
success: false,
message: '用户不存在'
});
}
const oldRole = users[0][0].role;
// 更新用户信息
await db.pool.execute(
'UPDATE users SET name = ?, role = ?, class = ? WHERE id = ?',
[name, role, className || null, userId]
);
// 如果角色改变,更新相关记录
if (oldRole !== role) {
// 删除旧角色的记录
if (oldRole === 'student') {
await db.pool.execute('DELETE FROM students WHERE user_id = ?', [userId]);
} else if (oldRole === 'teacher') {
await db.pool.execute('DELETE FROM teachers WHERE user_id = ?', [userId]);
}
// 创建新角色的记录
if (role === 'student') {
await db.pool.execute(
'INSERT INTO students (user_id, class) VALUES (?, ?)',
[userId, className || null]
);
} else if (role === 'teacher') {
// 教师不需要额外表
}
} else if (role === 'student' && className) {
// 如果是学生且班级有变化,更新班级
await db.pool.execute(
'UPDATE students SET class = ? WHERE user_id = ?',
[className, userId]
);
}
res.json({
success: true,
message: '用户更新成功'
});
} catch (error) {
console.error('更新用户错误:', error);
res.status(500).json({
success: false,
message: '服务器错误'
});
}
});
/**
* 删除用户
*/
router.delete('/users/:id', requireAuth, requireRole(['admin']), async (req, res) => {
try {
const userId = req.params.id;
// 检查用户是否存在
const users = await db.pool.execute(
'SELECT role FROM users WHERE id = ?',
[userId]
);
if (users[0].length === 0) {
return res.status(404).json({
success: false,
message: '用户不存在'
});
}
const userRole = users[0][0].role;
// 删除相关记录
if (userRole === 'student') {
await db.pool.execute('DELETE FROM students WHERE user_id = ?', [userId]);
} else if (userRole === 'teacher') {
await db.pool.execute('DELETE FROM teachers WHERE user_id = ?', [userId]);
}
// 删除用户
await db.pool.execute('DELETE FROM users WHERE id = ?', [userId]);
res.json({
success: true,
message: '用户删除成功'
});
} catch (error) {
console.error('删除用户错误:', error);
res.status(500).json({
success: false,
message: '服务器错误'
});
}
});
/**
* 获取所有班级
*/
router.get('/classes', requireAuth, requireRole(['admin']), async (req, res) => {
try {
const classes = await db.pool.execute(
'SELECT DISTINCT class_name FROM students ORDER BY class_name'
);
res.json({
success: true,
data: classes
});
} catch (error) {
console.error('获取班级列表错误:', error);
res.status(500).json({
success: false,
message: '服务器错误'
});
}
});
/**
* 获取统计数据
*/
router.get('/stats', requireAuth, requireRole(['admin']), async (req, res) => {
try {
// 用户统计
const userStats = await db.pool.execute(
'SELECT role, COUNT(*) as count FROM users GROUP BY role'
);
// 班级统计
const classStats = await db.pool.execute(
'SELECT class_name, COUNT(*) as count FROM students GROUP BY class_name'
);
// 课程统计
const courseStats = await db.pool.execute(
'SELECT COUNT(*) as total_courses FROM courses'
);
// 成绩统计
const gradeStats = await db.pool.execute(
'SELECT COUNT(*) as total_grades FROM scores'
);
res.json({
success: true,
data: {
users: userStats[0],
classes: classStats[0],
total_courses: courseStats[0][0].total_courses,
total_grades: gradeStats[0][0].total_grades
}
});
} catch (error) {
console.error('获取统计数据错误:', error);
res.status(500).json({
success: false,
message: '服务器错误'
});
}
});
module.exports = router;

175
backend/routes/auth.js Normal file
View File

@@ -0,0 +1,175 @@
const express = require('express');
const bcrypt = require('bcryptjs');
const router = express.Router();
const db = require('../config/database');
// 登录
router.post('/login', async (req, res) => {
try {
const { id, password, role } = req.body;
// 输入验证
if (!id || !password || !role) {
return res.status(400).json({
success: false,
message: '请输入完整的登录信息'
});
}
// 查询用户
const users = await db.query(
'SELECT * FROM users WHERE id = ? AND role = ?',
[id, role]
);
if (users.length === 0) {
return res.status(401).json({
success: false,
message: '用户名或密码错误'
});
}
const user = users[0];
// 验证密码
const isValidPassword = await bcrypt.compare(password, user.password);
if (!isValidPassword) {
return res.status(401).json({
success: false,
message: '用户名或密码错误'
});
}
// 设置会话
req.session.user = {
id: user.id,
name: user.name,
role: user.role,
class: user.class
};
// 如果是学生,获取学生信息
if (user.role === 'student') {
const [students] = await db.pool.execute(
'SELECT * FROM students WHERE id = ?',
[user.id]
);
if (students[0].length > 0) {
req.session.user.studentInfo = students[0][0];
}
}
res.json({
success: true,
message: '登录成功',
user: req.session.user
});
} catch (error) {
console.error('登录错误:', error);
res.status(500).json({
success: false,
message: '服务器错误'
});
}
});
// 注册
router.post('/register', async (req, res) => {
try {
const { id, name, password, role, class: userClass } = req.body;
// 输入验证
if (!id || !name || !password || !role) {
return res.status(400).json({
success: false,
message: '请填写所有必填字段ID、姓名、密码、角色'
});
}
// 学生和教师需要班级字段,管理员不需要
if ((role === 'student' || role === 'teacher') && !userClass) {
return res.status(400).json({
success: false,
message: '学生和教师需要填写班级'
});
}
// 检查用户ID是否存在
const existingUsers = await db.query(
'SELECT id FROM users WHERE id = ?',
[id]
);
if (existingUsers.length > 0) {
return res.status(400).json({
success: false,
message: '用户ID已存在'
});
}
// 哈希密码
const salt = await bcrypt.genSalt(10);
const passwordHash = await bcrypt.hash(password, salt);
// 创建用户
await db.pool.execute(
'INSERT INTO users (id, name, password, role, class) VALUES (?, ?, ?, ?, ?)',
[id, name, passwordHash, role, userClass || null]
);
// 如果是学生,创建学生记录
if (role === 'student') {
await db.pool.execute(
'INSERT INTO students (id, name, class) VALUES (?, ?, ?)',
[id, name, userClass]
);
}
res.json({
success: true,
message: '注册成功'
});
} catch (error) {
console.error('注册错误:', error);
res.status(500).json({
success: false,
message: '服务器错误'
});
}
});
// 注销
router.post('/logout', (req, res) => {
req.session.destroy(err => {
if (err) {
return res.status(500).json({
success: false,
message: '注销失败'
});
}
res.clearCookie('session_cookie');
res.json({
success: true,
message: '注销成功'
});
});
});
// 获取当前用户信息
router.get('/me', (req, res) => {
if (!req.session.user) {
return res.status(401).json({
success: false,
message: '未登录'
});
}
res.json({
success: true,
user: req.session.user
});
});
module.exports = router;

143
backend/routes/student.js Normal file
View File

@@ -0,0 +1,143 @@
const express = require('express');
const router = express.Router();
const db = require('../config/database');
const { requireAuth, requireRole } = require('../middleware/auth');
// 获取学生成绩
router.get('/grades', requireAuth, requireRole(['student']), async (req, res) => {
try {
const userId = req.session.user.id;
// 获取学生信息
const students = await db.pool.execute(
'SELECT id FROM students WHERE user_id = ?',
[userId]
);
if (students[0].length === 0) {
return res.status(404).json({
success: false,
message: '学生信息不存在'
});
}
const studentId = students[0][0].id;
// 获取成绩信息
const grades = await db.pool.execute(`
SELECT s.*, c.course_code, c.course_name, c.credit,
u.name as teacher_name
FROM scores s
JOIN courses c ON s.course_id = c.id
JOIN users u ON s.teacher_id = u.id
WHERE s.student_id = ?
ORDER BY s.exam_date DESC
`, [studentId]);
// 计算统计信息
let totalCredits = 0;
let totalGradePoints = 0;
let totalCourses = grades.length;
grades.forEach(grade => {
totalCredits += parseFloat(grade.credit);
if (grade.grade_point) {
totalGradePoints += parseFloat(grade.grade_point) * parseFloat(grade.credit);
}
});
const gpa = totalCredits > 0 ? (totalGradePoints / totalCredits).toFixed(2) : 0;
res.json({
success: true,
grades,
statistics: {
totalCourses,
totalCredits,
gpa,
averageScore: totalCourses > 0 ?
(grades.reduce((sum, g) => sum + parseFloat(g.score || 0), 0) / totalCourses).toFixed(1) : 0
}
});
} catch (error) {
console.error('获取成绩错误:', error);
res.status(500).json({
success: false,
message: '服务器错误'
});
}
});
// 获取成绩详情
router.get('/grades/:id', requireAuth, requireRole(['student']), async (req, res) => {
try {
const gradeId = req.params.id;
const userId = req.session.user.id;
const grades = await db.pool.execute(`
SELECT s.*, c.course_code, c.course_name, c.credit, c.semester,
u.full_name as teacher_name, u.email as teacher_email,
st.student_id as student_number, st.class_name, st.major
FROM scores s
JOIN courses c ON s.course_id = c.id
JOIN users u ON s.teacher_id = u.id
JOIN students st ON s.student_id = st.id
WHERE s.id = ? AND st.user_id = ?
`, [gradeId, userId]);
if (grades[0].length === 0) {
return res.status(404).json({
success: false,
message: '成绩不存在'
});
}
res.json({
success: true,
grade: grades[0]
});
} catch (error) {
console.error('获取成绩详情错误:', error);
res.status(500).json({
success: false,
message: '服务器错误'
});
}
});
// 获取学生个人信息
router.get('/profile', requireAuth, requireRole(['student']), async (req, res) => {
try {
const userId = req.session.user.id;
const students = await db.pool.execute(`
SELECT s.*, u.username, u.email, u.created_at as account_created
FROM students s
JOIN users u ON s.user_id = u.id
WHERE u.id = ?
`, [userId]);
if (students[0].length === 0) {
return res.status(404).json({
success: false,
message: '学生信息不存在'
});
}
res.json({
success: true,
profile: students[0]
});
} catch (error) {
console.error('获取个人信息错误:', error);
res.status(500).json({
success: false,
message: '服务器错误'
});
}
});
module.exports = router;

243
backend/routes/teacher.js Normal file
View File

@@ -0,0 +1,243 @@
const express = require('express');
const router = express.Router();
const db = require('../config/database');
const { requireAuth, requireRole } = require('../middleware/auth');
// 获取教师教授的课程
router.get('/courses', requireAuth, requireRole(['teacher']), async (req, res) => {
try {
const teacherId = req.session.user.id;
const courses = await db.pool.execute(
'SELECT * FROM courses WHERE teacher_id = ? ORDER BY course_code',
[teacherId]
);
res.json({
success: true,
courses
});
} catch (error) {
console.error('获取课程错误:', error);
res.status(500).json({
success: false,
message: '服务器错误'
});
}
});
// 录入成绩
router.post('/grades', requireAuth, requireRole(['teacher']), async (req, res) => {
try {
const teacherId = req.session.user.id;
const { studentId, courseId, score, examDate, remark } = req.body;
// 验证输入
if (!studentId || !courseId || score === undefined) {
return res.status(400).json({
success: false,
message: '请填写必填字段'
});
}
// 检查学生和课程是否存在
const students = await db.pool.execute(
'SELECT id FROM students WHERE student_id = ?',
[studentId]
);
if (students[0].length === 0) {
return res.status(404).json({
success: false,
message: '学生不存在'
});
}
// 计算绩点和等级
const numericScore = parseFloat(score);
let gradePoint = 0;
let gradeLevel = 'F';
if (numericScore >= 90) {
gradePoint = 4.0; gradeLevel = 'A';
} else if (numericScore >= 80) {
gradePoint = 3.0; gradeLevel = 'B';
} else if (numericScore >= 70) {
gradePoint = 2.0; gradeLevel = 'C';
} else if (numericScore >= 60) {
gradePoint = 1.0; gradeLevel = 'D';
}
// 插入成绩
const result = await db.pool.execute(
`INSERT INTO scores (student_id, course_id, teacher_id, score,
grade_point, grade_level, exam_date, remark)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
[students[0][0].id, courseId, teacherId, numericScore,
gradePoint, gradeLevel, examDate, remark]
);
res.json({
success: true,
message: '成绩录入成功',
gradeId: result.insertId
});
} catch (error) {
console.error('录入成绩错误:', error);
// 处理唯一约束错误
if (error.code === 'ER_DUP_ENTRY') {
return res.status(400).json({
success: false,
message: '该学生此课程成绩已存在'
});
}
res.status(500).json({
success: false,
message: '服务器错误'
});
}
});
// 查询成绩(按班级、课程)
router.get('/grades', requireAuth, requireRole(['teacher']), async (req, res) => {
try {
const { class_name, course_id } = req.query;
const teacherId = req.session.user.id;
let query = `
SELECT sc.*, st.student_id, st.full_name, st.class_name,
c.course_code, c.course_name, c.credit
FROM scores sc
JOIN students st ON sc.student_id = st.id
JOIN courses c ON sc.course_id = c.id
WHERE sc.teacher_id = ?
`;
const params = [teacherId];
if (class_name) {
query += ' AND st.class_name = ?';
params.push(class_name);
}
if (course_id) {
query += ' AND sc.course_id = ?';
params.push(course_id);
}
query += ' ORDER BY st.class_name, st.student_id, sc.exam_date DESC';
const grades = await db.pool.execute(query, params);
res.json({
success: true,
grades
});
} catch (error) {
console.error('查询成绩错误:', error);
res.status(500).json({
success: false,
message: '服务器错误'
});
}
});
// 更新成绩
router.put('/grades/:id', requireAuth, requireRole(['teacher']), async (req, res) => {
try {
const gradeId = req.params.id;
const teacherId = req.session.user.id;
const { score, examDate, remark } = req.body;
// 验证成绩是否存在且属于该教师
const existingGrades = await db.pool.execute(
'SELECT id FROM scores WHERE id = ? AND teacher_id = ?',
[gradeId, teacherId]
);
if (existingGrades[0].length === 0) {
return res.status(404).json({
success: false,
message: '成绩不存在或无权修改'
});
}
// 计算新的绩点和等级
const numericScore = parseFloat(score);
let gradePoint = 0;
let gradeLevel = 'F';
if (numericScore >= 90) {
gradePoint = 4.0; gradeLevel = 'A';
} else if (numericScore >= 80) {
gradePoint = 3.0; gradeLevel = 'B';
} else if (numericScore >= 70) {
gradePoint = 2.0; gradeLevel = 'C';
} else if (numericScore >= 60) {
gradePoint = 1.0; gradeLevel = 'D';
}
// 更新成绩
await db.pool.execute(
`UPDATE scores SET score = ?, grade_point = ?, grade_level = ?,
exam_date = ?, remark = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ?`,
[numericScore, gradePoint, gradeLevel, examDate, remark, gradeId]
);
res.json({
success: true,
message: '成绩更新成功'
});
} catch (error) {
console.error('更新成绩错误:', error);
res.status(500).json({
success: false,
message: '服务器错误'
});
}
});
// 删除成绩
router.delete('/grades/:id', requireAuth, requireRole(['teacher']), async (req, res) => {
try {
const gradeId = req.params.id;
const teacherId = req.session.user.id;
// 验证成绩是否存在且属于该教师
const existingGrades = await db.pool.execute(
'SELECT id FROM scores WHERE id = ? AND teacher_id = ?',
[gradeId, teacherId]
);
if (existingGrades[0].length === 0) {
return res.status(404).json({
success: false,
message: '成绩不存在或无权删除'
});
}
// 删除成绩
await db.pool.execute('DELETE FROM scores WHERE id = ?', [gradeId]);
res.json({
success: true,
message: '成绩删除成功'
});
} catch (error) {
console.error('删除成绩错误:', error);
res.status(500).json({
success: false,
message: '服务器错误'
});
}
});
module.exports = router;

128
backend/server.js Normal file
View File

@@ -0,0 +1,128 @@
const express = require('express');
const cors = require('cors');
const session = require('express-session');
const MySQLStore = require('express-mysql-session')(session);
const path = require('path');
require('dotenv').config();
// 导入路由
const authRoutes = require('./routes/auth');
const studentRoutes = require('./routes/student');
const teacherRoutes = require('./routes/teacher');
const adminRoutes = require('./routes/admin');
// 数据库配置
const db = require('./config/database');
const app = express();
const PORT = process.env.PORT || 3000;
// 中间件
app.use(cors({
origin: 'http://localhost:3000',
credentials: true
}));
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// 会话配置
const sessionStore = new MySQLStore({
expiration: 86400000, // 1天
createDatabaseTable: true,
schema: {
tableName: 'sessions',
columnNames: {
session_id: 'session_id',
expires: 'expires',
data: 'data'
}
}
}, db.pool);
app.use(session({
key: 'session_cookie',
secret: process.env.SESSION_SECRET || 'your-secret-key',
store: sessionStore,
resave: false,
saveUninitialized: false,
cookie: {
maxAge: 86400000,
httpOnly: true,
secure: process.env.NODE_ENV === 'production'
}
}));
// 静态文件服务
app.use(express.static(path.join(__dirname, '../frontend')));
// 重定向旧路径 /frontend/html/* 到 /html/*
app.get('/frontend/html/*', (req, res) => {
const path = req.params[0];
res.redirect(`/html/${path}`);
});
// 路由
app.use('/api/auth', authRoutes);
app.use('/api/student', studentRoutes);
app.use('/api/teacher', teacherRoutes);
app.use('/api/admin', adminRoutes);
// 认证中间件
const { requireAuth, requireRole } = require('./middleware/auth');
// 页面路由
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, '../frontend/html/login.html'));
});
app.get('/dashboard', requireAuth, (req, res) => {
// 根据用户角色重定向到不同的仪表板
const role = req.session.user?.role;
switch (role) {
case 'student':
res.redirect('/html/student_dashboard.html');
break;
case 'teacher':
res.redirect('/html/teacher_dashboard.html');
break;
case 'admin':
res.redirect('/html/admin_dashboard.html');
break;
default:
// 如果没有角色信息,重定向到登录页面
res.redirect('/');
break;
}
});
// 学生页面路由
app.get('/student/*', requireAuth, requireRole(['student', 'admin', 'teacher']), (req, res, next) => {
next();
});
// 教师页面路由
app.get('/teacher/*', requireAuth, requireRole(['teacher', 'admin']), (req, res, next) => {
next();
});
// 管理员页面路由
app.get('/admin/*', requireAuth, requireRole(['admin']), (req, res, next) => {
next();
});
// 404处理
app.use((req, res) => {
res.status(404).json({ error: 'Not found' });
});
// 错误处理
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({ error: 'Internal server error' });
});
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
console.log(`访问地址: http://localhost:${PORT}`);
});

214
database/init.sql Normal file
View File

@@ -0,0 +1,214 @@
-- 学生成绩管理系统数据库初始化脚本
-- 创建数据库
CREATE DATABASE IF NOT EXISTS score_management DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE score_management;
-- 用户表(学生、教师、管理员)
CREATE TABLE IF NOT EXISTS users (
id VARCHAR(20) PRIMARY KEY COMMENT '用户ID学号/工号)',
name VARCHAR(50) NOT NULL COMMENT '姓名',
password VARCHAR(100) NOT NULL COMMENT '密码bcrypt加密',
role ENUM('student', 'teacher', 'admin') NOT NULL COMMENT '角色',
class VARCHAR(20) DEFAULT NULL COMMENT '班级学生和教师需要管理员为NULL',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
INDEX idx_role (role),
INDEX idx_class (class)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
-- students 表:学生详细信息(与 users 表 id 关联)
CREATE TABLE IF NOT EXISTS students (
id VARCHAR(20) PRIMARY KEY COMMENT '学生ID与users表id一致',
name VARCHAR(50) NOT NULL COMMENT '姓名',
class VARCHAR(20) COMMENT '班级',
FOREIGN KEY (id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='学生详细信息表';
-- scores 表:成绩记录
CREATE TABLE IF NOT EXISTS scores (
id INT AUTO_INCREMENT PRIMARY KEY COMMENT '成绩记录ID',
student_id VARCHAR(20) NOT NULL COMMENT '学生ID',
course VARCHAR(100) NOT NULL COMMENT '课程名称',
score DECIMAL(5,2) NOT NULL CHECK (score >= 0 AND score <= 100) COMMENT '成绩',
teacher_id VARCHAR(20) NOT NULL COMMENT '教师ID',
class VARCHAR(20) NOT NULL COMMENT '班级',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
FOREIGN KEY (student_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (teacher_id) REFERENCES users(id) ON DELETE CASCADE,
INDEX idx_student_id (student_id),
INDEX idx_teacher_id (teacher_id),
INDEX idx_class (class)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='成绩记录表';
-- 班级表
CREATE TABLE IF NOT EXISTS classes (
id INT PRIMARY KEY AUTO_INCREMENT COMMENT '班级ID',
class_name VARCHAR(50) NOT NULL COMMENT '班级名称',
grade VARCHAR(20) COMMENT '年级',
major VARCHAR(100) COMMENT '专业',
teacher_id INT COMMENT '班主任ID',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
INDEX idx_class_name (class_name),
INDEX idx_teacher_id (teacher_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='班级表';
-- 课程表
CREATE TABLE IF NOT EXISTS courses (
id INT PRIMARY KEY AUTO_INCREMENT COMMENT '课程ID',
course_code VARCHAR(20) UNIQUE NOT NULL COMMENT '课程代码',
course_name VARCHAR(100) NOT NULL COMMENT '课程名称',
credit DECIMAL(3,1) NOT NULL DEFAULT 2.0 COMMENT '学分',
teacher_id INT NOT NULL COMMENT '授课教师ID',
class_id INT NOT NULL COMMENT '授课班级ID',
semester VARCHAR(20) COMMENT '学期',
academic_year VARCHAR(20) COMMENT '学年',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
INDEX idx_course_code (course_code),
INDEX idx_teacher_id (teacher_id),
INDEX idx_class_id (class_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='课程表';
-- 成绩表
CREATE TABLE IF NOT EXISTS grades (
id INT PRIMARY KEY AUTO_INCREMENT COMMENT '成绩ID',
student_id INT NOT NULL COMMENT '学生ID',
course_id INT NOT NULL COMMENT '课程ID',
usual_score DECIMAL(5,2) COMMENT '平时成绩',
midterm_score DECIMAL(5,2) COMMENT '期中成绩',
final_score DECIMAL(5,2) COMMENT '期末成绩',
total_score DECIMAL(5,2) COMMENT '总评成绩',
grade_point DECIMAL(3,2) COMMENT '绩点',
grade_level VARCHAR(10) COMMENT '成绩等级A/B/C/D/F',
teacher_id INT NOT NULL COMMENT '录入教师ID',
remark TEXT COMMENT '备注',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
UNIQUE KEY uk_student_course (student_id, course_id),
INDEX idx_student_id (student_id),
INDEX idx_course_id (course_id),
INDEX idx_teacher_id (teacher_id),
INDEX idx_total_score (total_score)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='成绩表';
-- 操作日志表
CREATE TABLE IF NOT EXISTS operation_logs (
id INT PRIMARY KEY AUTO_INCREMENT COMMENT '日志ID',
user_id INT NOT NULL COMMENT '操作用户ID',
operation_type VARCHAR(50) NOT NULL COMMENT '操作类型',
operation_target VARCHAR(100) COMMENT '操作目标',
operation_details TEXT COMMENT '操作详情',
ip_address VARCHAR(45) COMMENT 'IP地址',
user_agent TEXT COMMENT '用户代理',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '操作时间',
INDEX idx_user_id (user_id),
INDEX idx_operation_type (operation_type),
INDEX idx_created_at (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='操作日志表';
-- 插入初始数据
-- 插入管理员用户密码admin123
INSERT INTO users (username, password, name, role, email, phone) VALUES
('admin', '$2b$10$N9qo8uLOickgx2ZMRZoMy.MrqK.3.6Z1zXjJX.3Q7JzQ7JzQ7JzQ7', '系统管理员', 'admin', 'admin@school.edu', '13800138000');
-- 插入示例班级
INSERT INTO classes (class_name, grade, major) VALUES
('计算机科学与技术2023级1班', '2023', '计算机科学与技术'),
('软件工程2023级1班', '2023', '软件工程'),
('人工智能2023级1班', '2023', '人工智能');
-- 插入示例教师密码teacher123
INSERT INTO users (username, password, name, role, class_id, email, phone) VALUES
('t1001', '$2b$10$N9qo8uLOickgx2ZMRZoMy.MrqK.3.6Z1zXjJX.3Q7JzQ7JzQ7JzQ7', '张老师', 'teacher', 1, 'zhang@school.edu', '13800138001'),
('t1002', '$2b$10$N9qo8uLOickgx2ZMRZoMy.MrqK.3.6Z1zXjJX.3Q7JzQ7JzQ7JzQ7', '李老师', 'teacher', 2, 'li@school.edu', '13800138002');
-- 插入示例学生密码student123
INSERT INTO users (username, password, name, role, class_id, email, phone) VALUES
('s2023001', '$2b$10$N9qo8uLOickgx2ZMRZoMy.MrqK.3.6Z1zXjJX.3Q7JzQ7JzQ7JzQ7', '张三', 'student', 1, 'zhangsan@school.edu', '13800138111'),
('s2023002', '$2b$10$N9qo8uLOickgx2ZMRZoMy.MrqK.3.6Z1zXjJX.3Q7JzQ7JzQ7JzQ7', '李四', 'student', 1, 'lisi@school.edu', '13800138112'),
('s2023003', '$2b$10$N9qo8uLOickgx2ZMRZoMy.MrqK.3.6Z1zXjJX.3Q7JzQ7JzQ7JzQ7', '王五', 'student', 2, 'wangwu@school.edu', '13800138113');
-- 插入示例课程
INSERT INTO courses (course_code, course_name, credit, teacher_id, class_id, semester, academic_year) VALUES
('CS101', '计算机基础', 3.0, 2, 1, '2023-2024-1', '2023-2024'),
('CS201', '数据结构', 4.0, 2, 1, '2023-2024-1', '2023-2024'),
('SE101', '软件工程导论', 3.0, 3, 2, '2023-2024-1', '2023-2024'),
('AI101', '人工智能基础', 3.5, 3, 3, '2023-2024-1', '2023-2024');
-- 插入示例成绩
INSERT INTO grades (student_id, course_id, usual_score, midterm_score, final_score, total_score, grade_point, grade_level, teacher_id) VALUES
(3, 1, 85.00, 78.00, 82.00, 81.50, 3.2, 'B', 2),
(3, 2, 90.00, 85.00, 88.00, 87.50, 3.7, 'A', 2),
(4, 1, 78.00, 82.00, 80.00, 80.50, 3.0, 'B', 2),
(5, 3, 88.00, 85.00, 90.00, 88.50, 3.8, 'A', 3);
-- 更新班级表的班主任信息
UPDATE classes SET teacher_id = 2 WHERE id = 1;
UPDATE classes SET teacher_id = 3 WHERE id = 2;
-- 创建视图:学生成绩详情视图
CREATE OR REPLACE VIEW student_grade_details AS
SELECT
g.id,
g.student_id,
u1.name AS student_name,
u1.username AS student_no,
g.course_id,
c.course_code,
c.course_name,
c.credit,
g.usual_score,
g.midterm_score,
g.final_score,
g.total_score,
g.grade_point,
g.grade_level,
g.teacher_id,
u2.name AS teacher_name,
g.remark,
g.created_at,
g.updated_at
FROM grades g
JOIN users u1 ON g.student_id = u1.id
JOIN courses c ON g.course_id = c.id
JOIN users u2 ON g.teacher_id = u2.id;
-- 创建视图:班级成绩统计视图
CREATE OR REPLACE VIEW class_grade_statistics AS
SELECT
cl.id AS class_id,
cl.class_name,
c.id AS course_id,
c.course_code,
c.course_name,
COUNT(g.id) AS student_count,
AVG(g.total_score) AS avg_score,
MAX(g.total_score) AS max_score,
MIN(g.total_score) AS min_score,
SUM(CASE WHEN g.grade_level = 'A' THEN 1 ELSE 0 END) AS a_count,
SUM(CASE WHEN g.grade_level = 'B' THEN 1 ELSE 0 END) AS b_count,
SUM(CASE WHEN g.grade_level = 'C' THEN 1 ELSE 0 END) AS c_count,
SUM(CASE WHEN g.grade_level = 'D' THEN 1 ELSE 0 END) AS d_count,
SUM(CASE WHEN g.grade_level = 'F' THEN 1 ELSE 0 END) AS f_count
FROM classes cl
JOIN courses c ON cl.id = c.class_id
LEFT JOIN grades g ON c.id = g.course_id
GROUP BY cl.id, c.id;
-- 显示表结构信息
SHOW TABLES;
-- 显示各表记录数
SELECT 'users' AS table_name, COUNT(*) AS record_count FROM users
UNION ALL
SELECT 'classes', COUNT(*) FROM classes
UNION ALL
SELECT 'courses', COUNT(*) FROM courses
UNION ALL
SELECT 'grades', COUNT(*) FROM grades
UNION ALL
SELECT 'operation_logs', COUNT(*) FROM operation_logs;
-- 显示视图
SHOW FULL TABLES WHERE TABLE_TYPE = 'VIEW';

View File

@@ -0,0 +1,213 @@
/*
Navicat Premium Dump SQL
Source Server : test
Source Server Type : MySQL
Source Server Version : 80042 (8.0.42)
Source Host : localhost:3306
Source Schema : score_management
Target Server Type : MySQL
Target Server Version : 80042 (8.0.42)
File Encoding : 65001
Date: 21/12/2025 21:37:55
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for classes
-- ----------------------------
DROP TABLE IF EXISTS `classes`;
CREATE TABLE `classes` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '班级ID',
`class_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '班级名称',
`grade` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '年级',
`major` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '专业',
`teacher_id` int NULL DEFAULT NULL COMMENT '班主任ID',
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE,
INDEX `idx_class_name`(`class_name` ASC) USING BTREE,
INDEX `idx_teacher_id`(`teacher_id` ASC) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '班级表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of classes
-- ----------------------------
-- ----------------------------
-- Table structure for courses
-- ----------------------------
DROP TABLE IF EXISTS `courses`;
CREATE TABLE `courses` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '课程ID',
`course_code` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '课程代码',
`course_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '课程名称',
`credit` decimal(3, 1) NOT NULL DEFAULT 2.0 COMMENT '学分',
`teacher_id` int NOT NULL COMMENT '授课教师ID',
`class_id` int NOT NULL COMMENT '授课班级ID',
`semester` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '学期',
`academic_year` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '学年',
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `course_code`(`course_code` ASC) USING BTREE,
INDEX `idx_course_code`(`course_code` ASC) USING BTREE,
INDEX `idx_teacher_id`(`teacher_id` ASC) USING BTREE,
INDEX `idx_class_id`(`class_id` ASC) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '课程表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of courses
-- ----------------------------
-- ----------------------------
-- Table structure for grades
-- ----------------------------
DROP TABLE IF EXISTS `grades`;
CREATE TABLE `grades` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '成绩ID',
`student_id` int NOT NULL COMMENT '学生ID',
`course_id` int NOT NULL COMMENT '课程ID',
`usual_score` decimal(5, 2) NULL DEFAULT NULL COMMENT '平时成绩',
`midterm_score` decimal(5, 2) NULL DEFAULT NULL COMMENT '期中成绩',
`final_score` decimal(5, 2) NULL DEFAULT NULL COMMENT '期末成绩',
`total_score` decimal(5, 2) NULL DEFAULT NULL COMMENT '总评成绩',
`grade_point` decimal(3, 2) NULL DEFAULT NULL COMMENT '绩点',
`grade_level` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '成绩等级A/B/C/D/F',
`teacher_id` int NOT NULL COMMENT '录入教师ID',
`remark` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '备注',
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `uk_student_course`(`student_id` ASC, `course_id` ASC) USING BTREE,
INDEX `idx_student_id`(`student_id` ASC) USING BTREE,
INDEX `idx_course_id`(`course_id` ASC) USING BTREE,
INDEX `idx_teacher_id`(`teacher_id` ASC) USING BTREE,
INDEX `idx_total_score`(`total_score` ASC) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '成绩表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of grades
-- ----------------------------
-- ----------------------------
-- Table structure for operation_logs
-- ----------------------------
DROP TABLE IF EXISTS `operation_logs`;
CREATE TABLE `operation_logs` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '日志ID',
`user_id` int NOT NULL COMMENT '操作用户ID',
`operation_type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '操作类型',
`operation_target` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '操作目标',
`operation_details` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '操作详情',
`ip_address` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT 'IP地址',
`user_agent` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '用户代理',
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '操作时间',
PRIMARY KEY (`id`) USING BTREE,
INDEX `idx_user_id`(`user_id` ASC) USING BTREE,
INDEX `idx_operation_type`(`operation_type` ASC) USING BTREE,
INDEX `idx_created_at`(`created_at` ASC) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '操作日志表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of operation_logs
-- ----------------------------
-- ----------------------------
-- Table structure for scores
-- ----------------------------
DROP TABLE IF EXISTS `scores`;
CREATE TABLE `scores` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '成绩记录ID',
`student_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '学生ID',
`course` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '课程名称',
`score` decimal(5, 2) NOT NULL COMMENT '成绩',
`teacher_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '教师ID',
`class` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '班级',
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`) USING BTREE,
INDEX `idx_student_id`(`student_id` ASC) USING BTREE,
INDEX `idx_teacher_id`(`teacher_id` ASC) USING BTREE,
INDEX `idx_class`(`class` ASC) USING BTREE,
CONSTRAINT `scores_ibfk_1` FOREIGN KEY (`student_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT,
CONSTRAINT `scores_ibfk_2` FOREIGN KEY (`teacher_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT,
CONSTRAINT `scores_chk_1` CHECK ((`score` >= 0) and (`score` <= 100))
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '成绩记录表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of scores
-- ----------------------------
-- ----------------------------
-- Table structure for sessions
-- ----------------------------
DROP TABLE IF EXISTS `sessions`;
CREATE TABLE `sessions` (
`session_id` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
`expires` int UNSIGNED NOT NULL,
`data` mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL,
PRIMARY KEY (`session_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of sessions
-- ----------------------------
INSERT INTO `sessions` VALUES ('KY6QaavAiws7rkdEBFIFDoHefl2bxzlI', 1766334525, '{\"cookie\":{\"originalMaxAge\":86400000,\"expires\":\"2025-12-21T16:28:44.650Z\",\"secure\":false,\"httpOnly\":true,\"path\":\"/\"},\"user\":{\"id\":\"teststudent\",\"name\":\"????\",\"role\":\"student\",\"class\":\"2023?1?\"}}');
INSERT INTO `sessions` VALUES ('KaDFs4HogLmkjS0HAs6qki6g2FmE3sTL', 1766334465, '{\"cookie\":{\"originalMaxAge\":86400000,\"expires\":\"2025-12-21T16:27:45.314Z\",\"secure\":false,\"httpOnly\":true,\"path\":\"/\"},\"user\":{\"id\":\"teststudent\",\"name\":\"????\",\"role\":\"student\",\"class\":\"2023?1?\"}}');
INSERT INTO `sessions` VALUES ('QAhXDQ1FOlhU6RhaFOm3ghRtLOW4hBTd', 1766334812, '{\"cookie\":{\"originalMaxAge\":86400000,\"expires\":\"2025-12-21T16:33:31.987Z\",\"secure\":false,\"httpOnly\":true,\"path\":\"/\"},\"user\":{\"id\":\"teststudent\",\"name\":\"????\",\"role\":\"student\",\"class\":\"2023?1?\"}}');
INSERT INTO `sessions` VALUES ('SAuQyktAI9gAHpXbjARpe-9BL42pDRiV', 1766334764, '{\"cookie\":{\"originalMaxAge\":86400000,\"expires\":\"2025-12-21T16:32:43.695Z\",\"secure\":false,\"httpOnly\":true,\"path\":\"/\"},\"user\":{\"id\":\"teststudent\",\"name\":\"????\",\"role\":\"student\",\"class\":\"2023?1?\"}}');
INSERT INTO `sessions` VALUES ('XduN1lYhGPeIaLTHbLTNVnTCBtKUCkJR', 1766334689, '{\"cookie\":{\"originalMaxAge\":86400000,\"expires\":\"2025-12-21T16:31:28.994Z\",\"secure\":false,\"httpOnly\":true,\"path\":\"/\"},\"user\":{\"id\":\"teststudent\",\"name\":\"????\",\"role\":\"student\",\"class\":\"2023?1?\"}}');
INSERT INTO `sessions` VALUES ('Y59PFvvqK7M0DKZshc6ONTmFQjzGyMmV', 1766334426, '{\"cookie\":{\"originalMaxAge\":86400000,\"expires\":\"2025-12-21T16:27:05.673Z\",\"secure\":false,\"httpOnly\":true,\"path\":\"/\"},\"user\":{\"id\":\"teststudent\",\"name\":\"????\",\"role\":\"student\",\"class\":\"2023?1?\"}}');
INSERT INTO `sessions` VALUES ('rlscT2Pi2EAyLXHs1CNXyQmNSiW8vEo4', 1766334271, '{\"cookie\":{\"originalMaxAge\":86400000,\"expires\":\"2025-12-21T16:24:30.682Z\",\"secure\":false,\"httpOnly\":true,\"path\":\"/\"},\"user\":{\"id\":\"teststudent\",\"name\":\"????\",\"role\":\"student\",\"class\":\"2023?1?\"}}');
INSERT INTO `sessions` VALUES ('rsaOCJRjYQLPtUWlDmUFJgWcCYZbOCgJ', 1766410574, '{\"cookie\":{\"originalMaxAge\":86400000,\"expires\":\"2025-12-21T15:54:39.935Z\",\"secure\":false,\"httpOnly\":true,\"path\":\"/\"},\"user\":{\"id\":\"123\",\"name\":\"经济局\",\"role\":\"student\",\"class\":\"123\"}}');
INSERT INTO `sessions` VALUES ('wXxRpNTGY0wqLaHsebSAsw1I6Pb7Ed6w', 1766410584, '{\"cookie\":{\"originalMaxAge\":86400000,\"expires\":\"2025-12-22T13:35:43.191Z\",\"secure\":false,\"httpOnly\":true,\"path\":\"/\"},\"user\":{\"id\":\"567\",\"name\":\"急急急\",\"role\":\"teacher\",\"class\":\"567\"}}');
-- ----------------------------
-- Table structure for students
-- ----------------------------
DROP TABLE IF EXISTS `students`;
CREATE TABLE `students` (
`id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '学生ID与users表id一致',
`name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '姓名',
`class` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '班级',
PRIMARY KEY (`id`) USING BTREE,
CONSTRAINT `students_ibfk_1` FOREIGN KEY (`id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '学生详细信息表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of students
-- ----------------------------
INSERT INTO `students` VALUES ('123', '经济局', '123');
INSERT INTO `students` VALUES ('test123', '????', '????');
INSERT INTO `students` VALUES ('teststudent', '????', '2023?1?');
-- ----------------------------
-- Table structure for users
-- ----------------------------
DROP TABLE IF EXISTS `users`;
CREATE TABLE `users` (
`id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '用户ID学号/工号)',
`name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '姓名',
`password` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '密码bcrypt加密',
`role` enum('student','teacher','admin') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '角色',
`class` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '班级学生和教师需要管理员为NULL',
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE,
INDEX `idx_role`(`role` ASC) USING BTREE,
INDEX `idx_class`(`class` ASC) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '用户表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of users
-- ----------------------------
INSERT INTO `users` VALUES ('123', '经济局', '$2a$10$l0NwrM2fNGgPdqDFqXEGx.UfqOIp8womtWN8/omq1fK15zII7b4Nm', 'student', '123', '2025-12-20 22:47:59', '2025-12-20 22:47:59');
INSERT INTO `users` VALUES ('567', '急急急', '$2a$10$27a0L4fC0rLjK4.Kpq0CceK1cD4O0cW6XTxwvs4eIcYpKnpvEQdVG', 'teacher', '567', '2025-12-21 20:36:17', '2025-12-21 20:36:17');
INSERT INTO `users` VALUES ('test123', '????', '$2a$10$61WfURr1uI1e71EWwWXOlOUamTR1/AzH2Kb.6bZKVttdmWGk7V366', 'student', '????', '2025-12-20 23:49:32', '2025-12-20 23:49:32');
INSERT INTO `users` VALUES ('teststudent', '????', '$2a$10$PfNB72GBDBmu8DLcTLptW.4clzmk9qsjjlxfAMZvAJD8B.QUoXXAK', 'student', '2023?1?', '2025-12-21 00:24:16', '2025-12-21 00:24:16');
SET FOREIGN_KEY_CHECKS = 1;

215
frontend/css/main.css Normal file
View File

@@ -0,0 +1,215 @@
/* 首页通用交互样式 */
/* 滚动时导航栏样式 */
.navbar-scrolled {
background-color: rgba(255, 255, 255, 0.95) !important;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(10px);
}
.navbar-scrolled .navbar-brand,
.navbar-scrolled .nav-link {
color: #333 !important;
}
.navbar-scrolled .navbar-toggler-icon {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba(0, 0, 0, 0.7)' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e") !important;
}
/* 滚动动画效果 */
.feature-card, .hero-content {
opacity: 0;
transform: translateY(20px);
transition: opacity 0.6s ease, transform 0.6s ease;
}
.feature-card.animate-in,
.hero-content.animate-in {
opacity: 1;
transform: translateY(0);
}
/* 返回顶部按钮 */
#backToTop {
position: fixed;
bottom: 30px;
right: 30px;
width: 50px;
height: 50px;
background-color: #4e73df;
color: white;
border: none;
border-radius: 50%;
cursor: pointer;
opacity: 0;
visibility: hidden;
transition: all 0.3s ease;
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.2rem;
box-shadow: 0 4px 15px rgba(78, 115, 223, 0.3);
}
#backToTop:hover {
background-color: #2e59d9;
transform: translateY(-3px);
box-shadow: 0 6px 20px rgba(78, 115, 223, 0.4);
}
#backToTop.show {
opacity: 1;
visibility: visible;
}
/* 通知样式 */
.notification {
position: fixed;
top: 20px;
right: 20px;
padding: 15px 20px;
border-radius: 8px;
background-color: white;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
z-index: 9999;
opacity: 0;
transform: translateX(100%);
transition: opacity 0.3s ease, transform 0.3s ease;
max-width: 350px;
display: flex;
align-items: center;
}
.notification.show {
opacity: 1;
transform: translateX(0);
}
.notification-content {
display: flex;
align-items: center;
gap: 10px;
}
.notification i {
font-size: 1.2rem;
}
.notification-success {
border-left: 4px solid #1cc88a;
}
.notification-success i {
color: #1cc88a;
}
.notification-error {
border-left: 4px solid #e74a3b;
}
.notification-error i {
color: #e74a3b;
}
.notification-info {
border-left: 4px solid #36b9cc;
}
.notification-info i {
color: #36b9cc;
}
/* 移动端菜单优化 */
@media (max-width: 991.98px) {
.navbar-collapse {
background-color: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
margin-top: 10px;
}
.navbar-nav .nav-link {
padding: 10px 15px;
border-radius: 4px;
margin-bottom: 5px;
}
.navbar-nav .nav-link:hover {
background-color: #f8f9fc;
}
}
/* 平滑滚动优化 */
html {
scroll-behavior: smooth;
}
/* 当前页面高亮 */
.nav-link.active {
color: #4e73df !important;
font-weight: 600;
}
.nav-link.active::after {
content: '';
position: absolute;
bottom: -2px;
left: 0;
width: 100%;
height: 2px;
background-color: #4e73df;
border-radius: 2px;
}
/* 按钮悬停效果增强 */
.btn {
transition: all 0.3s ease;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
}
/* 卡片悬停效果 */
.feature-card {
transition: all 0.3s ease;
}
.feature-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
}
/* 加载动画 */
.loading-spinner {
display: inline-block;
width: 20px;
height: 20px;
border: 3px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
border-top-color: white;
animation: spin 1s ease-in-out infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* 响应式调整 */
@media (max-width: 768px) {
#backToTop {
bottom: 20px;
right: 20px;
width: 45px;
height: 45px;
}
.notification {
left: 20px;
right: 20px;
max-width: none;
}
}

1150
frontend/css/style.css Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,353 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>学生成绩管理系统 - 管理员仪表板</title>
<link rel="stylesheet" href="../css/style.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
</head>
</head>
<body>
<!-- 顶部导航栏 -->
<nav class="navbar">
<div class="navbar-brand">
<i class="fas fa-graduation-cap"></i>
<span>XX学校成绩管理系统</span>
</div>
<div class="navbar-menu">
<a href="index.html" class="btn btn-secondary">
<i class="fas fa-home"></i> 主页
</a>
<div class="navbar-user">
<span class="user-name" id="userName">管理员</span>
<button id="logoutBtn" class="btn btn-primary">
<i class="fas fa-sign-out-alt"></i> 退出
</button>
</div>
</div>
</nav>
<!-- 仪表板容器 -->
<div class="dashboard-container">
<!-- 左侧侧边栏 -->
<aside class="sidebar">
<div class="sidebar-header">
<div class="user-info">
<div class="user-avatar">
<i class="fas fa-user-shield"></i>
</div>
<div class="user-details">
<h3 id="adminName">管理员</h3>
<p>系统管理员 | 权限:<span id="adminRole">超级管理员</span></p>
</div>
</div>
</div>
<ul class="sidebar-menu">
<li>
<a href="#" class="active">
<i class="fas fa-tachometer-alt"></i>
<span>仪表板</span>
</a>
</li>
<li>
<a href="user_management.html">
<i class="fas fa-users"></i>
<span>用户管理</span>
</a>
</li>
<li>
<a href="student_management.html">
<i class="fas fa-user-graduate"></i>
<span>学生管理</span>
</a>
</li>
<li>
<a href="teacher_management.html">
<i class="fas fa-chalkboard-teacher"></i>
<span>教师管理</span>
</a>
</li>
<li>
<a href="grade_statistics.html">
<i class="fas fa-chart-bar"></i>
<span>成绩统计</span>
</a>
</li>
<li>
<a href="system_settings.html">
<i class="fas fa-cog"></i>
<span>系统设置</span>
</a>
</li>
<li>
<a href="data_export.html">
<i class="fas fa-download"></i>
<span>数据导出</span>
</a>
</li>
<li>
<a href="audit_log.html">
<i class="fas fa-history"></i>
<span>操作日志</span>
</a>
</li>
</ul>
</aside>
<!-- 主内容区 -->
<main class="main-content">
<div class="content-header">
<div>
<h1 class="page-title">管理员仪表板</h1>
<div class="breadcrumb">
<a href="index.html">主页</a>
<i class="fas fa-chevron-right"></i>
<span>管理员仪表板</span>
</div>
</div>
<div class="current-time" id="currentTime"></div>
</div>
<!-- 统计卡片 -->
<div class="stats-grid">
<div class="stat-card">
<div class="stat-icon users">
<i class="fas fa-users"></i>
</div>
<div class="stat-content">
<div class="stat-value" id="totalUsers">1,248</div>
<div class="stat-label">总用户数</div>
<div class="stat-change positive">
<i class="fas fa-arrow-up"></i> 12 本周新增
</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon students">
<i class="fas fa-user-graduate"></i>
</div>
<div class="stat-content">
<div class="stat-value" id="totalStudents">3,567</div>
<div class="stat-label">学生总数</div>
<div class="stat-change positive">
<i class="fas fa-arrow-up"></i> 45 本周新增
</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon teachers">
<i class="fas fa-chalkboard-teacher"></i>
</div>
<div class="stat-content">
<div class="stat-value" id="totalTeachers">128</div>
<div class="stat-label">教师总数</div>
<div class="stat-change">
<i class="fas fa-minus"></i> 无变化
</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon courses">
<i class="fas fa-book"></i>
</div>
<div class="stat-content">
<div class="stat-value" id="totalCourses">89</div>
<div class="stat-label">课程总数</div>
<div class="stat-change positive">
<i class="fas fa-arrow-up"></i> 3 本周新增
</div>
</div>
</div>
</div>
<!-- 功能卡片 -->
<div class="function-grid">
<div class="function-card" onclick="window.location.href='user_management.html'">
<div class="function-icon">
<i class="fas fa-users"></i>
</div>
<h3 class="function-title">用户管理</h3>
<p class="function-description">
管理所有用户账户,包括添加、编辑、删除用户,设置用户角色和权限。
</p>
<button class="btn btn-primary">进入管理</button>
</div>
<div class="function-card" onclick="window.location.href='student_management.html'">
<div class="function-icon">
<i class="fas fa-user-graduate"></i>
</div>
<h3 class="function-title">学生管理</h3>
<p class="function-description">
管理学生信息,包括学籍管理、班级分配、信息维护和批量导入导出。
</p>
<button class="btn btn-primary">进入管理</button>
</div>
<div class="function-card" onclick="window.location.href='teacher_management.html'">
<div class="function-icon">
<i class="fas fa-chalkboard-teacher"></i>
</div>
<h3 class="function-title">教师管理</h3>
<p class="function-description">
管理教师信息,包括教师分配、课程安排、权限设置和绩效考核。
</p>
<button class="btn btn-primary">进入管理</button>
</div>
<div class="function-card" onclick="window.location.href='grade_statistics.html'">
<div class="function-icon">
<i class="fas fa-chart-bar"></i>
</div>
<h3 class="function-title">成绩统计</h3>
<p class="function-description">
查看全校成绩统计,生成分析报告,支持图表展示和数据导出。
</p>
<button class="btn btn-primary">查看统计</button>
</div>
</div>
<!-- 系统状态 -->
<div class="system-status">
<h2 class="section-title">
<i class="fas fa-server"></i>
系统状态
</h2>
<div class="status-list">
<div class="status-item">
<div class="status-indicator online"></div>
<div>
<div class="status-label">数据库服务</div>
<div class="status-value">运行正常 | 响应时间: 12ms</div>
</div>
</div>
<div class="status-item">
<div class="status-indicator online"></div>
<div>
<div class="status-label">Web服务器</div>
<div class="status-value">运行正常 | 在线用户: 156</div>
</div>
</div>
<div class="status-item">
<div class="status-indicator online"></div>
<div>
<div class="status-label">文件存储</div>
<div class="status-value">使用率: 65% | 剩余: 35GB</div>
</div>
</div>
<div class="status-item">
<div class="status-indicator warning"></div>
<div>
<div class="status-label">备份服务</div>
<div class="status-value">上次备份: 2天前 | 建议立即备份</div>
</div>
</div>
</div>
</div>
<!-- 最近活动 -->
<div class="recent-activities">
<h2 class="section-title">
<i class="fas fa-history"></i>
最近活动
</h2>
<ul class="activity-list">
<li class="activity-item">
<div class="activity-icon">
<i class="fas fa-user-plus"></i>
</div>
<div class="activity-content">
<div class="activity-title">新增学生用户</div>
<div class="activity-time">10分钟前 | 操作人: 管理员</div>
</div>
</li>
<li class="activity-item">
<div class="activity-icon">
<i class="fas fa-edit"></i>
</div>
<div class="activity-content">
<div class="activity-title">修改教师信息</div>
<div class="activity-time">1小时前 | 操作人: 管理员</div>
</div>
</li>
<li class="activity-item">
<div class="activity-icon">
<i class="fas fa-chart-bar"></i>
</div>
<div class="activity-content">
<div class="activity-title">生成成绩统计报告</div>
<div class="activity-time">3小时前 | 操作人: 系统</div>
</div>
</li>
<li class="activity-item">
<div class="activity-icon">
<i class="fas fa-download"></i>
</div>
<div class="activity-content">
<div class="activity-title">导出用户数据</div>
<div class="activity-time">5小时前 | 操作人: 管理员</div>
</div>
</li>
<li class="activity-item">
<div class="activity-icon">
<i class="fas fa-cog"></i>
</div>
<div class="activity-content">
<div class="activity-title">系统设置更新</div>
<div class="activity-time">1天前 | 操作人: 管理员</div>
</div>
</li>
</ul>
</div>
</main>
</div>
<script>
// 更新当前时间
function updateCurrentTime() {
const now = new Date();
const options = {
year: 'numeric',
month: 'long',
day: 'numeric',
weekday: 'long',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
};
document.getElementById('currentTime').textContent = now.toLocaleDateString('zh-CN', options);
}
// 页面加载时初始化
document.addEventListener('DOMContentLoaded', function() {
updateCurrentTime();
setInterval(updateCurrentTime, 1000);
// 退出登录
document.getElementById('logoutBtn').addEventListener('click', function() {
if (confirm('确定要退出登录吗?')) {
// 清除登录状态
localStorage.removeItem('token');
localStorage.removeItem('userRole');
localStorage.removeItem('userInfo');
// 跳转到登录页面
window.location.href = 'login.html';
}
});
// 加载用户信息
const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}');
if (userInfo.name) {
document.getElementById('adminName').textContent = userInfo.name;
document.getElementById('userName').textContent = userInfo.name;
}
});
</script>
</body>
</html>

View File

@@ -0,0 +1,459 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>成绩录入 - XX学校成绩管理系统</title>
<link rel="stylesheet" href="../css/style.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
</head>
<body>
<!-- 导航栏 -->
<nav class="navbar">
<div class="navbar-brand">
<a href="index.html"><i class="fas fa-graduation-cap"></i> XX学校成绩管理系统</a>
</div>
<div class="navbar-menu">
<a href="teacher_dashboard.html" class="navbar-item"><i class="fas fa-tachometer-alt"></i> 教师仪表板</a>
<a href="grade_entry.html" class="navbar-item active"><i class="fas fa-edit"></i> 成绩录入</a>
<a href="grade_management.html" class="navbar-item"><i class="fas fa-list"></i> 成绩管理</a>
<div class="user-info">
<span class="user-name">李老师</span>
<span class="user-role">教师</span>
<a href="login.html" class="btn btn-outline btn-small"><i class="fas fa-sign-out-alt"></i> 退出</a>
</div>
</div>
</nav>
<!-- 面包屑导航 -->
<div class="container">
<div class="breadcrumb">
<a href="index.html">主页</a>
<i class="fas fa-chevron-right"></i>
<a href="teacher_dashboard.html">教师仪表板</a>
<i class="fas fa-chevron-right"></i>
<span>成绩录入</span>
</div>
</div>
<!-- 主内容区 -->
<div class="grade-entry-container">
<div class="entry-header">
<h1><i class="fas fa-edit"></i> 成绩录入</h1>
<p>请选择班级和课程,然后为每个学生录入成绩</p>
</div>
<div class="entry-form">
<div class="form-row">
<div class="form-group">
<label for="class-select"><i class="fas fa-users"></i> 选择班级</label>
<select id="class-select" class="form-control">
<option value="">请选择班级</option>
<option value="class1">计算机科学与技术2021级1班</option>
<option value="class2">计算机科学与技术2021级2班</option>
<option value="class3">软件工程2021级1班</option>
<option value="class4">软件工程2021级2班</option>
<option value="class5">网络工程2021级1班</option>
</select>
</div>
<div class="form-group">
<label for="course-name"><i class="fas fa-book"></i> 课程名称</label>
<input type="text" id="course-name" placeholder="请输入课程名称,如:数据结构、操作系统等">
</div>
</div>
<div class="form-group">
<label for="exam-date"><i class="fas fa-calendar-alt"></i> 考试日期</label>
<input type="date" id="exam-date">
</div>
<div class="form-group">
<label><i class="fas fa-info-circle"></i> 备注说明(可选)</label>
<textarea id="remarks" rows="3" placeholder="可输入本次考试的说明信息,如:期中考试、期末考试等"></textarea>
</div>
<div class="students-table-container">
<h3><i class="fas fa-user-graduate"></i> 学生成绩录入</h3>
<p class="text-muted">选择班级后,系统将自动加载该班级的学生列表</p>
<div id="students-loading" class="loading" style="display: none;">
<i class="fas fa-spinner"></i>
<p>正在加载学生列表...</p>
</div>
<div id="no-students" class="no-students" style="display: none;">
<i class="fas fa-user-slash"></i>
<p>该班级暂无学生数据,请先添加学生信息</p>
</div>
<table id="students-table" class="students-table" style="display: none;">
<thead>
<tr>
<th>学号</th>
<th>姓名</th>
<th>平时成绩</th>
<th>期中成绩</th>
<th>期末成绩</th>
<th>总评成绩</th>
</tr>
</thead>
<tbody id="students-tbody">
<!-- 学生数据将通过JavaScript动态加载 -->
</tbody>
</table>
</div>
<div class="form-actions">
<button id="cancel-btn" class="btn btn-secondary btn-large">
<i class="fas fa-times"></i> 取消
</button>
<button id="submit-btn" class="btn btn-primary btn-large" disabled>
<i class="fas fa-check"></i> 提交成绩
</button>
</div>
</div>
<div class="card">
<h3><i class="fas fa-lightbulb"></i> 使用说明</h3>
<ul class="text-muted">
<li>首先选择要录入成绩的班级和课程</li>
<li>系统会自动加载该班级的学生列表</li>
<li>为每个学生输入平时成绩、期中成绩和期末成绩</li>
<li>总评成绩将根据权重自动计算默认权重平时20%期中30%期末50%</li>
<li>确认无误后点击“提交成绩”按钮保存数据</li>
<li>提交后可以在“成绩管理”页面查看和修改已录入的成绩</li>
</ul>
</div>
</div>
<!-- JavaScript -->
<script>
// 模拟学生数据
const mockStudents = [
{ id: '2021001', name: '张三', usual: '', midterm: '', final: '', total: '' },
{ id: '2021002', name: '李四', usual: '', midterm: '', final: '', total: '' },
{ id: '2021003', name: '王五', usual: '', midterm: '', final: '', total: '' },
{ id: '2021004', name: '赵六', usual: '', midterm: '', final: '', total: '' },
{ id: '2021005', name: '钱七', usual: '', midterm: '', final: '', total: '' },
{ id: '2021006', name: '孙八', usual: '', midterm: '', final: '', total: '' },
{ id: '2021007', name: '周九', usual: '', midterm: '', final: '', total: '' },
{ id: '2021008', name: '吴十', usual: '', midterm: '', final: '', total: '' }
];
// DOM元素
const classSelect = document.getElementById('class-select');
const courseNameInput = document.getElementById('course-name');
const examDateInput = document.getElementById('exam-date');
const remarksInput = document.getElementById('remarks');
const studentsLoading = document.getElementById('students-loading');
const noStudents = document.getElementById('no-students');
const studentsTable = document.getElementById('students-table');
const studentsTbody = document.getElementById('students-tbody');
const cancelBtn = document.getElementById('cancel-btn');
const submitBtn = document.getElementById('submit-btn');
// 设置默认考试日期为今天
const today = new Date().toISOString().split('T')[0];
examDateInput.value = today;
// 班级选择变化事件
classSelect.addEventListener('change', function() {
const selectedClass = this.value;
if (!selectedClass) {
hideStudentsTable();
submitBtn.disabled = true;
return;
}
// 显示加载状态
showLoading();
// 模拟API调用延迟
setTimeout(() => {
loadStudentsForClass(selectedClass);
}, 1000);
});
// 加载学生数据
function loadStudentsForClass(className) {
// 模拟API响应
if (mockStudents.length > 0) {
renderStudentsTable(mockStudents);
} else {
showNoStudents();
}
}
// 渲染学生表格
function renderStudentsTable(students) {
hideLoading();
// 清空表格
studentsTbody.innerHTML = '';
// 添加学生行
students.forEach((student, index) => {
const row = document.createElement('tr');
row.innerHTML = `
<td>${student.id}</td>
<td>${student.name}</td>
<td>
<input type="number" class="grade-input usual-grade"
data-index="${index}" min="0" max="100"
placeholder="0-100" value="${student.usual}">
</td>
<td>
<input type="number" class="grade-input midterm-grade"
data-index="${index}" min="0" max="100"
placeholder="0-100" value="${student.midterm}">
</td>
<td>
<input type="number" class="grade-input final-grade"
data-index="${index}" min="0" max="100"
placeholder="0-100" value="${student.final}">
</td>
<td>
<span class="total-grade" data-index="${index}">${student.total || ''}</span>
</td>
`;
studentsTbody.appendChild(row);
});
// 显示表格
studentsTable.style.display = 'table';
noStudents.style.display = 'none';
// 启用提交按钮
submitBtn.disabled = false;
// 添加成绩输入事件监听
addGradeInputListeners();
}
// 显示加载状态
function showLoading() {
studentsLoading.style.display = 'block';
studentsTable.style.display = 'none';
noStudents.style.display = 'none';
}
// 隐藏加载状态
function hideLoading() {
studentsLoading.style.display = 'none';
}
// 显示无学生数据
function showNoStudents() {
hideLoading();
studentsTable.style.display = 'none';
noStudents.style.display = 'block';
}
// 隐藏学生表格
function hideStudentsTable() {
studentsTable.style.display = 'none';
noStudents.style.display = 'none';
}
// 添加成绩输入事件监听
function addGradeInputListeners() {
const usualGrades = document.querySelectorAll('.usual-grade');
const midtermGrades = document.querySelectorAll('.midterm-grade');
const finalGrades = document.querySelectorAll('.final-grade');
usualGrades.forEach(input => {
input.addEventListener('input', calculateTotalGrade);
});
midtermGrades.forEach(input => {
input.addEventListener('input', calculateTotalGrade);
});
finalGrades.forEach(input => {
input.addEventListener('input', calculateTotalGrade);
});
}
// 计算总评成绩
function calculateTotalGrade(event) {
const input = event.target;
const index = input.getAttribute('data-index');
// 获取三个成绩
const usualInput = document.querySelector(`.usual-grade[data-index="${index}"]`);
const midtermInput = document.querySelector(`.midterm-grade[data-index="${index}"]`);
const finalInput = document.querySelector(`.final-grade[data-index="${index}"]`);
const totalSpan = document.querySelector(`.total-grade[data-index="${index}"]`);
const usual = parseFloat(usualInput.value) || 0;
const midterm = parseFloat(midtermInput.value) || 0;
const final = parseFloat(finalInput.value) || 0;
// 计算总评成绩权重平时20%期中30%期末50%
const total = (usual * 0.2) + (midterm * 0.3) + (final * 0.5);
// 显示总评成绩(四舍五入到整数)
totalSpan.textContent = Math.round(total);
// 根据成绩范围设置颜色
if (total >= 90) {
totalSpan.style.color = '#38a169';
totalSpan.style.fontWeight = 'bold';
} else if (total >= 60) {
totalSpan.style.color = '#d69e2e';
totalSpan.style.fontWeight = 'normal';
} else {
totalSpan.style.color = '#e53e3e';
totalSpan.style.fontWeight = 'bold';
}
}
// 表单验证
function validateForm() {
const className = classSelect.value;
const courseName = courseNameInput.value.trim();
if (!className) {
alert('请选择班级');
return false;
}
if (!courseName) {
alert('请输入课程名称');
courseNameInput.focus();
return false;
}
// 检查是否有学生成绩未填写
const usualGrades = document.querySelectorAll('.usual-grade');
const midtermGrades = document.querySelectorAll('.midterm-grade');
const finalGrades = document.querySelectorAll('.final-grade');
for (let i = 0; i < usualGrades.length; i++) {
const usual = usualGrades[i].value.trim();
const midterm = midtermGrades[i].value.trim();
const final = finalGrades[i].value.trim();
if (!usual || !midterm || !final) {
alert(`请填写第${i + 1}行学生的所有成绩`);
return false;
}
const usualNum = parseFloat(usual);
const midtermNum = parseFloat(midterm);
const finalNum = parseFloat(final);
if (usualNum < 0 || usualNum > 100 ||
midtermNum < 0 || midtermNum > 100 ||
finalNum < 0 || finalNum > 100) {
alert(`${i + 1}行学生的成绩必须在0-100之间`);
return false;
}
}
return true;
}
// 收集表单数据
function collectFormData() {
const className = classSelect.options[classSelect.selectedIndex].text;
const courseName = courseNameInput.value.trim();
const examDate = examDateInput.value;
const remarks = remarksInput.value.trim();
const grades = [];
const rows = studentsTbody.querySelectorAll('tr');
rows.forEach((row, index) => {
const studentId = row.cells[0].textContent;
const studentName = row.cells[1].textContent;
const usual = row.cells[2].querySelector('input').value;
const midterm = row.cells[3].querySelector('input').value;
const final = row.cells[4].querySelector('input').value;
const total = row.cells[5].querySelector('span').textContent;
grades.push({
studentId,
studentName,
usual: parseFloat(usual),
midterm: parseFloat(midterm),
final: parseFloat(final),
total: parseFloat(total)
});
});
return {
className,
courseName,
examDate,
remarks,
grades
};
}
// 提交表单
function submitForm() {
if (!validateForm()) {
return;
}
const formData = collectFormData();
// 显示提交中状态
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 提交中...';
submitBtn.disabled = true;
// 模拟API调用
setTimeout(() => {
// 模拟成功响应
alert(`成绩提交成功!\n班级:${formData.className}\n课程:${formData.courseName}\n共录入${formData.grades.length}名学生成绩`);
// 重置按钮状态
submitBtn.innerHTML = '<i class="fas fa-check"></i> 提交成绩';
submitBtn.disabled = false;
// 重置表单
resetForm();
}, 1500);
}
// 重置表单
function resetForm() {
classSelect.value = '';
courseNameInput.value = '';
examDateInput.value = today;
remarksInput.value = '';
// 清空学生表格
studentsTbody.innerHTML = '';
studentsTable.style.display = 'none';
noStudents.style.display = 'none';
// 禁用提交按钮
submitBtn.disabled = true;
}
// 取消按钮事件
cancelBtn.addEventListener('click', function() {
if (confirm('确定要取消吗?所有未保存的更改将会丢失。')) {
resetForm();
}
});
// 提交按钮事件
submitBtn.addEventListener('click', submitForm);
// 页面加载时初始化
document.addEventListener('DOMContentLoaded', function() {
// 检查是否有必要的数据
if (!classSelect.value) {
submitBtn.disabled = true;
}
});
</script>
</body>
</html>

View File

@@ -0,0 +1,584 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>成绩查询/管理 - 学生成绩管理系统</title>
<link rel="stylesheet" href="../css/style.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
</head>
<body>
<!-- 导航栏 -->
<nav class="navbar">
<div class="nav-container">
<div class="nav-brand">
<a href="index.html">
<i class="fas fa-graduation-cap"></i>
<span>XX学校成绩管理系统</span>
</a>
</div>
<div class="nav-menu">
<a href="teacher_dashboard.html" class="nav-link">
<i class="fas fa-tachometer-alt"></i>
<span>教师仪表板</span>
</a>
<a href="grade_entry.html" class="nav-link">
<i class="fas fa-edit"></i>
<span>成绩录入</span>
</a>
<a href="grade_management.html" class="nav-link active">
<i class="fas fa-search"></i>
<span>成绩管理</span>
</a>
</div>
<div class="nav-user">
<div class="user-info">
<i class="fas fa-user-circle"></i>
<span>张老师</span>
</div>
<a href="login.html" class="btn-logout">
<i class="fas fa-sign-out-alt"></i>
<span>退出</span>
</a>
</div>
</div>
</nav>
<!-- 主内容区 -->
<main class="main-content">
<div class="container">
<!-- 面包屑导航 -->
<div class="breadcrumb">
<a href="index.html">主页</a>
<i class="fas fa-chevron-right"></i>
<a href="teacher_dashboard.html">教师仪表板</a>
<i class="fas fa-chevron-right"></i>
<span>成绩查询/管理</span>
</div>
<!-- 页面标题 -->
<div class="page-header">
<h1 class="page-title">成绩查询/管理</h1>
<p class="page-description">查询、修改和删除学生成绩记录</p>
</div>
<!-- 筛选区域 -->
<div class="filter-section">
<h2 class="filter-title">
<i class="fas fa-filter"></i>
筛选条件
</h2>
<div class="filter-form">
<div class="form-group">
<label for="class-select">班级</label>
<select id="class-select" class="form-control">
<option value="">全部班级</option>
<option value="class1">计算机科学与技术1班</option>
<option value="class2">计算机科学与技术2班</option>
<option value="class3">软件工程1班</option>
<option value="class4">软件工程2班</option>
<option value="class5">网络工程1班</option>
</select>
</div>
<div class="form-group">
<label for="course-select">课程</label>
<select id="course-select" class="form-control">
<option value="">全部课程</option>
<option value="course1">数据结构</option>
<option value="course2">操作系统</option>
<option value="course3">计算机网络</option>
<option value="course4">数据库系统</option>
<option value="course5">软件工程</option>
</select>
</div>
<div class="form-group">
<label for="student-id">学生学号</label>
<input type="text" id="student-id" class="form-control" placeholder="输入学生学号">
</div>
<div class="form-group">
<label for="student-name">学生姓名</label>
<input type="text" id="student-name" class="form-control" placeholder="输入学生姓名">
</div>
<div class="filter-actions">
<button id="search-btn" class="btn-search">
<i class="fas fa-search"></i>
查询
</button>
<button id="reset-btn" class="btn-reset">
<i class="fas fa-redo"></i>
重置
</button>
</div>
</div>
</div>
<!-- 结果区域 -->
<div class="results-section">
<div class="results-header">
<h2 class="results-title">查询结果</h2>
<div class="results-actions">
<button id="export-btn" class="btn-export">
<i class="fas fa-file-export"></i>
导出成绩
</button>
<button id="batch-delete-btn" class="btn-batch-delete">
<i class="fas fa-trash-alt"></i>
批量删除
</button>
</div>
</div>
<div class="table-responsive">
<table class="grade-table">
<thead>
<tr>
<th><input type="checkbox" id="select-all"></th>
<th>学号</th>
<th>姓名</th>
<th>班级</th>
<th>课程</th>
<th>平时成绩</th>
<th>期中成绩</th>
<th>期末成绩</th>
<th>总评成绩</th>
<th>等级</th>
<th>操作</th>
</tr>
</thead>
<tbody id="grade-table-body">
<!-- 数据将通过JavaScript动态加载 -->
<tr>
<td colspan="11" class="loading">
<i class="fas fa-spinner fa-spin"></i>
<p>正在加载成绩数据...</p>
</td>
</tr>
</tbody>
</table>
</div>
<!-- 分页 -->
<div class="pagination">
<button id="prev-page" disabled>上一页</button>
<button class="active">1</button>
<button>2</button>
<button>3</button>
<button id="next-page">下一页</button>
</div>
</div>
</div>
</main>
<!-- 页脚 -->
<footer class="footer">
<div class="container">
<p>&copy; 2023 XX学校成绩管理系统. 版权所有.</p>
<p>技术支持: 计算机科学与技术学院</p>
</div>
</footer>
<script>
// 模拟成绩数据
const mockGrades = [
{
id: 1,
studentId: "20230001",
studentName: "张三",
className: "计算机科学与技术1班",
courseName: "数据结构",
regularScore: 85,
midtermScore: 88,
finalScore: 90,
totalScore: 88.5,
grade: "优秀"
},
{
id: 2,
studentId: "20230002",
studentName: "李四",
className: "计算机科学与技术1班",
courseName: "数据结构",
regularScore: 78,
midtermScore: 82,
finalScore: 85,
totalScore: 82.3,
grade: "良好"
},
{
id: 3,
studentId: "20230003",
studentName: "王五",
className: "计算机科学与技术2班",
courseName: "操作系统",
regularScore: 92,
midtermScore: 88,
finalScore: 95,
totalScore: 91.8,
grade: "优秀"
},
{
id: 4,
studentId: "20230004",
studentName: "赵六",
className: "计算机科学与技术2班",
courseName: "操作系统",
regularScore: 65,
midtermScore: 70,
finalScore: 68,
totalScore: 67.9,
grade: "及格"
},
{
id: 5,
studentId: "20230005",
studentName: "钱七",
className: "软件工程1班",
courseName: "数据库系统",
regularScore: 88,
midtermScore: 85,
finalScore: 90,
totalScore: 87.9,
grade: "良好"
},
{
id: 6,
studentId: "20230006",
studentName: "孙八",
className: "软件工程1班",
courseName: "数据库系统",
regularScore: 75,
midtermScore: 78,
finalScore: 80,
totalScore: 78.1,
grade: "中等"
},
{
id: 7,
studentId: "20230007",
studentName: "周九",
className: "软件工程2班",
courseName: "软件工程",
regularScore: 90,
midtermScore: 92,
finalScore: 88,
totalScore: 89.6,
grade: "优秀"
},
{
id: 8,
studentId: "20230008",
studentName: "吴十",
className: "软件工程2班",
courseName: "软件工程",
regularScore: 82,
midtermScore: 85,
finalScore: 87,
totalScore: 85.1,
grade: "良好"
}
];
// DOM元素
const searchBtn = document.getElementById('search-btn');
const resetBtn = document.getElementById('reset-btn');
const exportBtn = document.getElementById('export-btn');
const batchDeleteBtn = document.getElementById('batch-delete-btn');
const selectAllCheckbox = document.getElementById('select-all');
const gradeTableBody = document.getElementById('grade-table-body');
const classSelect = document.getElementById('class-select');
const courseSelect = document.getElementById('course-select');
const studentIdInput = document.getElementById('student-id');
const studentNameInput = document.getElementById('student-name');
// 当前选中的成绩ID
let selectedGradeIds = new Set();
// 初始化页面
function initPage() {
renderGradeTable(mockGrades);
setupEventListeners();
updateSelectedCount();
}
// 渲染成绩表格
function renderGradeTable(grades) {
if (grades.length === 0) {
gradeTableBody.innerHTML = `
<tr>
<td colspan="11" class="no-results">
<i class="fas fa-search"></i>
<h3>未找到相关成绩记录</h3>
<p>请尝试调整筛选条件或添加新的成绩记录</p>
</td>
</tr>
`;
return;
}
let tableHTML = '';
grades.forEach(grade => {
// 根据成绩确定CSS类
let gradeClass = 'grade-cell';
if (grade.totalScore >= 90) {
gradeClass += ' grade-excellent';
} else if (grade.totalScore >= 80) {
gradeClass += ' grade-good';
} else if (grade.totalScore < 60) {
gradeClass += ' grade-poor';
}
// 根据总评成绩确定等级
let gradeLevel = grade.grade;
tableHTML += `
<tr data-grade-id="${grade.id}">
<td>
<input type="checkbox" class="grade-checkbox" data-id="${grade.id}">
</td>
<td>${grade.studentId}</td>
<td>${grade.studentName}</td>
<td>${grade.className}</td>
<td>${grade.courseName}</td>
<td>${grade.regularScore}</td>
<td>${grade.midtermScore}</td>
<td>${grade.finalScore}</td>
<td class="${gradeClass}">${grade.totalScore.toFixed(1)}</td>
<td>${gradeLevel}</td>
<td>
<div class="action-buttons">
<button class="btn-edit" onclick="editGrade(${grade.id})">
<i class="fas fa-edit"></i> 修改
</button>
<button class="btn-delete" onclick="deleteGrade(${grade.id})">
<i class="fas fa-trash"></i> 删除
</button>
</div>
</td>
</tr>
`;
});
gradeTableBody.innerHTML = tableHTML;
// 重新绑定复选框事件
document.querySelectorAll('.grade-checkbox').forEach(checkbox => {
checkbox.addEventListener('change', function() {
const gradeId = parseInt(this.getAttribute('data-id'));
if (this.checked) {
selectedGradeIds.add(gradeId);
} else {
selectedGradeIds.delete(gradeId);
selectAllCheckbox.checked = false;
}
updateSelectedCount();
});
});
}
// 设置事件监听器
function setupEventListeners() {
// 查询按钮
searchBtn.addEventListener('click', function() {
performSearch();
});
// 重置按钮
resetBtn.addEventListener('click', function() {
classSelect.value = '';
courseSelect.value = '';
studentIdInput.value = '';
studentNameInput.value = '';
renderGradeTable(mockGrades);
selectedGradeIds.clear();
selectAllCheckbox.checked = false;
updateSelectedCount();
});
// 导出按钮
exportBtn.addEventListener('click', function() {
exportGrades();
});
// 批量删除按钮
batchDeleteBtn.addEventListener('click', function() {
if (selectedGradeIds.size === 0) {
alert('请先选择要删除的成绩记录!');
return;
}
if (confirm(`确定要删除选中的 ${selectedGradeIds.size} 条成绩记录吗?此操作不可撤销。`)) {
batchDeleteGrades();
}
});
// 全选复选框
selectAllCheckbox.addEventListener('change', function() {
const checkboxes = document.querySelectorAll('.grade-checkbox');
checkboxes.forEach(checkbox => {
checkbox.checked = this.checked;
const gradeId = parseInt(checkbox.getAttribute('data-id'));
if (this.checked) {
selectedGradeIds.add(gradeId);
} else {
selectedGradeIds.delete(gradeId);
}
});
updateSelectedCount();
});
// 输入框回车键搜索
[studentIdInput, studentNameInput].forEach(input => {
input.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
performSearch();
}
});
});
}
// 执行搜索
function performSearch() {
const selectedClass = classSelect.value;
const selectedCourse = courseSelect.value;
const studentId = studentIdInput.value.trim();
const studentName = studentNameInput.value.trim();
// 显示加载状态
gradeTableBody.innerHTML = `
<tr>
<td colspan="11" class="loading">
<i class="fas fa-spinner fa-spin"></i>
<p>正在搜索成绩数据...</p>
</td>
</tr>
`;
// 模拟API延迟
setTimeout(() => {
let filteredGrades = mockGrades.filter(grade => {
// 班级筛选
if (selectedClass && grade.className !== selectedClass) {
return false;
}
// 课程筛选
if (selectedCourse && grade.courseName !== selectedCourse) {
return false;
}
// 学号筛选
if (studentId && !grade.studentId.includes(studentId)) {
return false;
}
// 姓名筛选
if (studentName && !grade.studentName.includes(studentName)) {
return false;
}
return true;
});
renderGradeTable(filteredGrades);
selectedGradeIds.clear();
selectAllCheckbox.checked = false;
updateSelectedCount();
}, 500);
}
// 导出成绩
function exportGrades() {
// 这里应该调用后端API导出成绩
// 暂时使用模拟导出
alert('成绩导出功能正在开发中将支持Excel和CSV格式导出。');
// 模拟导出过程
exportBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 导出中...';
exportBtn.disabled = true;
setTimeout(() => {
exportBtn.innerHTML = '<i class="fas fa-file-export"></i> 导出成绩';
exportBtn.disabled = false;
// 在实际应用中,这里会触发文件下载
// 暂时显示成功消息
showNotification('成绩导出成功!文件已开始下载。', 'success');
}, 1500);
}
// 批量删除成绩
function batchDeleteGrades() {
// 这里应该调用后端API删除成绩
// 暂时使用模拟删除
batchDeleteBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 删除中...';
batchDeleteBtn.disabled = true;
setTimeout(() => {
// 从模拟数据中删除选中的成绩
selectedGradeIds.forEach(id => {
const index = mockGrades.findIndex(grade => grade.id === id);
if (index !== -1) {
mockGrades.splice(index, 1);
}
});
// 重新渲染表格
renderGradeTable(mockGrades);
selectedGradeIds.clear();
selectAllCheckbox.checked = false;
batchDeleteBtn.innerHTML = '<i class="fas fa-trash-alt"></i> 批量删除';
batchDeleteBtn.disabled = false;
showNotification(`成功删除 ${selectedGradeIds.size} 条成绩记录!`, 'success');
updateSelectedCount();
}, 1000);
}
// 编辑成绩
function editGrade(gradeId) {
// 这里应该跳转到编辑页面或打开编辑模态框
// 暂时显示提示
alert(`编辑成绩 ID: ${gradeId}\n在实际应用中,这里会打开编辑表单。`);
}
// 删除单个成绩
function deleteGrade(gradeId) {
if (confirm('确定要删除这条成绩记录吗?此操作不可撤销。')) {
// 这里应该调用后端API删除成绩
// 暂时使用模拟删除
const index = mockGrades.findIndex(grade => grade.id === gradeId);
if (index !== -1) {
mockGrades.splice(index, 1);
renderGradeTable(mockGrades);
selectedGradeIds.delete(gradeId);
updateSelectedCount();
showNotification('成绩记录已删除!', 'success');
}
}
}
// 更新选中计数
function updateSelectedCount() {
const count = selectedGradeIds.size;
if (count > 0) {
batchDeleteBtn.innerHTML = `<i class="fas fa-trash-alt"></i> 批量删除 (${count})`;
} else {
batchDeleteBtn.innerHTML = '<i class="fas fa-trash-alt"></i> 批量删除';
}
}
// 显示通知
function showNotification(message, type = 'info') {
// 在实际应用中,这里会显示一个美观的通知组件
// 暂时使用alert
alert(message);
}
// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', initPage);
</script>
</body>
</html>

142
frontend/html/index.html Normal file
View File

@@ -0,0 +1,142 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>学生成绩管理系统</title>
<link rel="stylesheet" href="../css/style.css">
<link rel="stylesheet" href="../css/main.css">
</head>
<body>
<!-- 导航栏 -->
<nav class="navbar">
<div class="navbar-brand">
<i class="fas fa-graduation-cap"></i>
<span>XX学校成绩管理系统</span>
</div>
<div class="navbar-menu">
<a href="index.html" class="btn btn-secondary">
<i class="fas fa-home"></i> 主页
</a>
<div class="navbar-user">
<a href="login.html" class="btn btn-primary" id="loginBtn">
<i class="fas fa-sign-in-alt"></i> 登录
</a>
<a href="register.html" class="btn btn-secondary" id="registerBtn">
<i class="fas fa-user-plus"></i> 注册
</a>
</div>
</div>
</nav>
<!-- 主体内容区 -->
<main>
<!-- 英雄区域 -->
<section class="hero-section">
<div class="hero-content">
<h1 class="hero-title">XX学校学生成绩管理系统</h1>
<p class="hero-subtitle">
高效、安全、智能的成绩管理平台,为学校师生提供全方位的成绩管理服务,
实现成绩录入、查询、统计和分析的一体化解决方案。
</p>
<div class="cta-buttons">
<a href="login.html" class="btn btn-primary" id="heroLoginBtn">
<i class="fas fa-sign-in-alt"></i> 立即登录
</a>
<a href="#features" class="btn btn-secondary">
<i class="fas fa-info-circle"></i> 了解功能
</a>
</div>
</div>
</section>
<!-- 功能特色 -->
<section id="features" class="features-section">
<h2 class="section-title">系统功能特色</h2>
<div class="features-grid">
<div class="feature-card">
<div class="feature-icon">
<i class="fas fa-user-graduate"></i>
</div>
<h3 class="feature-title">学生成绩查询</h3>
<p class="feature-description">
学生可随时查看个人成绩,包括各科成绩、平均分、排名等信息,
支持成绩趋势分析和历史记录查看。
</p>
</div>
<div class="feature-card">
<div class="feature-icon">
<i class="fas fa-chalkboard-teacher"></i>
</div>
<h3 class="feature-title">教师成绩管理</h3>
<p class="feature-description">
教师可便捷录入、修改、查询学生成绩,支持批量操作和成绩统计分析,
提供多种数据导出格式。
</p>
</div>
<div class="feature-card">
<div class="feature-icon">
<i class="fas fa-user-shield"></i>
</div>
<h3 class="feature-title">管理员权限控制</h3>
<p class="feature-description">
管理员可管理用户账户、学生信息,查看系统统计报表,
设置权限和系统参数,确保数据安全。
</p>
</div>
<div class="feature-card">
<div class="feature-icon">
<i class="fas fa-chart-line"></i>
</div>
<h3 class="feature-title">智能统计分析</h3>
<p class="feature-description">
提供丰富的图表统计功能,包括成绩分布、趋势分析、对比图表等,
帮助学校进行教学评估和决策支持。
</p>
</div>
</div>
</section>
<!-- 行动号召 -->
<section class="cta-section">
<h2 class="cta-title">立即体验智能成绩管理</h2>
<p style="font-size: 1.2rem; color: #666; margin-bottom: 40px; max-width: 600px; margin-left: auto; margin-right: auto;">
加入XX学校成绩管理系统体验高效、便捷的成绩管理服务。
</p>
<div class="cta-buttons">
<a href="login.html" class="btn btn-primary">
<i class="fas fa-sign-in-alt"></i> 开始使用
</a>
<a href="#" class="btn btn-secondary">
<i class="fas fa-book"></i> 查看文档
</a>
</div>
</section>
</main>
<!-- 页脚 -->
<footer class="footer">
<div class="footer-content">
<h3 style="margin-bottom: 20px;">XX学校学生成绩管理系统</h3>
<p style="color: #ccc; margin-bottom: 20px; max-width: 600px; margin-left: auto; margin-right: auto;">
致力于为学校提供专业、安全、高效的数字化成绩管理解决方案。
</p>
<div class="footer-links">
<a href="#"><i class="fas fa-info-circle"></i> 关于我们</a>
<a href="#"><i class="fas fa-envelope"></i> 联系我们</a>
<a href="#"><i class="fas fa-shield-alt"></i> 隐私政策</a>
<a href="#"><i class="fas fa-file-contract"></i> 服务条款</a>
</div>
<div class="copyright">
<p>© 2023 XX学校学生成绩管理系统 版权所有</p>
<p>联系电话010-12345678 | 邮箱contact@school.edu.cn</p>
</div>
</div>
</footer>
<script src="../js/main.js"></script>
</body>
</html>

61
frontend/html/login.html Normal file
View File

@@ -0,0 +1,61 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>学生成绩管理系统 - 登录</title>
<link rel="stylesheet" href="../css/style.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
</head>
<body>
<div class="auth-container">
<div class="auth-header">
<h1><i class="fas fa-graduation-cap"></i> 学生成绩管理系统</h1>
<p>请登录您的账户</p>
</div>
<div class="auth-card">
<form id="loginForm">
<div class="form-group">
<label for="id">
<i class="fas fa-user"></i> 学号/工号
</label>
<input type="text" id="id" name="id" required
placeholder="请输入学号/工号">
</div>
<div class="form-group">
<label for="password">
<i class="fas fa-lock"></i> 密码
</label>
<input type="password" id="password" name="password" required
placeholder="请输入密码">
</div>
<div class="form-group">
<label for="role">
<i class="fas fa-user-tag"></i> 角色
</label>
<select id="role" name="role" required>
<option value="">请选择角色</option>
<option value="student">学生</option>
<option value="teacher">教师</option>
<option value="admin">管理员</option>
</select>
</div>
<div class="form-group">
<button type="submit" class="btn btn-primary">
<i class="fas fa-sign-in-alt"></i> 登录
</button>
</div>
<div class="auth-footer">
<p>没有账户? <a href="register.html">立即注册</a></p>
</div>
</form>
</div>
<script src="../js/auth.js"></script>
</body>
</html>

106
frontend/html/register.html Normal file
View File

@@ -0,0 +1,106 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>学生成绩管理系统 - 注册</title>
<link rel="stylesheet" href="../css/style.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
</head>
<body>
<div class="auth-container">
<div class="auth-header">
<h1><i class="fas fa-graduation-cap"></i> 学生成绩管理系统</h1>
<p>创建新账户</p>
</div>
<div class="auth-card">
<form id="registerForm">
<div class="form-group">
<label for="id">
<i class="fas fa-id-card"></i> 学号/工号
</label>
<input type="text" id="id" name="id" required
placeholder="请输入学号或工号">
</div>
<div class="form-group">
<label for="name">
<i class="fas fa-user"></i> 姓名
</label>
<input type="text" id="name" name="name" required
placeholder="请输入姓名">
</div>
<div class="form-group">
<label for="password">
<i class="fas fa-lock"></i> 密码
</label>
<input type="password" id="password" name="password" required
placeholder="请输入密码">
</div>
<div class="form-group">
<label for="confirmPassword">
<i class="fas fa-lock"></i> 确认密码
</label>
<input type="password" id="confirmPassword" name="confirmPassword" required
placeholder="请再次输入密码">
</div>
<div class="form-group">
<label for="role">
<i class="fas fa-user-tag"></i> 角色
</label>
<select id="role" name="role" required>
<option value="">请选择角色</option>
<option value="student">学生</option>
<option value="teacher">教师</option>
<option value="admin">管理员</option>
</select>
</div>
<div class="form-group" id="classField" style="display: none;">
<label for="class">
<i class="fas fa-users"></i> 班级
</label>
<input type="text" id="class" name="class"
placeholder="请输入班级(学生/教师必填)">
</div>
<div class="form-group">
<button type="submit" class="btn btn-primary">
<i class="fas fa-user-plus"></i> 注册
</button>
</div>
<div class="auth-footer">
<p>已有账户? <a href="login.html">立即登录</a></p>
</div>
</form>
</div>
<script src="../js/auth.js"></script>
<script>
// 显示/隐藏班级字段
document.getElementById('role').addEventListener('change', function() {
const role = this.value;
const classField = document.getElementById('classField');
const classInput = document.getElementById('class');
if (role === 'student' || role === 'teacher') {
classField.style.display = 'block';
classInput.required = true;
} else {
classField.style.display = 'none';
classInput.required = false;
}
});
// 初始化AuthManager
document.addEventListener('DOMContentLoaded', () => {
new AuthManager();
});
</script>
</body>
</html>

View File

@@ -0,0 +1,195 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>学生成绩管理系统 - 学生仪表板</title>
<link rel="stylesheet" href="../css/style.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
</head>
</head>
<body>
<!-- 顶部导航栏 -->
<nav class="navbar">
<div class="navbar-brand">
<i class="fas fa-graduation-cap"></i>
<span>XX学校成绩管理系统</span>
</div>
<div class="navbar-menu">
<a href="index.html" class="btn btn-secondary">
<i class="fas fa-home"></i> 主页
</a>
<div class="navbar-user">
<span class="user-name" id="userName">加载中...</span>
<button id="logoutBtn" class="btn btn-primary">
<i class="fas fa-sign-out-alt"></i> 退出
</button>
</div>
</div>
</nav>
<!-- 仪表板容器 -->
<div class="dashboard-container">
<!-- 左侧侧边栏 -->
<aside class="sidebar">
<div class="sidebar-header">
<div class="user-info">
<div class="user-avatar">
<i class="fas fa-user-graduate"></i>
</div>
<div class="user-details">
<h3 id="studentName">加载中...</h3>
<p>学生 | 班级:<span id="studentClass">加载中...</span></p>
</div>
</div>
</div>
<ul class="sidebar-menu">
<li>
<a href="#" class="active">
<i class="fas fa-tachometer-alt"></i>
<span>仪表板</span>
</a>
</li>
<li>
<a href="#">
<i class="fas fa-chart-bar"></i>
<span>成绩查询</span>
</a>
</li>
<li>
<a href="#">
<i class="fas fa-book"></i>
<span>课程详情</span>
</a>
</li>
<li>
<a href="#">
<i class="fas fa-user"></i>
<span>个人信息</span>
</a>
</li>
<li>
<a href="#">
<i class="fas fa-chart-line"></i>
<span>成绩分析</span>
</a>
</li>
<li>
<a href="#">
<i class="fas fa-download"></i>
<span>成绩导出</span>
</a>
</li>
</ul>
</aside>
<!-- 主内容区 -->
<main class="main-content">
<div class="content-header">
<div>
<h1 class="page-title">学生仪表板</h1>
<div class="breadcrumb">
<a href="index.html">主页</a>
<i class="fas fa-chevron-right"></i>
<span>学生仪表板</span>
</div>
</div>
<div class="current-time" id="currentTime"></div>
</div>
<!-- 统计卡片 -->
<div class="stats-grid">
<div class="stat-card">
<div class="stat-icon gpa">
<i class="fas fa-star"></i>
</div>
<div class="stat-value" id="gpaValue">3.75</div>
<div class="stat-label">平均绩点</div>
</div>
<div class="stat-card">
<div class="stat-icon courses">
<i class="fas fa-book"></i>
</div>
<div class="stat-value" id="courseCount">8</div>
<div class="stat-label">已修课程</div>
</div>
<div class="stat-card">
<div class="stat-icon credits">
<i class="fas fa-certificate"></i>
</div>
<div class="stat-value" id="creditTotal">24</div>
<div class="stat-label">总学分</div>
</div>
<div class="stat-card">
<div class="stat-icon ranking">
<i class="fas fa-trophy"></i>
</div>
<div class="stat-value" id="classRank">5</div>
<div class="stat-label">班级排名</div>
</div>
</div>
<!-- 成绩表格 -->
<div class="grades-table">
<div class="table-header">
<h2 class="table-title">本学期成绩</h2>
<div class="table-actions">
<select class="form-select" style="width: 150px;">
<option value="all">所有学期</option>
<option value="2023-2" selected>2023-2024学年第二学期</option>
<option value="2023-1">2023-2024学年第一学期</option>
</select>
<button class="btn btn-primary">
<i class="fas fa-download"></i> 导出成绩单
</button>
</div>
</div>
<div class="table-container">
<table>
<thead>
<tr>
<th>课程名称</th>
<th>课程代码</th>
<th>学分</th>
<th>平时成绩</th>
<th>期末成绩</th>
<th>总成绩</th>
<th>绩点</th>
<th>操作</th>
</tr>
</thead>
<tbody id="gradesTableBody">
<!-- 成绩数据将通过JavaScript动态加载 -->
</tbody>
</table>
</div>
</div>
</main>
</div>
<script src="../js/student.js"></script>
<script src="../js/auth.js"></script>
<script>
// 更新当前时间
function updateCurrentTime() {
const now = new Date();
const options = {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
};
document.getElementById('currentTime').textContent = now.toLocaleString('zh-CN', options);
}
setInterval(updateCurrentTime, 1000);
updateCurrentTime();
</script>
</body>
</html>

View File

@@ -0,0 +1,451 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>学生管理 - XX学校成绩管理系统</title>
<link rel="stylesheet" href="../css/style.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
</head>
<body>
<!-- 导航栏 -->
<nav class="navbar">
<div class="container">
<div class="navbar-brand">
<a href="index.html">
<i class="fas fa-graduation-cap"></i>
<span>XX学校成绩管理系统</span>
</a>
</div>
<div class="navbar-menu">
<a href="index.html" class="navbar-item">
<i class="fas fa-home"></i>
<span>主页</span>
</a>
<a href="admin_dashboard.html" class="navbar-item">
<i class="fas fa-tachometer-alt"></i>
<span>控制面板</span>
</a>
<a href="student_management.html" class="navbar-item active">
<i class="fas fa-users"></i>
<span>学生管理</span>
</a>
<a href="grade_management.html" class="navbar-item">
<i class="fas fa-chart-bar"></i>
<span>成绩管理</span>
</a>
<a href="user_management.html" class="navbar-item">
<i class="fas fa-user-cog"></i>
<span>用户管理</span>
</a>
<div class="navbar-user">
<i class="fas fa-user-circle"></i>
<span>管理员</span>
<div class="user-dropdown">
<a href="#"><i class="fas fa-user"></i> 个人资料</a>
<a href="#"><i class="fas fa-cog"></i> 设置</a>
<a href="login.html"><i class="fas fa-sign-out-alt"></i> 退出登录</a>
</div>
</div>
</div>
</div>
</nav>
<!-- 主要内容区 -->
<main class="main-content">
<div class="container">
<div class="student-management">
<!-- 页面标题 -->
<div class="page-header">
<h1><i class="fas fa-users"></i> 学生管理</h1>
<p>管理学生基本信息,支持添加、编辑、删除和查询学生信息</p>
</div>
<!-- 筛选区域 -->
<div class="filter-section">
<div class="filter-row">
<div class="filter-group">
<label for="student-id"><i class="fas fa-id-card"></i> 学号</label>
<input type="text" id="student-id" placeholder="请输入学号">
</div>
<div class="filter-group">
<label for="student-name"><i class="fas fa-user"></i> 姓名</label>
<input type="text" id="student-name" placeholder="请输入姓名">
</div>
<div class="filter-group">
<label for="class-select"><i class="fas fa-school"></i> 班级</label>
<select id="class-select">
<option value="">全部班级</option>
<option value="计算机科学与技术1班">计算机科学与技术1班</option>
<option value="计算机科学与技术2班">计算机科学与技术2班</option>
<option value="软件工程1班">软件工程1班</option>
<option value="软件工程2班">软件工程2班</option>
<option value="网络工程1班">网络工程1班</option>
<option value="网络工程2班">网络工程2班</option>
</select>
</div>
<div class="filter-group">
<label for="gender-select"><i class="fas fa-venus-mars"></i> 性别</label>
<select id="gender-select">
<option value="">全部性别</option>
<option value="男"></option>
<option value="女"></option>
</select>
</div>
</div>
<div class="action-buttons">
<button id="add-btn" class="btn-add">
<i class="fas fa-plus"></i>
添加学生
</button>
<button id="search-btn" class="btn-search">
<i class="fas fa-search"></i>
查询学生
</button>
<button id="reset-btn" class="btn-reset">
<i class="fas fa-redo"></i>
重置条件
</button>
<button id="export-btn" class="btn-export">
<i class="fas fa-file-export"></i>
导出数据
</button>
</div>
</div>
<!-- 学生表格 -->
<div class="table-responsive">
<table class="student-table">
<thead>
<tr>
<th>学号</th>
<th>姓名</th>
<th>性别</th>
<th>班级</th>
<th>联系电话</th>
<th>邮箱</th>
<th>入学时间</th>
<th>操作</th>
</tr>
</thead>
<tbody id="student-table-body">
<!-- 数据将通过JavaScript动态加载 -->
<tr>
<td colspan="8" class="loading">
<i class="fas fa-spinner fa-spin"></i>
<p>正在加载学生数据...</p>
</td>
</tr>
</tbody>
</table>
</div>
<!-- 分页控件 -->
<div class="pagination" id="pagination">
<button id="prev-btn" disabled>
<i class="fas fa-chevron-left"></i>
上一页
</button>
<button class="active">1</button>
<button>2</button>
<button>3</button>
<button id="next-btn">
下一页
<i class="fas fa-chevron-right"></i>
</button>
</div>
</div>
</div>
</main>
<!-- 页脚 -->
<footer class="footer">
<div class="container">
<p>© 2023 XX学校成绩管理系统. 版权所有.</p>
<p>技术支持: 计算机科学与技术学院</p>
</div>
</footer>
<script>
// 模拟学生数据
const mockStudents = [
{
id: '20230001',
name: '张三',
gender: '男',
class: '计算机科学与技术1班',
phone: '13800138001',
email: 'zhangsan@example.com',
enrollmentDate: '2023-09-01'
},
{
id: '20230002',
name: '李四',
gender: '女',
class: '计算机科学与技术1班',
phone: '13800138002',
email: 'lisi@example.com',
enrollmentDate: '2023-09-01'
},
{
id: '20230003',
name: '王五',
gender: '男',
class: '计算机科学与技术2班',
phone: '13800138003',
email: 'wangwu@example.com',
enrollmentDate: '2023-09-01'
},
{
id: '20230004',
name: '赵六',
gender: '女',
class: '软件工程1班',
phone: '13800138004',
email: 'zhaoliu@example.com',
enrollmentDate: '2023-09-01'
},
{
id: '20230005',
name: '钱七',
gender: '男',
class: '软件工程2班',
phone: '13800138005',
email: 'qianqi@example.com',
enrollmentDate: '2023-09-01'
},
{
id: '20230006',
name: '孙八',
gender: '男',
class: '网络工程1班',
phone: '13800138006',
email: 'sunba@example.com',
enrollmentDate: '2023-09-01'
},
{
id: '20230007',
name: '周九',
gender: '女',
class: '网络工程2班',
phone: '13800138007',
email: 'zhoujiu@example.com',
enrollmentDate: '2023-09-01'
},
{
id: '20230008',
name: '吴十',
gender: '男',
class: '计算机科学与技术1班',
phone: '13800138008',
email: 'wushi@example.com',
enrollmentDate: '2023-09-01'
}
];
// 当前页数据
let currentPage = 1;
const pageSize = 5;
let filteredStudents = [...mockStudents];
// DOM元素
const studentTableBody = document.getElementById('student-table-body');
const pagination = document.getElementById('pagination');
const prevBtn = document.getElementById('prev-btn');
const nextBtn = document.getElementById('next-btn');
const searchBtn = document.getElementById('search-btn');
const resetBtn = document.getElementById('reset-btn');
const addBtn = document.getElementById('add-btn');
const exportBtn = document.getElementById('export-btn');
// 渲染学生表格
function renderStudentTable() {
const startIndex = (currentPage - 1) * pageSize;
const endIndex = startIndex + pageSize;
const pageStudents = filteredStudents.slice(startIndex, endIndex);
if (pageStudents.length === 0) {
studentTableBody.innerHTML = `
<tr>
<td colspan="8" class="no-results">
<i class="fas fa-user-slash"></i>
<h3>没有找到学生信息</h3>
<p>请尝试调整筛选条件或添加新学生</p>
</td>
</tr>
`;
return;
}
let tableHTML = '';
pageStudents.forEach(student => {
tableHTML += `
<tr>
<td>${student.id}</td>
<td>${student.name}</td>
<td>${student.gender}</td>
<td>${student.class}</td>
<td>${student.phone}</td>
<td>${student.email}</td>
<td>${student.enrollmentDate}</td>
<td>
<div class="action-buttons-cell">
<button class="btn-edit" onclick="editStudent('${student.id}')">
<i class="fas fa-edit"></i>
编辑
</button>
<button class="btn-delete" onclick="deleteStudent('${student.id}')">
<i class="fas fa-trash"></i>
删除
</button>
</div>
</td>
</tr>
`;
});
studentTableBody.innerHTML = tableHTML;
}
// 更新分页控件
function updatePagination() {
const totalPages = Math.ceil(filteredStudents.length / pageSize);
const paginationButtons = pagination.querySelectorAll('button:not(#prev-btn):not(#next-btn)');
// 更新页码按钮
paginationButtons.forEach((btn, index) => {
if (index < totalPages) {
btn.textContent = index + 1;
btn.style.display = 'inline-block';
btn.classList.toggle('active', index + 1 === currentPage);
} else {
btn.style.display = 'none';
}
});
// 更新上一页/下一页按钮状态
prevBtn.disabled = currentPage === 1;
nextBtn.disabled = currentPage === totalPages || totalPages === 0;
}
// 筛选学生
function filterStudents() {
const studentId = document.getElementById('student-id').value.trim();
const studentName = document.getElementById('student-name').value.trim();
const selectedClass = document.getElementById('class-select').value;
const selectedGender = document.getElementById('gender-select').value;
filteredStudents = mockStudents.filter(student => {
const matchesId = !studentId || student.id.includes(studentId);
const matchesName = !studentName || student.name.includes(studentName);
const matchesClass = !selectedClass || student.class === selectedClass;
const matchesGender = !selectedGender || student.gender === selectedGender;
return matchesId && matchesName && matchesClass && matchesGender;
});
currentPage = 1;
renderStudentTable();
updatePagination();
}
// 重置筛选条件
function resetFilters() {
document.getElementById('student-id').value = '';
document.getElementById('student-name').value = '';
document.getElementById('class-select').value = '';
document.getElementById('gender-select').value = '';
filteredStudents = [...mockStudents];
currentPage = 1;
renderStudentTable();
updatePagination();
}
// 添加学生
function addStudent() {
alert('添加学生功能将在后端API完成后实现');
// 这里可以打开一个模态框来添加学生信息
}
// 编辑学生
function editStudent(studentId) {
alert(`编辑学生 ${studentId} 功能将在后端API完成后实现`);
// 这里可以打开一个模态框来编辑学生信息
}
// 删除学生
function deleteStudent(studentId) {
if (confirm(`确定要删除学号为 ${studentId} 的学生吗?`)) {
alert(`删除学生 ${studentId} 功能将在后端API完成后实现`);
// 这里可以调用API删除学生
}
}
// 导出数据
function exportData() {
alert('导出数据功能将在后端API完成后实现');
// 这里可以调用API导出Excel或CSV文件
}
// 页面切换
function goToPage(page) {
currentPage = page;
renderStudentTable();
updatePagination();
}
// 初始化事件监听
function initEventListeners() {
searchBtn.addEventListener('click', filterStudents);
resetBtn.addEventListener('click', resetFilters);
addBtn.addEventListener('click', addStudent);
exportBtn.addEventListener('click', exportData);
prevBtn.addEventListener('click', () => {
if (currentPage > 1) {
goToPage(currentPage - 1);
}
});
nextBtn.addEventListener('click', () => {
const totalPages = Math.ceil(filteredStudents.length / pageSize);
if (currentPage < totalPages) {
goToPage(currentPage + 1);
}
});
// 页码按钮点击事件
pagination.addEventListener('click', (e) => {
if (e.target.tagName === 'BUTTON' &&
!e.target.id &&
!e.target.classList.contains('active')) {
const page = parseInt(e.target.textContent);
goToPage(page);
}
});
// 输入框回车搜索
document.getElementById('student-id').addEventListener('keypress', (e) => {
if (e.key === 'Enter') filterStudents();
});
document.getElementById('student-name').addEventListener('keypress', (e) => {
if (e.key === 'Enter') filterStudents();
});
}
// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', () => {
// 模拟API延迟加载
setTimeout(() => {
renderStudentTable();
updatePagination();
initEventListeners();
}, 500);
});
</script>
</body>
</html>

View File

@@ -0,0 +1,515 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>学生成绩管理系统 - 教师仪表板</title>
<link rel="stylesheet" href="../css/style.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
</head>
/* 仪表板布局 */
.dashboard-container {
display: flex;
min-height: calc(100vh - 80px);
}
/* 侧边栏 */
.sidebar {
width: 250px;
background: linear-gradient(180deg, #43e97b 0%, #38f9d7 100%);
color: white;
padding: 30px 0;
position: sticky;
top: 80px;
height: calc(100vh - 80px);
overflow-y: auto;
}
.sidebar-header {
padding: 0 25px 30px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
margin-bottom: 20px;
}
.user-info {
display: flex;
align-items: center;
gap: 15px;
}
.user-avatar {
width: 50px;
height: 50px;
border-radius: 50%;
background: white;
display: flex;
align-items: center;
justify-content: center;
color: #43e97b;
font-size: 20px;
}
.user-details h3 {
margin: 0 0 5px;
font-size: 1.1rem;
}
.user-details p {
margin: 0;
font-size: 0.9rem;
opacity: 0.8;
}
.sidebar-menu {
list-style: none;
padding: 0;
margin: 0;
}
.sidebar-menu li {
margin: 5px 0;
}
.sidebar-menu a {
display: flex;
align-items: center;
gap: 15px;
padding: 15px 25px;
color: white;
text-decoration: none;
transition: all 0.3s ease;
border-left: 3px solid transparent;
}
.sidebar-menu a:hover {
background: rgba(255, 255, 255, 0.1);
border-left-color: white;
}
.sidebar-menu a.active {
background: rgba(255, 255, 255, 0.2);
border-left-color: white;
}
.sidebar-menu i {
width: 20px;
text-align: center;
}
/* 主内容区 */
.main-content {
flex: 1;
padding: 30px;
background: #f8f9ff;
}
.content-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
}
.page-title {
font-size: 1.8rem;
color: #333;
margin: 0;
}
.breadcrumb {
display: flex;
align-items: center;
gap: 10px;
color: #666;
font-size: 0.9rem;
}
.breadcrumb a {
color: #43e97b;
text-decoration: none;
}
.breadcrumb i {
font-size: 0.8rem;
}
/* 功能卡片 */
.function-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 25px;
margin-bottom: 40px;
}
.function-card {
background: white;
border-radius: 15px;
padding: 30px;
text-align: center;
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
cursor: pointer;
border: 2px solid transparent;
}
.function-card:hover {
transform: translateY(-5px);
box-shadow: 0 15px 30px rgba(0, 0, 0, 0.1);
border-color: #43e97b;
}
.function-icon {
width: 80px;
height: 80px;
border-radius: 50%;
background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 20px;
font-size: 32px;
color: white;
}
.function-title {
font-size: 1.3rem;
color: #333;
margin-bottom: 10px;
}
.function-description {
color: #666;
line-height: 1.5;
margin-bottom: 20px;
}
/* 快速操作 */
.quick-actions {
background: white;
border-radius: 15px;
padding: 25px;
margin-bottom: 30px;
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.05);
}
.section-title {
font-size: 1.4rem;
color: #333;
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 10px;
}
.section-title i {
color: #43e97b;
}
.action-buttons {
display: flex;
gap: 15px;
flex-wrap: wrap;
}
.action-btn {
padding: 12px 25px;
background: #f8f9ff;
border: 1px solid #e0e0e0;
border-radius: 10px;
color: #333;
text-decoration: none;
display: flex;
align-items: center;
gap: 10px;
transition: all 0.3s ease;
}
.action-btn:hover {
background: #43e97b;
color: white;
border-color: #43e97b;
}
/* 最近活动 */
.recent-activities {
background: white;
border-radius: 15px;
padding: 25px;
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.05);
}
.activity-list {
list-style: none;
padding: 0;
margin: 0;
}
.activity-item {
display: flex;
align-items: center;
gap: 15px;
padding: 15px 0;
border-bottom: 1px solid #eee;
}
.activity-item:last-child {
border-bottom: none;
}
.activity-icon {
width: 40px;
height: 40px;
border-radius: 50%;
background: #f8f9ff;
display: flex;
align-items: center;
justify-content: center;
color: #43e97b;
}
.activity-content {
flex: 1;
}
.activity-title {
font-weight: 600;
color: #333;
margin-bottom: 5px;
}
.activity-time {
font-size: 0.9rem;
color: #999;
}
/* 响应式设计 */
@media (max-width: 992px) {
.dashboard-container {
flex-direction: column;
}
.sidebar {
width: 100%;
height: auto;
position: static;
padding: 20px 0;
}
.sidebar-menu {
display: flex;
overflow-x: auto;
padding: 0 20px;
}
.sidebar-menu li {
flex-shrink: 0;
}
.sidebar-menu a {
padding: 10px 15px;
border-left: none;
border-bottom: 3px solid transparent;
}
.sidebar-menu a:hover,
.sidebar-menu a.active {
border-left-color: transparent;
border-bottom-color: white;
}
}
@media (max-width: 768px) {
.main-content {
padding: 20px;
}
.content-header {
flex-direction: column;
align-items: flex-start;
gap: 15px;
}
.function-grid {
grid-template-columns: 1fr;
}
.action-buttons {
flex-direction: column;
}
.action-btn {
justify-content: center;
}
}
</style>
</head>
<body>
<!-- 顶部导航栏 -->
<nav class="navbar">
<div class="navbar-brand">
<i class="fas fa-graduation-cap"></i>
<span>XX学校成绩管理系统</span>
</div>
<div class="navbar-menu">
<a href="index.html" class="btn btn-secondary">
<i class="fas fa-home"></i> 主页
</a>
<div class="navbar-user">
<span class="user-name" id="userName">李老师</span>
<button id="logoutBtn" class="btn btn-primary">
<i class="fas fa-sign-out-alt"></i> 退出
</button>
</div>
</div>
</nav>
<!-- 仪表板容器 -->
<div class="dashboard-container">
<!-- 左侧侧边栏 -->
<aside class="sidebar">
<div class="sidebar-header">
<div class="user-info">
<div class="user-avatar">
<i class="fas fa-chalkboard-teacher"></i>
</div>
<div class="user-details">
<h3 id="teacherName">李老师</h3>
<p>教师 | 部门:<span id="teacherDept">计算机学院</span></p>
</div>
</div>
</div>
<ul class="sidebar-menu">
<li>
<a href="#" class="active">
<i class="fas fa-tachometer-alt"></i>
<span>仪表板</span>
</a>
</li>
<li>
<a href="grade_entry.html">
<i class="fas fa-edit"></i>
<span>成绩录入</span>
</a>
</li>
<li>
<a href="grade_query.html">
<i class="fas fa-search"></i>
<span>成绩查询</span>
</a>
</li>
<li>
<a href="grade_manage.html">
<i class="fas fa-cog"></i>
<span>成绩管理</span>
</a>
</li>
<li>
<a href="#">
<i class="fas fa-chart-bar"></i>
<span>统计分析</span>
</a>
</li>
<li>
<a href="#">
<i class="fas fa-download"></i>
<span>数据导出</span>
</a>
</li>
<li>
<a href="#">
<i class="fas fa-user"></i>
<span>个人信息</span>
</a>
</li>
</ul>
</aside>
<!-- 主内容区 -->
<main class="main-content">
<div class="content-header">
<div>
<h1 class="page-title">教师仪表板</h1>
<div class="breadcrumb">
<a href="index.html">主页</a>
<i class="fas fa-chevron-right"></i>
<span>教师仪表板</span>
</div>
</div>
<div class="current-time" id="currentTime"></div>
</div>
<!-- 功能卡片 -->
<div class="function-grid">
<div class="function-card" onclick="window.location.href='grade_entry.html'">
<div class="function-icon">
<i class="fas fa-edit"></i>
</div>
<h3 class="function-title">成绩录入</h3>
<p class="function-description">
为学生录入新的课程成绩,支持批量导入和单个录入。
</p>
<button class="btn btn-primary">开始录入</button>
</div>
<div class="function-card" onclick="window.location.href='grade_query.html'">
<div class="function-icon">
<i class="fas fa-search"></i>
</div>
<h3 class="function-title">成绩查询</h3>
<p class="function-description">
查询学生成绩,支持按班级、课程、学期等多维度筛选。
</p>
<button class="btn btn-primary">开始查询</button>
</div>
<div class="function-card" onclick="window.location.href='grade_manage.html'">
<div class="function-icon">
<i class="fas fa-cog"></i>
</div>
<h3 class="function-title">成绩管理</h3>
<p class="function-description">
修改或删除已录入的成绩,管理成绩记录和状态。
</p>
<button class="btn btn-primary">开始管理</button>
</div>
<div class="function-card" onclick="window.location.href='#'">
<div class="function-icon">
<i class="fas fa-chart-bar"></i>
</div>
<h3 class="function-title">统计分析</h3>
<p class="function-description">
查看成绩统计图表,分析教学效果和学生表现。
</p>
<button class="btn btn-primary">查看统计</button>
</div>
</div>
<!-- 快速操作 -->
<div class="quick-actions">
<h2 class="section-title">
<i class="fas fa-bolt"></i>
快速操作
</h2>
<div class="action-buttons">
<a href="grade_entry.html" class="action-btn">
<i class="fas fa-plus"></i>
录入新成绩
</a>
<a href="grade_query.html" class="action-btn">
<i class="fas fa-search"></i>
查询成绩
</a>
<a href="#" class="action-btn">
<i class="fas fa-download"></i>
导出成绩单
</a>
<a href="#" class="action-btn">
<i class="fas fa-chart-pie"></i>
生成统计报告
</a>
</div>
</div>
<!-- 最近活动

View File

@@ -0,0 +1,509 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>用户管理 - XX学校成绩管理系统</title>
<link rel="stylesheet" href="../css/style.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
</head>
<body>
<!-- 导航栏 -->
<nav class="navbar">
<div class="container">
<div class="navbar-brand">
<a href="index.html">
<i class="fas fa-graduation-cap"></i>
<span>XX学校成绩管理系统</span>
</a>
</div>
<div class="navbar-menu">
<div class="navbar-start">
<a href="admin_dashboard.html" class="navbar-item">
<i class="fas fa-tachometer-alt"></i>
<span>仪表板</span>
</a>
<a href="user_management.html" class="navbar-item active">
<i class="fas fa-users"></i>
<span>用户管理</span>
</a>
<a href="student_management.html" class="navbar-item">
<i class="fas fa-user-graduate"></i>
<span>学生管理</span>
</a>
<a href="grade_management.html" class="navbar-item">
<i class="fas fa-chart-bar"></i>
<span>成绩统计</span>
</a>
</div>
<div class="navbar-end">
<div class="navbar-item user-info">
<i class="fas fa-user-circle"></i>
<span>管理员</span>
<div class="dropdown">
<a href="#" class="dropdown-toggle">
<i class="fas fa-caret-down"></i>
</a>
<div class="dropdown-menu">
<a href="admin_dashboard.html" class="dropdown-item">
<i class="fas fa-tachometer-alt"></i>
仪表板
</a>
<a href="#" class="dropdown-item">
<i class="fas fa-cog"></i>
系统设置
</a>
<div class="dropdown-divider"></div>
<a href="index.html" class="dropdown-item">
<i class="fas fa-sign-out-alt"></i>
退出登录
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</nav>
<!-- 主内容区 -->
<main class="main-content">
<div class="container">
<div class="user-management">
<!-- 页面标题和面包屑导航 -->
<div class="page-header">
<h1>用户管理</h1>
<div class="breadcrumb">
<a href="index.html">主页</a> &gt;
<a href="admin_dashboard.html">管理员仪表板</a> &gt;
<span>用户管理</span>
</div>
</div>
<!-- 筛选区域 -->
<div class="filter-section">
<form class="filter-form" id="filter-form">
<div class="filter-group">
<label for="user-id">用户ID</label>
<input type="text" id="user-id" placeholder="请输入用户ID">
</div>
<div class="filter-group">
<label for="user-name">姓名</label>
<input type="text" id="user-name" placeholder="请输入姓名">
</div>
<div class="filter-group">
<label for="role-select">角色</label>
<select id="role-select">
<option value="">全部角色</option>
<option value="admin">管理员</option>
<option value="teacher">教师</option>
<option value="student">学生</option>
</select>
</div>
<div class="filter-group">
<label for="class-select">班级</label>
<select id="class-select">
<option value="">全部班级</option>
<option value="计算机科学与技术1班">计算机科学与技术1班</option>
<option value="计算机科学与技术2班">计算机科学与技术2班</option>
<option value="软件工程1班">软件工程1班</option>
<option value="软件工程2班">软件工程2班</option>
<option value="网络工程1班">网络工程1班</option>
<option value="网络工程2班">网络工程2班</option>
</select>
</div>
<div class="filter-actions">
<button type="button" class="btn-add" id="add-btn">
<i class="fas fa-plus"></i>
添加用户
</button>
<button type="button" class="btn-search" id="search-btn">
<i class="fas fa-search"></i>
查询
</button>
<button type="button" class="btn-reset" id="reset-btn">
<i class="fas fa-redo"></i>
重置
</button>
<button type="button" class="btn-export" id="export-btn">
<i class="fas fa-file-export"></i>
导出
</button>
</div>
</form>
</div>
<!-- 结果区域 -->
<div class="table-container">
<table class="user-table">
<thead>
<tr>
<th>用户ID</th>
<th>姓名</th>
<th>角色</th>
<th>班级</th>
<th>联系电话</th>
<th>邮箱</th>
<th>注册时间</th>
<th>操作</th>
</tr>
</thead>
<tbody id="user-table-body">
<!-- 数据将通过JavaScript动态加载 -->
<tr>
<td colspan="8" class="loading">
<i class="fas fa-spinner fa-spin"></i>
<p>正在加载用户数据...</p>
</td>
</tr>
</tbody>
</table>
</div>
<!-- 分页控件 -->
<div class="pagination" id="pagination">
<button id="prev-btn" disabled>
<i class="fas fa-chevron-left"></i>
上一页
</button>
<div id="page-numbers">
<button class="active">1</button>
<button>2</button>
<button>3</button>
</div>
<button id="next-btn">
下一页
<i class="fas fa-chevron-right"></i>
</button>
</div>
</div>
</div>
</main>
<!-- 页脚 -->
<footer class="footer">
<div class="container">
<p>&copy; 2023 XX学校成绩管理系统. 版权所有.</p>
<p>技术支持: 信息技术中心 | 联系电话: 010-12345678</p>
</div>
</footer>
<!-- JavaScript -->
<script>
// 模拟用户数据
const mockUsers = [
{
id: 'admin001',
name: '张管理员',
role: 'admin',
className: '',
phone: '13800138001',
email: 'admin@xxschool.edu.cn',
registerTime: '2023-01-15'
},
{
id: 'teacher001',
name: '李老师',
role: 'teacher',
className: '计算机科学与技术1班',
phone: '13800138002',
email: 'li.teacher@xxschool.edu.cn',
registerTime: '2023-02-10'
},
{
id: 'teacher002',
name: '王老师',
role: 'teacher',
className: '软件工程1班',
phone: '13800138003',
email: 'wang.teacher@xxschool.edu.cn',
registerTime: '2023-02-12'
},
{
id: 'student001',
name: '张三',
role: 'student',
className: '计算机科学与技术1班',
phone: '13800138004',
email: 'zhangsan@xxschool.edu.cn',
registerTime: '2023-03-01'
},
{
id: 'student002',
name: '李四',
role: 'student',
className: '计算机科学与技术1班',
phone: '13800138005',
email: 'lisi@xxschool.edu.cn',
registerTime: '2023-03-01'
},
{
id: 'student003',
name: '王五',
role: 'student',
className: '软件工程1班',
phone: '13800138006',
email: 'wangwu@xxschool.edu.cn',
registerTime: '2023-03-02'
},
{
id: 'student004',
name: '赵六',
role: 'student',
className: '软件工程1班',
phone: '13800138007',
email: 'zhaoliu@xxschool.edu.cn',
registerTime: '2023-03-02'
},
{
id: 'student005',
name: '钱七',
role: 'student',
className: '网络工程1班',
phone: '13800138008',
email: 'qianqi@xxschool.edu.cn',
registerTime: '2023-03-03'
},
{
id: 'student006',
name: '孙八',
role: 'student',
className: '网络工程2班',
phone: '13800138009',
email: 'sunba@xxschool.edu.cn',
registerTime: '2023-03-03'
},
{
id: 'student007',
name: '周九',
role: 'student',
className: '计算机科学与技术2班',
phone: '13800138010',
email: 'zhoujiu@xxschool.edu.cn',
registerTime: '2023-03-04'
}
];
// 当前显示的用户数据
let currentUsers = [...mockUsers];
let currentPage = 1;
const usersPerPage = 5;
// DOM元素
const userTableBody = document.getElementById('user-table-body');
const pagination = document.getElementById('pagination');
const prevBtn = document.getElementById('prev-btn');
const nextBtn = document.getElementById('next-btn');
const pageNumbers = document.getElementById('page-numbers');
const searchBtn = document.getElementById('search-btn');
const resetBtn = document.getElementById('reset-btn');
const addBtn = document.getElementById('add-btn');
const exportBtn = document.getElementById('export-btn');
const userIdInput = document.getElementById('user-id');
const userNameInput = document.getElementById('user-name');
const roleSelect = document.getElementById('role-select');
const classSelect = document.getElementById('class-select');
// 初始化
document.addEventListener('DOMContentLoaded', function() {
renderUserTable();
setupEventListeners();
updatePagination();
});
// 设置事件监听器
function setupEventListeners() {
searchBtn.addEventListener('click', handleSearch);
resetBtn.addEventListener('click', handleReset);
addBtn.addEventListener('click', handleAddUser);
exportBtn.addEventListener('click', handleExport);
prevBtn.addEventListener('click', goToPrevPage);
nextBtn.addEventListener('click', goToNextPage);
}
// 渲染用户表格
function renderUserTable() {
if (currentUsers.length === 0) {
userTableBody.innerHTML = `
<tr>
<td colspan="8" class="no-results">
<i class="fas fa-user-slash"></i>
<h3>没有找到用户</h3>
<p>请尝试其他筛选条件或添加新用户</p>
</td>
</tr>
`;
return;
}
// 计算当前页的用户
const startIndex = (currentPage - 1) * usersPerPage;
const endIndex = startIndex + usersPerPage;
const pageUsers = currentUsers.slice(startIndex, endIndex);
let tableHTML = '';
pageUsers.forEach(user => {
// 角色徽章
let roleBadge = '';
if (user.role === 'admin') {
roleBadge = '<span class="role-badge role-admin">管理员</span>';
} else if (user.role === 'teacher') {
roleBadge = '<span class="role-badge role-teacher">教师</span>';
} else {
roleBadge = '<span class="role-badge role-student">学生</span>';
}
tableHTML += `
<tr>
<td>${user.id}</td>
<td>${user.name}</td>
<td>${roleBadge}</td>
<td>${user.className || '—'}</td>
<td>${user.phone}</td>
<td>${user.email}</td>
<td>${user.registerTime}</td>
<td>
<div class="action-buttons-cell">
<button class="btn-edit" onclick="editUser('${user.id}')">
<i class="fas fa-edit"></i>
编辑
</button>
<button class="btn-delete" onclick="deleteUser('${user.id}')">
<i class="fas fa-trash"></i>
删除
</button>
</div>
</td>
</tr>
`;
});
userTableBody.innerHTML = tableHTML;
}
// 更新分页
function updatePagination() {
const totalPages = Math.ceil(currentUsers.length / usersPerPage);
// 更新按钮状态
prevBtn.disabled = currentPage === 1;
nextBtn.disabled = currentPage === totalPages || totalPages === 0;
// 更新页码按钮
let pageButtonsHTML = '';
for (let i = 1; i <= totalPages; i++) {
if (i <= 5) { // 最多显示5个页码
pageButtonsHTML += `<button class="${i === currentPage ? 'active' : ''}" onclick="goToPage(${i})">${i}</button>`;
}
}
pageNumbers.innerHTML = pageButtonsHTML;
}
// 处理搜索
function handleSearch() {
const userId = userIdInput.value.trim();
const userName = userNameInput.value.trim();
const role = roleSelect.value;
const className = classSelect.value;
currentUsers = mockUsers.filter(user => {
// 用户ID筛选
if (userId && !user.id.toLowerCase().includes(userId.toLowerCase())) {
return false;
}
// 姓名筛选
if (userName && !user.name.toLowerCase().includes(userName.toLowerCase())) {
return false;
}
// 角色筛选
if (role && user.role !== role) {
return false;
}
// 班级筛选
if (className && user.className !== className) {
return false;
}
return true;
});
currentPage = 1;
renderUserTable();
updatePagination();
}
// 处理重置
function handleReset() {
userIdInput.value = '';
userNameInput.value = '';
roleSelect.value = '';
classSelect.value = '';
currentUsers = [...mockUsers];
currentPage = 1;
renderUserTable();
updatePagination();
}
// 处理添加用户
function handleAddUser() {
alert('添加用户功能将在后端API完成后实现');
// 这里可以打开一个模态框来添加新用户
}
// 处理导出
function handleExport() {
alert('导出功能将在后端API完成后实现');
// 这里可以导出为Excel或CSV格式
}
// 编辑用户
function editUser(userId) {
alert(`编辑用户 ${userId} - 功能将在后端API完成后实现`);
// 这里可以打开一个模态框来编辑用户信息
}
// 删除用户
function deleteUser(userId) {
if (confirm(`确定要删除用户 ${userId} 吗?此操作不可撤销。`)) {
// 从模拟数据中删除
const index = mockUsers.findIndex(user => user.id === userId);
if (index !== -1) {
mockUsers.splice(index, 1);
// 更新当前显示的数据
handleSearch();
alert('用户删除成功');
}
}
}
// 分页函数
function goToPage(page) {
currentPage = page;
renderUserTable();
updatePagination();
}
function goToPrevPage() {
if (currentPage > 1) {
currentPage--;
renderUserTable();
updatePagination();
}
}
function goToNextPage() {
const totalPages = Math.ceil(currentUsers.length / usersPerPage);
if (currentPage < totalPages) {
currentPage++;
renderUserTable();
updatePagination();
}
}
</script>
</body>
</html>

591
frontend/js/admin.js Normal file
View File

@@ -0,0 +1,591 @@
class AdminDashboard {
constructor() {
// 动态设置API基础URL支持file:///协议和localhost:3000访问
this.apiBase = window.location.protocol === 'file:' ? 'http://localhost:3000/api' : '/api';
this.currentUser = null;
this.stats = {};
this.users = [];
this.students = [];
this.teachers = [];
this.init();
}
async init() {
// 检查登录状态
if (!await this.checkAuth()) {
window.location.href = '/frontend/html/login.html';
return;
}
// 加载用户信息
await this.loadUserInfo();
// 加载统计数据
await this.loadStats();
// 加载用户数据
await this.loadUsers();
// 绑定事件
this.bindEvents();
// 更新界面
this.updateUI();
// 初始化图表
this.initCharts();
}
async checkAuth() {
try {
const response = await fetch(`${this.apiBase}/auth/me`, {
credentials: 'include'
});
if (!response.ok) {
return false;
}
const data = await response.json();
return data.success && data.user.role === 'admin';
} catch (error) {
console.error('认证检查失败:', error);
return false;
}
}
async loadUserInfo() {
try {
const response = await fetch(`${this.apiBase}/auth/me`, {
credentials: 'include'
});
if (response.ok) {
const data = await response.json();
if (data.success) {
this.currentUser = data.user;
}
}
} catch (error) {
console.error('加载用户信息失败:', error);
}
}
async loadStats() {
try {
const response = await fetch(`${this.apiBase}/admin/stats`, {
credentials: 'include'
});
if (response.ok) {
const data = await response.json();
if (data.success) {
this.stats = data.stats;
this.updateStatsUI();
}
}
} catch (error) {
console.error('加载统计数据失败:', error);
this.showNotification('加载统计数据失败', 'error');
}
}
async loadUsers() {
try {
const response = await fetch(`${this.apiBase}/admin/users`, {
credentials: 'include'
});
if (response.ok) {
const data = await response.json();
if (data.success) {
this.users = data.users;
this.renderUsersTable();
}
}
} catch (error) {
console.error('加载用户数据失败:', error);
this.showNotification('加载用户数据失败', 'error');
}
}
updateStatsUI() {
// 更新统计卡片
const statElements = {
'totalUsers': 'totalUsers',
'totalStudents': 'totalStudents',
'totalTeachers': 'totalTeachers',
'totalCourses': 'totalCourses',
'totalGrades': 'totalGrades',
'avgScore': 'avgScore'
};
Object.entries(statElements).forEach(([key, elementId]) => {
const element = document.getElementById(elementId);
if (element && this.stats[key] !== undefined) {
element.textContent = this.stats[key];
}
});
// 更新时间
const timeElement = document.getElementById('currentTime');
if (timeElement) {
timeElement.textContent = new Date().toLocaleString();
}
}
renderUsersTable() {
const tableBody = document.getElementById('usersTableBody');
if (!tableBody) return;
if (this.users.length === 0) {
tableBody.innerHTML = `
<tr>
<td colspan="7" class="text-center">
<div class="no-data">
<i class="fas fa-info-circle"></i>
<p>暂无用户数据</p>
</div>
</td>
</tr>
`;
return;
}
tableBody.innerHTML = this.users.map(user => {
const roleClass = this.getRoleClass(user.role);
return `
<tr>
<td><input type="checkbox" class="user-checkbox" data-id="${user.id}"></td>
<td>${user.user_id}</td>
<td>${user.full_name}</td>
<td><span class="role-badge ${roleClass}">${user.role}</span></td>
<td>${user.class_name || 'N/A'}</td>
<td>${user.email || 'N/A'}</td>
<td>
<div class="action-buttons">
<button class="btn-edit" data-id="${user.id}">
<i class="fas fa-edit"></i> 编辑
</button>
<button class="btn-delete" data-id="${user.id}">
<i class="fas fa-trash"></i> 删除
</button>
</div>
</td>
</tr>
`;
}).join('');
}
getRoleClass(role) {
switch (role) {
case 'admin': return 'role-admin';
case 'teacher': return 'role-teacher';
case 'student': return 'role-student';
default: return 'role-default';
}
}
bindEvents() {
// 导航菜单点击
document.querySelectorAll('.nav-link').forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
const page = link.dataset.page;
this.loadPage(page);
});
});
// 搜索按钮
document.getElementById('searchBtn')?.addEventListener('click', () => {
this.handleSearch();
});
// 重置按钮
document.getElementById('resetBtn')?.addEventListener('click', () => {
this.resetFilters();
});
// 添加用户按钮
document.getElementById('addUserBtn')?.addEventListener('click', () => {
this.addUser();
});
// 导出按钮
document.getElementById('exportBtn')?.addEventListener('click', () => {
this.exportUsers();
});
// 批量删除按钮
document.getElementById('batchDeleteBtn')?.addEventListener('click', () => {
this.batchDeleteUsers();
});
// 表格操作按钮事件委托
document.addEventListener('click', (e) => {
if (e.target.closest('.btn-edit')) {
const userId = e.target.closest('.btn-edit').dataset.id;
this.editUser(userId);
}
if (e.target.closest('.btn-delete')) {
const userId = e.target.closest('.btn-delete').dataset.id;
this.deleteUser(userId);
}
});
// 退出登录
document.getElementById('logoutBtn')?.addEventListener('click', () => {
this.handleLogout();
});
// 刷新按钮
document.getElementById('refreshBtn')?.addEventListener('click', () => {
this.refreshData();
});
}
async loadPage(page) {
// 这里可以实现页面切换逻辑
// 暂时使用简单跳转
switch (page) {
case 'users':
window.location.href = '/frontend/html/user_management.html';
break;
case 'students':
window.location.href = '/frontend/html/student_management.html';
break;
case 'teachers':
// 可以跳转到教师管理页面
break;
case 'grades':
window.location.href = '/frontend/html/grade_management.html';
break;
case 'settings':
// 可以跳转到系统设置页面
break;
}
}
handleSearch() {
const userId = document.getElementById('userIdFilter')?.value || '';
const name = document.getElementById('nameFilter')?.value || '';
const role = document.getElementById('roleFilter')?.value || '';
const className = document.getElementById('classFilter')?.value || '';
// 这里可以实现搜索逻辑
this.showNotification('搜索功能待实现', 'info');
}
resetFilters() {
document.getElementById('userIdFilter').value = '';
document.getElementById('nameFilter').value = '';
document.getElementById('roleFilter').value = '';
document.getElementById('classFilter').value = '';
// 重新加载数据
this.loadUsers();
}
async addUser() {
// 这里可以打开添加用户模态框
const userData = {
user_id: prompt('请输入用户ID:'),
full_name: prompt('请输入姓名:'),
role: prompt('请输入角色 (admin/teacher/student):'),
email: prompt('请输入邮箱:'),
class_name: prompt('请输入班级 (学生/教师可选):')
};
if (!userData.user_id || !userData.full_name || !userData.role) {
this.showNotification('用户ID、姓名和角色为必填项', 'error');
return;
}
try {
const response = await fetch(`${this.apiBase}/admin/users`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(userData)
});
const data = await response.json();
if (data.success) {
this.showNotification('用户添加成功', 'success');
await this.loadUsers();
} else {
this.showNotification(data.message || '添加失败', 'error');
}
} catch (error) {
console.error('添加用户失败:', error);
this.showNotification('添加用户失败', 'error');
}
}
async exportUsers() {
try {
const response = await fetch(`${this.apiBase}/admin/users/export`, {
credentials: 'include'
});
if (response.ok) {
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `用户列表_${new Date().toISOString().split('T')[0]}.xlsx`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
}
} catch (error) {
console.error('导出失败:', error);
this.showNotification('导出失败', 'error');
}
}
async batchDeleteUsers() {
const checkboxes = document.querySelectorAll('.user-checkbox:checked');
if (checkboxes.length === 0) {
this.showNotification('请选择要删除的用户', 'warning');
return;
}
if (!confirm(`确定要删除选中的 ${checkboxes.length} 个用户吗?`)) {
return;
}
const userIds = Array.from(checkboxes).map(cb => cb.dataset.id);
try {
const response = await fetch(`${this.apiBase}/admin/users/batch`, {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ userIds })
});
const data = await response.json();
if (data.success) {
this.showNotification(`成功删除 ${userIds.length} 个用户`, 'success');
await this.loadUsers();
} else {
this.showNotification(data.message || '删除失败', 'error');
}
} catch (error) {
console.error('批量删除失败:', error);
this.showNotification('批量删除失败', 'error');
}
}
async editUser(userId) {
const user = this.users.find(u => u.id == userId);
if (!user) return;
// 这里可以打开编辑模态框
const newName = prompt('请输入新的姓名:', user.full_name);
if (newName === null) return;
const newRole = prompt('请输入新的角色:', user.role);
if (newRole === null) return;
try {
const response = await fetch(`${this.apiBase}/admin/users/${userId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({
full_name: newName,
role: newRole,
email: user.email,
class_name: user.class_name
})
});
const data = await response.json();
if (data.success) {
this.showNotification('用户更新成功', 'success');
await this.loadUsers();
} else {
this.showNotification(data.message || '更新失败', 'error');
}
} catch (error) {
console.error('更新用户失败:', error);
this.showNotification('更新用户失败', 'error');
}
}
async deleteUser(userId) {
if (!confirm('确定要删除这个用户吗?')) {
return;
}
try {
const response = await fetch(`${this.apiBase}/admin/users/${userId}`, {
method: 'DELETE',
credentials: 'include'
});
const data = await response.json();
if (data.success) {
this.showNotification('用户删除成功', 'success');
await this.loadUsers();
} else {
this.showNotification(data.message || '删除失败', 'error');
}
} catch (error) {
console.error('删除用户失败:', error);
this.showNotification('删除用户失败', 'error');
}
}
async handleLogout() {
try {
const response = await fetch(`${this.apiBase}/auth/logout`, {
method: 'POST',
credentials: 'include'
});
if (response.ok) {
window.location.href = '/html/login.html';
}
} catch (error) {
console.error('退出登录失败:', error);
}
}
async refreshData() {
await this.loadStats();
await this.loadUsers();
this.showNotification('数据已刷新', 'success');
}
updateUI() {
// 更新用户信息
if (this.currentUser) {
const userInfoElements = document.querySelectorAll('.user-info');
userInfoElements.forEach(el => {
el.textContent = `${this.currentUser.full_name} (${this.currentUser.role})`;
});
}
}
async initCharts() {
// 加载Chart.js库
await this.loadChartLibrary();
// 初始化用户分布饼图
this.initUserDistributionChart();
// 初始化成绩分布柱状图
this.initGradeDistributionChart();
}
showNotification(message, type = 'info') {
// 创建通知元素
const notification = document.createElement('div');
notification.className = `notification notification-${type}`;
notification.innerHTML = `
<i class="fas fa-${type === 'success' ? 'check-circle' : type === 'error' ? 'exclamation-circle' : type === 'warning' ? 'exclamation-triangle' : 'info-circle'}"></i>
<span>${message}</span>
<button class="notification-close">&times;</button>
`;
// 添加到页面
document.body.appendChild(notification);
// 添加关闭事件
notification.querySelector('.notification-close').addEventListener('click', () => {
notification.remove();
});
// 自动移除
setTimeout(() => {
if (notification.parentNode) {
notification.remove();
}
}, 5000);
}
async loadChartLibrary() {
if (typeof Chart !== 'undefined') return;
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/chart.js@3.9.1/dist/chart.min.js';
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
});
}
initUserDistributionChart() {
const ctx = document.getElementById('userDistributionChart');
if (!ctx) return;
// 模拟数据
const data = {
labels: ['学生', '教师', '管理员'],
datasets: [{
data: [this.stats.totalStudents || 100, this.stats.totalTeachers || 20, 1],
backgroundColor: [
'rgba(54, 162, 235, 0.8)',
'rgba(255, 206, 86, 0.8)',
'rgba(255, 99, 132, 0.8)'
]
}]
};
new Chart(ctx, {
type: 'pie',
data: data,
options: {
responsive: true,
plugins: {
legend: {
position: 'bottom'
}
}
}
});
}
initGradeDistributionChart() {
const ctx = document.getElementById('gradeDistributionChart');
if (!ctx) return;
// 模拟数据
const data = {
labels: ['A', 'B', 'C', 'D', 'F'],
datasets: [{
label: '成绩分布',
data: [25, 35, 20, 15, 5],
backgroundColor: [
'rgba(75, 192, 192, 0.8)',
'rgba(54, 162, 235, 0.8)',
'rgba(255, 206, 86, 0.8)',
'rgba(255, 159, 64, 0.8)',
'rgba(255, 99, 132, 0.8)'
]
}]
};
new Chart(ctx, {
type: 'bar',
data: data,
options: {
responsive: true,
scales: {
y: {
beginAtZero: true,
ticks: {
stepSize: 10
}
}
}
}
});
}
}

269
frontend/js/auth.js Normal file
View File

@@ -0,0 +1,269 @@
class AuthManager {
constructor() {
// 动态设置API基础URL支持file:///协议和localhost:3000访问
this.apiBase = window.location.protocol === 'file:' ? 'http://localhost:3000/api' : '/api';
this.initEventListeners();
this.checkAuthStatus();
}
async checkAuthStatus() {
try {
const response = await fetch(`${this.apiBase}/auth/me`);
const data = await response.json();
if (data.success && data.user) {
// 用户已登录,根据角色重定向到正确的仪表板
const userRole = data.user.role;
let redirectUrl = '/dashboard';
if (userRole === 'student') {
redirectUrl = '/frontend/html/student_dashboard.html';
} else if (userRole === 'teacher') {
redirectUrl = '/frontend/html/teacher_dashboard.html';
} else if (userRole === 'admin') {
redirectUrl = '/frontend/html/admin_dashboard.html';
}
// 如果当前页面是登录或注册页面,则重定向到仪表板
const currentPath = window.location.pathname;
if (currentPath.includes('login.html') || currentPath.includes('register.html')) {
window.location.href = redirectUrl;
}
// 如果当前页面不是正确的仪表板页面,则重定向
else if (!currentPath.includes(redirectUrl) &&
currentPath !== '/' &&
!currentPath.includes('index.html')) {
window.location.href = redirectUrl;
}
}
} catch (error) {
console.log('用户未登录');
}
}
initEventListeners() {
// 登录表单提交
const loginForm = document.getElementById('loginForm');
if (loginForm) {
loginForm.addEventListener('submit', (e) => this.handleLogin(e));
}
// 注册表单提交
const registerForm = document.getElementById('registerForm');
if (registerForm) {
registerForm.addEventListener('submit', (e) => this.handleRegister(e));
}
// 登出按钮
const logoutBtn = document.getElementById('logoutBtn');
if (logoutBtn) {
logoutBtn.addEventListener('click', (e) => this.handleLogout(e));
}
}
async handleLogin(e) {
e.preventDefault();
const form = e.target;
const formData = new FormData(form);
const data = Object.fromEntries(formData.entries());
const submitBtn = form.querySelector('button[type="submit"]');
const originalText = submitBtn.innerHTML;
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 登录中...';
submitBtn.disabled = true;
try {
const response = await fetch(`${this.apiBase}/auth/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
const result = await response.json();
if (result.success) {
this.showNotification('登录成功!正在跳转...', 'success');
// 根据用户角色跳转到不同的仪表板
const userRole = result.user?.role;
let redirectUrl = '/dashboard';
if (userRole === 'student') {
redirectUrl = '/frontend/html/student_dashboard.html';
} else if (userRole === 'teacher') {
redirectUrl = '/frontend/html/teacher_dashboard.html';
} else if (userRole === 'admin') {
redirectUrl = '/frontend/html/admin_dashboard.html';
}
// 延迟跳转以显示通知
setTimeout(() => {
window.location.href = redirectUrl;
}, 1500);
} else {
this.showNotification(result.message || '登录失败', 'error');
submitBtn.innerHTML = originalText;
submitBtn.disabled = false;
}
} catch (error) {
console.error('登录错误:', error);
this.showNotification('网络错误,请重试', 'error');
submitBtn.innerHTML = originalText;
submitBtn.disabled = false;
}
}
async handleRegister(e) {
e.preventDefault();
const form = e.target;
const formData = new FormData(form);
const data = Object.fromEntries(formData.entries());
// 验证密码
if (data.password !== data.confirmPassword) {
this.showNotification('两次输入的密码不一致', 'error');
return;
}
const submitBtn = form.querySelector('button[type="submit"]');
const originalText = submitBtn.innerHTML;
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 注册中...';
submitBtn.disabled = true;
try {
const response = await fetch(`${this.apiBase}/auth/register`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
const result = await response.json();
if (result.success) {
this.showNotification('注册成功!正在跳转到登录页面...', 'success');
setTimeout(() => {
window.location.href = '/html/login.html';
}, 1500);
} else {
this.showNotification(result.message || '注册失败', 'error');
submitBtn.innerHTML = originalText;
submitBtn.disabled = false;
}
} catch (error) {
console.error('注册错误:', error);
this.showNotification('网络错误,请重试', 'error');
submitBtn.innerHTML = originalText;
submitBtn.disabled = false;
}
}
async handleLogout(e) {
e.preventDefault();
if (!confirm('确定要退出登录吗?')) {
return;
}
try {
const response = await fetch(`${this.apiBase}/auth/logout`, {
method: 'POST'
});
const result = await response.json();
if (result.success) {
this.showNotification('已成功退出登录', 'success');
setTimeout(() => {
window.location.href = '/';
}, 1000);
} else {
this.showNotification('退出登录失败', 'error');
}
} catch (error) {
console.error('退出登录错误:', error);
this.showNotification('网络错误,请重试', 'error');
}
}
showNotification(message, type = 'info') {
// 移除现有的通知
const existingNotification = document.querySelector('.notification');
if (existingNotification) {
existingNotification.remove();
}
// 创建新的通知
const notification = document.createElement('div');
notification.className = `notification notification-${type}`;
notification.innerHTML = `
<i class="fas fa-${type === 'success' ? 'check-circle' : 'exclamation-circle'}"></i>
<span>${message}</span>
`;
// 样式
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: ${type === 'success' ? '#10b981' : type === 'error' ? '#ef4444' : '#3b82f6'};
color: white;
padding: 15px 20px;
border-radius: 8px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
display: flex;
align-items: center;
gap: 10px;
z-index: 1000;
animation: slideIn 0.3s ease;
`;
document.body.appendChild(notification);
// 3秒后自动移除
setTimeout(() => {
notification.style.animation = 'slideOut 0.3s ease';
setTimeout(() => notification.remove(), 300);
}, 3000);
// 添加动画关键帧
if (!document.querySelector('#notification-styles')) {
const style = document.createElement('style');
style.id = 'notification-styles';
style.textContent = `
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes slideOut {
from {
transform: translateX(0);
opacity: 1;
}
to {
transform: translateX(100%);
opacity: 0;
}
}
`;
document.head.appendChild(style);
}
}
}
// 初始化认证管理器
document.addEventListener('DOMContentLoaded', () => {
window.authManager = new AuthManager();
});

222
frontend/js/main.js Normal file
View File

@@ -0,0 +1,222 @@
// 首页通用JavaScript功能
// 主要处理导航栏交互、页面滚动效果等通用功能
class MainPage {
constructor() {
this.init();
}
init() {
// 初始化所有功能
this.initNavbar();
this.initScrollEffects();
this.initSmoothScroll();
this.initBackToTop();
this.initMobileMenu();
this.initAuthButtons();
}
// 初始化导航栏交互
initNavbar() {
const navbar = document.querySelector('.navbar');
if (!navbar) return;
// 滚动时改变导航栏样式
window.addEventListener('scroll', () => {
if (window.scrollY > 50) {
navbar.classList.add('navbar-scrolled');
} else {
navbar.classList.remove('navbar-scrolled');
}
});
// 初始化当前页面高亮
this.highlightCurrentPage();
}
// 高亮当前页面导航链接
highlightCurrentPage() {
const currentPath = window.location.pathname;
const navLinks = document.querySelectorAll('.nav-link');
navLinks.forEach(link => {
const href = link.getAttribute('href');
if (href && currentPath.includes(href.replace('.html', ''))) {
link.classList.add('active');
}
});
}
// 初始化滚动效果
initScrollEffects() {
// 滚动时显示/隐藏元素
const observerOptions = {
root: null,
rootMargin: '0px',
threshold: 0.1
};
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('animate-in');
}
});
}, observerOptions);
// 观察需要动画的元素
document.querySelectorAll('.feature-card, .hero-content').forEach(el => {
observer.observe(el);
});
}
// 初始化平滑滚动
initSmoothScroll() {
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', (e) => {
e.preventDefault();
const targetId = anchor.getAttribute('href');
if (targetId === '#') return;
const targetElement = document.querySelector(targetId);
if (targetElement) {
targetElement.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
}
});
});
}
// 初始化返回顶部按钮
initBackToTop() {
const backToTopBtn = document.createElement('button');
backToTopBtn.id = 'backToTop';
backToTopBtn.innerHTML = '<i class="fas fa-chevron-up"></i>';
backToTopBtn.title = '返回顶部';
document.body.appendChild(backToTopBtn);
// 滚动时显示/隐藏按钮
window.addEventListener('scroll', () => {
if (window.scrollY > 300) {
backToTopBtn.classList.add('show');
} else {
backToTopBtn.classList.remove('show');
}
});
// 点击返回顶部
backToTopBtn.addEventListener('click', () => {
window.scrollTo({
top: 0,
behavior: 'smooth'
});
});
}
// 初始化移动端菜单
initMobileMenu() {
const navbarToggler = document.querySelector('.navbar-toggler');
const navbarCollapse = document.querySelector('.navbar-collapse');
if (!navbarToggler || !navbarCollapse) return;
navbarToggler.addEventListener('click', () => {
navbarCollapse.classList.toggle('show');
});
// 点击菜单项后自动关闭移动菜单
document.querySelectorAll('.navbar-nav .nav-link').forEach(link => {
link.addEventListener('click', () => {
if (navbarCollapse.classList.contains('show')) {
navbarCollapse.classList.remove('show');
}
});
});
}
// 初始化认证按钮状态
initAuthButtons() {
// 检查用户是否已登录
this.checkLoginStatus().then(user => {
const loginBtn = document.getElementById('loginBtn');
const registerBtn = document.getElementById('registerBtn');
const heroLoginBtn = document.getElementById('heroLoginBtn');
if (user) {
// 用户已登录,显示仪表板按钮
// 根据用户角色设置正确的仪表板路径
let dashboardUrl = '/dashboard';
if (user.role === 'student') {
dashboardUrl = '/frontend/html/student_dashboard.html';
} else if (user.role === 'teacher') {
dashboardUrl = '/frontend/html/teacher_dashboard.html';
} else if (user.role === 'admin') {
dashboardUrl = '/frontend/html/admin_dashboard.html';
}
if (loginBtn) {
loginBtn.textContent = '进入仪表板';
loginBtn.href = dashboardUrl;
}
if (heroLoginBtn) {
heroLoginBtn.textContent = '进入仪表板';
heroLoginBtn.href = dashboardUrl;
}
if (registerBtn) {
registerBtn.style.display = 'none';
}
}
});
}
// 检查登录状态
async checkLoginStatus() {
try {
const apiBase = window.location.protocol === 'file:' ? 'http://localhost:3000/api' : '/api';
const response = await fetch(`${apiBase}/auth/me`);
const data = await response.json();
return data.success && data.user;
} catch (error) {
console.log('用户未登录');
return false;
}
}
// 显示通知
showNotification(message, type = 'info') {
// 创建通知元素
const notification = document.createElement('div');
notification.className = `notification notification-${type}`;
notification.innerHTML = `
<div class="notification-content">
<i class="fas ${type === 'success' ? 'fa-check-circle' : type === 'error' ? 'fa-exclamation-circle' : 'fa-info-circle'}"></i>
<span>${message}</span>
</div>
`;
// 添加到页面
document.body.appendChild(notification);
// 显示通知
setTimeout(() => {
notification.classList.add('show');
}, 10);
// 自动隐藏
setTimeout(() => {
notification.classList.remove('show');
setTimeout(() => {
if (notification.parentNode) {
notification.parentNode.removeChild(notification);
}
}, 300);
}, 3000);
}
}
// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', () => {
new MainPage();
});

438
frontend/js/student.js Normal file
View File

@@ -0,0 +1,438 @@
class StudentManager {
constructor() {
// 动态设置API基础URL支持file:///协议和localhost:3000访问
this.apiBase = window.location.protocol === 'file:' ? 'http://localhost:3000/api' : '/api';
this.initDashboard();
this.initGradeDetails();
this.loadProfile();
}
async initDashboard() {
const gradeList = document.getElementById('gradeList');
const statisticsElement = document.getElementById('statistics');
if (!gradeList) return;
try {
const response = await fetch(`${this.apiBase}/student/grades`, {
credentials: 'include'
});
if (response.status === 401) {
// 未登录,重定向到登录页
this.showNotification('请先登录', 'error');
setTimeout(() => {
window.location.href = '/html/login.html';
}, 1500);
return;
}
const data = await response.json();
if (data.success) {
this.renderGrades(data.grades);
this.renderStatistics(data.statistics);
this.updateChart(data.grades);
} else {
this.showNotification(data.message || '获取成绩失败', 'error');
}
} catch (error) {
console.error('获取成绩错误:', error);
this.showNotification('网络错误,请重试', 'error');
}
}
renderGrades(grades) {
const gradeList = document.getElementById('gradeList');
const gradeTable = document.getElementById('gradeTable');
if (!gradeTable) return;
if (grades.length === 0) {
gradeList.innerHTML = `
<div class="empty-state">
<i class="fas fa-clipboard-list fa-3x"></i>
<h3>暂无成绩记录</h3>
<p>你还没有任何成绩记录</p>
</div>
`;
return;
}
const tbody = gradeTable.querySelector('tbody');
tbody.innerHTML = '';
grades.forEach(grade => {
const row = document.createElement('tr');
// 根据分数设置颜色
let scoreClass = '';
if (grade.score >= 90) scoreClass = 'grade-excellent';
else if (grade.score >= 80) scoreClass = 'grade-good';
else if (grade.score >= 60) scoreClass = 'grade-pass';
else scoreClass = 'grade-fail';
row.innerHTML = `
<td>${grade.course_code}</td>
<td>${grade.course_name}</td>
<td>${grade.credit}</td>
<td class="${scoreClass}">
<span class="grade-badge">${grade.score}</span>
</td>
<td>${grade.grade_level || '-'}</td>
<td>${grade.grade_point || '-'}</td>
<td>${grade.teacher_name}</td>
<td>${new Date(grade.exam_date).toLocaleDateString()}</td>
<td>
<a href="/html/student/details.html?id=${grade.id}"
class="btn btn-sm btn-secondary">
<i class="fas fa-eye"></i> 查看
</a>
</td>
`;
tbody.appendChild(row);
});
}
renderStatistics(statistics) {
const element = document.getElementById('statistics');
if (!element) return;
element.innerHTML = `
<div class="stats-grid">
<div class="stat-card">
<div class="stat-icon student">
<i class="fas fa-book"></i>
</div>
<div class="stat-value">${statistics.totalCourses}</div>
<div class="stat-label">总课程数</div>
</div>
<div class="stat-card">
<div class="stat-icon course">
<i class="fas fa-star"></i>
</div>
<div class="stat-value">${statistics.totalCredits}</div>
<div class="stat-label">总学分</div>
</div>
<div class="stat-card">
<div class="stat-icon grade">
<i class="fas fa-chart-line"></i>
</div>
<div class="stat-value">${statistics.averageScore}</div>
<div class="stat-label">平均分</div>
</div>
<div class="stat-card">
<div class="stat-icon teacher">
<i class="fas fa-graduation-cap"></i>
</div>
<div class="stat-value">${statistics.gpa}</div>
<div class="stat-label">平均绩点</div>
</div>
</div>
`;
}
async loadProfile() {
const profileElement = document.getElementById('profileInfo');
if (!profileElement) return;
try {
const response = await fetch(`${this.apiBase}/student/profile`, {
credentials: 'include'
});
if (response.status === 401) {
// 未登录,重定向到登录页
this.showNotification('请先登录', 'error');
setTimeout(() => {
window.location.href = '/html/login.html';
}, 1500);
return;
}
const data = await response.json();
if (data.success) {
const profile = data.profile;
// 更新学生仪表板顶部信息
const userNameElement = document.getElementById('userName');
const studentNameElement = document.getElementById('studentName');
const studentClassElement = document.getElementById('studentClass');
if (userNameElement) {
userNameElement.textContent = profile.full_name || profile.username;
}
if (studentNameElement) {
studentNameElement.textContent = profile.full_name || profile.username;
}
if (studentClassElement) {
studentClassElement.textContent = profile.class_name || '未设置';
}
profileElement.innerHTML = `
<div class="profile-header">
<div class="profile-avatar">
<i class="fas fa-user-graduate"></i>
</div>
<div class="profile-info">
<h2>${profile.full_name}</h2>
<p class="profile-role">
<i class="fas fa-user-tag"></i> 学生
</p>
</div>
</div>
<div class="profile-details">
<div class="detail-item">
<i class="fas fa-id-card"></i>
<div>
<h4>学号</h4>
<p>${profile.student_id}</p>
</div>
</div>
<div class="detail-item">
<i class="fas fa-users"></i>
<div>
<h4>班级</h4>
<p>${profile.class_name}</p>
</div>
</div>
<div class="detail-item">
<i class="fas fa-book"></i>
<div>
<h4>专业</h4>
<p>${profile.major || '未设置'}</p>
</div>
</div>
<div class="detail-item">
<i class="fas fa-calendar-alt"></i>
<div>
<h4>入学年份</h4>
<p>${profile.enrollment_year || '未设置'}</p>
</div>
</div>
</div>
`;
} else {
// API返回失败
this.showNotification(data.message || '获取个人信息失败', 'error');
}
} catch (error) {
console.error('加载个人信息错误:', error);
this.showNotification('网络错误,请重试', 'error');
}
}
async initGradeDetails() {
const urlParams = new URLSearchParams(window.location.search);
const gradeId = urlParams.get('id');
if (!gradeId) return;
try {
const response = await fetch(`${this.apiBase}/student/grades/${gradeId}`, {
credentials: 'include'
});
const data = await response.json();
if (data.success) {
this.renderGradeDetails(data.grade);
} else {
this.showNotification('获取成绩详情失败', 'error');
setTimeout(() => window.history.back(), 1500);
}
} catch (error) {
console.error('获取成绩详情错误:', error);
this.showNotification('网络错误,请重试', 'error');
}
}
renderGradeDetails(grade) {
const container = document.getElementById('gradeDetails');
if (!container) return;
// 计算绩点描述
let gradeDescription = '';
if (grade.score >= 90) gradeDescription = '优秀';
else if (grade.score >= 80) gradeDescription = '良好';
else if (grade.score >= 70) gradeDescription = '中等';
else if (grade.score >= 60) gradeDescription = '及格';
else gradeDescription = '不及格';
container.innerHTML = `
<div class="grade-detail-card">
<div class="grade-header">
<h2>${grade.course_name} (${grade.course_code})</h2>
<div class="grade-score ${grade.score >= 60 ? 'score-pass' : 'score-fail'}">
${grade.score}
<span class="grade-description">${gradeDescription}</span>
</div>
</div>
<div class="grade-details-grid">
<div class="detail-section">
<h3><i class="fas fa-info-circle"></i> 基本信息</h3>
<div class="detail-row">
<span>学分:</span>
<strong>${grade.credit}</strong>
</div>
<div class="detail-row">
<span>学期:</span>
<strong>${grade.semester}</strong>
</div>
<div class="detail-row">
<span>考试日期:</span>
<strong>${new Date(grade.exam_date).toLocaleDateString()}</strong>
</div>
<div class="detail-row">
<span>等级:</span>
<strong class="grade-level-${grade.grade_level}">${grade.grade_level || '-'}</strong>
</div>
<div class="detail-row">
<span>绩点:</span>
<strong>${grade.grade_point || '-'}</strong>
</div>
</div>
<div class="detail-section">
<h3><i class="fas fa-user-graduate"></i> 学生信息</h3>
<div class="detail-row">
<span>姓名:</span>
<strong>${grade.full_name}</strong>
</div>
<div class="detail-row">
<span>学号:</span>
<strong>${grade.student_number}</strong>
</div>
<div class="detail-row">
<span>班级:</span>
<strong>${grade.class_name}</strong>
</div>
<div class="detail-row">
<span>专业:</span>
<strong>${grade.major || '未设置'}</strong>
</div>
</div>
<div class="detail-section">
<h3><i class="fas fa-chalkboard-teacher"></i> 教师信息</h3>
<div class="detail-row">
<span>任课教师:</span>
<strong>${grade.teacher_name}</strong>
</div>
<div class="detail-row">
<span>教师邮箱:</span>
<strong>${grade.teacher_email}</strong>
</div>
</div>
</div>
${grade.remark ? `
<div class="remark-section">
<h3><i class="fas fa-comment"></i> 备注</h3>
<p>${grade.remark}</p>
</div>
` : ''}
<div class="grade-actions">
<button onclick="window.print()" class="btn btn-secondary">
<i class="fas fa-print"></i> 打印成绩单
</button>
<button onclick="window.history.back()" class="btn btn-primary">
<i class="fas fa-arrow-left"></i> 返回
</button>
</div>
</div>
`;
}
updateChart(grades) {
const ctx = document.getElementById('gradeChart');
if (!ctx) return;
if (typeof Chart === 'undefined') {
// 如果没有Chart.js延迟加载
this.loadChartLibrary().then(() => this.updateChart(grades));
return;
}
const courseNames = grades.map(g => g.course_name);
const scores = grades.map(g => g.score);
// 销毁现有图表实例
if (window.gradeChart instanceof Chart) {
window.gradeChart.destroy();
}
window.gradeChart = new Chart(ctx, {
type: 'bar',
data: {
labels: courseNames,
datasets: [{
label: '分数',
data: scores,
backgroundColor: scores.map(score => {
if (score >= 90) return 'rgba(75, 192, 192, 0.7)';
if (score >= 80) return 'rgba(54, 162, 235, 0.7)';
if (score >= 60) return 'rgba(255, 206, 86, 0.7)';
return 'rgba(255, 99, 132, 0.7)';
}),
borderColor: scores.map(score => {
if (score >= 90) return 'rgb(75, 192, 192)';
if (score >= 80) return 'rgb(54, 162, 235)';
if (score >= 60) return 'rgb(255, 206, 86)';
return 'rgb(255, 99, 132)';
}),
borderWidth: 1
}]
},
options: {
responsive: true,
plugins: {
title: {
display: true,
text: '各科成绩分布'
}
},
scales: {
y: {
beginAtZero: true,
max: 100
}
}
}
});
}
async loadChartLibrary() {
return new Promise((resolve, reject) => {
if (typeof Chart !== 'undefined') {
resolve();
return;
}
const script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/chart.js';
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
});
}
showNotification(message, type = 'info') {
// 使用AuthManager的通知系统或自己实现
if (window.authManager && window.authManager.showNotification) {
window.authManager.showNotification(message, type);
} else {
alert(message);
}
}
}
// 初始化学生管理器
document.addEventListener('DOMContentLoaded', () => {
if (window.location.pathname.includes('/student/')) {
window.studentManager = new StudentManager();
}
});

409
frontend/js/teacher.js Normal file
View File

@@ -0,0 +1,409 @@
class TeacherDashboard {
constructor() {
// 动态设置API基础URL支持file:///协议和localhost:3000访问
this.apiBase = window.location.protocol === 'file:' ? 'http://localhost:3000/api' : '/api';
this.currentUser = null;
this.courses = [];
this.grades = [];
this.init();
}
async init() {
// 检查登录状态
if (!await this.checkAuth()) {
window.location.href = '/frontend/html/login.html';
return;
}
// 加载用户信息
await this.loadUserInfo();
// 加载课程数据
await this.loadCourses();
// 加载成绩数据
await this.loadGrades();
// 绑定事件
this.bindEvents();
// 更新界面
this.updateUI();
}
async checkAuth() {
try {
const response = await fetch(`${this.apiBase}/auth/check`, {
credentials: 'include'
});
if (!response.ok) {
return false;
}
const data = await response.json();
return data.success && data.user.role === 'teacher';
} catch (error) {
console.error('认证检查失败:', error);
return false;
}
}
async loadUserInfo() {
try {
const response = await fetch(`${this.apiBase}/auth/me`, {
credentials: 'include'
});
if (response.ok) {
const data = await response.json();
if (data.success) {
this.currentUser = data.user;
}
}
} catch (error) {
console.error('加载用户信息失败:', error);
}
}
async loadCourses() {
try {
const response = await fetch(`${this.apiBase}/teacher/courses`, {
credentials: 'include'
});
if (response.ok) {
const data = await response.json();
if (data.success) {
this.courses = data.courses;
this.populateCourseSelectors();
}
}
} catch (error) {
console.error('加载课程失败:', error);
this.showNotification('加载课程失败', 'error');
}
}
async loadGrades(filters = {}) {
try {
const queryParams = new URLSearchParams(filters).toString();
const url = `${this.apiBase}/teacher/grades${queryParams ? '?' + queryParams : ''}`;
const response = await fetch(url, {
credentials: 'include'
});
if (response.ok) {
const data = await response.json();
if (data.success) {
this.grades = data.grades;
this.renderGradesTable();
}
}
} catch (error) {
console.error('加载成绩失败:', error);
this.showNotification('加载成绩失败', 'error');
}
}
populateCourseSelectors() {
// 填充课程选择器
const courseSelectors = document.querySelectorAll('.course-selector');
courseSelectors.forEach(select => {
select.innerHTML = '<option value="">请选择课程</option>';
this.courses.forEach(course => {
const option = document.createElement('option');
option.value = course.id;
option.textContent = `${course.course_code} - ${course.course_name}`;
select.appendChild(option);
});
});
}
renderGradesTable() {
const tableBody = document.getElementById('gradesTableBody');
if (!tableBody) return;
if (this.grades.length === 0) {
tableBody.innerHTML = `
<tr>
<td colspan="9" class="text-center">
<div class="no-data">
<i class="fas fa-info-circle"></i>
<p>暂无成绩数据</p>
</div>
</td>
</tr>
`;
return;
}
tableBody.innerHTML = this.grades.map(grade => {
const gradeClass = this.getGradeClass(grade.score);
return `
<tr>
<td><input type="checkbox" class="grade-checkbox" data-id="${grade.id}"></td>
<td>${grade.student_id}</td>
<td>${grade.full_name}</td>
<td>${grade.class_name}</td>
<td>${grade.course_code}</td>
<td>${grade.course_name}</td>
<td class="grade-cell ${gradeClass}">
<span class="grade-score">${grade.score}</span>
<span class="grade-level">${grade.grade_level}</span>
</td>
<td>${grade.exam_date ? new Date(grade.exam_date).toLocaleDateString() : '未设置'}</td>
<td>
<div class="action-buttons">
<button class="btn-edit" data-id="${grade.id}">
<i class="fas fa-edit"></i> 编辑
</button>
<button class="btn-delete" data-id="${grade.id}">
<i class="fas fa-trash"></i> 删除
</button>
</div>
</td>
</tr>
`;
}).join('');
// 更新统计信息
this.updateStats();
}
getGradeClass(score) {
if (score >= 90) return 'grade-excellent';
if (score >= 80) return 'grade-good';
if (score >= 70) return 'grade-medium';
if (score >= 60) return 'grade-pass';
return 'grade-fail';
}
updateStats() {
if (this.grades.length === 0) return;
const totalStudents = new Set(this.grades.map(g => g.student_id)).size;
const avgScore = this.grades.reduce((sum, g) => sum + g.score, 0) / this.grades.length;
const passRate = (this.grades.filter(g => g.score >= 60).length / this.grades.length * 100).toFixed(1);
document.getElementById('totalStudents').textContent = totalStudents;
document.getElementById('avgScore').textContent = avgScore.toFixed(1);
document.getElementById('passRate').textContent = `${passRate}%`;
}
bindEvents() {
// 搜索按钮
document.getElementById('searchBtn')?.addEventListener('click', () => {
this.handleSearch();
});
// 重置按钮
document.getElementById('resetBtn')?.addEventListener('click', () => {
this.resetFilters();
});
// 导出按钮
document.getElementById('exportBtn')?.addEventListener('click', () => {
this.exportGrades();
});
// 批量删除按钮
document.getElementById('batchDeleteBtn')?.addEventListener('click', () => {
this.batchDeleteGrades();
});
// 表格操作按钮事件委托
document.addEventListener('click', (e) => {
if (e.target.closest('.btn-edit')) {
const gradeId = e.target.closest('.btn-edit').dataset.id;
this.editGrade(gradeId);
}
if (e.target.closest('.btn-delete')) {
const gradeId = e.target.closest('.btn-delete').dataset.id;
this.deleteGrade(gradeId);
}
});
// 退出登录
document.getElementById('logoutBtn')?.addEventListener('click', () => {
this.handleLogout();
});
}
handleSearch() {
const className = document.getElementById('classFilter')?.value || '';
const courseId = document.getElementById('courseFilter')?.value || '';
this.loadGrades({ class_name: className, course_id: courseId });
}
resetFilters() {
document.getElementById('classFilter').value = '';
document.getElementById('courseFilter').value = '';
this.loadGrades();
}
async exportGrades() {
try {
const response = await fetch(`${this.apiBase}/teacher/grades/export`, {
credentials: 'include'
});
if (response.ok) {
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `成绩报表_${new Date().toISOString().split('T')[0]}.xlsx`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
}
} catch (error) {
console.error('导出失败:', error);
this.showNotification('导出失败', 'error');
}
}
async batchDeleteGrades() {
const checkboxes = document.querySelectorAll('.grade-checkbox:checked');
if (checkboxes.length === 0) {
this.showNotification('请选择要删除的成绩', 'warning');
return;
}
if (!confirm(`确定要删除选中的 ${checkboxes.length} 条成绩记录吗?`)) {
return;
}
const gradeIds = Array.from(checkboxes).map(cb => cb.dataset.id);
try {
const response = await fetch(`${this.apiBase}/teacher/grades/batch`, {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ gradeIds })
});
const data = await response.json();
if (data.success) {
this.showNotification(`成功删除 ${gradeIds.length} 条成绩记录`, 'success');
await this.loadGrades();
} else {
this.showNotification(data.message || '删除失败', 'error');
}
} catch (error) {
console.error('批量删除失败:', error);
this.showNotification('批量删除失败', 'error');
}
}
async editGrade(gradeId) {
const grade = this.grades.find(g => g.id == gradeId);
if (!grade) return;
// 这里可以打开编辑模态框
// 暂时使用简单提示框
const newScore = prompt('请输入新的分数:', grade.score);
if (newScore === null) return;
const numericScore = parseFloat(newScore);
if (isNaN(numericScore) || numericScore < 0 || numericScore > 100) {
this.showNotification('请输入0-100之间的有效分数', 'error');
return;
}
try {
const response = await fetch(`${this.apiBase}/teacher/grades/${gradeId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({
score: numericScore,
examDate: grade.exam_date,
remark: grade.remark
})
});
const data = await response.json();
if (data.success) {
this.showNotification('成绩更新成功', 'success');
await this.loadGrades();
} else {
this.showNotification(data.message || '更新失败', 'error');
}
} catch (error) {
console.error('更新成绩失败:', error);
this.showNotification('更新成绩失败', 'error');
}
}
async deleteGrade(gradeId) {
if (!confirm('确定要删除这条成绩记录吗?')) {
return;
}
try {
const response = await fetch(`${this.apiBase}/teacher/grades/${gradeId}`, {
method: 'DELETE',
credentials: 'include'
});
const data = await response.json();
if (data.success) {
this.showNotification('成绩删除成功', 'success');
await this.loadGrades();
} else {
this.showNotification(data.message || '删除失败', 'error');
}
} catch (error) {
console.error('删除成绩失败:', error);
this.showNotification('删除成绩失败', 'error');
}
}
async handleLogout() {
try {
const response = await fetch(`${this.apiBase}/auth/logout`, {
method: 'POST',
credentials: 'include'
});
if (response.ok) {
window.location.href = '/frontend/html/login.html';
}
} catch (error) {
console.error('退出登录失败:', error);
}
}
updateUI() {
// 更新用户信息
if (this.currentUser) {
const userInfoElements = document.querySelectorAll('.user-info');
userInfoElements.forEach(el => {
el.textContent = `${this.currentUser.full_name} (${this.currentUser.role})`;
});
}
}
showNotification(message, type = 'info') {
// 使用AuthManager的通知系统或简单alert
if (typeof AuthManager !== 'undefined' && AuthManager.showNotification) {
AuthManager.showNotification(message, type);
} else {
alert(`${type}: ${message}`);
}
}
}
// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', () => {
if (window.location.pathname.includes('/teacher/')) {
new TeacherDashboard();
}
});