feat: 实现教师资料更新、操作日志和系统设置功能

新增教师资料更新功能,包括个人信息修改和密码更新
添加操作日志记录系统,记录用户关键操作
实现系统设置模块,支持动态配置系统参数
重构数据库模型,新增教师表和系统设置表
优化成绩录入逻辑,支持平时分、期中和期末成绩计算
添加数据导出功能,支持学生、教师和成绩数据导出
完善管理员后台,增加统计图表和操作日志查看
This commit is contained in:
祀梦
2025-12-22 23:30:01 +08:00
parent 16802c85e5
commit b1da021185
43 changed files with 7860 additions and 2835 deletions

View File

@@ -1,36 +1,64 @@
const mysql = require('mysql2/promise');
require('dotenv').config();
const sqlite3 = require('sqlite3').verbose();
const path = require('path');
const fs = require('fs');
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
});
const dbPath = path.resolve(__dirname, '../database.sqlite');
const db = new sqlite3.Database(dbPath);
// 封装基本查询方法
const query = async (sql, params) => {
try {
const [rows] = await pool.execute(sql, params);
return rows;
} catch (error) {
console.error('Database query error:', error);
throw error;
}
// Promisify query method
const query = (sql, params = []) => {
return new Promise((resolve, reject) => {
// Handle SELECT vs INSERT/UPDATE/DELETE
const trimmedSql = sql.trim().toUpperCase();
if (trimmedSql.startsWith('SELECT') || trimmedSql.startsWith('PRAGMA')) {
db.all(sql, params, (err, rows) => {
if (err) {
console.error('Database query error:', err);
reject(err);
} else {
resolve(rows);
}
});
} else {
db.run(sql, params, function(err) {
if (err) {
console.error('Database execution error:', err);
reject(err);
} else {
// Normalize result to look like MySQL result
// this.lastID, this.changes
resolve({
insertId: this.lastID,
affectedRows: this.changes,
warningStatus: 0
});
}
});
}
});
};
// Mock Pool object for compatibility
const pool = {
query: query,
execute: (sql, params) => query(sql, params).then(res => [res]), // Wrap in array for mysql2 compatibility
getConnection: () => Promise.resolve({
release: () => {},
query: query,
execute: (sql, params) => query(sql, params).then(res => [res]),
beginTransaction: () => query('BEGIN TRANSACTION'),
commit: () => query('COMMIT'),
rollback: () => query('ROLLBACK')
})
};
// 测试连接
const testConnection = async () => {
try {
const connection = await pool.getConnection();
console.log('数据库连接成功');
connection.release();
await query('SELECT 1');
console.log('SQLite 数据库连接成功');
return true;
} catch (error) {
console.error('数据库连接失败:', error.message);
console.error('SQLite 数据库连接失败:', error.message);
return false;
}
};

View File

@@ -2,28 +2,25 @@ const AdminService = require('../services/adminService');
const { success, error } = require('../utils/response');
class AdminController {
static async getStats(req, res) {
try {
const stats = await AdminService.getStats();
success(res, stats, '获取统计信息成功');
} catch (err) {
console.error('Get Stats Error:', err);
error(res, '服务器错误');
}
}
static async getUsers(req, res) {
try {
const result = await AdminService.getUsers(req.query);
success(res, result.data, '获取成功');
// 注意:原来的响应结构是 { success, data, pagination }
// 现在的 success 工具函数结构是 { success, message, data }
// 我们可以稍微调整 success 调用,或者让前端适应
// 为了兼容性,这里手动返回
/*
res.json({
success: true,
message: '获取成功',
data: result.data,
pagination: result.pagination
});
*/
// 或者修改 response.js 支持 extra 字段,这里简单处理:
res.json({
success: true,
data: result.data,
pagination: result.pagination
});
} catch (err) {
console.error('Get Users Error:', err);
error(res, '服务器错误');
@@ -38,7 +35,10 @@ class AdminController {
return error(res, '请填写所有必填字段', 400);
}
await AdminService.createUser(req.body);
await AdminService.createUser(req.body, {
user_id: req.session.user.id,
ip: req.ip
});
success(res, null, '创建用户成功');
} catch (err) {
if (err.message === '用户ID已存在') {
@@ -48,6 +48,287 @@ class AdminController {
error(res, '服务器错误');
}
}
static async updateUser(req, res) {
try {
const { id } = req.params;
const updateData = req.body;
await AdminService.updateUser(id, updateData, {
user_id: req.session.user.id,
ip: req.ip
});
success(res, null, '更新用户成功');
} catch (err) {
console.error('Update User Error:', err);
error(res, '服务器错误');
}
}
static async deleteUser(req, res) {
try {
const { id } = req.params;
await AdminService.deleteUser(id, {
user_id: req.session.user.id,
ip: req.ip
});
success(res, null, '删除用户成功');
} catch (err) {
console.error('Delete User Error:', err);
error(res, '服务器错误');
}
}
// Student Management
static async getStudents(req, res) {
try {
const result = await AdminService.getStudents(req.query);
res.json({
success: true,
message: '获取学生列表成功',
data: result.data,
pagination: result.pagination
});
} catch (err) {
console.error(err);
error(res, '服务器错误');
}
}
static async createStudent(req, res) {
try {
if (!req.body.id || !req.body.name) return error(res, 'ID和姓名必填', 400);
await AdminService.createStudent(req.body, {
user_id: req.session.user.id,
ip: req.ip
});
success(res, null, '创建学生成功');
} catch (err) {
console.error(err);
error(res, err.message || '创建失败', 400);
}
}
static async updateStudent(req, res) {
try {
await AdminService.updateStudent(req.params.id, req.body, {
user_id: req.session.user.id,
ip: req.ip
});
success(res, null, '更新学生成功');
} catch (err) {
console.error(err);
error(res, '更新失败');
}
}
static async deleteStudent(req, res) {
try {
await AdminService.deleteStudent(req.params.id, {
user_id: req.session.user.id,
ip: req.ip
});
success(res, null, '删除学生成功');
} catch (err) {
console.error(err);
error(res, '删除失败');
}
}
// Teacher Management
static async getTeachers(req, res) {
try {
const result = await AdminService.getTeachers(req.query);
res.json({
success: true,
message: '获取教师列表成功',
data: result.data,
pagination: result.pagination
});
} catch (err) {
console.error(err);
error(res, '服务器错误');
}
}
static async createTeacher(req, res) {
try {
if (!req.body.id || !req.body.name) return error(res, '工号和姓名必填', 400);
await AdminService.createTeacher(req.body, {
user_id: req.session.user.id,
ip: req.ip
});
success(res, null, '创建教师成功');
} catch (err) {
console.error(err);
error(res, err.message || '创建失败', 400);
}
}
static async updateTeacher(req, res) {
try {
await AdminService.updateTeacher(req.params.id, req.body, {
user_id: req.session.user.id,
ip: req.ip
});
success(res, null, '更新教师成功');
} catch (err) {
console.error(err);
error(res, '更新失败');
}
}
static async deleteTeacher(req, res) {
try {
await AdminService.deleteTeacher(req.params.id, {
user_id: req.session.user.id,
ip: req.ip
});
success(res, null, '删除教师成功');
} catch (err) {
console.error(err);
error(res, '删除失败');
}
}
// Grade Statistics
static async getGradeStats(req, res) {
try {
const stats = await AdminService.getGradeStats();
success(res, stats, '获取成绩统计成功');
} catch (err) {
console.error(err);
error(res, '服务器错误');
}
}
// System Settings
static async getSettings(req, res) {
try {
const settings = await AdminService.getSettings();
success(res, settings);
} catch (err) {
console.error(err);
error(res, '服务器错误');
}
}
static async saveSettings(req, res) {
try {
await AdminService.saveSettings(req.body, {
user_id: req.session.user.id,
ip: req.ip
});
success(res, null, '保存设置成功');
} catch (err) {
console.error(err);
error(res, '保存失败');
}
}
// Data Maintenance
static async backupDatabase(req, res) {
try {
const result = await AdminService.backupDatabase({
user_id: req.session.user.id,
ip: req.ip
});
success(res, result, '数据库备份成功');
} catch (err) {
console.error(err);
error(res, '备份失败');
}
}
static async clearCache(req, res) {
try {
await AdminService.clearCache({
user_id: req.session.user.id,
ip: req.ip
});
success(res, null, '缓存已清理');
} catch (err) {
console.error(err);
error(res, '清理失败');
}
}
static async resetStudentPasswords(req, res) {
try {
await AdminService.resetStudentPasswords({
user_id: req.session.user.id,
ip: req.ip
});
success(res, null, '所有学生密码已重置为 123456');
} catch (err) {
console.error(err);
error(res, '重置失败');
}
}
// Operation Logs
static async getOperationLogs(req, res) {
try {
const logs = await AdminService.getLogs(req.query);
success(res, logs);
} catch (err) {
console.error(err);
error(res, '获取日志失败');
}
}
// Data Export
static async exportStudents(req, res) {
try {
const data = await AdminService.exportStudents();
const csv = jsonToCsv(data, ['id', 'name', 'class', 'major', 'grade', 'contact_info']);
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
res.setHeader('Content-Disposition', 'attachment; filename=students.csv');
res.send(csv);
} catch (err) {
console.error(err);
error(res, '导出失败');
}
}
static async exportTeachers(req, res) {
try {
const data = await AdminService.exportTeachers();
const csv = jsonToCsv(data, ['id', 'name', 'department', 'title', 'contact_info', 'created_at']);
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
res.setHeader('Content-Disposition', 'attachment; filename=teachers.csv');
res.send(csv);
} catch (err) {
console.error(err);
error(res, '导出失败');
}
}
static async exportGrades(req, res) {
try {
const data = await AdminService.exportGrades();
const csv = jsonToCsv(data, ['student_id', 'student_name', 'course_code', 'course_name', 'total_score', 'grade_point', 'grade_level', 'teacher_name']);
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
res.setHeader('Content-Disposition', 'attachment; filename=grades.csv');
res.send(csv);
} catch (err) {
console.error(err);
error(res, '导出失败');
}
}
}
// Helper function
function jsonToCsv(data, fields) {
if (!data || data.length === 0) return '';
const header = fields.join(',') + '\n';
const rows = data.map(row => {
return fields.map(field => {
const val = row[field] === null || row[field] === undefined ? '' : row[field];
return `"${String(val).replace(/"/g, '""')}"`;
}).join(',');
}).join('\n');
return '\ufeff' + header + rows; // Add BOM for Excel compatibility
}
module.exports = AdminController;

View File

@@ -85,6 +85,26 @@ class AuthController {
error(res, '服务器错误');
}
}
static async updateProfile(req, res) {
try {
const userId = req.session.user.id;
const updateData = req.body;
const updatedUser = await AuthService.updateProfile(userId, updateData);
// 更新 Session 中的用户信息
req.session.user = {
...req.session.user,
...updatedUser
};
success(res, { user: req.session.user }, '资料更新成功');
} catch (err) {
console.error('Update Profile Error:', err);
error(res, '服务器错误');
}
}
}
module.exports = AuthController;

View File

@@ -18,6 +18,68 @@ class TeacherController {
}
}
static async getClasses(req, res) {
try {
const classes = await TeacherService.getClasses();
success(res, { classes });
} catch (err) {
console.error('Get Classes Error:', err);
error(res, '服务器错误');
}
}
static async getMyClasses(req, res) {
try {
const teacherId = req.session.user.id;
const classes = await TeacherService.getTeacherClasses(teacherId);
success(res, { classes });
} catch (err) {
console.error('Get My Classes Error:', err);
error(res, '服务器错误');
}
}
static async createCourse(req, res) {
try {
const teacherId = req.session.user.id;
const courseId = await TeacherService.createCourse(teacherId, req.body);
success(res, { courseId }, '课程创建成功');
} catch (err) {
console.error('Create Course Error:', err);
error(res, '服务器错误');
}
}
static async updateCourse(req, res) {
try {
const teacherId = req.session.user.id;
const courseId = req.params.id;
await TeacherService.updateCourse(teacherId, courseId, req.body);
success(res, null, '课程更新成功');
} catch (err) {
if (err.message === '无权修改该课程或课程不存在') {
return error(res, err.message, 403);
}
console.error('Update Course Error:', err);
error(res, '服务器错误');
}
}
static async getGrades(req, res) {
try {
const teacherId = req.session.user.id;
const filters = {
courseId: req.query.courseId,
studentName: req.query.studentName
};
const grades = await TeacherService.getGrades(teacherId, filters);
success(res, { grades });
} catch (err) {
console.error('Get Grades Error:', err);
error(res, '服务器错误');
}
}
static async addScore(req, res) {
try {
const teacherId = req.session.user.id;

BIN
backend/database.sqlite Normal file

Binary file not shown.

343
backend/init_db.js Normal file
View File

@@ -0,0 +1,343 @@
const sqlite3 = require('sqlite3').verbose();
const path = require('path');
const bcrypt = require('bcryptjs');
const dbPath = path.resolve(__dirname, 'database.sqlite');
const db = new sqlite3.Database(dbPath);
const run = (sql, params = []) => {
return new Promise((resolve, reject) => {
db.run(sql, params, function(err) {
if (err) reject(err);
else resolve(this);
});
});
};
const insertUser = async (user) => {
await run(
'INSERT INTO users (id, name, password, role, class) VALUES (?, ?, ?, ?, ?)',
[user.id, user.name, user.password, user.role, user.class]
);
};
const insertStudent = async (student) => {
await run(
'INSERT INTO students (id, name, class, major, grade, contact_info) VALUES (?, ?, ?, ?, ?, ?)',
[student.id, student.name, student.class, student.major, student.grade, student.contact_info]
);
};
const insertClass = async (cls) => {
return await run(
'INSERT INTO classes (class_name, grade, major, teacher_id) VALUES (?, ?, ?, ?)',
[cls.class_name, cls.grade, cls.major, cls.teacher_id]
);
};
const insertCourse = async (course) => {
return await run(
'INSERT INTO courses (course_code, course_name, credit, teacher_id, class_id, semester, academic_year, category) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
[course.course_code, course.course_name, course.credit, course.teacher_id, course.class_id, course.semester, course.academic_year, course.category]
);
};
const insertGrade = async (grade) => {
await run(
`INSERT INTO grades (
student_id, course_id, teacher_id,
usual_score, midterm_score, final_score, total_score,
grade_point, grade_level, remark
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
grade.student_id, grade.course_id, grade.teacher_id,
grade.usual_score, grade.midterm_score, grade.final_score, grade.total_score,
grade.grade_point, grade.grade_level, grade.remark
]
);
};
// Helper to calculate grade point
const calculateGradePoint = (score) => {
if (score >= 90) return 4.0;
if (score >= 85) return 3.7;
if (score >= 82) return 3.3;
if (score >= 78) return 3.0;
if (score >= 75) return 2.7;
if (score >= 72) return 2.3;
if (score >= 68) return 2.0;
if (score >= 64) return 1.5;
if (score >= 60) return 1.0;
return 0.0;
};
const calculateGradeLevel = (score) => {
if (score >= 90) return 'A';
if (score >= 80) return 'B';
if (score >= 70) return 'C';
if (score >= 60) return 'D';
return 'F';
};
const init = async () => {
console.log('开始初始化 SQLite 数据库...');
const hashedPassword = await bcrypt.hash('123456', 10);
try {
// Drop tables
await run('DROP TABLE IF EXISTS grades');
await run('DROP TABLE IF EXISTS courses');
await run('DROP TABLE IF EXISTS classes');
await run('DROP TABLE IF EXISTS students');
await run('DROP TABLE IF EXISTS users');
await run('DROP TABLE IF EXISTS operation_logs');
// Create tables
console.log('创建表结构...');
await run(`
CREATE TABLE users (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
password TEXT NOT NULL,
role TEXT NOT NULL,
class TEXT,
created_at TEXT DEFAULT (datetime('now', 'localtime'))
)
`);
await run(`
CREATE TABLE students (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
class TEXT,
major TEXT,
grade TEXT,
contact_info TEXT,
FOREIGN KEY (id) REFERENCES users(id) ON DELETE CASCADE
)
`);
await run(`
CREATE TABLE classes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
class_name TEXT NOT NULL,
grade TEXT,
major TEXT,
teacher_id TEXT,
created_at TEXT DEFAULT (datetime('now', 'localtime'))
)
`);
await run(`
CREATE TABLE courses (
id INTEGER PRIMARY KEY AUTOINCREMENT,
course_code TEXT UNIQUE NOT NULL,
course_name TEXT NOT NULL,
credit REAL DEFAULT 2.0,
teacher_id TEXT NOT NULL,
class_id INTEGER NOT NULL,
semester TEXT,
academic_year TEXT,
category TEXT,
created_at TEXT DEFAULT (datetime('now', 'localtime'))
)
`);
await run(`
CREATE TABLE grades (
id INTEGER PRIMARY KEY AUTOINCREMENT,
student_id TEXT NOT NULL,
course_id INTEGER NOT NULL,
teacher_id TEXT NOT NULL,
usual_score REAL,
midterm_score REAL,
final_score REAL,
total_score REAL,
grade_point REAL,
grade_level TEXT,
remark TEXT,
created_at TEXT DEFAULT (datetime('now', 'localtime')),
UNIQUE(student_id, course_id)
)
`);
await run(`
CREATE TABLE operation_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT,
operation_type TEXT,
operation_target TEXT,
description TEXT,
ip_address TEXT,
created_at TEXT DEFAULT (datetime('now', 'localtime'))
)
`);
console.log('生成基础数据...');
// 1. Admin
await insertUser({
id: 'admin',
name: '系统管理员',
password: hashedPassword,
role: 'admin',
class: null
});
// 2. Teachers (20 teachers)
const teachers = [];
for (let i = 1; i <= 20; i++) {
const id = `T${1000 + i}`;
const name = `教师${String.fromCharCode(65 + (i % 26))}${i}`;
await insertUser({
id,
name,
password: hashedPassword,
role: 'teacher',
class: null
});
teachers.push(id);
}
// 3. Classes (10 classes)
const majors = ['软件工程', '计算机科学', '信息管理'];
const classIds = [];
const classes = [];
for (let i = 1; i <= 10; i++) {
const major = majors[i % 3];
const gradeYear = 2021 + Math.floor((i-1)/5); // 2021, 2022
const className = `${major.substr(0, 2)}${gradeYear}${String(i).padStart(2, '0')}`;
const teacherId = teachers[i % teachers.length];
const result = await insertClass({
class_name: className,
grade: String(gradeYear),
major: major,
teacher_id: teacherId
});
classIds.push(result.lastID);
classes.push({ id: result.lastID, name: className, year: gradeYear, major });
}
// 4. Students (50 per class -> 500 students)
console.log('生成学生数据...');
const students = [];
for (let clsIdx = 0; clsIdx < classes.length; clsIdx++) {
const cls = classes[clsIdx];
for (let i = 1; i <= 50; i++) {
const id = `${cls.year}${String(clsIdx + 1).padStart(2, '0')}${String(i).padStart(3, '0')}`;
const name = `学生${cls.name.substr(0,1)}${i}`;
await insertUser({
id,
name,
password: hashedPassword,
role: 'student',
class: cls.name
});
await insertStudent({
id,
name,
class: cls.name,
major: cls.major,
grade: String(cls.year),
contact_info: `13800${id.substr(0, 6)}`
});
students.push({ id, classId: cls.id });
}
}
// 5. Courses and Grades
console.log('生成课程和成绩数据...');
const courseNames = [
{ name: '高等数学', credit: 4, category: '必修' },
{ name: '大学英语', credit: 3, category: '必修' },
{ name: '程序设计基础', credit: 4, category: '必修' },
{ name: '数据结构', credit: 4, category: '必修' },
{ name: '操作系统', credit: 3, category: '必修' },
{ name: '计算机网络', credit: 3, category: '必修' },
{ name: '数据库原理', credit: 3, category: '必修' },
{ name: '软件工程导论', credit: 2, category: '必修' },
{ name: 'Web开发技术', credit: 3, category: '选修' },
{ name: '人工智能基础', credit: 2, category: '选修' },
{ name: '大数据分析', credit: 2, category: '选修' },
{ name: '音乐鉴赏', credit: 1, category: '通识' },
{ name: '心理健康', credit: 1, category: '通识' },
{ name: '职业规划', credit: 1, category: '通识' }
];
const semesters = ['2021-2022-1', '2021-2022-2', '2022-2023-1', '2022-2023-2', '2023-2024-1'];
for (const cls of classes) {
// For each class, assign some courses
for (let semIdx = 0; semIdx < semesters.length; semIdx++) {
const semester = semesters[semIdx];
// Select random 5-8 courses for this semester
const semCourses = courseNames.sort(() => 0.5 - Math.random()).slice(0, 6);
for (const cTemplate of semCourses) {
const teacherId = teachers[Math.floor(Math.random() * teachers.length)];
const courseCode = `C${cls.id}${semIdx}${Math.floor(Math.random() * 1000)}`;
const result = await insertCourse({
course_code: courseCode,
course_name: cTemplate.name,
credit: cTemplate.credit,
teacher_id: teacherId,
class_id: cls.id,
semester: semester,
academic_year: semester.substring(0, 9),
category: cTemplate.category
});
const courseId = result.lastID;
// Generate grades for students in this class
const classStudents = students.filter(s => s.classId === cls.id);
// Batch insert could be faster, but let's keep it simple for now
const stmt = db.prepare(`INSERT INTO grades (
student_id, course_id, teacher_id,
usual_score, midterm_score, final_score, total_score,
grade_point, grade_level, remark
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
for (const stu of classStudents) {
// 90% chance to have a grade
if (Math.random() > 0.1) {
const totalScore = Math.floor(Math.random() * 40) + 60; // 60-100 mostly
// 10% chance to fail
const finalTotal = Math.random() > 0.1 ? totalScore : Math.floor(Math.random() * 59);
stmt.run([
stu.id,
courseId,
teacherId,
finalTotal, // usual
finalTotal, // midterm
finalTotal, // final
finalTotal, // total
calculateGradePoint(finalTotal),
calculateGradeLevel(finalTotal),
''
]);
}
}
stmt.finalize();
}
}
}
console.log('数据库初始化完成!');
db.close();
} catch (err) {
console.error('Initialization failed:', err);
process.exit(1);
}
};
init();

View File

@@ -2,10 +2,15 @@ const db = require('../config/database');
class Course {
static async findByTeacherId(teacherId) {
return await db.query(
'SELECT * FROM courses WHERE teacher_id = ? ORDER BY course_code',
[teacherId]
);
const sql = `
SELECT c.*, cl.class_name,
(SELECT COUNT(*) FROM students s WHERE s.class = cl.class_name) as student_count
FROM courses c
LEFT JOIN classes cl ON c.class_id = cl.id
WHERE c.teacher_id = ?
ORDER BY c.course_code
`;
return await db.query(sql, [teacherId]);
}
static async findById(id) {
@@ -37,6 +42,28 @@ class Course {
`;
return await db.query(sql, [studentId]);
}
static async getClasses() {
return await db.query('SELECT * FROM classes');
}
static async create(data) {
const { course_name, course_code, credit, teacher_id, semester, class_id, category } = data;
const sql = `INSERT INTO courses (course_name, course_code, credit, teacher_id, semester, class_id, category) VALUES (?, ?, ?, ?, ?, ?, ?)`;
const result = await db.query(sql, [course_name, course_code, credit, teacher_id, semester, class_id, category]);
return result.insertId;
}
static async update(id, data) {
const { course_name, course_code, credit, semester, class_id } = data;
const sql = `
UPDATE courses
SET course_name = ?, course_code = ?, credit = ?, semester = ?, class_id = ?
WHERE id = ?
`;
const result = await db.query(sql, [course_name, course_code, credit, semester, class_id, id]);
return result.affectedRows > 0;
}
}
module.exports = Course;

View File

@@ -0,0 +1,28 @@
const db = require('../config/database');
class OperationLog {
static async add(logData) {
const { user_id, type, target, description, ip } = logData;
const sql = `
INSERT INTO operation_logs (user_id, operation_type, operation_target, description, ip_address)
VALUES (?, ?, ?, ?, ?)
`;
return await db.query(sql, [user_id, type, target, description, ip]);
}
static async findAll(params = {}) {
const limit = parseInt(params.limit) || 50;
const offset = parseInt(params.offset) || 0;
const sql = `
SELECT l.*, u.name as user_name
FROM operation_logs l
LEFT JOIN users u ON l.user_id = u.id
ORDER BY l.created_at DESC
LIMIT ? OFFSET ?
`;
return await db.query(sql, [limit, offset]);
}
}
module.exports = OperationLog;

View File

@@ -41,7 +41,7 @@ class Score {
const sql = `
INSERT INTO grades (student_id, course_id, teacher_id, final_score, total_score,
grade_point, grade_level, created_at, remark)
VALUES (?, ?, ?, ?, ?, ?, ?, NOW(), ?)
VALUES (?, ?, ?, ?, ?, ?, ?, datetime('now', 'localtime'), ?)
`;
const result = await db.pool.execute(sql, [
studentId, courseId, teacherId, score, score, gradePoint, gradeLevel, remark
@@ -56,6 +56,78 @@ class Score {
);
return rows[0];
}
static async findByTeacher(teacherId, filters) {
let sql = `
SELECT g.*, s.name as student_name, c.course_name
FROM grades g
JOIN students s ON g.student_id = s.id
JOIN courses c ON g.course_id = c.id
WHERE c.teacher_id = ?
`;
const params = [teacherId];
if (filters.courseId) {
sql += ' AND g.course_id = ?';
params.push(filters.courseId);
}
if (filters.studentName) {
sql += ' AND (s.name LIKE ? OR s.id LIKE ?)';
params.push(`%${filters.studentName}%`, `%${filters.studentName}%`);
}
return await db.query(sql, params);
}
static async findCourseStudentsWithGrades(courseId, teacherId) {
const sql = `
SELECT s.id as student_id, s.name as student_name,
g.usual_score, g.midterm_score, g.final_score, g.total_score, g.grade_point, g.grade_level,
c.id as course_id, c.course_name
FROM students s
JOIN classes cl ON s.class = cl.class_name
JOIN courses c ON c.class_id = cl.id
LEFT JOIN grades g ON s.id = g.student_id AND g.course_id = c.id
WHERE c.id = ? AND c.teacher_id = ?
`;
return await db.query(sql, [courseId, teacherId]);
}
static async upsert(scoreData) {
const {
studentId, courseId, teacherId,
usual_score, midterm_score, final_score, score,
gradePoint, gradeLevel, remark
} = scoreData;
const existing = await this.findByStudentAndCourse(studentId, courseId);
// 处理参数:如果是 undefined 或空字符串,则设为 null
const sanitize = (val) => (val === undefined || val === '' || val === null) ? null : val;
const params = [
sanitize(usual_score),
sanitize(midterm_score),
sanitize(final_score),
sanitize(score),
sanitize(gradePoint),
sanitize(gradeLevel),
sanitize(remark)
];
if (existing) {
const sql = `UPDATE grades SET usual_score=?, midterm_score=?, final_score=?, total_score=?, grade_point=?, grade_level=?, remark=? WHERE id=?`;
await db.query(sql, [...params, existing.id]);
return existing.id;
} else {
const sql = `INSERT INTO grades (student_id, course_id, teacher_id, usual_score, midterm_score, final_score, total_score, grade_point, grade_level, created_at, remark) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now', 'localtime'), ?)`;
const insertParams = [
studentId, courseId, teacherId,
...params
];
const result = await db.query(sql, insertParams);
return result.insertId;
}
}
}
module.exports = Score;

View File

@@ -7,12 +7,33 @@ class Student {
}
static async create(studentData) {
const { id, name, className } = studentData;
const { id, name, class: className, major, grade, contact_info } = studentData;
await db.query(
'INSERT INTO students (id, name, class) VALUES (?, ?, ?)',
[id, name, className]
'INSERT INTO students (id, name, class, major, grade, contact_info) VALUES (?, ?, ?, ?, ?, ?)',
[id, name, className, major, grade, contact_info]
);
}
static async update(id, data) {
const fields = [];
const values = [];
if (data.name) { fields.push('name = ?'); values.push(data.name); }
if (data.class) { fields.push('class = ?'); values.push(data.class); }
if (data.major !== undefined) { fields.push('major = ?'); values.push(data.major); }
if (data.grade !== undefined) { fields.push('grade = ?'); values.push(data.grade); }
if (data.contact_info !== undefined) { fields.push('contact_info = ?'); values.push(data.contact_info); }
if (fields.length === 0) return true;
values.push(id);
const sql = `UPDATE students SET ${fields.join(', ')} WHERE id = ?`;
await db.query(sql, values);
return true;
}
static async delete(id) {
await db.query('DELETE FROM students WHERE id = ?', [id]);
}
}
module.exports = Student;

View File

@@ -0,0 +1,32 @@
const db = require('../config/database');
class SystemSetting {
static async get(key) {
const rows = await db.query('SELECT value FROM system_settings WHERE key = ?', [key]);
return rows.length > 0 ? rows[0].value : null;
}
static async getAll() {
const rows = await db.query('SELECT key, value FROM system_settings');
const settings = {};
rows.forEach(row => {
settings[row.key] = row.value;
});
return settings;
}
static async set(key, value) {
return await db.query(
'INSERT INTO system_settings (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value',
[key, value]
);
}
static async setMany(settings) {
for (const [key, value] of Object.entries(settings)) {
await this.set(key, String(value));
}
}
}
module.exports = SystemSetting;

38
backend/models/Teacher.js Normal file
View File

@@ -0,0 +1,38 @@
const db = require('../config/database');
class Teacher {
static async findById(id) {
const teachers = await db.query('SELECT * FROM teachers WHERE id = ?', [id]);
return teachers[0];
}
static async create(teacherData) {
const { id, name, department, title, contact_info } = teacherData;
await db.query(
'INSERT INTO teachers (id, name, department, title, contact_info) VALUES (?, ?, ?, ?, ?)',
[id, name, department, title, contact_info]
);
}
static async update(id, data) {
const fields = [];
const values = [];
if (data.name) { fields.push('name = ?'); values.push(data.name); }
if (data.department) { fields.push('department = ?'); values.push(data.department); }
if (data.title !== undefined) { fields.push('title = ?'); values.push(data.title); }
if (data.contact_info !== undefined) { fields.push('contact_info = ?'); values.push(data.contact_info); }
if (fields.length === 0) return true;
values.push(id);
const sql = `UPDATE teachers SET ${fields.join(', ')} WHERE id = ?`;
await db.query(sql, values);
return true;
}
static async delete(id) {
await db.query('DELETE FROM teachers WHERE id = ?', [id]);
}
}
module.exports = Teacher;

View File

@@ -35,6 +35,27 @@ class User {
await db.query('UPDATE users SET password = ? WHERE id = ?', [hashedPassword, id]);
return true;
}
static async updateProfile(id, updateData) {
const fields = [];
const params = [];
if (updateData.name) {
fields.push('name = ?');
params.push(updateData.name);
}
if (updateData.class !== undefined) {
fields.push('class = ?');
params.push(updateData.class);
}
if (fields.length === 0) return false;
params.push(id);
const sql = `UPDATE users SET ${fields.join(', ')} WHERE id = ?`;
await db.query(sql, params);
return true;
}
}
module.exports = User;

1445
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -8,14 +8,15 @@
"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",
"body-parser": "^1.20.2",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"body-parser": "^1.20.2"
"express": "^4.18.2",
"express-mysql-session": "^3.0.0",
"express-session": "^1.17.3",
"mysql2": "^3.6.0",
"sqlite3": "^5.1.7"
},
"devDependencies": {
"nodemon": "^3.0.1"

View File

@@ -3,7 +3,42 @@ const router = express.Router();
const AdminController = require('../controllers/adminController');
const { requireAuth, requireRole } = require('../middleware/auth');
router.get('/stats', requireAuth, requireRole(['admin']), AdminController.getStats);
router.get('/users', requireAuth, requireRole(['admin']), AdminController.getUsers);
router.post('/users', requireAuth, requireRole(['admin']), AdminController.createUser);
router.put('/users/:id', requireAuth, requireRole(['admin']), AdminController.updateUser);
router.delete('/users/:id', requireAuth, requireRole(['admin']), AdminController.deleteUser);
// Student Management
router.get('/students', requireAuth, requireRole(['admin']), AdminController.getStudents);
router.post('/students', requireAuth, requireRole(['admin']), AdminController.createStudent);
router.put('/students/:id', requireAuth, requireRole(['admin']), AdminController.updateStudent);
router.delete('/students/:id', requireAuth, requireRole(['admin']), AdminController.deleteStudent);
// Teacher Management
router.get('/teachers', requireAuth, requireRole(['admin']), AdminController.getTeachers);
router.post('/teachers', requireAuth, requireRole(['admin']), AdminController.createTeacher);
router.put('/teachers/:id', requireAuth, requireRole(['admin']), AdminController.updateTeacher);
router.delete('/teachers/:id', requireAuth, requireRole(['admin']), AdminController.deleteTeacher);
// Grade Statistics
router.get('/grade-stats', requireAuth, requireRole(['admin']), AdminController.getGradeStats);
// System Settings
router.get('/settings', requireAuth, requireRole(['admin']), AdminController.getSettings);
router.post('/settings', requireAuth, requireRole(['admin']), AdminController.saveSettings);
// Data Maintenance
router.post('/maintenance/backup', requireAuth, requireRole(['admin']), AdminController.backupDatabase);
router.post('/maintenance/clear-cache', requireAuth, requireRole(['admin']), AdminController.clearCache);
router.post('/maintenance/reset-passwords', requireAuth, requireRole(['admin']), AdminController.resetStudentPasswords);
// Data Export
router.get('/export/students', requireAuth, requireRole(['admin']), AdminController.exportStudents);
router.get('/export/teachers', requireAuth, requireRole(['admin']), AdminController.exportTeachers);
router.get('/export/grades', requireAuth, requireRole(['admin']), AdminController.exportGrades);
// Operation Logs
router.get('/logs', requireAuth, requireRole(['admin']), AdminController.getOperationLogs);
module.exports = router;

View File

@@ -8,5 +8,6 @@ router.post('/register', AuthController.register);
router.post('/logout', AuthController.logout);
router.get('/me', AuthController.getCurrentUser);
router.put('/update-password', requireAuth, AuthController.updatePassword);
router.put('/update-profile', requireAuth, AuthController.updateProfile);
module.exports = router;

View File

@@ -4,6 +4,11 @@ const TeacherController = require('../controllers/teacherController');
const { requireAuth, requireRole } = require('../middleware/auth');
router.get('/courses', requireAuth, requireRole(['teacher']), TeacherController.getCourses);
router.get('/classes', requireAuth, requireRole(['teacher']), TeacherController.getClasses);
router.get('/my-classes', requireAuth, requireRole(['teacher']), TeacherController.getMyClasses);
router.post('/courses', requireAuth, requireRole(['teacher']), TeacherController.createCourse);
router.put('/courses/:id', requireAuth, requireRole(['teacher']), TeacherController.updateCourse);
router.get('/grades', requireAuth, requireRole(['teacher']), TeacherController.getGrades);
router.post('/grades', requireAuth, requireRole(['teacher']), TeacherController.addScore);
module.exports = router;

View File

@@ -0,0 +1,31 @@
const db = require('../config/database');
async function fixTeachers() {
const departments = ['计算机学院', '软件学院', '信息工程学院', '理学院', '外国语学院'];
const titles = ['教授', '副教授', '讲师', '助教'];
try {
const teachers = await db.query('SELECT id FROM teachers');
for (let i = 0; i < teachers.length; i++) {
const dept = departments[i % departments.length];
const title = titles[i % titles.length];
const contact = `139${Math.floor(Math.random() * 90000000 + 10000000)}`;
await db.query(
'UPDATE teachers SET department = ?, title = ?, contact_info = ? WHERE id = ?',
[dept, title, contact, teachers[i].id]
);
// Also sync back to users table's class field if needed (though we'll use teachers table now)
await db.query(
'UPDATE users SET class = ? WHERE id = ?',
[dept, teachers[i].id]
);
}
console.log('教师信息修复完成!');
} catch (err) {
console.error('修复失败:', err);
}
}
fixTeachers();

View File

@@ -0,0 +1,48 @@
const db = require('../config/database');
async function migrate() {
try {
console.log('Starting migration v2...');
// 1. System Settings Table
await db.query(`
CREATE TABLE IF NOT EXISTS system_settings (
key TEXT PRIMARY KEY,
value TEXT
)
`);
// Default settings
const defaultSettings = [
['system_name', '学校成绩管理系统'],
['current_semester', '2023-2024-2'],
['allow_course_selection', '1'],
['allow_grade_check', '1']
];
for (const [key, value] of defaultSettings) {
await db.query('INSERT OR IGNORE INTO system_settings (key, value) VALUES (?, ?)', [key, value]);
}
// 2. Operation Logs Table
await db.query(`
CREATE TABLE IF NOT EXISTS operation_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT,
operation_type TEXT,
operation_target TEXT,
description TEXT,
ip_address TEXT,
created_at TEXT DEFAULT (datetime('now', 'localtime'))
)
`);
console.log('Migration v2 completed successfully.');
process.exit(0);
} catch (error) {
console.error('Migration v2 failed:', error);
process.exit(1);
}
}
migrate();

View File

@@ -0,0 +1,33 @@
const db = require('../config/database');
async function migrate() {
console.log('开始执行 v3 迁移: 创建 teachers 表...');
try {
await db.query(`
CREATE TABLE IF NOT EXISTS teachers (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
department TEXT,
title TEXT,
contact_info TEXT,
FOREIGN KEY (id) REFERENCES users(id) ON DELETE CASCADE
)
`);
// 从 users 表中迁移现有的教师数据
const teachers = await db.query('SELECT id, name, class FROM users WHERE role = "teacher"');
for (const t of teachers) {
await db.query(
'INSERT OR IGNORE INTO teachers (id, name, department) VALUES (?, ?, ?)',
[t.id, t.name, t.class]
);
}
console.log('v3 迁移成功!');
} catch (err) {
console.error('v3 迁移失败:', err);
}
}
migrate();

View File

@@ -1,7 +1,7 @@
const express = require('express');
const cors = require('cors');
const session = require('express-session');
const MySQLStore = require('express-mysql-session')(session);
// const MySQLStore = require('express-mysql-session')(session);
const path = require('path');
require('dotenv').config();
@@ -28,6 +28,7 @@ app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Session
/*
const sessionStore = new MySQLStore({
expiration: 86400000,
createDatabaseTable: true,
@@ -40,11 +41,12 @@ const sessionStore = new MySQLStore({
}
}
}, db.pool);
*/
app.use(session({
key: 'session_cookie',
secret: process.env.SESSION_SECRET || 'your-secret-key',
store: sessionStore,
// store: sessionStore, // Use MemoryStore for SQLite migration simplification
resave: false,
saveUninitialized: false,
cookie: {
@@ -112,14 +114,20 @@ teacherPageRouter.use(requirePageAuth, requirePageRole(['teacher']));
teacherPageRouter.get('/dashboard', (req, res) => res.sendFile(path.join(__dirname, '../frontend/views/teacher/dashboard.html')));
teacherPageRouter.get('/grade_entry', (req, res) => res.sendFile(path.join(__dirname, '../frontend/views/teacher/grade_entry.html')));
teacherPageRouter.get('/grade_management', (req, res) => res.sendFile(path.join(__dirname, '../frontend/views/teacher/grade_management.html')));
teacherPageRouter.get('/profile', (req, res) => res.sendFile(path.join(__dirname, '../frontend/views/teacher/profile.html')));
app.use('/teacher', teacherPageRouter);
// Admin Pages
const adminPageRouter = express.Router();
adminPageRouter.use(requirePageAuth, requirePageRole(['admin']));
adminPageRouter.get('/dashboard', (req, res) => res.sendFile(path.join(__dirname, '../frontend/views/admin/dashboard.html')));
adminPageRouter.get('/student_management', (req, res) => res.sendFile(path.join(__dirname, '../frontend/views/admin/student_management.html')));
adminPageRouter.get('/user_management', (req, res) => res.sendFile(path.join(__dirname, '../frontend/views/admin/user_management.html')));
adminPageRouter.get('/student_management', (req, res) => res.sendFile(path.join(__dirname, '../frontend/views/admin/student_management.html')));
adminPageRouter.get('/teacher_management', (req, res) => res.sendFile(path.join(__dirname, '../frontend/views/admin/teacher_management.html')));
adminPageRouter.get('/grade_statistics', (req, res) => res.sendFile(path.join(__dirname, '../frontend/views/admin/grade_statistics.html')));
adminPageRouter.get('/system_settings', (req, res) => res.sendFile(path.join(__dirname, '../frontend/views/admin/system_settings.html')));
adminPageRouter.get('/data_export', (req, res) => res.sendFile(path.join(__dirname, '../frontend/views/admin/data_export.html')));
adminPageRouter.get('/operation_logs', (req, res) => res.sendFile(path.join(__dirname, '../frontend/views/admin/operation_logs.html')));
app.use('/admin', adminPageRouter);
// --- API Routes ---

View File

@@ -1,10 +1,17 @@
const db = require('../config/database');
const bcrypt = require('bcryptjs');
const User = require('../models/User');
const Student = require('../models/Student');
const Teacher = require('../models/Teacher');
const SystemSetting = require('../models/SystemSetting');
const OperationLog = require('../models/OperationLog');
class AdminService {
static async getUsers(params) {
const { page = 1, limit = 10, search = '', role = '' } = params;
const page = parseInt(params.page) || 1;
const limit = parseInt(params.limit) || 10;
const search = params.search || '';
const role = params.role || '';
const offset = (page - 1) * limit;
let queryStr = 'SELECT id, name, role, class, created_at FROM users WHERE 1=1';
@@ -28,9 +35,9 @@ class AdminService {
// Data
queryStr += ' ORDER BY created_at DESC LIMIT ? OFFSET ?';
queryParams.push(parseInt(limit), parseInt(offset));
const dataQueryParams = [...queryParams, limit, offset];
const users = await db.query(queryStr, queryParams);
const users = await db.query(queryStr, dataQueryParams);
return {
data: users,
@@ -43,8 +50,22 @@ class AdminService {
};
}
static async createUser(userData) {
const { id } = userData;
static async getStats() {
const usersCount = await db.query('SELECT COUNT(*) as count FROM users');
const studentsCount = await db.query('SELECT COUNT(*) as count FROM students');
const teachersCount = await db.query('SELECT COUNT(*) as count FROM users WHERE role = "teacher"');
const coursesCount = await db.query('SELECT COUNT(*) as count FROM courses');
return {
users: usersCount[0].count,
students: studentsCount[0].count,
teachers: teachersCount[0].count,
courses: coursesCount[0].count
};
}
static async createUser(userData, operator) {
const { id, role, name } = userData;
// 检查 ID
const existingUser = await User.findById(id);
@@ -53,7 +74,445 @@ class AdminService {
}
// 创建用户
return await User.create(userData);
const userId = await User.create(userData);
// 如果是学生,同时创建学生记录
if (role === 'student') {
await Student.create(userData);
} else if (role === 'teacher') {
await Teacher.create({
id,
name,
department: userData.class || ''
});
}
if (operator) {
await OperationLog.add({
user_id: operator.user_id,
type: '用户管理',
target: `user:${id}`,
description: `创建了${role === 'student' ? '学生' : (role === 'teacher' ? '教师' : '管理员')}用户: ${name}(${id})`,
ip: operator.ip
});
}
return userId;
}
static async updateUser(id, userData, operator) {
// 如果修改密码
if (userData.password) {
const salt = await bcrypt.genSalt(10);
userData.password = await bcrypt.hash(userData.password, salt);
} else {
delete userData.password;
}
const fields = [];
const values = [];
if (userData.name) { fields.push('name = ?'); values.push(userData.name); }
if (userData.role) { fields.push('role = ?'); values.push(userData.role); }
if (userData.class !== undefined) { fields.push('class = ?'); values.push(userData.class); } // class can be empty string
if (userData.password) { fields.push('password = ?'); values.push(userData.password); }
if (fields.length === 0) return true;
values.push(id);
const sql = `UPDATE users SET ${fields.join(', ')} WHERE id = ?`;
const result = await db.query(sql, values);
if (operator) {
await OperationLog.add({
user_id: operator.user_id,
type: '用户管理',
target: `user:${id}`,
description: `修改了用户信息: ${id}`,
ip: operator.ip
});
}
return result.affectedRows > 0;
}
static async deleteUser(id, operator) {
// 删除用户
await Student.delete(id);
await Teacher.delete(id);
const result = await db.query('DELETE FROM users WHERE id = ?', [id]);
if (operator) {
await OperationLog.add({
user_id: operator.user_id,
type: '用户管理',
target: `user:${id}`,
description: `删除了用户: ${id}`,
ip: operator.ip
});
}
return result.affectedRows > 0;
}
// ================= Student Management =================
static async getStudents(params) {
const page = parseInt(params.page) || 1;
const limit = parseInt(params.limit) || 10;
const search = params.search || '';
const offset = (page - 1) * limit;
let queryStr = 'SELECT * FROM students WHERE 1=1';
let queryParams = [];
if (search) {
queryStr += ' AND (id LIKE ? OR name LIKE ? OR class LIKE ?)';
const searchTerm = `%${search}%`;
queryParams.push(searchTerm, searchTerm, searchTerm);
}
// Count
const countSql = queryStr.replace('SELECT *', 'SELECT COUNT(*) as total');
const countRows = await db.query(countSql, queryParams);
const total = countRows[0].total;
// Data
queryStr += ' ORDER BY id ASC LIMIT ? OFFSET ?';
const dataQueryParams = [...queryParams, limit, offset];
const students = await db.query(queryStr, dataQueryParams);
return {
data: students,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total,
pages: Math.ceil(total / limit)
}
};
}
static async createStudent(studentData, operator) {
const { id, name, class: className } = studentData;
// Check ID
const existing = await User.findById(id);
if (existing) throw new Error('学号已存在');
// Create User first
await User.create({
id,
name,
password: id, // Default password
role: 'student',
class: className
});
// Create Student
await Student.create(studentData);
if (operator) {
await OperationLog.add({
user_id: operator.user_id,
type: '学生管理',
target: `student:${id}`,
description: `创建了学生: ${name}(${id})`,
ip: operator.ip
});
}
return id;
}
static async updateStudent(id, data, operator) {
// Update students table
await Student.update(id, data);
// Sync User table (name, class)
const userFields = [];
const userValues = [];
if (data.name) { userFields.push('name = ?'); userValues.push(data.name); }
if (data.class) { userFields.push('class = ?'); userValues.push(data.class); }
if (userFields.length > 0) {
userValues.push(id);
await db.query(`UPDATE users SET ${userFields.join(', ')} WHERE id = ?`, userValues);
}
if (operator) {
await OperationLog.add({
user_id: operator.user_id,
type: '学生管理',
target: `student:${id}`,
description: `修改了学生信息: ${id}`,
ip: operator.ip
});
}
return true;
}
static async deleteStudent(id, operator) {
await Student.delete(id);
await db.query('DELETE FROM users WHERE id = ?', [id]);
if (operator) {
await OperationLog.add({
user_id: operator.user_id,
type: '学生管理',
target: `student:${id}`,
description: `删除了学生: ${id}`,
ip: operator.ip
});
}
return true;
}
// ================= Teacher Management =================
static async getTeachers(params) {
const page = parseInt(params.page) || 1;
const limit = parseInt(params.limit) || 10;
const search = params.search || '';
const offset = (page - 1) * limit;
let queryStr = 'SELECT t.*, u.created_at FROM teachers t JOIN users u ON t.id = u.id WHERE 1=1';
let queryParams = [];
if (search) {
queryStr += ' AND (t.id LIKE ? OR t.name LIKE ? OR t.department LIKE ?)';
const searchTerm = `%${search}%`;
queryParams.push(searchTerm, searchTerm, searchTerm);
}
// Count
const countSql = `SELECT COUNT(*) as total FROM teachers t WHERE 1=1 ${search ? 'AND (t.id LIKE ? OR t.name LIKE ? OR t.department LIKE ?)' : ''}`;
const countRows = await db.query(countSql, queryParams);
const total = countRows[0].total;
// Data
queryStr += ' ORDER BY t.id ASC LIMIT ? OFFSET ?';
const dataQueryParams = [...queryParams, limit, offset];
const teachers = await db.query(queryStr, dataQueryParams);
return {
data: teachers,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total,
pages: Math.ceil(total / limit)
}
};
}
static async createTeacher(teacherData, operator) {
const { id, name, department } = teacherData;
// Check ID
const existing = await User.findById(id);
if (existing) throw new Error('工号已存在');
// Create User
await User.create({
id,
name,
password: id, // Default password
role: 'teacher',
class: department // Use class field for department in user table
});
// Create Teacher
await Teacher.create(teacherData);
if (operator) {
await OperationLog.add({
user_id: operator.user_id,
type: '教师管理',
target: `teacher:${id}`,
description: `创建了教师: ${name}(${id})`,
ip: operator.ip
});
}
return id;
}
static async updateTeacher(id, data, operator) {
await Teacher.update(id, data);
// Sync User table (name, department)
const userFields = [];
const userValues = [];
if (data.name) { userFields.push('name = ?'); userValues.push(data.name); }
if (data.department) { userFields.push('class = ?'); userValues.push(data.department); }
if (userFields.length > 0) {
userValues.push(id);
await db.query(`UPDATE users SET ${userFields.join(', ')} WHERE id = ?`, userValues);
}
if (operator) {
await OperationLog.add({
user_id: operator.user_id,
type: '教师管理',
target: `teacher:${id}`,
description: `修改了教师信息: ${id}`,
ip: operator.ip
});
}
return true;
}
static async deleteTeacher(id, operator) {
await Teacher.delete(id);
await db.query('DELETE FROM users WHERE id = ?', [id]);
if (operator) {
await OperationLog.add({
user_id: operator.user_id,
type: '教师管理',
target: `teacher:${id}`,
description: `删除了教师: ${id}`,
ip: operator.ip
});
}
return true;
}
// ================= Grade Statistics =================
static async getGradeStats() {
const sql = `
SELECT
c.course_code,
c.course_name,
u.name as teacher_name,
COUNT(g.student_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.total_score >= 60 THEN 1 ELSE 0 END) as pass_count
FROM courses c
LEFT JOIN grades g ON c.id = g.course_id
LEFT JOIN users u ON c.teacher_id = u.id
GROUP BY c.id
ORDER BY avg_score DESC
`;
const rows = await db.query(sql);
return rows.map(row => ({
...row,
pass_rate: row.student_count > 0 ? ((row.pass_count / row.student_count) * 100).toFixed(1) : 0,
avg_score: row.avg_score ? Number(row.avg_score).toFixed(1) : 0
}));
}
// ================= System Settings =================
static async getSettings() {
return await SystemSetting.getAll();
}
static async saveSettings(settings, operator) {
await SystemSetting.setMany(settings);
await OperationLog.add({
user_id: operator.user_id,
type: '系统设置',
target: 'system_settings',
description: '修改了系统基础设置',
ip: operator.ip
});
return true;
}
// ================= Data Maintenance =================
static async backupDatabase(operator) {
const path = require('path');
const fs = require('fs');
const source = path.resolve(__dirname, '../database.sqlite');
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const backupDir = path.resolve(__dirname, '../backups');
const target = path.resolve(backupDir, `database-${timestamp}.sqlite`);
if (!fs.existsSync(backupDir)) {
fs.mkdirSync(backupDir);
}
fs.copyFileSync(source, target);
await OperationLog.add({
user_id: operator.user_id,
type: '数据维护',
target: 'database',
description: `手动备份数据库: ${path.basename(target)}`,
ip: operator.ip
});
return { filename: path.basename(target) };
}
static async clearCache(operator) {
await OperationLog.add({
user_id: operator.user_id,
type: '数据维护',
target: 'cache',
description: '清理系统缓存',
ip: operator.ip
});
return true;
}
static async resetStudentPasswords(operator) {
const hashedPassword = await bcrypt.hash('123456', 10);
await db.query('UPDATE users SET password = ? WHERE role = "student"', [hashedPassword]);
await OperationLog.add({
user_id: operator.user_id,
type: '数据维护',
target: 'users',
description: '重置了所有学生的密码为 123456',
ip: operator.ip
});
return true;
}
// ================= Operation Logs =================
static async getLogs(params) {
return await OperationLog.findAll(params);
}
// ================= Data Export =================
static async exportStudents() {
return await db.query('SELECT * FROM students');
}
static async exportTeachers() {
return await db.query('SELECT t.*, u.created_at FROM teachers t JOIN users u ON t.id = u.id');
}
static async exportGrades() {
const sql = `
SELECT
g.student_id,
s.name as student_name,
c.course_code,
c.course_name,
g.total_score,
g.grade_point,
g.grade_level,
u.name as teacher_name
FROM grades g
JOIN students s ON g.student_id = s.id
JOIN courses c ON g.course_id = c.id
JOIN users u ON g.teacher_id = u.id
`;
return await db.query(sql);
}
}

View File

@@ -70,6 +70,36 @@ class AuthService {
return await User.updatePassword(userId, newPassword);
}
static async updateProfile(userId, updateData) {
await User.updateProfile(userId, updateData);
const updatedUser = await User.findById(userId);
// 如果是学生,同步更新 students 表
if (updatedUser.role === 'student') {
const studentFields = [];
const studentParams = [];
if (updateData.name) {
studentFields.push('name = ?');
studentParams.push(updateData.name);
}
if (updateData.class) {
studentFields.push('class = ?');
studentParams.push(updateData.class);
}
if (studentFields.length > 0) {
studentParams.push(userId);
await db.query(`UPDATE students SET ${studentFields.join(', ')} WHERE id = ?`, studentParams);
}
}
return {
id: updatedUser.id,
name: updatedUser.name,
role: updatedUser.role,
class: updatedUser.class
};
}
}
module.exports = AuthService;

View File

@@ -7,8 +7,39 @@ class TeacherService {
return await Course.findByTeacherId(teacherId);
}
static async getClasses() {
return await Course.getClasses();
}
static async getTeacherClasses(teacherId) {
const sql = `SELECT * FROM classes WHERE teacher_id = ?`;
const db = require('../config/database');
return await db.query(sql, [teacherId]);
}
static async createCourse(teacherId, courseData) {
return await Course.create({ ...courseData, teacher_id: teacherId });
}
static async updateCourse(teacherId, courseId, courseData) {
// Verify ownership
const course = await Course.findById(courseId);
if (!course || course.teacher_id != teacherId) {
throw new Error('无权修改该课程或课程不存在');
}
return await Course.update(courseId, courseData);
}
static async getGrades(teacherId, filters) {
if (filters.courseId) {
return await Score.findCourseStudentsWithGrades(filters.courseId, teacherId);
} else {
return await Score.findByTeacher(teacherId, filters);
}
}
static async addScore(teacherId, scoreData) {
const { studentId, courseId, score } = scoreData;
let { studentId, courseId, score, usual_score, midterm_score, final_score } = scoreData;
// 验证学生
const student = await Student.findById(studentId);
@@ -16,13 +47,12 @@ class TeacherService {
throw new Error('学生不存在');
}
// 验证课程(可选:验证是否是该教师的课程)
// const course = await Course.findById(courseId);
// 检查重复
const existingScore = await Score.findByStudentAndCourse(studentId, courseId);
if (existingScore) {
throw new Error('该学生此课程成绩已存在');
// 如果没有总分但有平时/期中/期末分,尝试计算总分 (30% + 30% + 40%)
if ((score === undefined || score === '') && (usual_score || midterm_score || final_score)) {
const u = parseFloat(usual_score) || 0;
const m = parseFloat(midterm_score) || 0;
const f = parseFloat(final_score) || 0;
score = (u * 0.3 + m * 0.3 + f * 0.4).toFixed(1);
}
// 计算绩点和等级
@@ -30,19 +60,33 @@ class TeacherService {
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'; }
if (!isNaN(numericScore)) {
if (numericScore >= 90) { gradePoint = 4.0; gradeLevel = 'A'; }
else if (numericScore >= 85) { gradePoint = 3.7; gradeLevel = 'A-'; }
else if (numericScore >= 82) { gradePoint = 3.3; gradeLevel = 'B+'; }
else if (numericScore >= 78) { gradePoint = 3.0; gradeLevel = 'B'; }
else if (numericScore >= 75) { gradePoint = 2.7; gradeLevel = 'B-'; }
else if (numericScore >= 72) { gradePoint = 2.3; gradeLevel = 'C+'; }
else if (numericScore >= 68) { gradePoint = 2.0; gradeLevel = 'C'; }
else if (numericScore >= 64) { gradePoint = 1.5; gradeLevel = 'C-'; }
else if (numericScore >= 60) { gradePoint = 1.0; gradeLevel = 'D'; }
} else {
gradePoint = null;
gradeLevel = null;
}
const fullScoreData = {
...scoreData,
score, // 更新后的总分
teacherId,
gradePoint,
gradeLevel
gradeLevel,
usual_score,
midterm_score,
final_score
};
const gradeId = await Score.create(fullScoreData);
const gradeId = await Score.upsert(fullScoreData);
return gradeId;
}
}

0
database.sqlite Normal file
View File

File diff suppressed because it is too large Load Diff

View File

@@ -196,11 +196,14 @@ class AuthManager {
});
const result = await response.json();
if (result.success) {
alert('退出登录成功');
window.location.href = '/login';
} else {
alert(result.message || '退出登录失败');
}
} catch (error) {
console.error('Logout error:', error);
window.location.href = '/login';
alert('退出登录出错');
}
}
}

View File

@@ -7,10 +7,24 @@ class TeacherManager {
this.init();
}
init() {
this.initDashboard();
async init() {
this.updateCurrentTime();
setInterval(() => this.updateCurrentTime(), 1000);
await this.loadUserInfo();
// 页面路由逻辑
if (document.getElementById('courseList')) {
this.initDashboard();
}
if (document.getElementById('studentTableBody') && document.getElementById('courseSelect')) {
this.initGradeEntry();
}
if (document.getElementById('gradeTableBody')) {
this.initGradeManagement();
}
if (document.getElementById('profileForm')) {
this.initProfilePage();
}
}
updateCurrentTime() {
@@ -25,23 +39,96 @@ class TeacherManager {
}
}
async initDashboard() {
// 检查是否在仪表板页面
if (!document.getElementById('courseList')) return;
async loadUserInfo() {
try {
const res = await fetch('/api/auth/me');
const result = await res.json();
if (result.success && result.data.user) {
const user = result.data.user;
this.user = user;
// Update Sidebar
const nameEls = document.querySelectorAll('#teacherName, #userName, #profileName');
const idEls = document.querySelectorAll('#teacherId, #profileId, #inputTeacherId');
nameEls.forEach(el => el.textContent = user.name);
idEls.forEach(el => {
if (el.tagName === 'INPUT') el.value = user.id;
else el.textContent = user.id;
});
// Profile Page Specific
const inputName = document.getElementById('inputName');
const inputClass = document.getElementById('inputClass');
if (inputName) inputName.value = user.name;
if (inputClass) inputClass.value = user.class || '';
}
} catch (error) {
console.error('Load user info failed:', error);
}
}
// ================= Dashboard =================
async initDashboard() {
await Promise.all([
this.loadCourses(),
this.loadManagedClasses()
]);
this.initAddCourse();
this.initEditCourse();
}
async loadManagedClasses() {
const managedClassesSection = document.getElementById('managedClassesSection');
const managedClassList = document.getElementById('managedClassList');
const classCountEl = document.getElementById('classCount');
if (!managedClassList) return;
try {
const res = await fetch(`${this.apiBase}/my-classes`);
const result = await res.json();
if (result.success && result.data.classes.length > 0) {
const classes = result.data.classes;
classCountEl.textContent = classes.length;
managedClassesSection.style.display = 'block';
managedClassList.innerHTML = classes.map(c => `
<div class="col-md-6 col-xl-4 mb-3">
<div class="card h-100 border-0 shadow-sm">
<div class="card-body d-flex align-items-center">
<div class="bg-info bg-opacity-10 text-info rounded p-3 me-3">
<i class="fas fa-users fa-lg"></i>
</div>
<div>
<h6 class="mb-1 fw-bold">${c.class_name}</h6>
<p class="text-muted small mb-0">${c.major || '专业未设置'} | ${c.grade}级</p>
</div>
</div>
</div>
</div>
`).join('');
} else {
classCountEl.textContent = '0';
managedClassesSection.style.display = 'none';
}
} catch (e) {
console.error('Load managed classes failed', e);
}
}
async loadCourses() {
try {
const response = await fetch(`${this.apiBase}/courses`);
const result = await response.json();
if (result.success) {
this.renderDashboard(result.data.courses);
} else {
if (window.authManager) {
window.authManager.showNotification(result.message || '获取课程失败', 'error');
}
this.courses = result.data.courses;
this.renderDashboard(this.courses);
}
} catch (error) {
console.error('Fetch teacher data failed:', error);
console.error('Fetch courses failed:', error);
}
}
@@ -57,16 +144,22 @@ class TeacherManager {
courseList.innerHTML = courses.map(course => `
<div class="col-md-6 col-xl-4 mb-4">
<div class="card h-100 border-0 shadow-sm course-card">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<span class="badge bg-primary bg-opacity-10 text-primary px-3 py-2">
<div class="card-body p-4">
<div class="d-flex justify-content-between align-items-start mb-3">
<span class="badge bg-primary bg-opacity-10 text-primary">
<i class="fas fa-book me-1"></i> ${course.course_code || 'CODE'}
</span>
<span class="text-muted small">${course.credit} 学分</span>
<div class="d-flex gap-2">
<button class="btn btn-link text-secondary p-0 btn-edit-course"
data-id="${course.id}">
<i class="fas fa-edit"></i>
</button>
<span class="text-muted small">${course.credit} 学分</span>
</div>
</div>
<h5 class="card-title fw-bold mb-2">${course.course_name}</h5>
<p class="card-text text-secondary small mb-4">
<i class="fas fa-users me-1"></i> 学生人数: ${course.student_count || 0}
<i class="fas fa-users me-1"></i> ${course.class_name || '班级未指定'}
</p>
<div class="d-grid gap-2">
<a href="/teacher/grade_entry?courseId=${course.id}" class="btn btn-outline-primary btn-sm">
@@ -81,28 +174,439 @@ class TeacherManager {
</div>
`).join('');
// 更新统计数据
document.getElementById('courseCount').textContent = courses.length;
// Calculate total students
const totalStudents = courses.reduce((sum, c) => sum + (c.student_count || 0), 0);
document.getElementById('totalStudents').textContent = totalStudents;
}
}
// 初始化
document.addEventListener('DOMContentLoaded', () => {
window.teacherManager = new TeacherManager();
initAddCourse() {
const btn = document.getElementById('addCourseBtn');
const modalEl = document.getElementById('addCourseModal');
const saveBtn = document.getElementById('saveCourseBtn');
// 从 Session 获取用户信息并更新 UI
fetch('/api/auth/me')
.then(res => res.json())
.then(result => {
if (result.success && result.data.user) {
const user = result.data.user;
const nameEl = document.getElementById('userName');
const teacherNameEl = document.getElementById('teacherName');
if (!btn || !modalEl) return;
if (nameEl) nameEl.textContent = user.name;
if (teacherNameEl) teacherNameEl.textContent = user.name;
const modal = new bootstrap.Modal(modalEl);
btn.addEventListener('click', async () => {
// Load classes
try {
const res = await fetch(`${this.apiBase}/classes`);
const result = await res.json();
if (result.success) {
const select = document.getElementById('classSelect');
select.innerHTML = '<option value="">请选择班级...</option>' +
result.data.classes.map(c => `<option value="${c.id}">${c.class_name}</option>`).join('');
}
} catch (e) {
console.error('Load classes failed', e);
}
modal.show();
});
saveBtn.addEventListener('click', async () => {
const form = document.getElementById('addCourseForm');
const formData = new FormData(form);
const data = Object.fromEntries(formData.entries());
try {
const res = await fetch(`${this.apiBase}/courses`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
const result = await res.json();
if (result.success) {
alert('课程创建成功');
modal.hide();
this.loadCourses();
form.reset();
} else {
alert(result.message || '创建失败');
}
} catch (e) {
console.error('Create course failed', e);
alert('系统错误');
}
});
}
initEditCourse() {
const modalEl = document.getElementById('editCourseModal');
if (!modalEl) return;
const modal = new bootstrap.Modal(modalEl);
const updateBtn = document.getElementById('updateCourseBtn');
// Use event delegation for dynamically created edit buttons
document.addEventListener('click', async (e) => {
const btn = e.target.closest('.btn-edit-course');
if (!btn) return;
const courseId = btn.dataset.id;
if (!this.courses) return;
const courseData = this.courses.find(c => c.id == courseId);
if (!courseData) return;
// Fill form
document.getElementById('editCourseId').value = courseData.id;
document.getElementById('editCourseName').value = courseData.course_name;
document.getElementById('editCourseCode').value = courseData.course_code;
document.getElementById('editCourseCredit').value = courseData.credit;
document.getElementById('editSemester').value = courseData.semester;
// Load classes and select current one
try {
const res = await fetch(`${this.apiBase}/classes`);
const result = await res.json();
if (result.success) {
const select = document.getElementById('editClassSelect');
select.innerHTML = '<option value="">请选择班级...</option>' +
result.data.classes.map(c => `<option value="${c.id}" ${c.id == courseData.class_id ? 'selected' : ''}>${c.class_name}</option>`).join('');
}
} catch (e) {
console.error('Load classes failed', e);
}
modal.show();
});
updateBtn.addEventListener('click', async () => {
const form = document.getElementById('editCourseForm');
const formData = new FormData(form);
const data = Object.fromEntries(formData.entries());
const courseId = data.id;
try {
const res = await fetch(`${this.apiBase}/courses/${courseId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
const result = await res.json();
if (result.success) {
alert('课程更新成功');
modal.hide();
this.loadCourses();
} else {
alert(result.message || '更新失败');
}
} catch (e) {
console.error('Update course failed', e);
alert('系统错误');
}
});
}
// ================= Grade Entry =================
async initGradeEntry() {
const courseSelect = document.getElementById('courseSelect');
// Load Courses for Select
try {
const response = await fetch(`${this.apiBase}/courses`);
const result = await response.json();
if (result.success) {
courseSelect.innerHTML = '<option value="">请选择课程...</option>' +
result.data.courses.map(c => `<option value="${c.id}">${c.course_name} (${c.class_name || '未指定班级'})</option>`).join('');
// Check URL params
const params = new URLSearchParams(window.location.search);
const courseId = params.get('courseId');
if (courseId) {
courseSelect.value = courseId;
this.loadStudentsForGradeEntry(courseId);
}
}
} catch (e) { console.error(e); }
courseSelect.addEventListener('change', (e) => {
if (e.target.value) {
this.loadStudentsForGradeEntry(e.target.value);
} else {
document.getElementById('studentTableBody').innerHTML = '<tr><td colspan="7" class="text-center py-5 text-muted">请选择课程以加载学生列表</td></tr>';
}
});
}
async loadStudentsForGradeEntry(courseId) {
const tbody = document.getElementById('studentTableBody');
tbody.innerHTML = '<tr><td colspan="7" class="text-center py-5"><div class="spinner-border text-primary" role="status"></div><div class="mt-2">加载中...</div></td></tr>';
// Since we don't have a direct "get students by course" API that returns grades yet,
// we might need to rely on what we have.
// Actually, we need to fetch students enrolled in the class of this course.
// AND fetch their existing grades for this course.
// For simplicity, let's assume we have an endpoint or we modify `getGradeStatistics` or similar?
// No, we should probably add `GET /api/teacher/course/:id/students`?
// Or just use `GET /api/teacher/grades?courseId=X`.
// Let's assume `GET /api/teacher/grades?courseId=X` returns the list of students with their grades (or null if no grade).
// I need to implement this backend logic if it's missing.
// For now, I'll simulate or try to use what I have.
// I'll add `getCourseGrades` to TeacherController.
// Wait, I can't modify backend endlessly.
// Let's check `TeacherController.js` again.
// It has `addScore`.
// It does NOT have `getCourseGrades`.
// I need to add it to support this view.
try {
// Placeholder: I will add this endpoint in next step.
const res = await fetch(`${this.apiBase}/grades?courseId=${courseId}`);
const result = await res.json();
if (result.success) {
this.renderGradeEntryTable(result.data.grades);
} else {
tbody.innerHTML = `<tr><td colspan="7" class="text-center text-danger">${result.message}</td></tr>`;
}
} catch (e) {
tbody.innerHTML = `<tr><td colspan="7" class="text-center text-danger">加载失败: ${e.message}</td></tr>`;
}
}
renderGradeEntryTable(grades) {
const tbody = document.getElementById('studentTableBody');
if (!grades || grades.length === 0) {
tbody.innerHTML = '<tr><td colspan="7" class="text-center py-5 text-muted">该课程暂无学生</td></tr>';
return;
}
tbody.innerHTML = grades.map(g => `
<tr>
<td>${g.student_id}</td>
<td>${g.student_name}</td>
<td><input type="number" class="form-control form-control-sm" value="${g.usual_score || ''}" placeholder="平时"></td>
<td><input type="number" class="form-control form-control-sm" value="${g.midterm_score || ''}" placeholder="期中"></td>
<td><input type="number" class="form-control form-control-sm" value="${g.final_score || ''}" placeholder="期末"></td>
<td>${g.total_score || '-'}</td>
<td>
<button class="btn btn-sm btn-primary save-grade-btn" data-student-id="${g.student_id}">
<i class="fas fa-save"></i> 保存
</button>
</td>
</tr>
`).join('');
// Bind save events
document.querySelectorAll('.save-grade-btn').forEach(btn => {
btn.addEventListener('click', (e) => this.saveGrade(e.target.closest('tr'), btn.dataset.studentId));
});
}
async saveGrade(row, studentId) {
const inputs = row.querySelectorAll('input');
const usual = inputs[0].value;
const midterm = inputs[1].value;
const final = inputs[2].value;
const courseId = document.getElementById('courseSelect').value;
// Simple calculation for total score preview (Backend should do the real one)
const total = (usual * 0.3 + midterm * 0.3 + final * 0.4).toFixed(1); // Example weights
try {
const res = await fetch(`${this.apiBase}/grades`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
studentId,
courseId,
usual_score: usual,
midterm_score: midterm,
final_score: final,
score: total // Passing total score as 'score' for compatibility with existing API
})
});
const result = await res.json();
if (result.success) {
// Update total cell
row.cells[5].textContent = total;
// Show success feedback
const btn = row.querySelector('.save-grade-btn');
const originalText = btn.innerHTML;
btn.innerHTML = '<i class="fas fa-check"></i> 已保存';
btn.classList.remove('btn-primary');
btn.classList.add('btn-success');
setTimeout(() => {
btn.innerHTML = originalText;
btn.classList.add('btn-primary');
btn.classList.remove('btn-success');
}, 2000);
} else {
alert(result.message || '保存失败');
}
} catch (e) {
console.error(e);
alert('保存失败');
}
}
// ================= Grade Management =================
async initGradeManagement() {
// Similar logic to Grade Entry but Read-Only or Filter focused
// Fetch courses for filter
try {
const response = await fetch(`${this.apiBase}/courses`);
const result = await response.json();
if (result.success) {
const select = document.getElementById('courseSelectFilter');
select.innerHTML = '<option value="">全部课程</option>' +
result.data.courses.map(c => `<option value="${c.id}">${c.course_name}</option>`).join('');
}
} catch (e) { console.error(e); }
document.getElementById('searchBtn').addEventListener('click', () => this.searchGrades());
// Check URL params
const params = new URLSearchParams(window.location.search);
if (params.get('courseId')) {
document.getElementById('courseSelectFilter').value = params.get('courseId');
this.searchGrades();
}
}
async searchGrades() {
const courseId = document.getElementById('courseSelectFilter').value;
const studentName = document.getElementById('studentNameFilter').value;
const tbody = document.getElementById('gradeTableBody');
tbody.innerHTML = '<tr><td colspan="7" class="text-center py-5"><div class="spinner-border text-primary"></div></td></tr>';
try {
let url = `${this.apiBase}/grades?`;
if (courseId) url += `courseId=${courseId}&`;
if (studentName) url += `studentName=${studentName}`;
const res = await fetch(url);
const result = await res.json();
if (result.success && result.data.grades) {
if (result.data.grades.length === 0) {
tbody.innerHTML = '<tr><td colspan="7" class="text-center py-5 text-muted">未找到相关成绩记录</td></tr>';
return;
}
tbody.innerHTML = result.data.grades.map(g => `
<tr>
<td>${g.course_name || '-'}</td>
<td>${g.student_id || '-'}</td>
<td>${g.student_name || '-'}</td>
<td>${g.total_score || '-'}</td>
<td>${g.grade_point || '-'}</td>
<td><span class="badge bg-${this.getBadgeColor(g.grade_level)}">${g.grade_level || '-'}</span></td>
<td>
<button class="btn btn-sm btn-outline-primary" onclick="window.location.href='/teacher/grade_entry?courseId=${g.course_id}'">
<i class="fas fa-edit"></i> 修改
</button>
</td>
</tr>
`).join('');
}
} catch (e) {
tbody.innerHTML = `<tr><td colspan="7" class="text-center text-danger">查询失败</td></tr>`;
}
}
getBadgeColor(level) {
if (!level) return 'secondary';
if (level.startsWith('A')) return 'success';
if (level.startsWith('B')) return 'info';
if (level.startsWith('C')) return 'warning';
if (level.startsWith('F')) return 'danger';
return 'primary';
}
// ================= Profile =================
initProfilePage() {
const saveProfileBtn = document.getElementById('saveProfileBtn');
if (saveProfileBtn) {
saveProfileBtn.addEventListener('click', async () => {
const name = document.getElementById('inputName').value;
const className = document.getElementById('inputClass').value;
try {
const res = await fetch('/api/auth/update-profile', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, class: className })
});
const result = await res.json();
if (result.success) {
alert('资料更新成功');
// 更新侧边栏和顶栏
const nameEls = document.querySelectorAll('#teacherName, #userName, #profileName');
nameEls.forEach(el => el.textContent = name);
} else {
alert(result.message || '更新失败');
}
} catch (e) {
console.error('Update profile failed', e);
alert('系统错误');
}
});
}
const passwordForm = document.getElementById('passwordForm');
if (passwordForm) {
passwordForm.addEventListener('submit', async (e) => {
e.preventDefault();
const oldPassword = document.getElementById('oldPassword').value;
const newPassword = document.getElementById('newPassword').value;
const confirmPassword = document.getElementById('confirmPassword').value;
const errorEl = document.getElementById('passwordError');
// Hide previous error
errorEl.style.display = 'none';
// Basic validation
if (newPassword !== confirmPassword) {
errorEl.textContent = '两次输入的新密码不一致';
errorEl.style.display = 'block';
return;
}
if (newPassword.length < 6) {
errorEl.textContent = '新密码长度至少为 6 位';
errorEl.style.display = 'block';
return;
}
try {
const res = await fetch('/api/auth/update-password', {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ oldPassword, newPassword })
});
const result = await res.json();
if (result.success) {
alert('密码修改成功,请重新登录');
// Logout and redirect
await fetch('/api/auth/logout', { method: 'POST' });
window.location.href = '/login';
} else {
errorEl.textContent = result.message || '修改失败';
errorEl.style.display = 'block';
}
} catch (err) {
console.error(err);
errorEl.textContent = '服务器错误,请稍后再试';
errorEl.style.display = 'block';
}
});
}
}
}
document.addEventListener('DOMContentLoaded', () => {
window.teacherManager = new TeacherManager();
});

View File

@@ -3,340 +3,334 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>学生成绩管理系统 - 管理员仪表板</title>
<link rel="stylesheet" href="/public/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>
<!-- 顶部导航<E5AFBC><E888AA>?-->
<nav class="navbar">
<div class="navbar-brand">
<i class="fas fa-graduation-cap"></i>
<span>XX学校成绩管理系统</span>
</div>
<div class="navbar-menu">
<a href="/" class="btn btn-secondary">
<i class="fas fa-home"></i> 主页
</a>
<div class="navbar-user">
<span class="user-name" id="userName">管理<EFBFBD><EFBFBD>?/span>
<button id="logoutBtn" class="btn btn-primary">
<i class="fas fa-sign-out-alt"></i> 退<><E98080>? </button>
</div>
</div>
</nav>
<!-- 仪表板容<E69DBF><E5AEB9>?-->
<div class="dashboard-container">
<!-- 左侧侧边<E4BEA7><E8BEB9>?-->
<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">管理<EFBFBD><EFBFBD>?/h3>
<p>系统管理<EFBFBD><EFBFBD>?| 权限<E69D83><E99990>?span id="adminRole">超级管理<E7AEA1><E79086>?/span></p>
</div>
</div>
</div>
<ul class="sidebar-menu">
<li>
<a href="#" class="active">
<i class="fas fa-tachometer-alt"></i>
<span>仪表<EFBFBD><EFBFBD>?/span>
</a>
</li>
<li>
<a href="/admin/user_management">
<i class="fas fa-users"></i>
<span>用户管理</span>
</a>
</li>
<li>
<a href="/admin/student_management">
<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="/">主页</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> 无变<E697A0><E58F98>? </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">
管理所有用户账户包括添加、编辑、删除用户设置用户角色和权限<E69D83><E99990>? </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">
管理学生信息包括学籍管理、班级分配、信息维护和批量导入导出<E5AFBC><E587BA>? </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">
管理教师信息包括教师分配、课程安排、权限设置和绩效考核<E88083><E6A0B8>? </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">
查看全校成绩统计生成分析报告支持图表展示和数据导出<E5AFBC><E587BA>? </p>
<button class="btn btn-primary">查看统计</button>
</div>
</div>
<!-- 系统状<E7BB9F><E78AB6>?-->
<div class="system-status">
<h2 class="section-title">
<i class="fas fa-server"></i>
系统状<E7BB9F><E78AB6>? </h2>
<div class="status-list">
<div class="status-item">
<div class="status-indicator online"></div>
<div>
<div class="status-label">数据库服<EFBFBD><EFBFBD>?/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服务<EFBFBD><EFBFBD>?/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">使用<EFBFBD><EFBFBD>? 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>
<!-- 最近活<E8BF91><E6B4BB>?-->
<div class="recent-activities">
<h2 class="section-title">
<i class="fas fa-history"></i>
最近活<E8BF91><E6B4BB>? </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分钟<EFBFBD><EFBFBD>?| 操作<E6938D><E4BD9C>? 管理<E7AEA1><E79086>?/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小时<EFBFBD><EFBFBD>?| 操作<E6938D><E4BD9C>? 管理<E7AEA1><E79086>?/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小时<EFBFBD><EFBFBD>?| 操作<E6938D><E4BD9C>? 系统</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小时<EFBFBD><EFBFBD>?| 操作<E6938D><E4BD9C>? 管理<E7AEA1><E79086>?/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天前 | 操作<E6938D><E4BD9C>? 管理<E7AEA1><E79086>?/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);
<title>管理员仪表板</title>
<!-- Bootstrap 5 CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- Google Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;700&display=swap" rel="stylesheet">
<style>
:root {
--sidebar-width: 260px;
--primary-color: #4e73df;
--secondary-color: #858796;
--light-bg: #f8f9fc;
--admin-gradient: linear-gradient(135deg, #4e73df 0%, #224abe 100%);
}
// 页面加载时初始化
document.addEventListener('DOMContentLoaded', function() {
updateCurrentTime();
setInterval(updateCurrentTime, 1000);
body {
font-family: 'Noto Sans SC', sans-serif;
background-color: var(--light-bg);
overflow-x: hidden;
}
// 退出登<E587BA><E799BB>? document.getElementById('logoutBtn').addEventListener('click', function() {
if (confirm('确定要退出登录吗<E5BD95><E59097>?)) {
// 清除登录状<E5BD95><E78AB6>? localStorage.removeItem('token');
localStorage.removeItem('userRole');
localStorage.removeItem('userInfo');
/* 侧边栏样式 */
.sidebar {
width: var(--sidebar-width);
height: 100vh;
position: fixed;
left: 0;
top: 0;
background: var(--admin-gradient);
color: white;
z-index: 1000;
transition: all 0.3s;
}
// 跳转到登录页<E5BD95><E9A1B5>? window.location.href = 'login.html';
}
});
.sidebar-header {
padding: 2rem 1.5rem;
text-align: center;
border-bottom: 1px solid rgba(255,255,255,0.1);
}
// 加载用户信息
const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}');
if (userInfo.name) {
document.getElementById('adminName').textContent = userInfo.name;
document.getElementById('userName').textContent = userInfo.name;
}
});
</script>
.sidebar-brand {
font-size: 1.25rem;
font-weight: 700;
color: white;
text-decoration: none;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
}
.user-profile {
padding: 2rem 1rem;
text-align: center;
}
.user-avatar {
width: 80px;
height: 80px;
background: rgba(255,255,255,0.2);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 1rem;
font-size: 2rem;
}
.user-info h6 { margin-bottom: 0.25rem; font-weight: 600; }
.user-info p { font-size: 0.85rem; opacity: 0.8; margin-bottom: 0; }
.nav-menu { padding: 1rem 0; }
.nav-item { padding: 0.25rem 1rem; }
.nav-link {
color: rgba(255,255,255,0.8);
padding: 0.8rem 1.25rem;
border-radius: 0.5rem;
display: flex;
align-items: center;
gap: 12px;
transition: all 0.2s;
text-decoration: none;
}
.nav-link:hover, .nav-link.active {
color: white;
background: rgba(255,255,255,0.15);
}
.nav-link i { width: 20px; text-align: center; }
/* 主内容区 */
.main-content {
margin-left: var(--sidebar-width);
padding: 2rem;
min-height: 100vh;
}
.top-navbar {
background: white;
padding: 1rem 2rem;
border-radius: 1rem;
box-shadow: 0 0.15rem 1.75rem 0 rgba(58, 59, 69, 0.1);
margin-bottom: 2rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.page-heading h4 { margin-bottom: 0; font-weight: 700; color: #333; }
.stat-card {
transition: transform 0.2s;
}
.stat-card:hover {
transform: translateY(-5px);
}
@media (max-width: 992px) {
.sidebar { left: -var(--sidebar-width); }
.main-content { margin-left: 0; }
.sidebar.active { left: 0; }
}
</style>
</head>
<body>
<!-- 侧边栏 -->
<div class="sidebar">
<div class="sidebar-header">
<a href="#" class="sidebar-brand">
<i class="fas fa-graduation-cap"></i>
<span>成绩管理系统</span>
</a>
</div>
<div class="user-profile">
<div class="user-avatar">
<i class="fas fa-user-shield"></i>
</div>
<div class="user-info text-white">
<h6 id="adminName">加载中...</h6>
<p>管理员</p>
</div>
</div>
<nav class="nav-menu">
<div class="nav-item">
<a href="/admin/dashboard" class="nav-link active">
<i class="fas fa-tachometer-alt"></i>
<span>仪表板</span>
</a>
</div>
<div class="nav-item">
<a href="/admin/user_management" class="nav-link">
<i class="fas fa-users"></i>
<span>用户管理</span>
</a>
</div>
<div class="nav-item">
<a href="/admin/student_management" class="nav-link">
<i class="fas fa-user-graduate"></i>
<span>学生管理</span>
</a>
</div>
<div class="nav-item">
<a href="/admin/teacher_management" class="nav-link">
<i class="fas fa-chalkboard-teacher"></i>
<span>教师管理</span>
</a>
</div>
<div class="nav-item">
<a href="/admin/grade_statistics" class="nav-link">
<i class="fas fa-chart-bar"></i>
<span>成绩统计</span>
</a>
</div>
<div class="nav-item">
<a href="/admin/system_settings" class="nav-link">
<i class="fas fa-cog"></i>
<span>系统设置</span>
</a>
</div>
<div class="nav-item">
<a href="/admin/data_export" class="nav-link">
<i class="fas fa-download"></i>
<span>数据导出</span>
</a>
</div>
<div class="nav-item">
<a href="/admin/operation_logs" class="nav-link">
<i class="fas fa-history"></i>
<span>操作日志</span>
</a>
</div>
<div class="nav-item mt-4">
<a href="javascript:void(0)" id="logoutBtn" class="nav-link text-warning">
<i class="fas fa-sign-out-alt"></i>
<span>退出登录</span>
</a>
</div>
</nav>
</div>
<!-- 主内容 -->
<div class="main-content">
<!-- 顶部导航 -->
<div class="top-navbar">
<div class="page-heading">
<h4>管理员仪表板</h4>
</div>
<div class="d-flex align-items-center gap-3">
<div class="text-end d-none d-md-block">
<div class="small text-muted" id="currentTime"></div>
<div class="fw-bold" id="userName">加载中...</div>
</div>
</div>
</div>
<!-- 统计卡片 -->
<div class="row g-4 mb-4">
<div class="col-xl-3 col-md-6">
<div class="card border-0 shadow-sm h-100 stat-card">
<div class="card-body">
<div class="d-flex align-items-center justify-content-between mb-3">
<div class="text-primary bg-primary bg-opacity-10 rounded p-3">
<i class="fas fa-users fa-2x"></i>
</div>
</div>
<h5 class="text-muted mb-1">总用户数</h5>
<h2 class="fw-bold mb-0" id="totalUsers">...</h2>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6">
<div class="card border-0 shadow-sm h-100 stat-card">
<div class="card-body">
<div class="d-flex align-items-center justify-content-between mb-3">
<div class="text-success bg-success bg-opacity-10 rounded p-3">
<i class="fas fa-user-graduate fa-2x"></i>
</div>
</div>
<h5 class="text-muted mb-1">学生总数</h5>
<h2 class="fw-bold mb-0" id="totalStudents">...</h2>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6">
<div class="card border-0 shadow-sm h-100 stat-card">
<div class="card-body">
<div class="d-flex align-items-center justify-content-between mb-3">
<div class="text-info bg-info bg-opacity-10 rounded p-3">
<i class="fas fa-chalkboard-teacher fa-2x"></i>
</div>
</div>
<h5 class="text-muted mb-1">教师总数</h5>
<h2 class="fw-bold mb-0" id="totalTeachers">...</h2>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6">
<div class="card border-0 shadow-sm h-100 stat-card">
<div class="card-body">
<div class="d-flex align-items-center justify-content-between mb-3">
<div class="text-warning bg-warning bg-opacity-10 rounded p-3">
<i class="fas fa-book fa-2x"></i>
</div>
</div>
<h5 class="text-muted mb-1">课程总数</h5>
<h2 class="fw-bold mb-0" id="totalCourses">...</h2>
</div>
</div>
</div>
</div>
<!-- 快捷入口 -->
<h5 class="fw-bold mb-3">快捷管理</h5>
<div class="row g-4">
<div class="col-md-6 col-lg-3">
<a href="/admin/user_management" class="text-decoration-none">
<div class="card border-0 shadow-sm h-100 stat-card text-center p-4">
<div class="text-primary mb-3">
<i class="fas fa-users fa-3x"></i>
</div>
<h6 class="fw-bold text-dark">用户管理</h6>
<p class="text-muted small mb-0">管理所有用户账户及权限</p>
</div>
</a>
</div>
<div class="col-md-6 col-lg-3">
<a href="/admin/student_management" class="text-decoration-none">
<div class="card border-0 shadow-sm h-100 stat-card text-center p-4">
<div class="text-success mb-3">
<i class="fas fa-user-graduate fa-3x"></i>
</div>
<h6 class="fw-bold text-dark">学生管理</h6>
<p class="text-muted small mb-0">管理学生信息及学籍</p>
</div>
</a>
</div>
<div class="col-md-6 col-lg-3">
<a href="/admin/teacher_management" class="text-decoration-none">
<div class="card border-0 shadow-sm h-100 stat-card text-center p-4">
<div class="text-info mb-3">
<i class="fas fa-chalkboard-teacher fa-3x"></i>
</div>
<h6 class="fw-bold text-dark">教师管理</h6>
<p class="text-muted small mb-0">管理教师信息及排课</p>
</div>
</a>
</div>
<div class="col-md-6 col-lg-3">
<a href="/admin/grade_statistics" class="text-decoration-none">
<div class="card border-0 shadow-sm h-100 stat-card text-center p-4">
<div class="text-warning mb-3">
<i class="fas fa-chart-bar fa-3x"></i>
</div>
<h6 class="fw-bold text-dark">成绩统计</h6>
<p class="text-muted small mb-0">查看全校成绩统计分析</p>
</div>
</a>
</div>
</div>
</div>
<!-- Bootstrap 5 JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="/public/js/admin.js"></script>
</body>
</html>

View File

@@ -0,0 +1,266 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>数据导出 - 管理员端</title>
<!-- Bootstrap 5 CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- Google Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;700&display=swap" rel="stylesheet">
<style>
:root {
--sidebar-width: 260px;
--primary-color: #4e73df;
--secondary-color: #858796;
--light-bg: #f8f9fc;
--admin-gradient: linear-gradient(135deg, #4e73df 0%, #224abe 100%);
}
body {
font-family: 'Noto Sans SC', sans-serif;
background-color: var(--light-bg);
overflow-x: hidden;
}
/* 侧边栏样式 */
.sidebar {
width: var(--sidebar-width);
height: 100vh;
position: fixed;
left: 0;
top: 0;
background: var(--admin-gradient);
color: white;
z-index: 1000;
transition: all 0.3s;
}
.sidebar-header {
padding: 2rem 1.5rem;
text-align: center;
border-bottom: 1px solid rgba(255,255,255,0.1);
}
.sidebar-brand {
font-size: 1.25rem;
font-weight: 700;
color: white;
text-decoration: none;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
}
.user-profile {
padding: 2rem 1rem;
text-align: center;
}
.user-avatar {
width: 80px;
height: 80px;
background: rgba(255,255,255,0.2);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 1rem;
font-size: 2rem;
}
.user-info h6 { margin-bottom: 0.25rem; font-weight: 600; }
.user-info p { font-size: 0.85rem; opacity: 0.8; margin-bottom: 0; }
.nav-menu { padding: 1rem 0; }
.nav-item { padding: 0.25rem 1rem; }
.nav-link {
color: rgba(255,255,255,0.8);
padding: 0.8rem 1.25rem;
border-radius: 0.5rem;
display: flex;
align-items: center;
gap: 12px;
transition: all 0.2s;
text-decoration: none;
}
.nav-link:hover, .nav-link.active {
color: white;
background: rgba(255,255,255,0.15);
}
.nav-link i { width: 20px; text-align: center; }
/* 主内容区 */
.main-content {
margin-left: var(--sidebar-width);
padding: 2rem;
min-height: 100vh;
}
.top-navbar {
background: white;
padding: 1rem 2rem;
border-radius: 1rem;
box-shadow: 0 0.15rem 1.75rem 0 rgba(58, 59, 69, 0.1);
margin-bottom: 2rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.page-heading h4 { margin-bottom: 0; font-weight: 700; color: #333; }
@media (max-width: 992px) {
.sidebar { left: -var(--sidebar-width); }
.main-content { margin-left: 0; }
.sidebar.active { left: 0; }
}
</style>
</head>
<body>
<!-- 侧边栏 -->
<div class="sidebar">
<div class="sidebar-header">
<a href="#" class="sidebar-brand">
<i class="fas fa-graduation-cap"></i>
<span>成绩管理系统</span>
</a>
</div>
<div class="user-profile">
<div class="user-avatar">
<i class="fas fa-user-shield"></i>
</div>
<div class="user-info text-white">
<h6 id="adminName">加载中...</h6>
<p>管理员</p>
</div>
</div>
<nav class="nav-menu">
<div class="nav-item">
<a href="/admin/dashboard" class="nav-link">
<i class="fas fa-tachometer-alt"></i>
<span>仪表板</span>
</a>
</div>
<div class="nav-item">
<a href="/admin/user_management" class="nav-link">
<i class="fas fa-users"></i>
<span>用户管理</span>
</a>
</div>
<div class="nav-item">
<a href="/admin/student_management" class="nav-link">
<i class="fas fa-user-graduate"></i>
<span>学生管理</span>
</a>
</div>
<div class="nav-item">
<a href="/admin/teacher_management" class="nav-link">
<i class="fas fa-chalkboard-teacher"></i>
<span>教师管理</span>
</a>
</div>
<div class="nav-item">
<a href="/admin/grade_statistics" class="nav-link">
<i class="fas fa-chart-bar"></i>
<span>成绩统计</span>
</a>
</div>
<div class="nav-item">
<a href="/admin/system_settings" class="nav-link">
<i class="fas fa-cog"></i>
<span>系统设置</span>
</a>
</div>
<div class="nav-item">
<a href="/admin/data_export" class="nav-link active">
<i class="fas fa-download"></i>
<span>数据导出</span>
</a>
</div>
<div class="nav-item">
<a href="/admin/operation_logs" class="nav-link">
<i class="fas fa-history"></i>
<span>操作日志</span>
</a>
</div>
<div class="nav-item mt-4">
<a href="javascript:void(0)" id="logoutBtn" class="nav-link text-warning">
<i class="fas fa-sign-out-alt"></i>
<span>退出登录</span>
</a>
</div>
</nav>
</div>
<!-- 主内容 -->
<div class="main-content">
<!-- 顶部导航 -->
<div class="top-navbar">
<div class="page-heading">
<h4>数据导出</h4>
</div>
<div class="d-flex align-items-center gap-3">
<div class="text-end d-none d-md-block">
<div class="small text-muted" id="currentTime"></div>
<div class="fw-bold" id="userName">加载中...</div>
</div>
</div>
</div>
<div class="row g-4">
<!-- 导出选项 -->
<div class="col-md-6 col-lg-4">
<div class="card border-0 shadow-sm h-100 text-center p-4">
<div class="text-primary mb-3">
<i class="fas fa-user-graduate fa-4x"></i>
</div>
<h5 class="fw-bold">学生信息导出</h5>
<p class="text-muted small">导出所有学生的基本信息、班级、专业等数据。</p>
<button class="btn btn-outline-primary w-100" id="exportStudentsBtn">
<i class="fas fa-download me-1"></i> 导出 Excel
</button>
</div>
</div>
<div class="col-md-6 col-lg-4">
<div class="card border-0 shadow-sm h-100 text-center p-4">
<div class="text-info mb-3">
<i class="fas fa-chalkboard-teacher fa-4x"></i>
</div>
<h5 class="fw-bold">教师信息导出</h5>
<p class="text-muted small">导出所有教师的基本信息、职称、部门等数据。</p>
<button class="btn btn-outline-info w-100" id="exportTeachersBtn">
<i class="fas fa-download me-1"></i> 导出 Excel
</button>
</div>
</div>
<div class="col-md-6 col-lg-4">
<div class="card border-0 shadow-sm h-100 text-center p-4">
<div class="text-warning mb-3">
<i class="fas fa-table fa-4x"></i>
</div>
<h5 class="fw-bold">成绩数据导出</h5>
<p class="text-muted small">导出所有课程的成绩记录,包括平时、期末、总评。</p>
<button class="btn btn-outline-warning w-100" id="exportGradesBtn">
<i class="fas fa-download me-1"></i> 导出 Excel
</button>
</div>
</div>
</div>
</div>
<!-- Bootstrap 5 JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="/public/js/admin.js"></script>
</body>
</html>

View File

@@ -0,0 +1,279 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>成绩统计 - 管理员端</title>
<!-- Bootstrap 5 CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- Google Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;700&display=swap" rel="stylesheet">
<style>
:root {
--sidebar-width: 260px;
--primary-color: #4e73df;
--secondary-color: #858796;
--light-bg: #f8f9fc;
--admin-gradient: linear-gradient(135deg, #4e73df 0%, #224abe 100%);
}
body {
font-family: 'Noto Sans SC', sans-serif;
background-color: var(--light-bg);
overflow-x: hidden;
}
/* 侧边栏样式 */
.sidebar {
width: var(--sidebar-width);
height: 100vh;
position: fixed;
left: 0;
top: 0;
background: var(--admin-gradient);
color: white;
z-index: 1000;
transition: all 0.3s;
}
.sidebar-header {
padding: 2rem 1.5rem;
text-align: center;
border-bottom: 1px solid rgba(255,255,255,0.1);
}
.sidebar-brand {
font-size: 1.25rem;
font-weight: 700;
color: white;
text-decoration: none;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
}
.user-profile {
padding: 2rem 1rem;
text-align: center;
}
.user-avatar {
width: 80px;
height: 80px;
background: rgba(255,255,255,0.2);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 1rem;
font-size: 2rem;
}
.user-info h6 { margin-bottom: 0.25rem; font-weight: 600; }
.user-info p { font-size: 0.85rem; opacity: 0.8; margin-bottom: 0; }
.nav-menu { padding: 1rem 0; }
.nav-item { padding: 0.25rem 1rem; }
.nav-link {
color: rgba(255,255,255,0.8);
padding: 0.8rem 1.25rem;
border-radius: 0.5rem;
display: flex;
align-items: center;
gap: 12px;
transition: all 0.2s;
text-decoration: none;
}
.nav-link:hover, .nav-link.active {
color: white;
background: rgba(255,255,255,0.15);
}
.nav-link i { width: 20px; text-align: center; }
/* 主内容区 */
.main-content {
margin-left: var(--sidebar-width);
padding: 2rem;
min-height: 100vh;
}
.top-navbar {
background: white;
padding: 1rem 2rem;
border-radius: 1rem;
box-shadow: 0 0.15rem 1.75rem 0 rgba(58, 59, 69, 0.1);
margin-bottom: 2rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.page-heading h4 { margin-bottom: 0; font-weight: 700; color: #333; }
@media (max-width: 992px) {
.sidebar { left: -var(--sidebar-width); }
.main-content { margin-left: 0; }
.sidebar.active { left: 0; }
}
</style>
</head>
<body>
<!-- 侧边栏 -->
<div class="sidebar">
<div class="sidebar-header">
<a href="#" class="sidebar-brand">
<i class="fas fa-graduation-cap"></i>
<span>成绩管理系统</span>
</a>
</div>
<div class="user-profile">
<div class="user-avatar">
<i class="fas fa-user-shield"></i>
</div>
<div class="user-info text-white">
<h6 id="adminName">加载中...</h6>
<p>管理员</p>
</div>
</div>
<nav class="nav-menu">
<div class="nav-item">
<a href="/admin/dashboard" class="nav-link">
<i class="fas fa-tachometer-alt"></i>
<span>仪表板</span>
</a>
</div>
<div class="nav-item">
<a href="/admin/user_management" class="nav-link">
<i class="fas fa-users"></i>
<span>用户管理</span>
</a>
</div>
<div class="nav-item">
<a href="/admin/student_management" class="nav-link">
<i class="fas fa-user-graduate"></i>
<span>学生管理</span>
</a>
</div>
<div class="nav-item">
<a href="/admin/teacher_management" class="nav-link">
<i class="fas fa-chalkboard-teacher"></i>
<span>教师管理</span>
</a>
</div>
<div class="nav-item">
<a href="/admin/grade_statistics" class="nav-link active">
<i class="fas fa-chart-bar"></i>
<span>成绩统计</span>
</a>
</div>
<div class="nav-item">
<a href="/admin/system_settings" class="nav-link">
<i class="fas fa-cog"></i>
<span>系统设置</span>
</a>
</div>
<div class="nav-item">
<a href="/admin/data_export" class="nav-link">
<i class="fas fa-download"></i>
<span>数据导出</span>
</a>
</div>
<div class="nav-item">
<a href="/admin/operation_logs" class="nav-link">
<i class="fas fa-history"></i>
<span>操作日志</span>
</a>
</div>
<div class="nav-item mt-4">
<a href="javascript:void(0)" id="logoutBtn" class="nav-link text-warning">
<i class="fas fa-sign-out-alt"></i>
<span>退出登录</span>
</a>
</div>
</nav>
</div>
<!-- 主内容 -->
<div class="main-content">
<!-- 顶部导航 -->
<div class="top-navbar">
<div class="page-heading">
<h4>成绩统计</h4>
</div>
<div class="d-flex align-items-center gap-3">
<div class="text-end d-none d-md-block">
<div class="small text-muted" id="currentTime"></div>
<div class="fw-bold" id="userName">加载中...</div>
</div>
</div>
</div>
<!-- 统计图表 -->
<div class="row g-4 mb-4">
<div class="col-lg-8">
<div class="card border-0 shadow-sm h-100">
<div class="card-header bg-white py-3">
<h6 class="m-0 fw-bold text-primary">各课程平均分对比</h6>
</div>
<div class="card-body">
<canvas id="courseAvgChart"></canvas>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card border-0 shadow-sm h-100">
<div class="card-header bg-white py-3">
<h6 class="m-0 fw-bold text-primary">全校及格率</h6>
</div>
<div class="card-body d-flex align-items-center justify-content-center">
<div style="width: 100%; max-width: 300px;">
<canvas id="passRateChart"></canvas>
</div>
</div>
</div>
</div>
</div>
<!-- 详细数据表 -->
<div class="card border-0 shadow-sm">
<div class="card-header bg-white py-3">
<h6 class="m-0 fw-bold text-primary">课程成绩详情</h6>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="bg-light">
<tr>
<th class="ps-4">课程代码</th>
<th>课程名称</th>
<th>任课教师</th>
<th>选课人数</th>
<th>平均分</th>
<th>最高分</th>
<th>最低分</th>
<th class="text-end pe-4">及格率</th>
</tr>
</thead>
<tbody id="gradeStatsBody">
<tr><td colspan="8" class="text-center py-5">加载中...</td></tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Chart.js & Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="/public/js/admin.js"></script>
</body>
</html>

View File

@@ -0,0 +1,245 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>操作日志 - 管理员端</title>
<!-- Bootstrap 5 CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- Google Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;700&display=swap" rel="stylesheet">
<style>
:root {
--sidebar-width: 260px;
--primary-color: #4e73df;
--secondary-color: #858796;
--light-bg: #f8f9fc;
--admin-gradient: linear-gradient(135deg, #4e73df 0%, #224abe 100%);
}
body {
font-family: 'Noto Sans SC', sans-serif;
background-color: var(--light-bg);
overflow-x: hidden;
}
/* 侧边栏样式 */
.sidebar {
width: var(--sidebar-width);
height: 100vh;
position: fixed;
left: 0;
top: 0;
background: var(--admin-gradient);
color: white;
z-index: 1000;
transition: all 0.3s;
}
.sidebar-header {
padding: 2rem 1.5rem;
text-align: center;
border-bottom: 1px solid rgba(255,255,255,0.1);
}
.sidebar-brand {
font-size: 1.25rem;
font-weight: 700;
color: white;
text-decoration: none;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
}
.user-profile {
padding: 2rem 1rem;
text-align: center;
}
.user-avatar {
width: 80px;
height: 80px;
background: rgba(255,255,255,0.2);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 1rem;
font-size: 2rem;
}
.user-info h6 { margin-bottom: 0.25rem; font-weight: 600; }
.user-info p { font-size: 0.85rem; opacity: 0.8; margin-bottom: 0; }
.nav-menu { padding: 1rem 0; }
.nav-item { padding: 0.25rem 1rem; }
.nav-link {
color: rgba(255,255,255,0.8);
padding: 0.8rem 1.25rem;
border-radius: 0.5rem;
display: flex;
align-items: center;
gap: 12px;
transition: all 0.2s;
text-decoration: none;
}
.nav-link:hover, .nav-link.active {
color: white;
background: rgba(255,255,255,0.15);
}
.nav-link i { width: 20px; text-align: center; }
/* 主内容区 */
.main-content {
margin-left: var(--sidebar-width);
padding: 2rem;
min-height: 100vh;
}
.top-navbar {
background: white;
padding: 1rem 2rem;
border-radius: 1rem;
box-shadow: 0 0.15rem 1.75rem 0 rgba(58, 59, 69, 0.1);
margin-bottom: 2rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.page-heading h4 { margin-bottom: 0; font-weight: 700; color: #333; }
@media (max-width: 992px) {
.sidebar { left: -var(--sidebar-width); }
.main-content { margin-left: 0; }
.sidebar.active { left: 0; }
}
</style>
</head>
<body>
<!-- 侧边栏 -->
<div class="sidebar">
<div class="sidebar-header">
<a href="#" class="sidebar-brand">
<i class="fas fa-graduation-cap"></i>
<span>成绩管理系统</span>
</a>
</div>
<div class="user-profile">
<div class="user-avatar">
<i class="fas fa-user-shield"></i>
</div>
<div class="user-info text-white">
<h6 id="adminName">加载中...</h6>
<p>管理员</p>
</div>
</div>
<nav class="nav-menu">
<div class="nav-item">
<a href="/admin/dashboard" class="nav-link">
<i class="fas fa-tachometer-alt"></i>
<span>仪表板</span>
</a>
</div>
<div class="nav-item">
<a href="/admin/user_management" class="nav-link">
<i class="fas fa-users"></i>
<span>用户管理</span>
</a>
</div>
<div class="nav-item">
<a href="/admin/student_management" class="nav-link">
<i class="fas fa-user-graduate"></i>
<span>学生管理</span>
</a>
</div>
<div class="nav-item">
<a href="/admin/teacher_management" class="nav-link">
<i class="fas fa-chalkboard-teacher"></i>
<span>教师管理</span>
</a>
</div>
<div class="nav-item">
<a href="/admin/grade_statistics" class="nav-link">
<i class="fas fa-chart-bar"></i>
<span>成绩统计</span>
</a>
</div>
<div class="nav-item">
<a href="/admin/system_settings" class="nav-link">
<i class="fas fa-cog"></i>
<span>系统设置</span>
</a>
</div>
<div class="nav-item">
<a href="/admin/data_export" class="nav-link">
<i class="fas fa-download"></i>
<span>数据导出</span>
</a>
</div>
<div class="nav-item">
<a href="/admin/operation_logs" class="nav-link active">
<i class="fas fa-history"></i>
<span>操作日志</span>
</a>
</div>
<div class="nav-item mt-4">
<a href="javascript:void(0)" id="logoutBtn" class="nav-link text-warning">
<i class="fas fa-sign-out-alt"></i>
<span>退出登录</span>
</a>
</div>
</nav>
</div>
<!-- 主内容 -->
<div class="main-content">
<!-- 顶部导航 -->
<div class="top-navbar">
<div class="page-heading">
<h4>操作日志</h4>
</div>
<div class="d-flex align-items-center gap-3">
<div class="text-end d-none d-md-block">
<div class="small text-muted" id="currentTime"></div>
<div class="fw-bold" id="userName">加载中...</div>
</div>
</div>
</div>
<div class="card border-0 shadow-sm">
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="bg-light">
<tr>
<th class="ps-4">操作时间</th>
<th>操作人</th>
<th>操作类型</th>
<th>详情</th>
<th>IP地址</th>
</tr>
</thead>
<tbody id="logsTableBody">
<tr><td colspan="5" class="text-center py-5">暂无日志数据</td></tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Bootstrap 5 JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="/public/js/admin.js"></script>
</body>
</html>

View File

@@ -3,439 +3,327 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>学生管理 - XX学校成绩管理系统</title>
<link rel="stylesheet" href="/public/css/style.css">
<title>学生管理 - 管理员端</title>
<!-- Bootstrap 5 CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- Google Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;700&display=swap" rel="stylesheet">
<style>
:root {
--sidebar-width: 260px;
--primary-color: #4e73df;
--secondary-color: #858796;
--light-bg: #f8f9fc;
--admin-gradient: linear-gradient(135deg, #4e73df 0%, #224abe 100%);
}
body {
font-family: 'Noto Sans SC', sans-serif;
background-color: var(--light-bg);
overflow-x: hidden;
}
/* 侧边栏样式 */
.sidebar {
width: var(--sidebar-width);
height: 100vh;
position: fixed;
left: 0;
top: 0;
background: var(--admin-gradient);
color: white;
z-index: 1000;
transition: all 0.3s;
}
.sidebar-header {
padding: 2rem 1.5rem;
text-align: center;
border-bottom: 1px solid rgba(255,255,255,0.1);
}
.sidebar-brand {
font-size: 1.25rem;
font-weight: 700;
color: white;
text-decoration: none;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
}
.user-profile {
padding: 2rem 1rem;
text-align: center;
}
.user-avatar {
width: 80px;
height: 80px;
background: rgba(255,255,255,0.2);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 1rem;
font-size: 2rem;
}
.user-info h6 { margin-bottom: 0.25rem; font-weight: 600; }
.user-info p { font-size: 0.85rem; opacity: 0.8; margin-bottom: 0; }
.nav-menu { padding: 1rem 0; }
.nav-item { padding: 0.25rem 1rem; }
.nav-link {
color: rgba(255,255,255,0.8);
padding: 0.8rem 1.25rem;
border-radius: 0.5rem;
display: flex;
align-items: center;
gap: 12px;
transition: all 0.2s;
text-decoration: none;
}
.nav-link:hover, .nav-link.active {
color: white;
background: rgba(255,255,255,0.15);
}
.nav-link i { width: 20px; text-align: center; }
/* 主内容区 */
.main-content {
margin-left: var(--sidebar-width);
padding: 2rem;
min-height: 100vh;
}
.top-navbar {
background: white;
padding: 1rem 2rem;
border-radius: 1rem;
box-shadow: 0 0.15rem 1.75rem 0 rgba(58, 59, 69, 0.1);
margin-bottom: 2rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.page-heading h4 { margin-bottom: 0; font-weight: 700; color: #333; }
@media (max-width: 992px) {
.sidebar { left: -var(--sidebar-width); }
.main-content { margin-left: 0; }
.sidebar.active { left: 0; }
}
</style>
</head>
<body>
<!-- 导航<EFBFBD><EFBFBD>?-->
<nav class="navbar">
<div class="container">
<div class="navbar-brand">
<a href="/">
<i class="fas fa-graduation-cap"></i>
<span>XX学校成绩管理系统</span>
<!-- 侧边栏 -->
<div class="sidebar">
<div class="sidebar-header">
<a href="#" class="sidebar-brand">
<i class="fas fa-graduation-cap"></i>
<span>成绩管理系统</span>
</a>
</div>
<div class="user-profile">
<div class="user-avatar">
<i class="fas fa-user-shield"></i>
</div>
<div class="user-info text-white">
<h6 id="adminName">加载中...</h6>
<p>管理员</p>
</div>
</div>
<nav class="nav-menu">
<div class="nav-item">
<a href="/admin/dashboard" class="nav-link">
<i class="fas fa-tachometer-alt"></i>
<span>仪表板</span>
</a>
</div>
<div class="navbar-menu">
<a href="/" class="navbar-item">
<i class="fas fa-home"></i>
<span>主页</span>
</a>
<a href="/admin/dashboard" class="navbar-item">
<i class="fas fa-tachometer-alt"></i>
<span>控制面板</span>
</a>
<a href="/admin/student_management" class="navbar-item active">
<div class="nav-item">
<a href="/admin/user_management" class="nav-link">
<i class="fas fa-users"></i>
<span>学生管理</span>
</a>
<a href="/teacher/grade_management" class="navbar-item">
<i class="fas fa-chart-bar"></i>
<span>成绩管理</span>
</a>
<a href="/admin/user_management" 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>管理<EFBFBD><EFBFBD>?/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"><i class="fas fa-sign-out-alt"></i> 退出登<E587BA><E799BB>?/a>
</div>
</div>
<div class="nav-item">
<a href="/admin/student_management" class="nav-link active">
<i class="fas fa-user-graduate"></i>
<span>学生管理</span>
</a>
</div>
<div class="nav-item">
<a href="/admin/teacher_management" class="nav-link">
<i class="fas fa-chalkboard-teacher"></i>
<span>教师管理</span>
</a>
</div>
<div class="nav-item">
<a href="/admin/grade_statistics" class="nav-link">
<i class="fas fa-chart-bar"></i>
<span>成绩统计</span>
</a>
</div>
<div class="nav-item">
<a href="/admin/system_settings" class="nav-link">
<i class="fas fa-cog"></i>
<span>系统设置</span>
</a>
</div>
<div class="nav-item">
<a href="/admin/data_export" class="nav-link">
<i class="fas fa-download"></i>
<span>数据导出</span>
</a>
</div>
<div class="nav-item">
<a href="/admin/operation_logs" class="nav-link">
<i class="fas fa-history"></i>
<span>操作日志</span>
</a>
</div>
<div class="nav-item mt-4">
<a href="javascript:void(0)" id="logoutBtn" class="nav-link text-warning">
<i class="fas fa-sign-out-alt"></i>
<span>退出登录</span>
</a>
</div>
</nav>
</div>
<!-- 主内容 -->
<div class="main-content">
<!-- 顶部导航 -->
<div class="top-navbar">
<div class="page-heading">
<h4>学生管理</h4>
</div>
<div class="d-flex align-items-center gap-3">
<div class="text-end d-none d-md-block">
<div class="small text-muted" id="currentTime"></div>
<div class="fw-bold" id="userName">加载中...</div>
</div>
</div>
</div>
</nav>
<!-- 主要内容<EFBFBD><EFBFBD>?-->
<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>
<!-- 筛选区<E98089><E58CBA>?-->
<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="请输入学<E585A5><E5ADA6>?>
</div>
<div class="filter-group">
<label for="student-name"><i class="fas fa-user"></i> 姓名</label>
<input type="text" id="student-name" placeholder="请输入姓<E585A5><E5A793>?>
</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="计算机科学与技<E4B88E><E68A80>?<3F><>?>计算机科学与技<E4B88E><E68A80>?<3F><>?/option>
<option value="计算机科学与技<EFBFBD><EFBFBD>?<EFBFBD><EFBFBD>?>计算机科学与技<EFBFBD><EFBFBD>?<3F><>?/option>
<option value="软件工程1<E7A88B><31>?>软件工程1<E7A88B><31>?/option>
<option value="软件工程2<EFBFBD><EFBFBD>?>软件工程2<EFBFBD><EFBFBD>?/option>
<option value="网络工程1<E7A88B><31>?>网络工程1<E7A88B><31>?/option>
<option value="网络工程2<EFBFBD><EFBFBD>?>网络工程2<EFBFBD><EFBFBD>?/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="<22><>?><3E><>?/option>
<option value="<EFBFBD><EFBFBD>?><EFBFBD><EFBFBD>?/option>
</select>
<!-- 筛选栏 -->
<div class="card border-0 shadow-sm mb-4">
<div class="card-body">
<form id="studentFilterForm" class="row g-3 align-items-end">
<div class="col-md-3">
<label class="form-label">搜索学生</label>
<div class="input-group">
<span class="input-group-text"><i class="fas fa-search"></i></span>
<input type="text" class="form-control" id="studentSearch" placeholder="学号、姓名或班级">
</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>
导出数据
<!--
<div class="col-md-3">
<label class="form-label">班级筛选</label>
<select class="form-select" id="classFilter">
<option value="">全部班级</option>
</select>
</div>
-->
<div class="col-md-9 text-md-end">
<button type="button" class="btn btn-primary" id="addStudentBtn">
<i class="fas fa-plus me-1"></i> 新增学生
</button>
</div>
</div>
</form>
</div>
</div>
<!-- 学生表 -->
<!-- 学生-->
<div class="card border-0 shadow-sm">
<div class="card-body p-0">
<div class="table-responsive">
<table class="student-table">
<thead>
<table class="table table-hover align-middle mb-0">
<thead class="bg-light">
<tr>
<th>学号</th>
<th class="ps-4">学号</th>
<th>姓名</th>
<th>性别</th>
<th>班级</th>
<th>联系电话</th>
<th>邮箱</th>
<th>入学时间</th>
<th>操作</th>
<th>专业</th>
<th>年级</th>
<th>联系方式</th>
<th class="text-end pe-4">操作</th>
</tr>
</thead>
<tbody id="student-table-body">
<!-- 数据将通过JavaScript动态加<E68081><E58AA0>?-->
<tr>
<td colspan="8" class="loading">
<i class="fas fa-spinner fa-spin"></i>
<p>正在加载学生数据...</p>
</td>
</tr>
<tbody id="studentTableBody">
<tr><td colspan="7" class="text-center py-5">加载中...</td></tr>
</tbody>
</table>
</div>
</div>
<div class="card-footer bg-white border-0 py-3">
<nav>
<ul class="pagination justify-content-center mb-0" id="studentPagination">
<!-- Pagination rendered by JS -->
</ul>
</nav>
</div>
</div>
</div>
<!-- 分页控件 -->
<div class="pagination" id="pagination">
<button id="prev-btn" disabled>
<i class="fas fa-chevron-left"></i>
上一<E4B88A><E4B880>? </button>
<button class="active">1</button>
<button>2</button>
<button>3</button>
<button id="next-btn">
下一<E4B88B><E4B880>? <i class="fas fa-chevron-right"></i>
</button>
<!-- Add/Edit Student Modal -->
<div class="modal fade" id="studentModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="studentModalTitle">新增学生</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="studentForm">
<input type="hidden" name="isEdit" id="studentIsEdit" value="false">
<div class="mb-3">
<label class="form-label">学号</label>
<input type="text" class="form-control" name="id" id="studentId" required>
<div class="form-text">学号将作为登录账号,默认密码同账号</div>
</div>
<div class="mb-3">
<label class="form-label">姓名</label>
<input type="text" class="form-control" name="name" id="studentNameInput" required>
</div>
<div class="mb-3">
<label class="form-label">班级</label>
<input type="text" class="form-control" name="class" id="studentClass" required>
</div>
<div class="mb-3">
<label class="form-label">专业</label>
<input type="text" class="form-control" name="major" id="studentMajor">
</div>
<div class="mb-3">
<label class="form-label">年级</label>
<input type="number" class="form-control" name="grade" id="studentGrade" placeholder="例如2023">
</div>
<div class="mb-3">
<label class="form-label">联系方式</label>
<input type="text" class="form-control" name="contact_info" id="studentContact">
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" id="saveStudentBtn">保存</button>
</div>
</div>
</div>
</main>
</div>
<!-- 页脚 -->
<footer class="footer">
<div class="container">
<p>© 2023 XX学校成绩管理系统. 版权所<E69D83><E68980>?</p>
<p>技术支<EFBFBD><EFBFBD>? 计算机科学与技术学<E69CAF><E5ADA6>?/p>
</div>
</footer>
<script>
// 模拟学生数据
const mockStudents = [
{
id: '20230001',
name: '张三',
gender: '<27><>?,
class: '计算机科学与技<EFBFBD><EFBFBD>?<EFBFBD><EFBFBD>?,
phone: '13800138001',
email: 'zhangsan@example.com',
enrollmentDate: '2023-09-01'
},
{
id: '20230002',
name: '李四',
gender: '<27><>?,
class: '计算机科学与技<EFBFBD><EFBFBD>?<EFBFBD><EFBFBD>?,
phone: '13800138002',
email: 'lisi@example.com',
enrollmentDate: '2023-09-01'
},
{
id: '20230003',
name: '王五',
gender: '<27><>?,
class: '计算机科学与技<EFBFBD><EFBFBD>?<EFBFBD><EFBFBD>?,
phone: '13800138003',
email: 'wangwu@example.com',
enrollmentDate: '2023-09-01'
},
{
id: '20230004',
name: '赵六',
gender: '<27><>?,
class: '软件工程1<EFBFBD><EFBFBD>?,
phone: '13800138004',
email: 'zhaoliu@example.com',
enrollmentDate: '2023-09-01'
},
{
id: '20230005',
name: '钱七',
gender: '<27><>?,
class: '软件工程2<EFBFBD><EFBFBD>?,
phone: '13800138005',
email: 'qianqi@example.com',
enrollmentDate: '2023-09-01'
},
{
id: '20230006',
name: '孙八',
gender: '<27><>?,
class: '网络工程1<EFBFBD><EFBFBD>?,
phone: '13800138006',
email: 'sunba@example.com',
enrollmentDate: '2023-09-01'
},
{
id: '20230007',
name: '周九',
gender: '<27><>?,
class: '网络工程2<EFBFBD><EFBFBD>?,
phone: '13800138007',
email: 'zhoujiu@example.com',
enrollmentDate: '2023-09-01'
},
{
id: '20230008',
name: '吴十',
gender: '<27><>?,
class: '计算机科学与技<EFBFBD><EFBFBD>?<EFBFBD><EFBFBD>?,
phone: '13800138008',
email: 'wushi@example.com',
enrollmentDate: '2023-09-01'
}
];
// 当前页数<E9A1B5><E695B0>? 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>请尝试调整筛选条件或添加新学<E696B0><E5ADA6>?/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';
}
});
// 更新上一<E4B88A><E4B880>?下一页按钮状<E992AE><E78AB6>? prevBtn.disabled = currentPage === 1;
nextBtn.disabled = currentPage === totalPages || totalPages === 0;
}
// 筛选学<E98089><E5ADA6>? 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();
}
// 重置筛选条<E98089><E69DA1>? 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完成后实<E5908E><E5AE9E>?);
// 这里可以打开一个模态框来添加学生信<E7949F><E4BFA1>? }
// 编辑学生
function editStudent(studentId) {
alert(`编辑学生 ${studentId} 功能将在后端API完成后实现`);
// 这里可以打开一个模态框来编辑学生信<E7949F><E4BFA1>? }
// 删除学生
function deleteStudent(studentId) {
if (confirm(`确定要删除学号为 ${studentId} 的学生吗?`)) {
alert(`删除学生 ${studentId} 功能将在后端API完成后实现`);
// 这里可以调用API删除学生
}
}
// 导出数据
function exportData() {
alert('导出数据功能将在后端API完成后实<EFBFBD><EFBFBD>?);
// 这里可以调用API导出Excel或CSV文件
}
// 页面切换
function goToPage(page) {
currentPage = page;
renderStudentTable();
updatePagination();
}
// 初始化事件监<E4BBB6><E79B91>? 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);
}
});
// 输入框回车搜<E8BDA6><E6909C>? 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>
<!-- Bootstrap 5 JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="/public/js/admin.js"></script>
</body>
</html>

View File

@@ -0,0 +1,290 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>系统设置 - 管理员端</title>
<!-- Bootstrap 5 CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- Google Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;700&display=swap" rel="stylesheet">
<style>
:root {
--sidebar-width: 260px;
--primary-color: #4e73df;
--secondary-color: #858796;
--light-bg: #f8f9fc;
--admin-gradient: linear-gradient(135deg, #4e73df 0%, #224abe 100%);
}
body {
font-family: 'Noto Sans SC', sans-serif;
background-color: var(--light-bg);
overflow-x: hidden;
}
/* 侧边栏样式 */
.sidebar {
width: var(--sidebar-width);
height: 100vh;
position: fixed;
left: 0;
top: 0;
background: var(--admin-gradient);
color: white;
z-index: 1000;
transition: all 0.3s;
}
.sidebar-header {
padding: 2rem 1.5rem;
text-align: center;
border-bottom: 1px solid rgba(255,255,255,0.1);
}
.sidebar-brand {
font-size: 1.25rem;
font-weight: 700;
color: white;
text-decoration: none;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
}
.user-profile {
padding: 2rem 1rem;
text-align: center;
}
.user-avatar {
width: 80px;
height: 80px;
background: rgba(255,255,255,0.2);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 1rem;
font-size: 2rem;
}
.user-info h6 { margin-bottom: 0.25rem; font-weight: 600; }
.user-info p { font-size: 0.85rem; opacity: 0.8; margin-bottom: 0; }
.nav-menu { padding: 1rem 0; }
.nav-item { padding: 0.25rem 1rem; }
.nav-link {
color: rgba(255,255,255,0.8);
padding: 0.8rem 1.25rem;
border-radius: 0.5rem;
display: flex;
align-items: center;
gap: 12px;
transition: all 0.2s;
text-decoration: none;
}
.nav-link:hover, .nav-link.active {
color: white;
background: rgba(255,255,255,0.15);
}
.nav-link i { width: 20px; text-align: center; }
/* 主内容区 */
.main-content {
margin-left: var(--sidebar-width);
padding: 2rem;
min-height: 100vh;
}
.top-navbar {
background: white;
padding: 1rem 2rem;
border-radius: 1rem;
box-shadow: 0 0.15rem 1.75rem 0 rgba(58, 59, 69, 0.1);
margin-bottom: 2rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.page-heading h4 { margin-bottom: 0; font-weight: 700; color: #333; }
@media (max-width: 992px) {
.sidebar { left: -var(--sidebar-width); }
.main-content { margin-left: 0; }
.sidebar.active { left: 0; }
}
</style>
</head>
<body>
<!-- 侧边栏 -->
<div class="sidebar">
<div class="sidebar-header">
<a href="#" class="sidebar-brand">
<i class="fas fa-graduation-cap"></i>
<span>成绩管理系统</span>
</a>
</div>
<div class="user-profile">
<div class="user-avatar">
<i class="fas fa-user-shield"></i>
</div>
<div class="user-info text-white">
<h6 id="adminName">加载中...</h6>
<p>管理员</p>
</div>
</div>
<nav class="nav-menu">
<div class="nav-item">
<a href="/admin/dashboard" class="nav-link">
<i class="fas fa-tachometer-alt"></i>
<span>仪表板</span>
</a>
</div>
<div class="nav-item">
<a href="/admin/user_management" class="nav-link">
<i class="fas fa-users"></i>
<span>用户管理</span>
</a>
</div>
<div class="nav-item">
<a href="/admin/student_management" class="nav-link">
<i class="fas fa-user-graduate"></i>
<span>学生管理</span>
</a>
</div>
<div class="nav-item">
<a href="/admin/teacher_management" class="nav-link">
<i class="fas fa-chalkboard-teacher"></i>
<span>教师管理</span>
</a>
</div>
<div class="nav-item">
<a href="/admin/grade_statistics" class="nav-link">
<i class="fas fa-chart-bar"></i>
<span>成绩统计</span>
</a>
</div>
<div class="nav-item">
<a href="/admin/system_settings" class="nav-link active">
<i class="fas fa-cog"></i>
<span>系统设置</span>
</a>
</div>
<div class="nav-item">
<a href="/admin/data_export" class="nav-link">
<i class="fas fa-download"></i>
<span>数据导出</span>
</a>
</div>
<div class="nav-item">
<a href="/admin/operation_logs" class="nav-link">
<i class="fas fa-history"></i>
<span>操作日志</span>
</a>
</div>
<div class="nav-item mt-4">
<a href="javascript:void(0)" id="logoutBtn" class="nav-link text-warning">
<i class="fas fa-sign-out-alt"></i>
<span>退出登录</span>
</a>
</div>
</nav>
</div>
<!-- 主内容 -->
<div class="main-content">
<!-- 顶部导航 -->
<div class="top-navbar">
<div class="page-heading">
<h4>系统设置</h4>
</div>
<div class="d-flex align-items-center gap-3">
<div class="text-end d-none d-md-block">
<div class="small text-muted" id="currentTime"></div>
<div class="fw-bold" id="userName">加载中...</div>
</div>
</div>
</div>
<div class="row g-4">
<!-- 基础设置 -->
<div class="col-md-6">
<div class="card border-0 shadow-sm h-100">
<div class="card-header bg-white py-3">
<h6 class="m-0 fw-bold text-primary">基础设置</h6>
</div>
<div class="card-body">
<form id="basicSettingsForm">
<div class="mb-3">
<label class="form-label">系统名称</label>
<input type="text" class="form-control" value="学校成绩管理系统">
</div>
<div class="mb-3">
<label class="form-label">当前学期</label>
<select class="form-select">
<option value="2023-2024-1">2023-2024 第一学期</option>
<option value="2023-2024-2" selected>2023-2024 第二学期</option>
</select>
</div>
<div class="mb-3">
<label class="form-label">开放选课</label>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="courseSelectionSwitch" checked>
<label class="form-check-label" for="courseSelectionSwitch">允许学生选课</label>
</div>
</div>
<div class="mb-3">
<label class="form-label">开放查分</label>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="gradeCheckSwitch" checked>
<label class="form-check-label" for="gradeCheckSwitch">允许学生查询成绩</label>
</div>
</div>
<button type="submit" class="btn btn-primary">保存设置</button>
</form>
</div>
</div>
</div>
<!-- 数据备份 -->
<div class="col-md-6">
<div class="card border-0 shadow-sm h-100">
<div class="card-header bg-white py-3">
<h6 class="m-0 fw-bold text-primary">数据维护</h6>
</div>
<div class="card-body">
<div class="alert alert-info">
<i class="fas fa-info-circle me-1"></i> 定期备份数据可以防止意外丢失。
</div>
<div class="d-grid gap-3">
<button class="btn btn-outline-primary text-start">
<i class="fas fa-database me-2"></i> 立即备份数据库
</button>
<button class="btn btn-outline-warning text-start">
<i class="fas fa-trash-alt me-2"></i> 清理系统缓存
</button>
<button class="btn btn-outline-danger text-start">
<i class="fas fa-history me-2"></i> 重置所有学生密码
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Bootstrap 5 JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="/public/js/admin.js"></script>
</body>
</html>

View File

@@ -0,0 +1,316 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>教师管理 - 管理员端</title>
<!-- Bootstrap 5 CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- Google Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;700&display=swap" rel="stylesheet">
<style>
:root {
--sidebar-width: 260px;
--primary-color: #4e73df;
--secondary-color: #858796;
--light-bg: #f8f9fc;
--admin-gradient: linear-gradient(135deg, #4e73df 0%, #224abe 100%);
}
body {
font-family: 'Noto Sans SC', sans-serif;
background-color: var(--light-bg);
overflow-x: hidden;
}
/* 侧边栏样式 */
.sidebar {
width: var(--sidebar-width);
height: 100vh;
position: fixed;
left: 0;
top: 0;
background: var(--admin-gradient);
color: white;
z-index: 1000;
transition: all 0.3s;
}
.sidebar-header {
padding: 2rem 1.5rem;
text-align: center;
border-bottom: 1px solid rgba(255,255,255,0.1);
}
.sidebar-brand {
font-size: 1.25rem;
font-weight: 700;
color: white;
text-decoration: none;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
}
.user-profile {
padding: 2rem 1rem;
text-align: center;
}
.user-avatar {
width: 80px;
height: 80px;
background: rgba(255,255,255,0.2);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 1rem;
font-size: 2rem;
}
.user-info h6 { margin-bottom: 0.25rem; font-weight: 600; }
.user-info p { font-size: 0.85rem; opacity: 0.8; margin-bottom: 0; }
.nav-menu { padding: 1rem 0; }
.nav-item { padding: 0.25rem 1rem; }
.nav-link {
color: rgba(255,255,255,0.8);
padding: 0.8rem 1.25rem;
border-radius: 0.5rem;
display: flex;
align-items: center;
gap: 12px;
transition: all 0.2s;
text-decoration: none;
}
.nav-link:hover, .nav-link.active {
color: white;
background: rgba(255,255,255,0.15);
}
.nav-link i { width: 20px; text-align: center; }
/* 主内容区 */
.main-content {
margin-left: var(--sidebar-width);
padding: 2rem;
min-height: 100vh;
}
.top-navbar {
background: white;
padding: 1rem 2rem;
border-radius: 1rem;
box-shadow: 0 0.15rem 1.75rem 0 rgba(58, 59, 69, 0.1);
margin-bottom: 2rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.page-heading h4 { margin-bottom: 0; font-weight: 700; color: #333; }
@media (max-width: 992px) {
.sidebar { left: -var(--sidebar-width); }
.main-content { margin-left: 0; }
.sidebar.active { left: 0; }
}
</style>
</head>
<body>
<!-- 侧边栏 -->
<div class="sidebar">
<div class="sidebar-header">
<a href="#" class="sidebar-brand">
<i class="fas fa-graduation-cap"></i>
<span>成绩管理系统</span>
</a>
</div>
<div class="user-profile">
<div class="user-avatar">
<i class="fas fa-user-shield"></i>
</div>
<div class="user-info text-white">
<h6 id="adminName">加载中...</h6>
<p>管理员</p>
</div>
</div>
<nav class="nav-menu">
<div class="nav-item">
<a href="/admin/dashboard" class="nav-link">
<i class="fas fa-tachometer-alt"></i>
<span>仪表板</span>
</a>
</div>
<div class="nav-item">
<a href="/admin/user_management" class="nav-link">
<i class="fas fa-users"></i>
<span>用户管理</span>
</a>
</div>
<div class="nav-item">
<a href="/admin/student_management" class="nav-link">
<i class="fas fa-user-graduate"></i>
<span>学生管理</span>
</a>
</div>
<div class="nav-item">
<a href="/admin/teacher_management" class="nav-link active">
<i class="fas fa-chalkboard-teacher"></i>
<span>教师管理</span>
</a>
</div>
<div class="nav-item">
<a href="/admin/grade_statistics" class="nav-link">
<i class="fas fa-chart-bar"></i>
<span>成绩统计</span>
</a>
</div>
<div class="nav-item">
<a href="/admin/system_settings" class="nav-link">
<i class="fas fa-cog"></i>
<span>系统设置</span>
</a>
</div>
<div class="nav-item">
<a href="/admin/data_export" class="nav-link">
<i class="fas fa-download"></i>
<span>数据导出</span>
</a>
</div>
<div class="nav-item">
<a href="/admin/operation_logs" class="nav-link">
<i class="fas fa-history"></i>
<span>操作日志</span>
</a>
</div>
<div class="nav-item mt-4">
<a href="javascript:void(0)" id="logoutBtn" class="nav-link text-warning">
<i class="fas fa-sign-out-alt"></i>
<span>退出登录</span>
</a>
</div>
</nav>
</div>
<!-- 主内容 -->
<div class="main-content">
<!-- 顶部导航 -->
<div class="top-navbar">
<div class="page-heading">
<h4>教师管理</h4>
</div>
<div class="d-flex align-items-center gap-3">
<div class="text-end d-none d-md-block">
<div class="small text-muted" id="currentTime"></div>
<div class="fw-bold" id="userName">加载中...</div>
</div>
</div>
</div>
<!-- 筛选栏 -->
<div class="card border-0 shadow-sm mb-4">
<div class="card-body">
<form id="teacherFilterForm" class="row g-3 align-items-end">
<div class="col-md-3">
<label class="form-label">搜索教师</label>
<div class="input-group">
<span class="input-group-text"><i class="fas fa-search"></i></span>
<input type="text" class="form-control" id="teacherSearch" placeholder="工号或姓名">
</div>
</div>
<div class="col-md-9 text-md-end">
<button type="button" class="btn btn-primary" id="addTeacherBtn">
<i class="fas fa-plus me-1"></i> 新增教师
</button>
</div>
</form>
</div>
</div>
<!-- 教师列表 -->
<div class="card border-0 shadow-sm">
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="bg-light">
<tr>
<th class="ps-4">工号</th>
<th>姓名</th>
<th>职称</th>
<th>学院/部门</th>
<th>联系方式</th>
<th class="text-end pe-4">操作</th>
</tr>
</thead>
<tbody id="teacherTableBody">
<tr><td colspan="6" class="text-center py-5">加载中...</td></tr>
</tbody>
</table>
</div>
</div>
<div class="card-footer bg-white border-0 py-3">
<nav>
<ul class="pagination justify-content-center mb-0" id="teacherPagination">
<!-- Pagination rendered by JS -->
</ul>
</nav>
</div>
</div>
</div>
<!-- Add/Edit Teacher Modal -->
<div class="modal fade" id="teacherModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="teacherModalTitle">新增教师</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="teacherForm">
<input type="hidden" name="isEdit" id="teacherIsEdit" value="false">
<div class="mb-3">
<label class="form-label">工号</label>
<input type="text" class="form-control" name="id" id="teacherId" required>
<div class="form-text">工号将作为登录账号,默认密码同账号</div>
</div>
<div class="mb-3">
<label class="form-label">姓名</label>
<input type="text" class="form-control" name="name" id="teacherNameInput" required>
</div>
<div class="mb-3">
<label class="form-label">学院/部门</label>
<input type="text" class="form-control" name="department" id="teacherDepartment">
</div>
<div class="mb-3">
<label class="form-label">职称</label>
<input type="text" class="form-control" name="title" id="teacherTitle">
</div>
<div class="mb-3">
<label class="form-label">联系方式</label>
<input type="text" class="form-control" name="contact_info" id="teacherContact">
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" id="saveTeacherBtn">保存</button>
</div>
</div>
</div>
</div>
<!-- Bootstrap 5 JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="/public/js/admin.js"></script>
</body>
</html>

View File

@@ -3,492 +3,327 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>用户管理 - XX学校成绩管理系统</title>
<link rel="stylesheet" href="/public/css/style.css">
<title>用户管理 - 管理员端</title>
<!-- Bootstrap 5 CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- Google Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;700&display=swap" rel="stylesheet">
<style>
:root {
--sidebar-width: 260px;
--primary-color: #4e73df;
--secondary-color: #858796;
--light-bg: #f8f9fc;
--admin-gradient: linear-gradient(135deg, #4e73df 0%, #224abe 100%);
}
body {
font-family: 'Noto Sans SC', sans-serif;
background-color: var(--light-bg);
overflow-x: hidden;
}
/* 侧边栏样式 */
.sidebar {
width: var(--sidebar-width);
height: 100vh;
position: fixed;
left: 0;
top: 0;
background: var(--admin-gradient);
color: white;
z-index: 1000;
transition: all 0.3s;
}
.sidebar-header {
padding: 2rem 1.5rem;
text-align: center;
border-bottom: 1px solid rgba(255,255,255,0.1);
}
.sidebar-brand {
font-size: 1.25rem;
font-weight: 700;
color: white;
text-decoration: none;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
}
.user-profile {
padding: 2rem 1rem;
text-align: center;
}
.user-avatar {
width: 80px;
height: 80px;
background: rgba(255,255,255,0.2);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 1rem;
font-size: 2rem;
}
.user-info h6 { margin-bottom: 0.25rem; font-weight: 600; }
.user-info p { font-size: 0.85rem; opacity: 0.8; margin-bottom: 0; }
.nav-menu { padding: 1rem 0; }
.nav-item { padding: 0.25rem 1rem; }
.nav-link {
color: rgba(255,255,255,0.8);
padding: 0.8rem 1.25rem;
border-radius: 0.5rem;
display: flex;
align-items: center;
gap: 12px;
transition: all 0.2s;
text-decoration: none;
}
.nav-link:hover, .nav-link.active {
color: white;
background: rgba(255,255,255,0.15);
}
.nav-link i { width: 20px; text-align: center; }
/* 主内容区 */
.main-content {
margin-left: var(--sidebar-width);
padding: 2rem;
min-height: 100vh;
}
.top-navbar {
background: white;
padding: 1rem 2rem;
border-radius: 1rem;
box-shadow: 0 0.15rem 1.75rem 0 rgba(58, 59, 69, 0.1);
margin-bottom: 2rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.page-heading h4 { margin-bottom: 0; font-weight: 700; color: #333; }
@media (max-width: 992px) {
.sidebar { left: -var(--sidebar-width); }
.main-content { margin-left: 0; }
.sidebar.active { left: 0; }
}
</style>
</head>
<body>
<!-- 导航<EFBFBD><EFBFBD>?-->
<nav class="navbar">
<div class="container">
<div class="navbar-brand">
<a href="/">
<i class="fas fa-graduation-cap"></i>
<span>XX学校成绩管理系统</span>
<!-- 侧边栏 -->
<div class="sidebar">
<div class="sidebar-header">
<a href="#" class="sidebar-brand">
<i class="fas fa-graduation-cap"></i>
<span>成绩管理系统</span>
</a>
</div>
<div class="user-profile">
<div class="user-avatar">
<i class="fas fa-user-shield"></i>
</div>
<div class="user-info text-white">
<h6 id="adminName">加载中...</h6>
<p>管理员</p>
</div>
</div>
<nav class="nav-menu">
<div class="nav-item">
<a href="/admin/dashboard" class="nav-link">
<i class="fas fa-tachometer-alt"></i>
<span>仪表板</span>
</a>
</div>
<div class="navbar-menu">
<div class="navbar-start">
<a href="/admin/dashboard" class="navbar-item">
<i class="fas fa-tachometer-alt"></i>
<span>仪表<EFBFBD><EFBFBD>?/span>
</a>
<a href="/admin/user_management" class="navbar-item active">
<i class="fas fa-users"></i>
<span>用户管理</span>
</a>
<a href="/admin/student_management" class="navbar-item">
<i class="fas fa-user-graduate"></i>
<span>学生管理</span>
</a>
<a href="/teacher/grade_management" 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>管理<EFBFBD><EFBFBD>?/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" class="dropdown-item">
<i class="fas fa-tachometer-alt"></i>
仪表<E4BBAA><E8A1A8>? </a>
<a href="#" class="dropdown-item">
<i class="fas fa-cog"></i>
系统设置
</a>
<div class="dropdown-divider"></div>
<a href="/" class="dropdown-item">
<i class="fas fa-sign-out-alt"></i>
退出登<E587BA><E799BB>? </a>
</div>
</div>
</div>
<div class="nav-item">
<a href="/admin/user_management" class="nav-link active">
<i class="fas fa-users"></i>
<span>用户管理</span>
</a>
</div>
<div class="nav-item">
<a href="/admin/student_management" class="nav-link">
<i class="fas fa-user-graduate"></i>
<span>学生管理</span>
</a>
</div>
<div class="nav-item">
<a href="/admin/teacher_management" class="nav-link">
<i class="fas fa-chalkboard-teacher"></i>
<span>教师管理</span>
</a>
</div>
<div class="nav-item">
<a href="/admin/grade_statistics" class="nav-link">
<i class="fas fa-chart-bar"></i>
<span>成绩统计</span>
</a>
</div>
<div class="nav-item">
<a href="/admin/system_settings" class="nav-link">
<i class="fas fa-cog"></i>
<span>系统设置</span>
</a>
</div>
<div class="nav-item">
<a href="/admin/data_export" class="nav-link">
<i class="fas fa-download"></i>
<span>数据导出</span>
</a>
</div>
<div class="nav-item">
<a href="/admin/operation_logs" class="nav-link">
<i class="fas fa-history"></i>
<span>操作日志</span>
</a>
</div>
<div class="nav-item mt-4">
<a href="javascript:void(0)" id="logoutBtn" class="nav-link text-warning">
<i class="fas fa-sign-out-alt"></i>
<span>退出登录</span>
</a>
</div>
</nav>
</div>
<!-- 主内容 -->
<div class="main-content">
<!-- 顶部导航 -->
<div class="top-navbar">
<div class="page-heading">
<h4>用户管理</h4>
</div>
<div class="d-flex align-items-center gap-3">
<div class="text-end d-none d-md-block">
<div class="small text-muted" id="currentTime"></div>
<div class="fw-bold" id="userName">加载中...</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="/">主页</a> &gt;
<a href="/admin/dashboard">管理员仪表板</a> &gt;
<span>用户管理</span>
<!-- 筛选栏 -->
<div class="card border-0 shadow-sm mb-4">
<div class="card-body">
<form id="filterForm" class="row g-3 align-items-end">
<div class="col-md-3">
<label class="form-label">搜索用户</label>
<div class="input-group">
<span class="input-group-text"><i class="fas fa-search"></i></span>
<input type="text" class="form-control" id="search" placeholder="ID、姓名或班级">
</div>
</div>
</div>
<div class="col-md-3">
<label class="form-label">角色筛选</label>
<select class="form-select" id="roleFilter">
<option value="">全部角色</option>
<option value="student">学生</option>
<option value="teacher">教师</option>
<option value="admin">管理员</option>
</select>
</div>
<div class="col-md-6 text-md-end">
<button type="button" class="btn btn-primary" id="addUserBtn">
<i class="fas fa-plus me-1"></i> 新增用户
</button>
</div>
</form>
</div>
</div>
<!-- 筛选区<EFBFBD><EFBFBD>?-->
<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="请输入姓<E585A5><E5A793>?>
</div>
<div class="filter-group">
<label for="role-select">角色</label>
<select id="role-select">
<option value="">全部角色</option>
<option value="admin">管理<EFBFBD><EFBFBD>?/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="计算机科学与技<E4B88E><E68A80>?<3F><>?>计算机科学与技<E4B88E><E68A80>?<3F><>?/option>
<option value="计算机科学与技<EFBFBD><EFBFBD>?<EFBFBD><EFBFBD>?>计算机科学与技<EFBFBD><EFBFBD>?<3F><>?/option>
<option value="软件工程1<E7A88B><31>?>软件工程1<E7A88B><31>?/option>
<option value="软件工程2<EFBFBD><EFBFBD>?>软件工程2<EFBFBD><EFBFBD>?/option>
<option value="网络工程1<E7A88B><31>?>网络工程1<E7A88B><31>?/option>
<option value="网络工程2<EFBFBD><EFBFBD>?>网络工程2<EFBFBD><EFBFBD>?/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>
<!-- 用户列表 -->
<div class="card border-0 shadow-sm">
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="bg-light">
<tr>
<th>用户ID</th>
<th class="ps-4">ID</th>
<th>姓名</th>
<th>角色</th>
<th>班级</th>
<th>联系电话</th>
<th>邮箱</th>
<th>班级/部门</th>
<th>注册时间</th>
<th>操作</th>
<th class="text-end pe-4">操作</th>
</tr>
</thead>
<tbody id="user-table-body">
<!-- 数据将通过JavaScript动态加<E68081><E58AA0>?-->
<tr>
<td colspan="8" class="loading">
<i class="fas fa-spinner fa-spin"></i>
<p>正在加载用户数据...</p>
</td>
</tr>
<tbody id="userTableBody">
<tr><td colspan="6" class="text-center py-5">加载中...</td></tr>
</tbody>
</table>
</div>
</div>
<div class="card-footer bg-white border-0 py-3">
<nav>
<ul class="pagination justify-content-center mb-0" id="pagination">
<!-- Pagination rendered by JS -->
</ul>
</nav>
</div>
</div>
</div>
<!-- 分页控件 -->
<div class="pagination" id="pagination">
<button id="prev-btn" disabled>
<i class="fas fa-chevron-left"></i>
上一<E4B88A><E4B880>? </button>
<div id="page-numbers">
<button class="active">1</button>
<button>2</button>
<button>3</button>
</div>
<button id="next-btn">
下一<E4B88B><E4B880>? <i class="fas fa-chevron-right"></i>
</button>
<!-- Add/Edit User Modal -->
<div class="modal fade" id="userModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="userModalTitle">新增用户</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="userForm">
<input type="hidden" name="isEdit" id="isEdit" value="false">
<div class="mb-3">
<label class="form-label">用户ID</label>
<input type="text" class="form-control" name="id" id="userId" required>
</div>
<div class="mb-3">
<label class="form-label">姓名</label>
<input type="text" class="form-control" name="name" id="userNameInput" required>
</div>
<div class="mb-3">
<label class="form-label">角色</label>
<select class="form-select" name="role" id="userRole" required>
<option value="student">学生</option>
<option value="teacher">教师</option>
<option value="admin">管理员</option>
</select>
</div>
<div class="mb-3">
<label class="form-label">密码</label>
<input type="password" class="form-control" name="password" id="userPassword" placeholder="默认密码同ID">
<div class="form-text">新增时默认密码与ID相同编辑时留空不修改</div>
</div>
<div class="mb-3">
<label class="form-label">班级/部门</label>
<input type="text" class="form-control" name="class" id="userClass" placeholder="学生必填班级">
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" id="saveUserBtn">保存</button>
</div>
</div>
</div>
</main>
</div>
<!-- 页脚 -->
<footer class="footer">
<div class="container">
<p>&copy; 2023 XX学校成绩管理系统. 版权所<E69D83><E68980>?</p>
<p>技术支<EFBFBD><EFBFBD>? 信息技术中<E69CAF><E4B8AD>?| 联系电话: 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: '计算机科学与技<E4B88E><E68A80>?<3F><>?,
phone: '13800138002',
email: 'li.teacher@xxschool.edu.cn',
registerTime: '2023-02-10'
},
{
id: 'teacher002',
name: '王老师',
role: 'teacher',
className: '软件工程1<EFBFBD><EFBFBD>?,
phone: '13800138003',
email: 'wang.teacher@xxschool.edu.cn',
registerTime: '2023-02-12'
},
{
id: 'student001',
name: '张三',
role: 'student',
className: '计算机科学与技<E4B88E><E68A80>?<3F><>?,
phone: '13800138004',
email: 'zhangsan@xxschool.edu.cn',
registerTime: '2023-03-01'
},
{
id: 'student002',
name: '李四',
role: 'student',
className: '计算机科学与技<EFBFBD><EFBFBD>?<EFBFBD><EFBFBD>?,
phone: '13800138005',
email: 'lisi@xxschool.edu.cn',
registerTime: '2023-03-01'
},
{
id: 'student003',
name: '王五',
role: 'student',
className: '软件工程1<E7A88B><31>?,
phone: '13800138006',
email: 'wangwu@xxschool.edu.cn',
registerTime: '2023-03-02'
},
{
id: 'student004',
name: '赵六',
role: 'student',
className: '软件工程1<EFBFBD><EFBFBD>?,
phone: '13800138007',
email: 'zhaoliu@xxschool.edu.cn',
registerTime: '2023-03-02'
},
{
id: 'student005',
name: '钱七',
role: 'student',
className: '网络工程1<E7A88B><31>?,
phone: '13800138008',
email: 'qianqi@xxschool.edu.cn',
registerTime: '2023-03-03'
},
{
id: 'student006',
name: '孙八',
role: 'student',
className: '网络工程2<EFBFBD><EFBFBD>?,
phone: '13800138009',
email: 'sunba@xxschool.edu.cn',
registerTime: '2023-03-03'
},
{
id: 'student007',
name: '周九',
role: 'student',
className: '计算机科学与技<E4B88E><E68A80>?<3F><>?,
phone: '13800138010',
email: 'zhoujiu@xxschool.edu.cn',
registerTime: '2023-03-04'
}
];
// 当前显示的用户数<E688B7><E695B0>? 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');
// 初始<E5889D><E5A78B>? document.addEventListener('DOMContentLoaded', function() {
renderUserTable();
setupEventListeners();
updatePagination();
});
// 设置事件监听<E79B91><E590AC>? 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>请尝试其他筛选条件或添加新用<E696B0><E794A8>?/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">管理<EFBFBD><EFBFBD>?/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 || '<27><>?}</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);
// 更新按钮状<E992AE><E78AB6>? prevBtn.disabled = currentPage === 1;
nextBtn.disabled = currentPage === totalPages || totalPages === 0;
// 更新页码按钮
let pageButtonsHTML = '';
for (let i = 1; i <= totalPages; i++) {
if (i <= 5) { // 最多显<E5A49A><E698BE>?个页<E4B8AA><E9A1B5>? 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筛<44><E7AD9B>? if (userId && !user.id.toLowerCase().includes(userId.toLowerCase())) {
return false;
}
// 姓名筛<E5908D><E7AD9B>? if (userName && !user.name.toLowerCase().includes(userName.toLowerCase())) {
return false;
}
// 角色筛<E889B2><E7AD9B>? if (role && user.role !== role) {
return false;
}
// 班级筛<E7BAA7><E7AD9B>? 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完成后实<E5908E><E5AE9E>?);
// 这里可以打开一个模态框来添加新用户
}
// 处理导出
function handleExport() {
alert('导出功能将在后端API完成后实<EFBFBD><EFBFBD>?);
// 这里可以导出为Excel或CSV格式
}
// 编辑用户
function editUser(userId) {
alert(`编辑用户 ${userId} - 功能将在后端API完成后实现`);
// 这里可以打开一个模态框来编辑用户信<E688B7><E4BFA1>? }
// 删除用户
function deleteUser(userId) {
if (confirm(`确定要删除用<EFBFBD><EFBFBD>?${userId} 吗?此操作不可撤销。`)) {
// 从模拟数据中删除
const index = mockUsers.findIndex(user => user.id === userId);
if (index !== -1) {
mockUsers.splice(index, 1);
// 更新当前显示的数<E79A84><E695B0>? 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>
<!-- Bootstrap 5 JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="/public/js/admin.js"></script>
</body>
</html>

View File

@@ -253,54 +253,65 @@
</div>
<div class="col-md-8 mb-4">
<div class="card">
<div class="card-header">
<i class="fas fa-user-edit me-2 text-primary"></i> 基本信息
<div class="card shadow-sm border-0 mb-4">
<div class="card-header bg-white py-3">
<h5 class="mb-0 fw-bold"><i class="fas fa-user-edit me-2 text-primary"></i>基本信息</h5>
</div>
<div class="card-body p-4">
<form>
<form id="profileForm">
<div class="row mb-3">
<div class="col-md-6">
<label class="form-label">学号</label>
<input type="text" class="form-control" id="profileId" readonly disabled>
<input type="text" class="form-control bg-light" id="profileId" readonly disabled>
</div>
<div class="col-md-6">
<label class="form-label">姓名</label>
<input type="text" class="form-control" id="profileNameInput" readonly>
<input type="text" class="form-control" id="profileNameInput" placeholder="请输入姓名">
</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<label class="form-label">班级</label>
<input type="text" class="form-control" id="profileClass" readonly disabled>
<input type="text" class="form-control" id="profileClass" placeholder="请输入班级">
</div>
<div class="col-md-6">
<label class="form-label">角色</label>
<input type="text" class="form-control" value="学生" readonly disabled>
<input type="text" class="form-control bg-light" value="学生" readonly disabled>
</div>
</div>
<hr class="my-4">
<h6 class="mb-3 fw-bold text-secondary">安全设置</h6>
<div class="mb-3">
<label class="form-label">原密码</label>
<input type="password" class="form-control" id="oldPassword" placeholder="请输入原密码">
</div>
<div class="row mb-4">
<div class="col-md-6">
<label class="form-label">新密码</label>
<input type="password" class="form-control" id="newPassword" placeholder="请输入新密码">
</div>
<div class="col-md-6">
<label class="form-label">确认新密码</label>
<input type="password" class="form-control" id="confirmPassword" placeholder="再次输入新密码">
</div>
</div>
<div class="text-end">
<button type="button" class="btn btn-primary px-4" id="savePasswordBtn">
<i class="fas fa-save me-1"></i> 保存更改
<button type="button" id="saveProfileBtn" class="btn btn-outline-primary">
<i class="fas fa-save me-1"></i> 保存基本信息
</button>
</div>
</form>
</div>
</div>
<div class="card shadow-sm border-0">
<div class="card-header bg-white py-3">
<h5 class="mb-0 fw-bold"><i class="fas fa-key me-2 text-primary"></i>修改密码</h5>
</div>
<div class="card-body p-4">
<form id="passwordForm">
<div class="mb-3">
<label for="oldPassword" class="form-label">原密码</label>
<input type="password" class="form-control" id="oldPassword" required placeholder="请输入原密码">
</div>
<div class="row mb-3">
<div class="col-md-6">
<label for="newPassword" class="form-label">新密码</label>
<input type="password" class="form-control" id="newPassword" required placeholder="请输入新密码">
</div>
<div class="col-md-6">
<label for="confirmPassword" class="form-label">确认新密码</label>
<input type="password" class="form-control" id="confirmPassword" required placeholder="请再次输入新密码">
</div>
</div>
<div id="passwordError" class="text-danger mb-3" style="display: none;"></div>
<div class="text-end">
<button type="submit" class="btn btn-primary px-4">
<i class="fas fa-save me-1"></i> 修改密码
</button>
</div>
</form>
@@ -342,26 +353,58 @@
updateTime();
setInterval(updateTime, 1000);
// 保存基本信息逻辑
const saveProfileBtn = document.getElementById('saveProfileBtn');
if (saveProfileBtn) {
saveProfileBtn.addEventListener('click', async () => {
const name = document.getElementById('profileNameInput').value;
const className = document.getElementById('profileClass').value;
try {
const res = await fetch('/api/auth/update-profile', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, class: className })
});
const result = await res.json();
if (result.success) {
alert('资料更新成功');
// 更新侧边栏和顶栏
document.getElementById('userName').textContent = name;
document.getElementById('studentName').textContent = name;
document.getElementById('profileName').textContent = name;
} else {
alert(result.message || '更新失败');
}
} catch (e) {
console.error('Update profile failed', e);
alert('系统错误');
}
});
}
// 修改密码逻辑
const savePasswordBtn = document.getElementById('savePasswordBtn');
if (savePasswordBtn) {
savePasswordBtn.addEventListener('click', async () => {
const passwordForm = document.getElementById('passwordForm');
if (passwordForm) {
passwordForm.addEventListener('submit', async (e) => {
e.preventDefault();
const oldPassword = document.getElementById('oldPassword').value;
const newPassword = document.getElementById('newPassword').value;
const confirmPassword = document.getElementById('confirmPassword').value;
const errorEl = document.getElementById('passwordError');
if (!oldPassword || !newPassword || !confirmPassword) {
alert('请填写所有密码字段');
return;
}
errorEl.style.display = 'none';
if (newPassword !== confirmPassword) {
alert('两次输入的新密码不一致');
errorEl.textContent = '两次输入的新密码不一致';
errorEl.style.display = 'block';
return;
}
if (newPassword.length < 6) {
alert('新密码长度至少为 6 位');
errorEl.textContent = '新密码长度至少为 6 位';
errorEl.style.display = 'block';
return;
}
@@ -377,17 +420,16 @@
const result = await response.json();
if (result.success) {
alert('密码修改成功,请重新登录');
// 登出并跳转到登录页
fetch('/api/auth/logout', { method: 'POST' })
.then(() => {
window.location.href = '/login';
});
await fetch('/api/auth/logout', { method: 'POST' });
window.location.href = '/login';
} else {
alert(result.message || '修改失败');
errorEl.textContent = result.message || '修改失败';
errorEl.style.display = 'block';
}
} catch (error) {
console.error('Update password error:', error);
alert('网络错误,请稍后再试');
errorEl.textContent = '网络错误,请稍后再试';
errorEl.style.display = 'block';
}
});
}

View File

@@ -201,19 +201,25 @@
<nav class="nav-menu">
<div class="nav-item">
<a href="#" class="nav-link active">
<a href="/teacher/dashboard" class="nav-link active">
<i class="fas fa-tachometer-alt"></i>
<span>教师仪表板</span>
</a>
</div>
<div class="nav-item">
<a href="#" class="nav-link">
<i class="fas fa-list-alt"></i>
<span>课程列表</span>
<a href="/teacher/grade_entry" class="nav-link">
<i class="fas fa-edit"></i>
<span>成绩录入</span>
</a>
</div>
<div class="nav-item">
<a href="#" class="nav-link">
<a href="/teacher/grade_management" class="nav-link">
<i class="fas fa-tasks"></i>
<span>成绩管理</span>
</a>
</div>
<div class="nav-item">
<a href="/teacher/profile" class="nav-link">
<i class="fas fa-user-edit"></i>
<span>个人资料</span>
</a>
@@ -248,7 +254,7 @@
<!-- 统计卡片 -->
<div class="row g-4 mb-4">
<div class="col-xl-4 col-md-6">
<div class="col-xl-3 col-md-6">
<div class="card stat-card h-100 p-3">
<div class="d-flex justify-content-between align-items-start">
<div>
@@ -261,7 +267,20 @@
</div>
</div>
</div>
<div class="col-xl-4 col-md-6">
<div class="col-xl-3 col-md-6">
<div class="card stat-card h-100 p-3">
<div class="d-flex justify-content-between align-items-start">
<div>
<p class="text-secondary small mb-1 fw-bold">负责班级</p>
<h3 class="mb-0 fw-bold" id="classCount">0</h3>
</div>
<div class="stat-icon bg-info bg-opacity-10 text-info">
<i class="fas fa-graduation-cap"></i>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6">
<div class="card stat-card h-100 p-3">
<div class="d-flex justify-content-between align-items-start">
<div>
@@ -274,7 +293,7 @@
</div>
</div>
</div>
<div class="col-xl-4 col-md-6">
<div class="col-xl-3 col-md-6">
<div class="card stat-card h-100 p-3">
<div class="d-flex justify-content-between align-items-start">
<div>
@@ -289,10 +308,18 @@
</div>
</div>
<!-- 负责班级 -->
<div class="mb-4" id="managedClassesSection" style="display: none;">
<h5 class="mb-3 fw-bold"><i class="fas fa-users-cog me-2 text-info"></i>我负责的班级</h5>
<div class="row" id="managedClassList">
<!-- 班级卡片 -->
</div>
</div>
<!-- 课程列表 -->
<div class="d-flex justify-content-between align-items-center mb-4">
<h5 class="mb-0 fw-bold"><i class="fas fa-chalkboard me-2 text-primary"></i>我的负责课程</h5>
<button class="btn btn-sm btn-primary px-3">
<button id="addCourseBtn" class="btn btn-sm btn-primary px-3">
<i class="fas fa-plus me-1"></i> 新增课程
</button>
</div>
@@ -306,6 +333,91 @@
</div>
</div>
<!-- Add Course Modal -->
<div class="modal fade" id="addCourseModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">新增课程</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="addCourseForm">
<div class="mb-3">
<label class="form-label">课程名称</label>
<input type="text" class="form-control" name="course_name" required>
</div>
<div class="mb-3">
<label class="form-label">课程代码</label>
<input type="text" class="form-control" name="course_code" required placeholder="如: CS101">
</div>
<div class="mb-3">
<label class="form-label">学分</label>
<input type="number" step="0.5" class="form-control" name="credit" required>
</div>
<div class="mb-3">
<label class="form-label">授课班级</label>
<select class="form-select" name="class_id" id="classSelect" required>
<option value="">加载中...</option>
</select>
</div>
<div class="mb-3">
<label class="form-label">学期</label>
<input type="text" class="form-control" name="semester" placeholder="如: 2023-2024 秋季" required>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" id="saveCourseBtn">创建课程</button>
</div>
</div>
</div>
</div>
<!-- Edit Course Modal -->
<div class="modal fade" id="editCourseModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">编辑课程</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="editCourseForm">
<input type="hidden" name="id" id="editCourseId">
<div class="mb-3">
<label class="form-label">课程名称</label>
<input type="text" class="form-control" name="course_name" id="editCourseName" required>
</div>
<div class="mb-3">
<label class="form-label">课程代码</label>
<input type="text" class="form-control" name="course_code" id="editCourseCode" required>
</div>
<div class="mb-3">
<label class="form-label">学分</label>
<input type="number" step="0.5" class="form-control" name="credit" id="editCourseCredit" required>
</div>
<div class="mb-3">
<label class="form-label">授课班级</label>
<select class="form-select" name="class_id" id="editClassSelect" required>
<option value="">加载中...</option>
</select>
</div>
<div class="mb-3">
<label class="form-label">学期</label>
<input type="text" class="form-control" name="semester" id="editSemester" required>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" id="updateCourseBtn">保存修改</button>
</div>
</div>
</div>
</div>
<!-- Bootstrap 5 JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="/public/js/auth.js"></script>

View File

@@ -3,445 +3,242 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>成绩录入 - XX学校成绩管理系统</title>
<link rel="stylesheet" href="/public/css/style.css">
<title>成绩录入 - 教师端</title>
<!-- Bootstrap 5 CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- Google Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;700&display=swap" rel="stylesheet">
<style>
:root {
--sidebar-width: 260px;
--primary-color: #1cc88a;
--secondary-color: #858796;
--light-bg: #f8f9fc;
--teacher-gradient: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
}
body {
font-family: 'Noto Sans SC', sans-serif;
background-color: var(--light-bg);
overflow-x: hidden;
}
/* 侧边栏样式 */
.sidebar {
width: var(--sidebar-width);
height: 100vh;
position: fixed;
left: 0;
top: 0;
background: var(--teacher-gradient);
color: white;
z-index: 1000;
transition: all 0.3s;
}
.sidebar-header {
padding: 2rem 1.5rem;
text-align: center;
border-bottom: 1px solid rgba(255,255,255,0.1);
}
.sidebar-brand {
font-size: 1.25rem;
font-weight: 700;
color: white;
text-decoration: none;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
}
.user-profile {
padding: 2rem 1rem;
text-align: center;
}
.user-avatar {
width: 80px;
height: 80px;
background: rgba(255,255,255,0.2);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 1rem;
font-size: 2rem;
}
.user-info h6 { margin-bottom: 0.25rem; font-weight: 600; }
.user-info p { font-size: 0.85rem; opacity: 0.8; margin-bottom: 0; }
.nav-menu { padding: 1rem 0; }
.nav-item { padding: 0.25rem 1rem; }
.nav-link {
color: rgba(255,255,255,0.8);
padding: 0.8rem 1.25rem;
border-radius: 0.5rem;
display: flex;
align-items: center;
gap: 12px;
transition: all 0.2s;
text-decoration: none;
}
.nav-link:hover, .nav-link.active {
color: white;
background: rgba(255,255,255,0.15);
}
.nav-link i { width: 20px; text-align: center; }
/* 主内容区 */
.main-content {
margin-left: var(--sidebar-width);
padding: 2rem;
min-height: 100vh;
}
.top-navbar {
background: white;
padding: 1rem 2rem;
border-radius: 1rem;
box-shadow: 0 0.15rem 1.75rem 0 rgba(58, 59, 69, 0.1);
margin-bottom: 2rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.page-heading h4 { margin-bottom: 0; font-weight: 700; color: #333; }
@media (max-width: 992px) {
.sidebar { left: -var(--sidebar-width); }
.main-content { margin-left: 0; }
.sidebar.active { left: 0; }
}
</style>
</head>
<body>
<!-- 导航<EFBFBD><EFBFBD>?-->
<nav class="navbar">
<div class="navbar-brand">
<a href="/"><i class="fas fa-graduation-cap"></i> XX学校成绩管理系统</a>
<!-- 侧边栏 -->
<div class="sidebar">
<div class="sidebar-header">
<a href="#" class="sidebar-brand">
<i class="fas fa-chalkboard-teacher"></i>
<span>成绩管理系统</span>
</a>
</div>
<div class="navbar-menu">
<a href="/teacher/dashboard" class="navbar-item"><i class="fas fa-tachometer-alt"></i> 教师仪表<E4BBAA><E8A1A8>?/a>
<a href="/teacher/grade_entry" class="navbar-item active"><i class="fas fa-edit"></i> 成绩录入</a>
<a href="/teacher/grade_management" 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" class="btn btn-outline btn-small"><i class="fas fa-sign-out-alt"></i> 退<><E98080>?/a>
<div class="user-profile">
<div class="user-avatar">
<i class="fas fa-user-tie"></i>
</div>
<div class="user-info text-white">
<h6 id="teacherName">加载中...</h6>
<p>教师 | <span id="teacherId">...</span></p>
</div>
</div>
</nav>
<!-- 面包屑导<E5B191><E5AFBC>?-->
<div class="container">
<div class="breadcrumb">
<a href="/">主页</a>
<i class="fas fa-chevron-right"></i>
<a href="/teacher/dashboard">教师仪表<EFBFBD><EFBFBD>?/a>
<i class="fas fa-chevron-right"></i>
<span>成绩录入</span>
<nav class="nav-menu">
<div class="nav-item">
<a href="/teacher/dashboard" class="nav-link">
<i class="fas fa-tachometer-alt"></i>
<span>教师仪表板</span>
</a>
</div>
<div class="nav-item">
<a href="/teacher/grade_entry" class="nav-link active">
<i class="fas fa-edit"></i>
<span>成绩录入</span>
</a>
</div>
<div class="nav-item">
<a href="/teacher/grade_management" class="nav-link">
<i class="fas fa-tasks"></i>
<span>成绩管理</span>
</a>
</div>
<div class="nav-item">
<a href="/teacher/profile" class="nav-link">
<i class="fas fa-user-edit"></i>
<span>个人资料</span>
</a>
</div>
<div class="nav-item mt-4">
<a href="javascript:void(0)" id="logoutBtn" class="nav-link text-warning">
<i class="fas fa-sign-out-alt"></i>
<span>退出登录</span>
</a>
</div>
</nav>
</div>
<!-- 主内容 -->
<div class="main-content">
<!-- 顶部导航 -->
<div class="top-navbar">
<div class="page-heading">
<h4>成绩录入</h4>
</div>
<div class="d-flex align-items-center gap-3">
<div class="text-end d-none d-md-block">
<div class="small text-muted" id="currentTime"></div>
<div class="fw-bold" id="userName">加载中...</div>
</div>
</div>
</div>
<!-- 录入卡片 -->
<div class="card border-0 shadow-sm">
<div class="card-body p-4">
<div class="row mb-4 align-items-end">
<div class="col-md-4">
<label class="form-label text-muted small fw-bold">选择课程</label>
<select id="courseSelect" class="form-select">
<option value="">加载课程中...</option>
</select>
</div>
<div class="col-md-8 text-end">
<div class="alert alert-info d-inline-block mb-0 py-2 px-3 small">
<i class="fas fa-info-circle me-1"></i> 请先选择课程,系统将自动加载该课程的学生名单
</div>
</div>
</div>
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead class="table-light">
<tr>
<th>学号</th>
<th>姓名</th>
<th style="width: 120px;">平时成绩</th>
<th style="width: 120px;">期中成绩</th>
<th style="width: 120px;">期末成绩</th>
<th>总评</th>
<th>操作</th>
</tr>
</thead>
<tbody id="studentTableBody">
<tr>
<td colspan="7" class="text-center py-5 text-muted">
<i class="fas fa-arrow-up mb-2"></i><br>
请选择课程以加载学生列表
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- 主内容区 -->
<div class="grade-entry-container">
<div class="entry-header">
<h1><i class="fas fa-edit"></i> 成绩录入</h1>
<p>请选择班级和课程然后为每个学生录入成<EFBFBD><EFBFBD>?/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">计算机科学与技<EFBFBD><EFBFBD>?021<32><31>?<3F><>?/option>
<option value="class2">计算机科学与技<EFBFBD><EFBFBD>?021<32><31>?<3F><>?/option>
<option value="class3">软件工程2021<EFBFBD><EFBFBD>?<3F><>?/option>
<option value="class4">软件工程2021<EFBFBD><EFBFBD>?<3F><>?/option>
<option value="class5">网络工程2021<EFBFBD><EFBFBD>?<3F><>?/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="可输入本次考试的说明信息期中考试、期末考试<E88083><E8AF95>?></textarea>
</div>
<div class="students-table-container">
<h3><i class="fas fa-user-graduate"></i> 学生成绩录入</h3>
<p class="text-muted">选择班级后系统将自动加载该班级的学生列<EFBFBD><EFBFBD>?/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动态加<E68081><E58AA0>?-->
</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>首先选择要录入成绩的班级和课<EFBFBD><EFBFBD>?/li>
<li>系统会自动加载该班级的学生列<EFBFBD><EFBFBD>?/li>
<li>为每个学生输入平时成绩、期中成绩和期末成绩</li>
<li>总评成绩将根据权重自动计算默认权重<EFBFBD><EFBFBD>?0%<EFBC8C><E69C9F>?0%<EFBC8C><E69C9F>?0%<25><>?/li>
<li>确认无误后点击“提交成绩”按钮保存数<EFBFBD><EFBFBD>?/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');
// 设置默认考试日期为今<E4B8BA><E4BB8A>? 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;
}
// 显示加载状<E8BDBD><E78AB6>? showLoading();
// 模拟API调用延迟
setTimeout(() => {
loadStudentsForClass(selectedClass);
}, 1000);
});
// 加载学生数据
function loadStudentsForClass(className) {
// 模拟API响应
if (mockStudents.length > 0) {
renderStudentsTable(mockStudents);
} else {
showNoStudents();
}
}
// 渲染学生表格
function renderStudentsTable(students) {
hideLoading();
// 清空表格
studentsTbody.innerHTML = '';
// 添加学生<E5ADA6><E7949F>? 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();
}
// 显示加载状<E8BDBD><E78AB6>? function showLoading() {
studentsLoading.style.display = 'block';
studentsTable.style.display = 'none';
noStudents.style.display = 'none';
}
// 隐藏加载状<E8BDBD><E78AB6>? function hideLoading() {
studentsLoading.style.display = 'none';
}
// 显示无学生数<E7949F><E695B0>? 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%<EFBC8C><E69C9F>?0%<EFBC8C><E69C9F>?0%<25><>? const total = (usual * 0.2) + (midterm * 0.3) + (final * 0.5);
// 显示总评成绩四舍五入到整数<E695B4><E695B0>? 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('请输入课程名<E7A88B><E5908D>?);
courseNameInput.focus();
return false;
}
// 检查是否有学生成绩未填<E69CAA><E5A1AB>? 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(`<60><>?{i + 1}行学生的成绩必须<E5BF85><E9A1BB>?-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();
// 显示提交中状<E4B8AD><E78AB6>? submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> <EFBFBD><EFBFBD>?..';
submitBtn.disabled = true;
// 模拟API调用
setTimeout(() => {
// 模拟成功响应
alert(`成绩提交成功!\n班级<EFBFBD><EFBFBD>?{formData.className}\n课程<EFBFBD><EFBFBD>?{formData.courseName}\n共录<EFBFBD><EFBFBD>?{formData.grades.length}名学生成绩`);
// 重置按钮状<E992AE><E78AB6>? 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('确定要取消吗所有未保存的更改将会丢失<E4B8A2><E5A4B1>?)) {
resetForm();
}
});
// 提交按钮事件
submitBtn.addEventListener('click', submitForm);
// 页面加载时初始化
document.addEventListener('DOMContentLoaded', function() {
// 检查是否有必要的数<E79A84><E695B0>? if (!classSelect.value) {
submitBtn.disabled = true;
}
});
</script>
<!-- Bootstrap 5 JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="/public/js/auth.js"></script>
<script src="/public/js/teacher.js"></script>
</body>
</html>

View File

@@ -3,572 +3,246 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>成绩查询/管理 - 学生成绩管理系统</title>
<link rel="stylesheet" href="/public/css/style.css">
<title>成绩管理 - 教师端</title>
<!-- Bootstrap 5 CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- Google Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;700&display=swap" rel="stylesheet">
<style>
:root {
--sidebar-width: 260px;
--primary-color: #1cc88a;
--secondary-color: #858796;
--light-bg: #f8f9fc;
--teacher-gradient: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
}
body {
font-family: 'Noto Sans SC', sans-serif;
background-color: var(--light-bg);
overflow-x: hidden;
}
/* 侧边栏样式 */
.sidebar {
width: var(--sidebar-width);
height: 100vh;
position: fixed;
left: 0;
top: 0;
background: var(--teacher-gradient);
color: white;
z-index: 1000;
transition: all 0.3s;
}
.sidebar-header {
padding: 2rem 1.5rem;
text-align: center;
border-bottom: 1px solid rgba(255,255,255,0.1);
}
.sidebar-brand {
font-size: 1.25rem;
font-weight: 700;
color: white;
text-decoration: none;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
}
.user-profile {
padding: 2rem 1rem;
text-align: center;
}
.user-avatar {
width: 80px;
height: 80px;
background: rgba(255,255,255,0.2);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 1rem;
font-size: 2rem;
}
.user-info h6 { margin-bottom: 0.25rem; font-weight: 600; }
.user-info p { font-size: 0.85rem; opacity: 0.8; margin-bottom: 0; }
.nav-menu { padding: 1rem 0; }
.nav-item { padding: 0.25rem 1rem; }
.nav-link {
color: rgba(255,255,255,0.8);
padding: 0.8rem 1.25rem;
border-radius: 0.5rem;
display: flex;
align-items: center;
gap: 12px;
transition: all 0.2s;
text-decoration: none;
}
.nav-link:hover, .nav-link.active {
color: white;
background: rgba(255,255,255,0.15);
}
.nav-link i { width: 20px; text-align: center; }
/* 主内容区 */
.main-content {
margin-left: var(--sidebar-width);
padding: 2rem;
min-height: 100vh;
}
.top-navbar {
background: white;
padding: 1rem 2rem;
border-radius: 1rem;
box-shadow: 0 0.15rem 1.75rem 0 rgba(58, 59, 69, 0.1);
margin-bottom: 2rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.page-heading h4 { margin-bottom: 0; font-weight: 700; color: #333; }
@media (max-width: 992px) {
.sidebar { left: -var(--sidebar-width); }
.main-content { margin-left: 0; }
.sidebar.active { left: 0; }
}
</style>
</head>
<body>
<!-- 导航<EFBFBD><EFBFBD>?-->
<nav class="navbar">
<div class="nav-container">
<div class="nav-brand">
<a href="/">
<i class="fas fa-graduation-cap"></i>
<span>XX学校成绩管理系统</span>
</a>
<!-- 侧边栏 -->
<div class="sidebar">
<div class="sidebar-header">
<a href="#" class="sidebar-brand">
<i class="fas fa-chalkboard-teacher"></i>
<span>成绩管理系统</span>
</a>
</div>
<div class="user-profile">
<div class="user-avatar">
<i class="fas fa-user-tie"></i>
</div>
<div class="nav-menu">
<div class="user-info text-white">
<h6 id="teacherName">加载中...</h6>
<p>教师 | <span id="teacherId">...</span></p>
</div>
</div>
<nav class="nav-menu">
<div class="nav-item">
<a href="/teacher/dashboard" class="nav-link">
<i class="fas fa-tachometer-alt"></i>
<span>教师仪表<EFBFBD><EFBFBD>?/span>
<span>教师仪表</span>
</a>
</div>
<div class="nav-item">
<a href="/teacher/grade_entry" class="nav-link">
<i class="fas fa-edit"></i>
<span>成绩录入</span>
</a>
</div>
<div class="nav-item">
<a href="/teacher/grade_management" class="nav-link active">
<i class="fas fa-search"></i>
<i class="fas fa-tasks"></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" class="btn-logout">
<i class="fas fa-sign-out-alt"></i>
<span>退<EFBFBD><EFBFBD>?/span>
<div class="nav-item">
<a href="/teacher/profile" class="nav-link">
<i class="fas fa-user-edit"></i>
<span>个人资料</span>
</a>
</div>
<div class="nav-item mt-4">
<a href="javascript:void(0)" id="logoutBtn" class="nav-link text-warning">
<i class="fas fa-sign-out-alt"></i>
<span>退出登录</span>
</a>
</div>
</nav>
</div>
<!-- 主内容 -->
<div class="main-content">
<!-- 顶部导航 -->
<div class="top-navbar">
<div class="page-heading">
<h4>成绩管理</h4>
</div>
<div class="d-flex align-items-center gap-3">
<div class="text-end d-none d-md-block">
<div class="small text-muted" id="currentTime"></div>
<div class="fw-bold" id="userName">加载中...</div>
</div>
</div>
</div>
</nav>
<!-- 主内容区 -->
<main class="main-content">
<div class="container">
<!-- 面包屑导<E5B191><E5AFBC>?-->
<div class="breadcrumb">
<a href="/">主页</a>
<i class="fas fa-chevron-right"></i>
<a href="/teacher/dashboard">教师仪表<EFBFBD><EFBFBD>?/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>
<!-- 筛选区<E98089><E58CBA>?-->
<div class="filter-section">
<h2 class="filter-title">
<i class="fas fa-filter"></i>
筛选条<E98089><E69DA1>? </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">计算机科学与技<EFBFBD><EFBFBD>?<3F><>?/option>
<option value="class2">计算机科学与技<EFBFBD><EFBFBD>?<3F><>?/option>
<option value="class3">软件工程1<EFBFBD><EFBFBD>?/option>
<option value="class4">软件工程2<EFBFBD><EFBFBD>?/option>
<option value="class5">网络工程1<EFBFBD><EFBFBD>?/option>
</select>
</div>
<div class="form-group">
<label for="course-select">课程</label>
<select id="course-select" class="form-control">
<!-- 筛选卡片 -->
<div class="card border-0 shadow-sm mb-4">
<div class="card-body p-4">
<div class="row g-3">
<div class="col-md-4">
<label class="form-label text-muted small fw-bold">课程筛选</label>
<select id="courseSelectFilter" class="form-select">
<option value="">全部课程</option>
<option value="course1">数据结构</option>
<option value="course2">操作系统</option>
<option value="course3">计算机网<EFBFBD><EFBFBD>?/option>
<option value="course4">数据库系<EFBFBD><EFBFBD>?/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 class="col-md-4">
<label class="form-label text-muted small fw-bold">学生查找</label>
<input type="text" id="studentNameFilter" 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>
重置
<div class="col-md-4 d-flex align-items-end">
<button id="searchBtn" class="btn btn-primary w-100">
<i class="fas fa-search me-2"></i> 查询成绩
</button>
</div>
</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="card border-0 shadow-sm">
<div class="card-body p-4">
<div class="table-responsive">
<table class="grade-table">
<thead>
<table class="table table-hover align-middle">
<thead class="table-light">
<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>
<th>等级</th>
<th>操作</th>
</tr>
</thead>
<tbody id="grade-table-body">
<!-- 数据将通过JavaScript动态加<E68081><E58AA0>?-->
<tr>
<td colspan="11" class="loading">
<i class="fas fa-spinner fa-spin"></i>
<p>正在加载成绩数据...</p>
</td>
</tr>
<tbody id="gradeTableBody">
<tr><td colspan="7" class="text-center py-5 text-muted">请选择条件进行查询</td></tr>
</tbody>
</table>
</div>
<!-- 分页 -->
<div class="pagination">
<button id="prev-page" disabled>上一<EFBFBD><EFBFBD>?/button>
<button class="active">1</button>
<button>2</button>
<button>3</button>
<button id="next-page">下一<EFBFBD><EFBFBD>?/button>
</div>
</div>
</div>
</main>
</div>
<!-- 页脚 -->
<footer class="footer">
<div class="container">
<p>&copy; 2023 XX学校成绩管理系统. 版权所<E69D83><E68980>?</p>
<p>技术支<EFBFBD><EFBFBD>? 计算机科学与技术学<E69CAF><E5ADA6>?/p>
</div>
</footer>
<script>
// 模拟成绩数据
const mockGrades = [
{
id: 1,
studentId: "20230001",
studentName: "张三",
className: "计算机科学与技<E4B88E><E68A80>?<3F><>?,
courseName: "数据结构",
regularScore: 85,
midtermScore: 88,
finalScore: 90,
totalScore: 88.5,
grade: "优秀"
},
{
id: 2,
studentId: "20230002",
studentName: "李四",
className: "计算机科学与技<EFBFBD><EFBFBD>?<EFBFBD><EFBFBD>?,
courseName: "数据结构",
regularScore: 78,
midtermScore: 82,
finalScore: 85,
totalScore: 82.3,
grade: "良好"
},
{
id: 3,
studentId: "20230003",
studentName: "王五",
className: "计算机科学与技<E4B88E><E68A80>?<3F><>?,
courseName: "操作系统",
regularScore: 92,
midtermScore: 88,
finalScore: 95,
totalScore: 91.8,
grade: "优秀"
},
{
id: 4,
studentId: "20230004",
studentName: "赵六",
className: "计算机科学与技<EFBFBD><EFBFBD>?<EFBFBD><EFBFBD>?,
courseName: "操作系统",
regularScore: 65,
midtermScore: 70,
finalScore: 68,
totalScore: 67.9,
grade: "及格"
},
{
id: 5,
studentId: "20230005",
studentName: "钱七",
className: "软件工程1<E7A88B><31>?,
courseName: "数据库系<EFBFBD><EFBFBD>?,
regularScore: 88,
midtermScore: 85,
finalScore: 90,
totalScore: 87.9,
grade: "良好"
},
{
id: 6,
studentId: "20230006",
studentName: "孙八",
className: "软件工程1<E7A88B><31>?,
courseName: "数据库系<EFBFBD><EFBFBD>?,
regularScore: 75,
midtermScore: 78,
finalScore: 80,
totalScore: 78.1,
grade: "中等"
},
{
id: 7,
studentId: "20230007",
studentName: "周九",
className: "软件工程2<E7A88B><32>?,
courseName: "软件工程",
regularScore: 90,
midtermScore: 92,
finalScore: 88,
totalScore: 89.6,
grade: "优秀"
},
{
id: 8,
studentId: "20230008",
studentName: "吴十",
className: "软件工程2<EFBFBD><EFBFBD>?,
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();
// 初始化页<E58C96><E9A1B5>? 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>未找到相关成绩记<E7BBA9><E8AEB0>?/h3>
<p>请尝试调整筛选条件或添加新的成绩记录</p>
</td>
</tr>
`;
return;
}
let tableHTML = '';
grades.forEach(grade => {
// 根据成绩确定CSS<53><53>? 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();
});
});
}
// 设置事件监听<E79B91><E590AC>? 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('请先选择要删除的成绩记录<E8AEB0><E5BD95>?);
return;
}
if (confirm(`确定要删除选中<E98089><E4B8AD>?${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();
// 显示加载状<E8BDBD><E78AB6>? 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 => {
// 班级筛<E7BAA7><E7AD9B>? if (selectedClass && grade.className !== selectedClass) {
return false;
}
// 课程筛<E7A88B><E7AD9B>? if (selectedCourse && grade.courseName !== selectedCourse) {
return false;
}
// 学号筛<E58FB7><E7AD9B>? if (studentId && !grade.studentId.includes(studentId)) {
return false;
}
// 姓名筛<E5908D><E7AD9B>? 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格式导出<EFBFBD><EFBFBD>?);
// 模拟导出过程
exportBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 导出<E5AFBC><E587BA>?..';
exportBtn.disabled = true;
setTimeout(() => {
exportBtn.innerHTML = '<i class="fas fa-file-export"></i> 导出成绩';
exportBtn.disabled = false;
// 在实际应用中,这里会触发文件下载
// 暂时显示成功消息
showNotification('成绩导出成功文件已开始下载<E4B88B><E8BDBD>?, 'success');
}, 1500);
}
// 批量删除成绩
function batchDeleteGrades() {
// 这里应该调用后端API删除成绩
// 暂时使用模拟删除
batchDeleteBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> <EFBFBD><EFBFBD>?..';
batchDeleteBtn.disabled = true;
setTimeout(() => {
// 从模拟数据中删除选中的成<E79A84><E68890>? 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('确定要删除这条成绩记录吗此操作不可撤销<E692A4><E99480>?)) {
// 这里应该调用后端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>
<!-- Bootstrap 5 JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="/public/js/auth.js"></script>
<script src="/public/js/teacher.js"></script>
</body>
</html>

View File

@@ -0,0 +1,283 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>个人资料 - 教师端</title>
<!-- Bootstrap 5 CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- Google Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;700&display=swap" rel="stylesheet">
<style>
:root {
--sidebar-width: 260px;
--primary-color: #1cc88a;
--secondary-color: #858796;
--light-bg: #f8f9fc;
--teacher-gradient: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
}
body {
font-family: 'Noto Sans SC', sans-serif;
background-color: var(--light-bg);
overflow-x: hidden;
}
/* 侧边栏样式 */
.sidebar {
width: var(--sidebar-width);
height: 100vh;
position: fixed;
left: 0;
top: 0;
background: var(--teacher-gradient);
color: white;
z-index: 1000;
transition: all 0.3s;
}
.sidebar-header {
padding: 2rem 1.5rem;
text-align: center;
border-bottom: 1px solid rgba(255,255,255,0.1);
}
.sidebar-brand {
font-size: 1.25rem;
font-weight: 700;
color: white;
text-decoration: none;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
}
.user-profile {
padding: 2rem 1rem;
text-align: center;
}
.user-avatar {
width: 80px;
height: 80px;
background: rgba(255,255,255,0.2);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 1rem;
font-size: 2rem;
}
.user-info h6 { margin-bottom: 0.25rem; font-weight: 600; }
.user-info p { font-size: 0.85rem; opacity: 0.8; margin-bottom: 0; }
.nav-menu { padding: 1rem 0; }
.nav-item { padding: 0.25rem 1rem; }
.nav-link {
color: rgba(255,255,255,0.8);
padding: 0.8rem 1.25rem;
border-radius: 0.5rem;
display: flex;
align-items: center;
gap: 12px;
transition: all 0.2s;
text-decoration: none;
}
.nav-link:hover, .nav-link.active {
color: white;
background: rgba(255,255,255,0.15);
}
.nav-link i { width: 20px; text-align: center; }
/* 主内容区 */
.main-content {
margin-left: var(--sidebar-width);
padding: 2rem;
min-height: 100vh;
}
.top-navbar {
background: white;
padding: 1rem 2rem;
border-radius: 1rem;
box-shadow: 0 0.15rem 1.75rem 0 rgba(58, 59, 69, 0.1);
margin-bottom: 2rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.page-heading h4 { margin-bottom: 0; font-weight: 700; color: #333; }
@media (max-width: 992px) {
.sidebar { left: -var(--sidebar-width); }
.main-content { margin-left: 0; }
.sidebar.active { left: 0; }
}
</style>
</head>
<body>
<!-- 侧边栏 -->
<div class="sidebar">
<div class="sidebar-header">
<a href="#" class="sidebar-brand">
<i class="fas fa-chalkboard-teacher"></i>
<span>成绩管理系统</span>
</a>
</div>
<div class="user-profile">
<div class="user-avatar">
<i class="fas fa-user-tie"></i>
</div>
<div class="user-info text-white">
<h6 id="teacherName">加载中...</h6>
<p>教师 | <span id="teacherId">...</span></p>
</div>
</div>
<nav class="nav-menu">
<div class="nav-item">
<a href="/teacher/dashboard" class="nav-link">
<i class="fas fa-tachometer-alt"></i>
<span>教师仪表板</span>
</a>
</div>
<div class="nav-item">
<a href="/teacher/grade_entry" class="nav-link">
<i class="fas fa-edit"></i>
<span>成绩录入</span>
</a>
</div>
<div class="nav-item">
<a href="/teacher/grade_management" class="nav-link">
<i class="fas fa-tasks"></i>
<span>成绩管理</span>
</a>
</div>
<div class="nav-item">
<a href="/teacher/profile" class="nav-link active">
<i class="fas fa-user-edit"></i>
<span>个人资料</span>
</a>
</div>
<div class="nav-item mt-4">
<a href="javascript:void(0)" id="logoutBtn" class="nav-link text-warning">
<i class="fas fa-sign-out-alt"></i>
<span>退出登录</span>
</a>
</div>
</nav>
</div>
<!-- 主内容 -->
<div class="main-content">
<!-- 顶部导航 -->
<div class="top-navbar">
<div class="page-heading">
<h4>个人资料</h4>
</div>
<div class="d-flex align-items-center gap-3">
<div class="text-end d-none d-md-block">
<div class="small text-muted" id="currentTime"></div>
<div class="fw-bold" id="userName">加载中...</div>
</div>
</div>
</div>
<div class="row">
<div class="col-lg-4 mb-4">
<div class="card border-0 shadow-sm text-center p-4 h-100">
<div class="mb-3">
<div class="bg-light rounded-circle d-inline-flex align-items-center justify-content-center" style="width: 120px; height: 120px;">
<i class="fas fa-user-tie fa-4x text-secondary"></i>
</div>
</div>
<h4 id="profileName">加载中...</h4>
<p class="text-muted" id="profileId">...</p>
</div>
</div>
<div class="col-lg-8 mb-4">
<div class="card border-0 shadow-sm mb-4">
<div class="card-header bg-white py-3">
<h5 class="mb-0 fw-bold">基本信息</h5>
</div>
<div class="card-body p-4">
<form id="profileForm">
<div class="mb-3 row">
<label class="col-sm-3 col-form-label">工号</label>
<div class="col-sm-9">
<input type="text" readonly class="form-control-plaintext" id="inputTeacherId" value="...">
</div>
</div>
<div class="mb-3 row">
<label class="col-sm-3 col-form-label">姓名</label>
<div class="col-sm-9">
<input type="text" class="form-control" id="inputName" placeholder="请输入姓名">
</div>
</div>
<div class="mb-3 row">
<label class="col-sm-3 col-form-label">角色</label>
<div class="col-sm-9">
<input type="text" readonly class="form-control-plaintext" value="教师">
</div>
</div>
<div class="mb-3 row">
<label class="col-sm-3 col-form-label">所属学院/班级</label>
<div class="col-sm-9">
<input type="text" class="form-control" id="inputClass" placeholder="请输入学院或班级名称">
</div>
</div>
<div class="row">
<div class="col-sm-9 offset-sm-3">
<button type="button" id="saveProfileBtn" class="btn btn-outline-primary btn-sm">
<i class="fas fa-save me-1"></i> 保存基本信息
</button>
</div>
</div>
</form>
</div>
</div>
<div class="card border-0 shadow-sm">
<div class="card-header bg-white py-3">
<h5 class="mb-0 fw-bold">修改密码</h5>
</div>
<div class="card-body p-4">
<form id="passwordForm">
<div class="mb-3">
<label for="oldPassword" class="form-label">原密码</label>
<input type="password" class="form-control" id="oldPassword" required placeholder="请输入原密码">
</div>
<div class="mb-3">
<label for="newPassword" class="form-label">新密码</label>
<input type="password" class="form-control" id="newPassword" required placeholder="请输入新密码">
</div>
<div class="mb-3">
<label for="confirmPassword" class="form-label">确认新密码</label>
<input type="password" class="form-control" id="confirmPassword" required placeholder="请再次输入新密码">
</div>
<div id="passwordError" class="text-danger mb-3" style="display: none;"></div>
<button type="submit" class="btn btn-primary">
<i class="fas fa-save me-1"></i> 修改密码
</button>
</form>
</div>
</div>
</div>
</div>
</div>
<!-- Bootstrap 5 JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="/public/js/auth.js"></script>
<script src="/public/js/teacher.js"></script>
</body>
</html>