commit 352698044b2df8169633e4be619a47505765430f Author: 祀梦 <3501646051@qq.com> Date: Sun Dec 21 21:50:37 2025 +0800 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e23eab2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,31 @@ +# Node.js +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.env + +# Python +__pycache__/ +*.pyc +*.pyo +*.pyd +.Python +env/ +venv/ +pip-log.txt +pip-delete-this-directory.txt +.venv/ +.env/ + +# OS +.DS_Store +Thumbs.db + +# IDE +.vscode/ +.idea/ + +# Build +dist/ +build/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..e889523 --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +# WebWork + +这是一个基于 Node.js 后端和原生 HTML/CSS/JS 前端的 Web 项目。 + +## 项目结构 + +- `backend/`: Node.js 服务端代码 +- `frontend/`: 前端静态资源 +- `database/`: 数据库初始化脚本 + +## 快速开始 + +1. 进入 `backend` 目录,运行 `npm install` 安装依赖。 +2. 配置 `.env` 文件(参考 `.env.example` 或根据需要创建)。 +3. 启动服务器:`npm start`。 diff --git a/backend/config/database.js b/backend/config/database.js new file mode 100644 index 0000000..5e39cf1 --- /dev/null +++ b/backend/config/database.js @@ -0,0 +1,64 @@ +const mysql = require('mysql2/promise'); +require('dotenv').config(); + +const pool = mysql.createPool({ + host: process.env.DB_HOST || 'localhost', + user: process.env.DB_USER || 'root', + password: process.env.DB_PASSWORD || '123456', + database: process.env.DB_NAME || 'score_management', + waitForConnections: true, + connectionLimit: 10, + queueLimit: 0 +}); + +// 测试数据库连接 +async function testConnection() { + try { + const connection = await pool.getConnection(); + console.log('数据库连接成功'); + connection.release(); + return true; + } catch (error) { + console.error('数据库连接失败:', error.message); + return false; + } +} + +// 执行查询 +async function query(sql, params) { + try { + const [rows] = await pool.execute(sql, params); + return rows; + } catch (error) { + console.error('数据库查询错误:', error.message); + throw error; + } +} + +// 执行事务 +async function executeTransaction(operations) { + const connection = await pool.getConnection(); + try { + await connection.beginTransaction(); + + for (const operation of operations) { + await connection.execute(operation.sql, operation.params); + } + + await connection.commit(); + console.log('事务执行成功'); + } catch (error) { + await connection.rollback(); + console.error('事务执行失败:', error.message); + throw error; + } finally { + connection.release(); + } +} + +module.exports = { + pool, + query, + executeTransaction, + testConnection +}; \ No newline at end of file diff --git a/backend/middleware/auth.js b/backend/middleware/auth.js new file mode 100644 index 0000000..e7a440a --- /dev/null +++ b/backend/middleware/auth.js @@ -0,0 +1,170 @@ +const db = require('../config/database'); + +/** + * 认证中间件 - 检查用户是否已登录 + */ +const requireAuth = (req, res, next) => { + if (!req.session.user) { + return res.status(401).json({ + success: false, + message: '请先登录' + }); + } + next(); +}; + +/** + * 角色权限中间件 - 检查用户是否具有指定角色 + * @param {Array} allowedRoles - 允许的角色数组 + */ +const requireRole = (allowedRoles) => { + return (req, res, next) => { + if (!req.session.user) { + return res.status(401).json({ + success: false, + message: '请先登录' + }); + } + + const userRole = req.session.user.role; + + if (!allowedRoles.includes(userRole)) { + return res.status(403).json({ + success: false, + message: '权限不足' + }); + } + + next(); + }; +}; + +/** + * 获取当前用户ID + */ +const getCurrentUserId = (req) => { + return req.session.user ? req.session.user.id : null; +}; + +/** + * 获取当前用户角色 + */ +const getCurrentUserRole = (req) => { + return req.session.user ? req.session.user.role : null; +}; + +/** + * 检查是否是自己的数据(学生只能访问自己的数据) + */ +const checkOwnership = async (req, res, next) => { + try { + const userId = getCurrentUserId(req); + const userRole = getCurrentUserRole(req); + + // 管理员和教师可以访问所有数据 + if (userRole === 'admin' || userRole === 'teacher') { + return next(); + } + + // 学生只能访问自己的数据 + if (userRole === 'student') { + const studentId = req.params.studentId || req.body.studentId; + + // 检查学生ID是否属于当前用户 + const [students] = await db.execute( + 'SELECT id FROM students WHERE user_id = ?', + [userId] + ); + + if (students.length === 0) { + return res.status(403).json({ + success: false, + message: '未找到学生信息' + }); + } + + const userStudentId = students[0].id; + + // 如果请求中没有指定学生ID,默认使用当前用户的学生ID + if (!studentId) { + req.studentId = userStudentId; + return next(); + } + + // 检查请求的学生ID是否与当前用户的学生ID匹配 + if (parseInt(studentId) !== userStudentId) { + return res.status(403).json({ + success: false, + message: '无权访问其他学生的数据' + }); + } + + req.studentId = userStudentId; + return next(); + } + + // 其他角色无权访问 + return res.status(403).json({ + success: false, + message: '权限不足' + }); + + } catch (error) { + console.error('所有权检查错误:', error); + res.status(500).json({ + success: false, + message: '服务器错误' + }); + } +}; + +/** + * 输入验证中间件 + */ +const validateInput = (schema) => { + return (req, res, next) => { + const { error } = schema.validate(req.body); + if (error) { + return res.status(400).json({ + success: false, + message: error.details[0].message + }); + } + next(); + }; +}; + +/** + * 日志记录中间件 + */ +const logOperation = (operation) => { + return async (req, res, next) => { + try { + const userId = getCurrentUserId(req); + const userRole = getCurrentUserRole(req); + const ip = req.ip || req.connection.remoteAddress; + + // 记录操作日志 + await db.execute( + 'INSERT INTO operation_logs (user_id, operation, ip_address, user_role) VALUES (?, ?, ?, ?)', + [userId, operation, ip, userRole] + ); + + next(); + } catch (error) { + console.error('日志记录错误:', error); + // 日志记录失败不影响主要操作 + next(); + } + }; +}; + +module.exports = { + requireAuth, + requireRole, + getCurrentUserId, + getCurrentUserRole, + checkOwnership, + validateInput, + logOperation +}; \ No newline at end of file diff --git a/backend/package-lock.json b/backend/package-lock.json new file mode 100644 index 0000000..8a956b6 --- /dev/null +++ b/backend/package-lock.json @@ -0,0 +1,1491 @@ +{ + "name": "grade-management-system", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "grade-management-system", + "version": "1.0.0", + "dependencies": { + "bcryptjs": "^2.4.3", + "body-parser": "^1.20.2", + "cors": "^2.8.5", + "dotenv": "^16.3.1", + "express": "^4.18.2", + "express-mysql-session": "^3.0.0", + "express-session": "^1.17.3", + "mysql2": "^3.6.0" + }, + "devDependencies": { + "nodemon": "^3.0.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==", + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-mysql-session": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/express-mysql-session/-/express-mysql-session-3.0.3.tgz", + "integrity": "sha512-sEYrzFrOs3er+Ie/uk1dt93qz4AQ9SU1mpJJ0HPs0MJ4t4hE9AcDRNq0sZQUwy2F/SbXusBt1E5+FY6KzSqXNg==", + "license": "MIT", + "dependencies": { + "debug": "4.3.4", + "mysql2": "3.10.2" + } + }, + "node_modules/express-mysql-session/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/express-mysql-session/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/express-mysql-session/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "license": "MIT" + }, + "node_modules/express-mysql-session/node_modules/mysql2": { + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.10.2.tgz", + "integrity": "sha512-KCXPEvAkO0RcHPr362O5N8tFY2fXvbjfkPvRY/wGumh4EOemo9Hm5FjQZqv/pCmrnuxGu5OxnSENG0gTXqKMgQ==", + "license": "MIT", + "dependencies": { + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.6.3", + "long": "^5.2.1", + "lru-cache": "^8.0.0", + "named-placeholders": "^1.1.3", + "seq-queue": "^0.0.5", + "sqlstring": "^2.3.2" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/express-session": { + "version": "1.18.2", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.2.tgz", + "integrity": "sha512-SZjssGQC7TzTs9rpPDuUrR23GNZ9+2+IkA/+IJWmvQilTr5OSliEHGF+D9scbIpdC6yGtTI0/VhaHoVes2AN/A==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.7", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.1.0", + "parseurl": "~1.3.3", + "safe-buffer": "5.2.1", + "uid-safe": "~2.1.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "license": "MIT", + "dependencies": { + "is-property": "^1.0.2" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", + "license": "MIT" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/lru-cache": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-8.0.5.tgz", + "integrity": "sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA==", + "license": "ISC", + "engines": { + "node": ">=16.14" + } + }, + "node_modules/lru.min": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.3.tgz", + "integrity": "sha512-Lkk/vx6ak3rYkRR0Nhu4lFUT2VDnQSxBe8Hbl7f36358p6ow8Bnvr8lrLt98H8J1aGxfhbX4Fs5tYg2+FTwr5Q==", + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/mysql2": { + "version": "3.16.0", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.16.0.tgz", + "integrity": "sha512-AEGW7QLLSuSnjCS4pk3EIqOmogegmze9h8EyrndavUQnIUcfkVal/sK7QznE+a3bc6rzPbAiui9Jcb+96tPwYA==", + "license": "MIT", + "dependencies": { + "aws-ssl-profiles": "^1.1.1", + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.7.0", + "long": "^5.2.1", + "lru.min": "^1.0.0", + "named-placeholders": "^1.1.3", + "seq-queue": "^0.0.5", + "sqlstring": "^2.3.2" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/mysql2/node_modules/iconv-lite": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.1.tgz", + "integrity": "sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz", + "integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==", + "license": "MIT", + "dependencies": { + "lru.min": "^1.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nodemon": { + "version": "3.1.11", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.11.tgz", + "integrity": "sha512-is96t8F/1//UHAjNPHpbsNY46ELPpftGUoSVNXwUfMk/qdjSylYrWSu1XavVTBOn526kFiOR733ATgNBCQyH0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/nodemon/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/seq-queue": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", + "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sqlstring": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", + "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "license": "MIT", + "dependencies": { + "random-bytes": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + } + } +} diff --git a/backend/package.json b/backend/package.json new file mode 100644 index 0000000..a6e01ca --- /dev/null +++ b/backend/package.json @@ -0,0 +1,26 @@ +{ + "name": "grade-management-system", + "version": "1.0.0", + "description": "学生成绩管理系统后端", + "main": "server.js", + "scripts": { + "start": "node server.js", + "dev": "nodemon server.js" + }, + "dependencies": { + "express": "^4.18.2", + "cors": "^2.8.5", + "express-session": "^1.17.3", + "express-mysql-session": "^3.0.0", + "mysql2": "^3.6.0", + "bcryptjs": "^2.4.3", + "dotenv": "^16.3.1", + "body-parser": "^1.20.2" + }, + "devDependencies": { + "nodemon": "^3.0.1" + }, + "engines": { + "node": ">=14.0.0" + } +} \ No newline at end of file diff --git a/backend/routes/admin.js b/backend/routes/admin.js new file mode 100644 index 0000000..cf50712 --- /dev/null +++ b/backend/routes/admin.js @@ -0,0 +1,311 @@ +const express = require('express'); +const router = express.Router(); +const db = require('../config/database'); +const { requireAuth, requireRole } = require('../middleware/auth'); + +/** + * 获取所有用户 + */ +router.get('/users', requireAuth, requireRole(['admin']), async (req, res) => { + try { + const { page = 1, limit = 10, search = '', role = '' } = req.query; + const offset = (page - 1) * limit; + + let query = 'SELECT id, name, role, class, created_at FROM users WHERE 1=1'; + let params = []; + + if (search) { + query += ' AND (id LIKE ? OR name LIKE ? OR class LIKE ?)'; + const searchTerm = `%${search}%`; + params.push(searchTerm, searchTerm, searchTerm); + } + + if (role) { + query += ' AND role = ?'; + params.push(role); + } + + // 获取总数 + const countQuery = query.replace('SELECT id, name, role, class, created_at', 'SELECT COUNT(*) as total'); + const countResult = await db.pool.execute(countQuery, params); + const total = countResult[0][0].total; + + // 获取分页数据 + query += ' ORDER BY created_at DESC LIMIT ? OFFSET ?'; + params.push(parseInt(limit), parseInt(offset)); + + const users = await db.pool.execute(query, params); + + res.json({ + success: true, + data: users, + pagination: { + page: parseInt(page), + limit: parseInt(limit), + total, + pages: Math.ceil(total / limit) + } + }); + + } catch (error) { + console.error('获取用户列表错误:', error); + res.status(500).json({ + success: false, + message: '服务器错误' + }); + } +}); + +/** + * 创建用户 + */ +router.post('/users', requireAuth, requireRole(['admin']), async (req, res) => { + try { + const { id, name, password, role, className } = req.body; + + // 输入验证 + if (!id || !name || !password || !role) { + return res.status(400).json({ + success: false, + message: '请填写所有必填字段' + }); + } + + // 检查用户ID是否存在 + const existingUsers = await db.pool.execute( + 'SELECT id FROM users WHERE id = ?', + [id] + ); + + if (existingUsers[0].length > 0) { + return res.status(400).json({ + success: false, + message: '用户ID已存在' + }); + } + + // 哈希密码 + const bcrypt = require('bcrypt'); + const salt = await bcrypt.genSalt(10); + const passwordHash = await bcrypt.hash(password, salt); + + // 创建用户 + const result = await db.pool.execute( + 'INSERT INTO users (id, name, password, role, class) VALUES (?, ?, ?, ?, ?)', + [id, name, passwordHash, role, className || null] + ); + + const userId = result[0].insertId; + + // 根据角色创建相关记录 + if (role === 'student') { + const studentId = 'STU' + Date.now().toString().slice(-6); + await db.pool.execute( + 'INSERT INTO students (user_id, student_id, full_name, class_name) VALUES (?, ?, ?, ?)', + [userId, studentId, fullName, className || '未分配班级'] + ); + } else if (role === 'teacher') { + await db.pool.execute( + 'INSERT INTO teachers (user_id, full_name) VALUES (?, ?)', + [userId, fullName] + ); + } + + res.json({ + success: true, + message: '用户创建成功', + userId + }); + + } catch (error) { + console.error('创建用户错误:', error); + res.status(500).json({ + success: false, + message: '服务器错误' + }); + } +}); + +/** + * 更新用户 + */ +router.put('/users/:id', requireAuth, requireRole(['admin']), async (req, res) => { + try { + const userId = req.params.id; + const { name, role, className } = req.body; + + // 检查用户是否存在 + const users = await db.pool.execute( + 'SELECT * FROM users WHERE id = ?', + [userId] + ); + + if (users[0].length === 0) { + return res.status(404).json({ + success: false, + message: '用户不存在' + }); + } + + const oldRole = users[0][0].role; + + // 更新用户信息 + await db.pool.execute( + 'UPDATE users SET name = ?, role = ?, class = ? WHERE id = ?', + [name, role, className || null, userId] + ); + + // 如果角色改变,更新相关记录 + if (oldRole !== role) { + // 删除旧角色的记录 + if (oldRole === 'student') { + await db.pool.execute('DELETE FROM students WHERE user_id = ?', [userId]); + } else if (oldRole === 'teacher') { + await db.pool.execute('DELETE FROM teachers WHERE user_id = ?', [userId]); + } + + // 创建新角色的记录 + if (role === 'student') { + await db.pool.execute( + 'INSERT INTO students (user_id, class) VALUES (?, ?)', + [userId, className || null] + ); + } else if (role === 'teacher') { + // 教师不需要额外表 + } + } else if (role === 'student' && className) { + // 如果是学生且班级有变化,更新班级 + await db.pool.execute( + 'UPDATE students SET class = ? WHERE user_id = ?', + [className, userId] + ); + } + + res.json({ + success: true, + message: '用户更新成功' + }); + + } catch (error) { + console.error('更新用户错误:', error); + res.status(500).json({ + success: false, + message: '服务器错误' + }); + } +}); + +/** + * 删除用户 + */ +router.delete('/users/:id', requireAuth, requireRole(['admin']), async (req, res) => { + try { + const userId = req.params.id; + + // 检查用户是否存在 + const users = await db.pool.execute( + 'SELECT role FROM users WHERE id = ?', + [userId] + ); + + if (users[0].length === 0) { + return res.status(404).json({ + success: false, + message: '用户不存在' + }); + } + + const userRole = users[0][0].role; + + // 删除相关记录 + if (userRole === 'student') { + await db.pool.execute('DELETE FROM students WHERE user_id = ?', [userId]); + } else if (userRole === 'teacher') { + await db.pool.execute('DELETE FROM teachers WHERE user_id = ?', [userId]); + } + + // 删除用户 + await db.pool.execute('DELETE FROM users WHERE id = ?', [userId]); + + res.json({ + success: true, + message: '用户删除成功' + }); + + } catch (error) { + console.error('删除用户错误:', error); + res.status(500).json({ + success: false, + message: '服务器错误' + }); + } +}); + +/** + * 获取所有班级 + */ +router.get('/classes', requireAuth, requireRole(['admin']), async (req, res) => { + try { + const classes = await db.pool.execute( + 'SELECT DISTINCT class_name FROM students ORDER BY class_name' + ); + + res.json({ + success: true, + data: classes + }); + + } catch (error) { + console.error('获取班级列表错误:', error); + res.status(500).json({ + success: false, + message: '服务器错误' + }); + } +}); + +/** + * 获取统计数据 + */ +router.get('/stats', requireAuth, requireRole(['admin']), async (req, res) => { + try { + // 用户统计 + const userStats = await db.pool.execute( + 'SELECT role, COUNT(*) as count FROM users GROUP BY role' + ); + + // 班级统计 + const classStats = await db.pool.execute( + 'SELECT class_name, COUNT(*) as count FROM students GROUP BY class_name' + ); + + // 课程统计 + const courseStats = await db.pool.execute( + 'SELECT COUNT(*) as total_courses FROM courses' + ); + + // 成绩统计 + const gradeStats = await db.pool.execute( + 'SELECT COUNT(*) as total_grades FROM scores' + ); + + res.json({ + success: true, + data: { + users: userStats[0], + classes: classStats[0], + total_courses: courseStats[0][0].total_courses, + total_grades: gradeStats[0][0].total_grades + } + }); + + } catch (error) { + console.error('获取统计数据错误:', error); + res.status(500).json({ + success: false, + message: '服务器错误' + }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/backend/routes/auth.js b/backend/routes/auth.js new file mode 100644 index 0000000..4f4954c --- /dev/null +++ b/backend/routes/auth.js @@ -0,0 +1,175 @@ +const express = require('express'); +const bcrypt = require('bcryptjs'); +const router = express.Router(); +const db = require('../config/database'); + +// 登录 +router.post('/login', async (req, res) => { + try { + const { id, password, role } = req.body; + + // 输入验证 + if (!id || !password || !role) { + return res.status(400).json({ + success: false, + message: '请输入完整的登录信息' + }); + } + + // 查询用户 + const users = await db.query( + 'SELECT * FROM users WHERE id = ? AND role = ?', + [id, role] + ); + + if (users.length === 0) { + return res.status(401).json({ + success: false, + message: '用户名或密码错误' + }); + } + + const user = users[0]; + + // 验证密码 + const isValidPassword = await bcrypt.compare(password, user.password); + if (!isValidPassword) { + return res.status(401).json({ + success: false, + message: '用户名或密码错误' + }); + } + + // 设置会话 + req.session.user = { + id: user.id, + name: user.name, + role: user.role, + class: user.class + }; + + // 如果是学生,获取学生信息 + if (user.role === 'student') { + const [students] = await db.pool.execute( + 'SELECT * FROM students WHERE id = ?', + [user.id] + ); + if (students[0].length > 0) { + req.session.user.studentInfo = students[0][0]; + } + } + + res.json({ + success: true, + message: '登录成功', + user: req.session.user + }); + + } catch (error) { + console.error('登录错误:', error); + res.status(500).json({ + success: false, + message: '服务器错误' + }); + } +}); + +// 注册 +router.post('/register', async (req, res) => { + try { + const { id, name, password, role, class: userClass } = req.body; + + // 输入验证 + if (!id || !name || !password || !role) { + return res.status(400).json({ + success: false, + message: '请填写所有必填字段(ID、姓名、密码、角色)' + }); + } + + // 学生和教师需要班级字段,管理员不需要 + if ((role === 'student' || role === 'teacher') && !userClass) { + return res.status(400).json({ + success: false, + message: '学生和教师需要填写班级' + }); + } + + // 检查用户ID是否存在 + const existingUsers = await db.query( + 'SELECT id FROM users WHERE id = ?', + [id] + ); + + if (existingUsers.length > 0) { + return res.status(400).json({ + success: false, + message: '用户ID已存在' + }); + } + + // 哈希密码 + const salt = await bcrypt.genSalt(10); + const passwordHash = await bcrypt.hash(password, salt); + + // 创建用户 + await db.pool.execute( + 'INSERT INTO users (id, name, password, role, class) VALUES (?, ?, ?, ?, ?)', + [id, name, passwordHash, role, userClass || null] + ); + + // 如果是学生,创建学生记录 + if (role === 'student') { + await db.pool.execute( + 'INSERT INTO students (id, name, class) VALUES (?, ?, ?)', + [id, name, userClass] + ); + } + + res.json({ + success: true, + message: '注册成功' + }); + + } catch (error) { + console.error('注册错误:', error); + res.status(500).json({ + success: false, + message: '服务器错误' + }); + } +}); + +// 注销 +router.post('/logout', (req, res) => { + req.session.destroy(err => { + if (err) { + return res.status(500).json({ + success: false, + message: '注销失败' + }); + } + res.clearCookie('session_cookie'); + res.json({ + success: true, + message: '注销成功' + }); + }); +}); + +// 获取当前用户信息 +router.get('/me', (req, res) => { + if (!req.session.user) { + return res.status(401).json({ + success: false, + message: '未登录' + }); + } + + res.json({ + success: true, + user: req.session.user + }); +}); + +module.exports = router; \ No newline at end of file diff --git a/backend/routes/student.js b/backend/routes/student.js new file mode 100644 index 0000000..9449ba3 --- /dev/null +++ b/backend/routes/student.js @@ -0,0 +1,143 @@ +const express = require('express'); +const router = express.Router(); +const db = require('../config/database'); +const { requireAuth, requireRole } = require('../middleware/auth'); + +// 获取学生成绩 +router.get('/grades', requireAuth, requireRole(['student']), async (req, res) => { + try { + const userId = req.session.user.id; + + // 获取学生信息 + const students = await db.pool.execute( + 'SELECT id FROM students WHERE user_id = ?', + [userId] + ); + + if (students[0].length === 0) { + return res.status(404).json({ + success: false, + message: '学生信息不存在' + }); + } + + const studentId = students[0][0].id; + + // 获取成绩信息 + const grades = await db.pool.execute(` + SELECT s.*, c.course_code, c.course_name, c.credit, + u.name as teacher_name + FROM scores s + JOIN courses c ON s.course_id = c.id + JOIN users u ON s.teacher_id = u.id + WHERE s.student_id = ? + ORDER BY s.exam_date DESC + `, [studentId]); + + // 计算统计信息 + let totalCredits = 0; + let totalGradePoints = 0; + let totalCourses = grades.length; + + grades.forEach(grade => { + totalCredits += parseFloat(grade.credit); + if (grade.grade_point) { + totalGradePoints += parseFloat(grade.grade_point) * parseFloat(grade.credit); + } + }); + + const gpa = totalCredits > 0 ? (totalGradePoints / totalCredits).toFixed(2) : 0; + + res.json({ + success: true, + grades, + statistics: { + totalCourses, + totalCredits, + gpa, + averageScore: totalCourses > 0 ? + (grades.reduce((sum, g) => sum + parseFloat(g.score || 0), 0) / totalCourses).toFixed(1) : 0 + } + }); + + } catch (error) { + console.error('获取成绩错误:', error); + res.status(500).json({ + success: false, + message: '服务器错误' + }); + } +}); + +// 获取成绩详情 +router.get('/grades/:id', requireAuth, requireRole(['student']), async (req, res) => { + try { + const gradeId = req.params.id; + const userId = req.session.user.id; + + const grades = await db.pool.execute(` + SELECT s.*, c.course_code, c.course_name, c.credit, c.semester, + u.full_name as teacher_name, u.email as teacher_email, + st.student_id as student_number, st.class_name, st.major + FROM scores s + JOIN courses c ON s.course_id = c.id + JOIN users u ON s.teacher_id = u.id + JOIN students st ON s.student_id = st.id + WHERE s.id = ? AND st.user_id = ? + `, [gradeId, userId]); + + if (grades[0].length === 0) { + return res.status(404).json({ + success: false, + message: '成绩不存在' + }); + } + + res.json({ + success: true, + grade: grades[0] + }); + + } catch (error) { + console.error('获取成绩详情错误:', error); + res.status(500).json({ + success: false, + message: '服务器错误' + }); + } +}); + +// 获取学生个人信息 +router.get('/profile', requireAuth, requireRole(['student']), async (req, res) => { + try { + const userId = req.session.user.id; + + const students = await db.pool.execute(` + SELECT s.*, u.username, u.email, u.created_at as account_created + FROM students s + JOIN users u ON s.user_id = u.id + WHERE u.id = ? + `, [userId]); + + if (students[0].length === 0) { + return res.status(404).json({ + success: false, + message: '学生信息不存在' + }); + } + + res.json({ + success: true, + profile: students[0] + }); + + } catch (error) { + console.error('获取个人信息错误:', error); + res.status(500).json({ + success: false, + message: '服务器错误' + }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/backend/routes/teacher.js b/backend/routes/teacher.js new file mode 100644 index 0000000..c27d3f5 --- /dev/null +++ b/backend/routes/teacher.js @@ -0,0 +1,243 @@ +const express = require('express'); +const router = express.Router(); +const db = require('../config/database'); +const { requireAuth, requireRole } = require('../middleware/auth'); + +// 获取教师教授的课程 +router.get('/courses', requireAuth, requireRole(['teacher']), async (req, res) => { + try { + const teacherId = req.session.user.id; + + const courses = await db.pool.execute( + 'SELECT * FROM courses WHERE teacher_id = ? ORDER BY course_code', + [teacherId] + ); + + res.json({ + success: true, + courses + }); + + } catch (error) { + console.error('获取课程错误:', error); + res.status(500).json({ + success: false, + message: '服务器错误' + }); + } +}); + +// 录入成绩 +router.post('/grades', requireAuth, requireRole(['teacher']), async (req, res) => { + try { + const teacherId = req.session.user.id; + const { studentId, courseId, score, examDate, remark } = req.body; + + // 验证输入 + if (!studentId || !courseId || score === undefined) { + return res.status(400).json({ + success: false, + message: '请填写必填字段' + }); + } + + // 检查学生和课程是否存在 + const students = await db.pool.execute( + 'SELECT id FROM students WHERE student_id = ?', + [studentId] + ); + + if (students[0].length === 0) { + return res.status(404).json({ + success: false, + message: '学生不存在' + }); + } + + // 计算绩点和等级 + const numericScore = parseFloat(score); + let gradePoint = 0; + let gradeLevel = 'F'; + + if (numericScore >= 90) { + gradePoint = 4.0; gradeLevel = 'A'; + } else if (numericScore >= 80) { + gradePoint = 3.0; gradeLevel = 'B'; + } else if (numericScore >= 70) { + gradePoint = 2.0; gradeLevel = 'C'; + } else if (numericScore >= 60) { + gradePoint = 1.0; gradeLevel = 'D'; + } + + // 插入成绩 + const result = await db.pool.execute( + `INSERT INTO scores (student_id, course_id, teacher_id, score, + grade_point, grade_level, exam_date, remark) + VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, + [students[0][0].id, courseId, teacherId, numericScore, + gradePoint, gradeLevel, examDate, remark] + ); + + res.json({ + success: true, + message: '成绩录入成功', + gradeId: result.insertId + }); + + } catch (error) { + console.error('录入成绩错误:', error); + + // 处理唯一约束错误 + if (error.code === 'ER_DUP_ENTRY') { + return res.status(400).json({ + success: false, + message: '该学生此课程成绩已存在' + }); + } + + res.status(500).json({ + success: false, + message: '服务器错误' + }); + } +}); + +// 查询成绩(按班级、课程) +router.get('/grades', requireAuth, requireRole(['teacher']), async (req, res) => { + try { + const { class_name, course_id } = req.query; + const teacherId = req.session.user.id; + + let query = ` + SELECT sc.*, st.student_id, st.full_name, st.class_name, + c.course_code, c.course_name, c.credit + FROM scores sc + JOIN students st ON sc.student_id = st.id + JOIN courses c ON sc.course_id = c.id + WHERE sc.teacher_id = ? + `; + + const params = [teacherId]; + + if (class_name) { + query += ' AND st.class_name = ?'; + params.push(class_name); + } + + if (course_id) { + query += ' AND sc.course_id = ?'; + params.push(course_id); + } + + query += ' ORDER BY st.class_name, st.student_id, sc.exam_date DESC'; + + const grades = await db.pool.execute(query, params); + + res.json({ + success: true, + grades + }); + + } catch (error) { + console.error('查询成绩错误:', error); + res.status(500).json({ + success: false, + message: '服务器错误' + }); + } +}); + +// 更新成绩 +router.put('/grades/:id', requireAuth, requireRole(['teacher']), async (req, res) => { + try { + const gradeId = req.params.id; + const teacherId = req.session.user.id; + const { score, examDate, remark } = req.body; + + // 验证成绩是否存在且属于该教师 + const existingGrades = await db.pool.execute( + 'SELECT id FROM scores WHERE id = ? AND teacher_id = ?', + [gradeId, teacherId] + ); + + if (existingGrades[0].length === 0) { + return res.status(404).json({ + success: false, + message: '成绩不存在或无权修改' + }); + } + + // 计算新的绩点和等级 + const numericScore = parseFloat(score); + let gradePoint = 0; + let gradeLevel = 'F'; + + if (numericScore >= 90) { + gradePoint = 4.0; gradeLevel = 'A'; + } else if (numericScore >= 80) { + gradePoint = 3.0; gradeLevel = 'B'; + } else if (numericScore >= 70) { + gradePoint = 2.0; gradeLevel = 'C'; + } else if (numericScore >= 60) { + gradePoint = 1.0; gradeLevel = 'D'; + } + + // 更新成绩 + await db.pool.execute( + `UPDATE scores SET score = ?, grade_point = ?, grade_level = ?, + exam_date = ?, remark = ?, updated_at = CURRENT_TIMESTAMP + WHERE id = ?`, + [numericScore, gradePoint, gradeLevel, examDate, remark, gradeId] + ); + + res.json({ + success: true, + message: '成绩更新成功' + }); + + } catch (error) { + console.error('更新成绩错误:', error); + res.status(500).json({ + success: false, + message: '服务器错误' + }); + } +}); + +// 删除成绩 +router.delete('/grades/:id', requireAuth, requireRole(['teacher']), async (req, res) => { + try { + const gradeId = req.params.id; + const teacherId = req.session.user.id; + + // 验证成绩是否存在且属于该教师 + const existingGrades = await db.pool.execute( + 'SELECT id FROM scores WHERE id = ? AND teacher_id = ?', + [gradeId, teacherId] + ); + + if (existingGrades[0].length === 0) { + return res.status(404).json({ + success: false, + message: '成绩不存在或无权删除' + }); + } + + // 删除成绩 + await db.pool.execute('DELETE FROM scores WHERE id = ?', [gradeId]); + + res.json({ + success: true, + message: '成绩删除成功' + }); + + } catch (error) { + console.error('删除成绩错误:', error); + res.status(500).json({ + success: false, + message: '服务器错误' + }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/backend/server.js b/backend/server.js new file mode 100644 index 0000000..4e9824b --- /dev/null +++ b/backend/server.js @@ -0,0 +1,128 @@ +const express = require('express'); +const cors = require('cors'); +const session = require('express-session'); +const MySQLStore = require('express-mysql-session')(session); +const path = require('path'); +require('dotenv').config(); + +// 导入路由 +const authRoutes = require('./routes/auth'); +const studentRoutes = require('./routes/student'); +const teacherRoutes = require('./routes/teacher'); +const adminRoutes = require('./routes/admin'); + +// 数据库配置 +const db = require('./config/database'); + +const app = express(); +const PORT = process.env.PORT || 3000; + +// 中间件 +app.use(cors({ + origin: 'http://localhost:3000', + credentials: true +})); +app.use(express.json()); +app.use(express.urlencoded({ extended: true })); + +// 会话配置 +const sessionStore = new MySQLStore({ + expiration: 86400000, // 1天 + createDatabaseTable: true, + schema: { + tableName: 'sessions', + columnNames: { + session_id: 'session_id', + expires: 'expires', + data: 'data' + } + } +}, db.pool); + +app.use(session({ + key: 'session_cookie', + secret: process.env.SESSION_SECRET || 'your-secret-key', + store: sessionStore, + resave: false, + saveUninitialized: false, + cookie: { + maxAge: 86400000, + httpOnly: true, + secure: process.env.NODE_ENV === 'production' + } +})); + +// 静态文件服务 +app.use(express.static(path.join(__dirname, '../frontend'))); + +// 重定向旧路径 /frontend/html/* 到 /html/* +app.get('/frontend/html/*', (req, res) => { + const path = req.params[0]; + res.redirect(`/html/${path}`); +}); + +// 路由 +app.use('/api/auth', authRoutes); +app.use('/api/student', studentRoutes); +app.use('/api/teacher', teacherRoutes); +app.use('/api/admin', adminRoutes); + +// 认证中间件 +const { requireAuth, requireRole } = require('./middleware/auth'); + +// 页面路由 +app.get('/', (req, res) => { + res.sendFile(path.join(__dirname, '../frontend/html/login.html')); +}); + +app.get('/dashboard', requireAuth, (req, res) => { + // 根据用户角色重定向到不同的仪表板 + const role = req.session.user?.role; + + switch (role) { + case 'student': + res.redirect('/html/student_dashboard.html'); + break; + case 'teacher': + res.redirect('/html/teacher_dashboard.html'); + break; + case 'admin': + res.redirect('/html/admin_dashboard.html'); + break; + default: + // 如果没有角色信息,重定向到登录页面 + res.redirect('/'); + break; + } +}); + +// 学生页面路由 +app.get('/student/*', requireAuth, requireRole(['student', 'admin', 'teacher']), (req, res, next) => { + next(); +}); + +// 教师页面路由 +app.get('/teacher/*', requireAuth, requireRole(['teacher', 'admin']), (req, res, next) => { + next(); +}); + +// 管理员页面路由 +app.get('/admin/*', requireAuth, requireRole(['admin']), (req, res, next) => { + next(); +}); + +// 404处理 +app.use((req, res) => { + res.status(404).json({ error: 'Not found' }); +}); + +// 错误处理 +app.use((err, req, res, next) => { + console.error(err.stack); + res.status(500).json({ error: 'Internal server error' }); +}); + +app.listen(PORT, () => { + console.log(`Server running on port ${PORT}`); + console.log(`访问地址: http://localhost:${PORT}`); +}); \ No newline at end of file diff --git a/database/init.sql b/database/init.sql new file mode 100644 index 0000000..1ed7ce3 --- /dev/null +++ b/database/init.sql @@ -0,0 +1,214 @@ +-- 学生成绩管理系统数据库初始化脚本 +-- 创建数据库 +CREATE DATABASE IF NOT EXISTS score_management DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +USE score_management; + +-- 用户表(学生、教师、管理员) +CREATE TABLE IF NOT EXISTS users ( + id VARCHAR(20) PRIMARY KEY COMMENT '用户ID(学号/工号)', + name VARCHAR(50) NOT NULL COMMENT '姓名', + password VARCHAR(100) NOT NULL COMMENT '密码(bcrypt加密)', + role ENUM('student', 'teacher', 'admin') NOT NULL COMMENT '角色', + class VARCHAR(20) DEFAULT NULL COMMENT '班级(学生和教师需要,管理员为NULL)', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + INDEX idx_role (role), + INDEX idx_class (class) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; + +-- students 表:学生详细信息(与 users 表 id 关联) +CREATE TABLE IF NOT EXISTS students ( + id VARCHAR(20) PRIMARY KEY COMMENT '学生ID(与users表id一致)', + name VARCHAR(50) NOT NULL COMMENT '姓名', + class VARCHAR(20) COMMENT '班级', + FOREIGN KEY (id) REFERENCES users(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='学生详细信息表'; + +-- scores 表:成绩记录 +CREATE TABLE IF NOT EXISTS scores ( + id INT AUTO_INCREMENT PRIMARY KEY COMMENT '成绩记录ID', + student_id VARCHAR(20) NOT NULL COMMENT '学生ID', + course VARCHAR(100) NOT NULL COMMENT '课程名称', + score DECIMAL(5,2) NOT NULL CHECK (score >= 0 AND score <= 100) COMMENT '成绩', + teacher_id VARCHAR(20) NOT NULL COMMENT '教师ID', + class VARCHAR(20) NOT NULL COMMENT '班级', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + FOREIGN KEY (student_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY (teacher_id) REFERENCES users(id) ON DELETE CASCADE, + INDEX idx_student_id (student_id), + INDEX idx_teacher_id (teacher_id), + INDEX idx_class (class) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='成绩记录表'; + +-- 班级表 +CREATE TABLE IF NOT EXISTS classes ( + id INT PRIMARY KEY AUTO_INCREMENT COMMENT '班级ID', + class_name VARCHAR(50) NOT NULL COMMENT '班级名称', + grade VARCHAR(20) COMMENT '年级', + major VARCHAR(100) COMMENT '专业', + teacher_id INT COMMENT '班主任ID', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + INDEX idx_class_name (class_name), + INDEX idx_teacher_id (teacher_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='班级表'; + +-- 课程表 +CREATE TABLE IF NOT EXISTS courses ( + id INT PRIMARY KEY AUTO_INCREMENT COMMENT '课程ID', + course_code VARCHAR(20) UNIQUE NOT NULL COMMENT '课程代码', + course_name VARCHAR(100) NOT NULL COMMENT '课程名称', + credit DECIMAL(3,1) NOT NULL DEFAULT 2.0 COMMENT '学分', + teacher_id INT NOT NULL COMMENT '授课教师ID', + class_id INT NOT NULL COMMENT '授课班级ID', + semester VARCHAR(20) COMMENT '学期', + academic_year VARCHAR(20) COMMENT '学年', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + INDEX idx_course_code (course_code), + INDEX idx_teacher_id (teacher_id), + INDEX idx_class_id (class_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='课程表'; + +-- 成绩表 +CREATE TABLE IF NOT EXISTS grades ( + id INT PRIMARY KEY AUTO_INCREMENT COMMENT '成绩ID', + student_id INT NOT NULL COMMENT '学生ID', + course_id INT NOT NULL COMMENT '课程ID', + usual_score DECIMAL(5,2) COMMENT '平时成绩', + midterm_score DECIMAL(5,2) COMMENT '期中成绩', + final_score DECIMAL(5,2) COMMENT '期末成绩', + total_score DECIMAL(5,2) COMMENT '总评成绩', + grade_point DECIMAL(3,2) COMMENT '绩点', + grade_level VARCHAR(10) COMMENT '成绩等级(A/B/C/D/F)', + teacher_id INT NOT NULL COMMENT '录入教师ID', + remark TEXT COMMENT '备注', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + UNIQUE KEY uk_student_course (student_id, course_id), + INDEX idx_student_id (student_id), + INDEX idx_course_id (course_id), + INDEX idx_teacher_id (teacher_id), + INDEX idx_total_score (total_score) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='成绩表'; + +-- 操作日志表 +CREATE TABLE IF NOT EXISTS operation_logs ( + id INT PRIMARY KEY AUTO_INCREMENT COMMENT '日志ID', + user_id INT NOT NULL COMMENT '操作用户ID', + operation_type VARCHAR(50) NOT NULL COMMENT '操作类型', + operation_target VARCHAR(100) COMMENT '操作目标', + operation_details TEXT COMMENT '操作详情', + ip_address VARCHAR(45) COMMENT 'IP地址', + user_agent TEXT COMMENT '用户代理', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '操作时间', + INDEX idx_user_id (user_id), + INDEX idx_operation_type (operation_type), + INDEX idx_created_at (created_at) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='操作日志表'; + +-- 插入初始数据 +-- 插入管理员用户(密码:admin123) +INSERT INTO users (username, password, name, role, email, phone) VALUES +('admin', '$2b$10$N9qo8uLOickgx2ZMRZoMy.MrqK.3.6Z1zXjJX.3Q7JzQ7JzQ7JzQ7', '系统管理员', 'admin', 'admin@school.edu', '13800138000'); + +-- 插入示例班级 +INSERT INTO classes (class_name, grade, major) VALUES +('计算机科学与技术2023级1班', '2023', '计算机科学与技术'), +('软件工程2023级1班', '2023', '软件工程'), +('人工智能2023级1班', '2023', '人工智能'); + +-- 插入示例教师(密码:teacher123) +INSERT INTO users (username, password, name, role, class_id, email, phone) VALUES +('t1001', '$2b$10$N9qo8uLOickgx2ZMRZoMy.MrqK.3.6Z1zXjJX.3Q7JzQ7JzQ7JzQ7', '张老师', 'teacher', 1, 'zhang@school.edu', '13800138001'), +('t1002', '$2b$10$N9qo8uLOickgx2ZMRZoMy.MrqK.3.6Z1zXjJX.3Q7JzQ7JzQ7JzQ7', '李老师', 'teacher', 2, 'li@school.edu', '13800138002'); + +-- 插入示例学生(密码:student123) +INSERT INTO users (username, password, name, role, class_id, email, phone) VALUES +('s2023001', '$2b$10$N9qo8uLOickgx2ZMRZoMy.MrqK.3.6Z1zXjJX.3Q7JzQ7JzQ7JzQ7', '张三', 'student', 1, 'zhangsan@school.edu', '13800138111'), +('s2023002', '$2b$10$N9qo8uLOickgx2ZMRZoMy.MrqK.3.6Z1zXjJX.3Q7JzQ7JzQ7JzQ7', '李四', 'student', 1, 'lisi@school.edu', '13800138112'), +('s2023003', '$2b$10$N9qo8uLOickgx2ZMRZoMy.MrqK.3.6Z1zXjJX.3Q7JzQ7JzQ7JzQ7', '王五', 'student', 2, 'wangwu@school.edu', '13800138113'); + +-- 插入示例课程 +INSERT INTO courses (course_code, course_name, credit, teacher_id, class_id, semester, academic_year) VALUES +('CS101', '计算机基础', 3.0, 2, 1, '2023-2024-1', '2023-2024'), +('CS201', '数据结构', 4.0, 2, 1, '2023-2024-1', '2023-2024'), +('SE101', '软件工程导论', 3.0, 3, 2, '2023-2024-1', '2023-2024'), +('AI101', '人工智能基础', 3.5, 3, 3, '2023-2024-1', '2023-2024'); + +-- 插入示例成绩 +INSERT INTO grades (student_id, course_id, usual_score, midterm_score, final_score, total_score, grade_point, grade_level, teacher_id) VALUES +(3, 1, 85.00, 78.00, 82.00, 81.50, 3.2, 'B', 2), +(3, 2, 90.00, 85.00, 88.00, 87.50, 3.7, 'A', 2), +(4, 1, 78.00, 82.00, 80.00, 80.50, 3.0, 'B', 2), +(5, 3, 88.00, 85.00, 90.00, 88.50, 3.8, 'A', 3); + +-- 更新班级表的班主任信息 +UPDATE classes SET teacher_id = 2 WHERE id = 1; +UPDATE classes SET teacher_id = 3 WHERE id = 2; + +-- 创建视图:学生成绩详情视图 +CREATE OR REPLACE VIEW student_grade_details AS +SELECT + g.id, + g.student_id, + u1.name AS student_name, + u1.username AS student_no, + g.course_id, + c.course_code, + c.course_name, + c.credit, + g.usual_score, + g.midterm_score, + g.final_score, + g.total_score, + g.grade_point, + g.grade_level, + g.teacher_id, + u2.name AS teacher_name, + g.remark, + g.created_at, + g.updated_at +FROM grades g +JOIN users u1 ON g.student_id = u1.id +JOIN courses c ON g.course_id = c.id +JOIN users u2 ON g.teacher_id = u2.id; + +-- 创建视图:班级成绩统计视图 +CREATE OR REPLACE VIEW class_grade_statistics AS +SELECT + cl.id AS class_id, + cl.class_name, + c.id AS course_id, + c.course_code, + c.course_name, + COUNT(g.id) AS student_count, + AVG(g.total_score) AS avg_score, + MAX(g.total_score) AS max_score, + MIN(g.total_score) AS min_score, + SUM(CASE WHEN g.grade_level = 'A' THEN 1 ELSE 0 END) AS a_count, + SUM(CASE WHEN g.grade_level = 'B' THEN 1 ELSE 0 END) AS b_count, + SUM(CASE WHEN g.grade_level = 'C' THEN 1 ELSE 0 END) AS c_count, + SUM(CASE WHEN g.grade_level = 'D' THEN 1 ELSE 0 END) AS d_count, + SUM(CASE WHEN g.grade_level = 'F' THEN 1 ELSE 0 END) AS f_count +FROM classes cl +JOIN courses c ON cl.id = c.class_id +LEFT JOIN grades g ON c.id = g.course_id +GROUP BY cl.id, c.id; + +-- 显示表结构信息 +SHOW TABLES; + +-- 显示各表记录数 +SELECT 'users' AS table_name, COUNT(*) AS record_count FROM users +UNION ALL +SELECT 'classes', COUNT(*) FROM classes +UNION ALL +SELECT 'courses', COUNT(*) FROM courses +UNION ALL +SELECT 'grades', COUNT(*) FROM grades +UNION ALL +SELECT 'operation_logs', COUNT(*) FROM operation_logs; + +-- 显示视图 +SHOW FULL TABLES WHERE TABLE_TYPE = 'VIEW'; \ No newline at end of file diff --git a/database/score_management.sql b/database/score_management.sql new file mode 100644 index 0000000..85ac38a --- /dev/null +++ b/database/score_management.sql @@ -0,0 +1,213 @@ +/* + Navicat Premium Dump SQL + + Source Server : test + Source Server Type : MySQL + Source Server Version : 80042 (8.0.42) + Source Host : localhost:3306 + Source Schema : score_management + + Target Server Type : MySQL + Target Server Version : 80042 (8.0.42) + File Encoding : 65001 + + Date: 21/12/2025 21:37:55 +*/ + +SET NAMES utf8mb4; +SET FOREIGN_KEY_CHECKS = 0; + +-- ---------------------------- +-- Table structure for classes +-- ---------------------------- +DROP TABLE IF EXISTS `classes`; +CREATE TABLE `classes` ( + `id` int NOT NULL AUTO_INCREMENT COMMENT '班级ID', + `class_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '班级名称', + `grade` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '年级', + `major` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '专业', + `teacher_id` int NULL DEFAULT NULL COMMENT '班主任ID', + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_class_name`(`class_name` ASC) USING BTREE, + INDEX `idx_teacher_id`(`teacher_id` ASC) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '班级表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of classes +-- ---------------------------- + +-- ---------------------------- +-- Table structure for courses +-- ---------------------------- +DROP TABLE IF EXISTS `courses`; +CREATE TABLE `courses` ( + `id` int NOT NULL AUTO_INCREMENT COMMENT '课程ID', + `course_code` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '课程代码', + `course_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '课程名称', + `credit` decimal(3, 1) NOT NULL DEFAULT 2.0 COMMENT '学分', + `teacher_id` int NOT NULL COMMENT '授课教师ID', + `class_id` int NOT NULL COMMENT '授课班级ID', + `semester` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '学期', + `academic_year` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '学年', + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `course_code`(`course_code` ASC) USING BTREE, + INDEX `idx_course_code`(`course_code` ASC) USING BTREE, + INDEX `idx_teacher_id`(`teacher_id` ASC) USING BTREE, + INDEX `idx_class_id`(`class_id` ASC) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '课程表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of courses +-- ---------------------------- + +-- ---------------------------- +-- Table structure for grades +-- ---------------------------- +DROP TABLE IF EXISTS `grades`; +CREATE TABLE `grades` ( + `id` int NOT NULL AUTO_INCREMENT COMMENT '成绩ID', + `student_id` int NOT NULL COMMENT '学生ID', + `course_id` int NOT NULL COMMENT '课程ID', + `usual_score` decimal(5, 2) NULL DEFAULT NULL COMMENT '平时成绩', + `midterm_score` decimal(5, 2) NULL DEFAULT NULL COMMENT '期中成绩', + `final_score` decimal(5, 2) NULL DEFAULT NULL COMMENT '期末成绩', + `total_score` decimal(5, 2) NULL DEFAULT NULL COMMENT '总评成绩', + `grade_point` decimal(3, 2) NULL DEFAULT NULL COMMENT '绩点', + `grade_level` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '成绩等级(A/B/C/D/F)', + `teacher_id` int NOT NULL COMMENT '录入教师ID', + `remark` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '备注', + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uk_student_course`(`student_id` ASC, `course_id` ASC) USING BTREE, + INDEX `idx_student_id`(`student_id` ASC) USING BTREE, + INDEX `idx_course_id`(`course_id` ASC) USING BTREE, + INDEX `idx_teacher_id`(`teacher_id` ASC) USING BTREE, + INDEX `idx_total_score`(`total_score` ASC) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '成绩表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of grades +-- ---------------------------- + +-- ---------------------------- +-- Table structure for operation_logs +-- ---------------------------- +DROP TABLE IF EXISTS `operation_logs`; +CREATE TABLE `operation_logs` ( + `id` int NOT NULL AUTO_INCREMENT COMMENT '日志ID', + `user_id` int NOT NULL COMMENT '操作用户ID', + `operation_type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '操作类型', + `operation_target` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '操作目标', + `operation_details` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '操作详情', + `ip_address` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT 'IP地址', + `user_agent` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '用户代理', + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '操作时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_user_id`(`user_id` ASC) USING BTREE, + INDEX `idx_operation_type`(`operation_type` ASC) USING BTREE, + INDEX `idx_created_at`(`created_at` ASC) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '操作日志表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of operation_logs +-- ---------------------------- + +-- ---------------------------- +-- Table structure for scores +-- ---------------------------- +DROP TABLE IF EXISTS `scores`; +CREATE TABLE `scores` ( + `id` int NOT NULL AUTO_INCREMENT COMMENT '成绩记录ID', + `student_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '学生ID', + `course` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '课程名称', + `score` decimal(5, 2) NOT NULL COMMENT '成绩', + `teacher_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '教师ID', + `class` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '班级', + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_student_id`(`student_id` ASC) USING BTREE, + INDEX `idx_teacher_id`(`teacher_id` ASC) USING BTREE, + INDEX `idx_class`(`class` ASC) USING BTREE, + CONSTRAINT `scores_ibfk_1` FOREIGN KEY (`student_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT, + CONSTRAINT `scores_ibfk_2` FOREIGN KEY (`teacher_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT, + CONSTRAINT `scores_chk_1` CHECK ((`score` >= 0) and (`score` <= 100)) +) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '成绩记录表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of scores +-- ---------------------------- + +-- ---------------------------- +-- Table structure for sessions +-- ---------------------------- +DROP TABLE IF EXISTS `sessions`; +CREATE TABLE `sessions` ( + `session_id` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, + `expires` int UNSIGNED NOT NULL, + `data` mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL, + PRIMARY KEY (`session_id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of sessions +-- ---------------------------- +INSERT INTO `sessions` VALUES ('KY6QaavAiws7rkdEBFIFDoHefl2bxzlI', 1766334525, '{\"cookie\":{\"originalMaxAge\":86400000,\"expires\":\"2025-12-21T16:28:44.650Z\",\"secure\":false,\"httpOnly\":true,\"path\":\"/\"},\"user\":{\"id\":\"teststudent\",\"name\":\"????\",\"role\":\"student\",\"class\":\"2023?1?\"}}'); +INSERT INTO `sessions` VALUES ('KaDFs4HogLmkjS0HAs6qki6g2FmE3sTL', 1766334465, '{\"cookie\":{\"originalMaxAge\":86400000,\"expires\":\"2025-12-21T16:27:45.314Z\",\"secure\":false,\"httpOnly\":true,\"path\":\"/\"},\"user\":{\"id\":\"teststudent\",\"name\":\"????\",\"role\":\"student\",\"class\":\"2023?1?\"}}'); +INSERT INTO `sessions` VALUES ('QAhXDQ1FOlhU6RhaFOm3ghRtLOW4hBTd', 1766334812, '{\"cookie\":{\"originalMaxAge\":86400000,\"expires\":\"2025-12-21T16:33:31.987Z\",\"secure\":false,\"httpOnly\":true,\"path\":\"/\"},\"user\":{\"id\":\"teststudent\",\"name\":\"????\",\"role\":\"student\",\"class\":\"2023?1?\"}}'); +INSERT INTO `sessions` VALUES ('SAuQyktAI9gAHpXbjARpe-9BL42pDRiV', 1766334764, '{\"cookie\":{\"originalMaxAge\":86400000,\"expires\":\"2025-12-21T16:32:43.695Z\",\"secure\":false,\"httpOnly\":true,\"path\":\"/\"},\"user\":{\"id\":\"teststudent\",\"name\":\"????\",\"role\":\"student\",\"class\":\"2023?1?\"}}'); +INSERT INTO `sessions` VALUES ('XduN1lYhGPeIaLTHbLTNVnTCBtKUCkJR', 1766334689, '{\"cookie\":{\"originalMaxAge\":86400000,\"expires\":\"2025-12-21T16:31:28.994Z\",\"secure\":false,\"httpOnly\":true,\"path\":\"/\"},\"user\":{\"id\":\"teststudent\",\"name\":\"????\",\"role\":\"student\",\"class\":\"2023?1?\"}}'); +INSERT INTO `sessions` VALUES ('Y59PFvvqK7M0DKZshc6ONTmFQjzGyMmV', 1766334426, '{\"cookie\":{\"originalMaxAge\":86400000,\"expires\":\"2025-12-21T16:27:05.673Z\",\"secure\":false,\"httpOnly\":true,\"path\":\"/\"},\"user\":{\"id\":\"teststudent\",\"name\":\"????\",\"role\":\"student\",\"class\":\"2023?1?\"}}'); +INSERT INTO `sessions` VALUES ('rlscT2Pi2EAyLXHs1CNXyQmNSiW8vEo4', 1766334271, '{\"cookie\":{\"originalMaxAge\":86400000,\"expires\":\"2025-12-21T16:24:30.682Z\",\"secure\":false,\"httpOnly\":true,\"path\":\"/\"},\"user\":{\"id\":\"teststudent\",\"name\":\"????\",\"role\":\"student\",\"class\":\"2023?1?\"}}'); +INSERT INTO `sessions` VALUES ('rsaOCJRjYQLPtUWlDmUFJgWcCYZbOCgJ', 1766410574, '{\"cookie\":{\"originalMaxAge\":86400000,\"expires\":\"2025-12-21T15:54:39.935Z\",\"secure\":false,\"httpOnly\":true,\"path\":\"/\"},\"user\":{\"id\":\"123\",\"name\":\"经济局\",\"role\":\"student\",\"class\":\"123\"}}'); +INSERT INTO `sessions` VALUES ('wXxRpNTGY0wqLaHsebSAsw1I6Pb7Ed6w', 1766410584, '{\"cookie\":{\"originalMaxAge\":86400000,\"expires\":\"2025-12-22T13:35:43.191Z\",\"secure\":false,\"httpOnly\":true,\"path\":\"/\"},\"user\":{\"id\":\"567\",\"name\":\"急急急\",\"role\":\"teacher\",\"class\":\"567\"}}'); + +-- ---------------------------- +-- Table structure for students +-- ---------------------------- +DROP TABLE IF EXISTS `students`; +CREATE TABLE `students` ( + `id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '学生ID(与users表id一致)', + `name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '姓名', + `class` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '班级', + PRIMARY KEY (`id`) USING BTREE, + CONSTRAINT `students_ibfk_1` FOREIGN KEY (`id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '学生详细信息表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of students +-- ---------------------------- +INSERT INTO `students` VALUES ('123', '经济局', '123'); +INSERT INTO `students` VALUES ('test123', '????', '????'); +INSERT INTO `students` VALUES ('teststudent', '????', '2023?1?'); + +-- ---------------------------- +-- Table structure for users +-- ---------------------------- +DROP TABLE IF EXISTS `users`; +CREATE TABLE `users` ( + `id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '用户ID(学号/工号)', + `name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '姓名', + `password` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '密码(bcrypt加密)', + `role` enum('student','teacher','admin') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '角色', + `class` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '班级(学生和教师需要,管理员为NULL)', + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_role`(`role` ASC) USING BTREE, + INDEX `idx_class`(`class` ASC) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '用户表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of users +-- ---------------------------- +INSERT INTO `users` VALUES ('123', '经济局', '$2a$10$l0NwrM2fNGgPdqDFqXEGx.UfqOIp8womtWN8/omq1fK15zII7b4Nm', 'student', '123', '2025-12-20 22:47:59', '2025-12-20 22:47:59'); +INSERT INTO `users` VALUES ('567', '急急急', '$2a$10$27a0L4fC0rLjK4.Kpq0CceK1cD4O0cW6XTxwvs4eIcYpKnpvEQdVG', 'teacher', '567', '2025-12-21 20:36:17', '2025-12-21 20:36:17'); +INSERT INTO `users` VALUES ('test123', '????', '$2a$10$61WfURr1uI1e71EWwWXOlOUamTR1/AzH2Kb.6bZKVttdmWGk7V366', 'student', '????', '2025-12-20 23:49:32', '2025-12-20 23:49:32'); +INSERT INTO `users` VALUES ('teststudent', '????', '$2a$10$PfNB72GBDBmu8DLcTLptW.4clzmk9qsjjlxfAMZvAJD8B.QUoXXAK', 'student', '2023?1?', '2025-12-21 00:24:16', '2025-12-21 00:24:16'); + +SET FOREIGN_KEY_CHECKS = 1; diff --git a/frontend/css/main.css b/frontend/css/main.css new file mode 100644 index 0000000..bd0656e --- /dev/null +++ b/frontend/css/main.css @@ -0,0 +1,215 @@ +/* 首页通用交互样式 */ + +/* 滚动时导航栏样式 */ +.navbar-scrolled { + background-color: rgba(255, 255, 255, 0.95) !important; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + backdrop-filter: blur(10px); +} + +.navbar-scrolled .navbar-brand, +.navbar-scrolled .nav-link { + color: #333 !important; +} + +.navbar-scrolled .navbar-toggler-icon { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba(0, 0, 0, 0.7)' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e") !important; +} + +/* 滚动动画效果 */ +.feature-card, .hero-content { + opacity: 0; + transform: translateY(20px); + transition: opacity 0.6s ease, transform 0.6s ease; +} + +.feature-card.animate-in, +.hero-content.animate-in { + opacity: 1; + transform: translateY(0); +} + +/* 返回顶部按钮 */ +#backToTop { + position: fixed; + bottom: 30px; + right: 30px; + width: 50px; + height: 50px; + background-color: #4e73df; + color: white; + border: none; + border-radius: 50%; + cursor: pointer; + opacity: 0; + visibility: hidden; + transition: all 0.3s ease; + z-index: 1000; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.2rem; + box-shadow: 0 4px 15px rgba(78, 115, 223, 0.3); +} + +#backToTop:hover { + background-color: #2e59d9; + transform: translateY(-3px); + box-shadow: 0 6px 20px rgba(78, 115, 223, 0.4); +} + +#backToTop.show { + opacity: 1; + visibility: visible; +} + +/* 通知样式 */ +.notification { + position: fixed; + top: 20px; + right: 20px; + padding: 15px 20px; + border-radius: 8px; + background-color: white; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); + z-index: 9999; + opacity: 0; + transform: translateX(100%); + transition: opacity 0.3s ease, transform 0.3s ease; + max-width: 350px; + display: flex; + align-items: center; +} + +.notification.show { + opacity: 1; + transform: translateX(0); +} + +.notification-content { + display: flex; + align-items: center; + gap: 10px; +} + +.notification i { + font-size: 1.2rem; +} + +.notification-success { + border-left: 4px solid #1cc88a; +} + +.notification-success i { + color: #1cc88a; +} + +.notification-error { + border-left: 4px solid #e74a3b; +} + +.notification-error i { + color: #e74a3b; +} + +.notification-info { + border-left: 4px solid #36b9cc; +} + +.notification-info i { + color: #36b9cc; +} + +/* 移动端菜单优化 */ +@media (max-width: 991.98px) { + .navbar-collapse { + background-color: white; + padding: 20px; + border-radius: 8px; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); + margin-top: 10px; + } + + .navbar-nav .nav-link { + padding: 10px 15px; + border-radius: 4px; + margin-bottom: 5px; + } + + .navbar-nav .nav-link:hover { + background-color: #f8f9fc; + } +} + +/* 平滑滚动优化 */ +html { + scroll-behavior: smooth; +} + +/* 当前页面高亮 */ +.nav-link.active { + color: #4e73df !important; + font-weight: 600; +} + +.nav-link.active::after { + content: ''; + position: absolute; + bottom: -2px; + left: 0; + width: 100%; + height: 2px; + background-color: #4e73df; + border-radius: 2px; +} + +/* 按钮悬停效果增强 */ +.btn { + transition: all 0.3s ease; +} + +.btn:hover { + transform: translateY(-2px); + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); +} + +/* 卡片悬停效果 */ +.feature-card { + transition: all 0.3s ease; +} + +.feature-card:hover { + transform: translateY(-5px); + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1); +} + +/* 加载动画 */ +.loading-spinner { + display: inline-block; + width: 20px; + height: 20px; + border: 3px solid rgba(255, 255, 255, 0.3); + border-radius: 50%; + border-top-color: white; + animation: spin 1s ease-in-out infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +/* 响应式调整 */ +@media (max-width: 768px) { + #backToTop { + bottom: 20px; + right: 20px; + width: 45px; + height: 45px; + } + + .notification { + left: 20px; + right: 20px; + max-width: none; + } +} \ No newline at end of file diff --git a/frontend/css/style.css b/frontend/css/style.css new file mode 100644 index 0000000..d44ae76 --- /dev/null +++ b/frontend/css/style.css @@ -0,0 +1,1150 @@ +/* 重置和基础样式 */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif; + background: #f8f9fa; + min-height: 100vh; + color: #333; +} + +/* 通用容器 */ +.container { + max-width: 1000px; + margin: 30px auto; + padding: 20px; + background: white; + border-radius: 10px; + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.05); +} + +/* 认证页面容器 */ +.auth-container { + max-width: 500px; + margin: 50px auto; + padding: 40px; + background: white; + border-radius: 15px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); +} + +.auth-card { + padding: 0; +} + +/* 卡片样式 */ +.card { + background: white; + border-radius: 15px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); + padding: 30px; + margin-bottom: 20px; + transition: transform 0.3s ease, box-shadow 0.3s ease; +} + +.card:hover { + transform: translateY(-5px); + box-shadow: 0 15px 40px rgba(0, 0, 0, 0.15); +} + +/* 按钮样式 */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 12px 24px; + border: none; + border-radius: 8px; + font-size: 16px; + font-weight: 500; + cursor: pointer; + transition: all 0.3s ease; + text-decoration: none; + gap: 8px; +} + +.btn-primary { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; +} + +.btn-primary:hover { + background: linear-gradient(135deg, #5a6fd8 0%, #6b4090 100%); + transform: translateY(-2px); + box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4); +} + +.btn-secondary { + background: #f8f9fa; + color: #495057; + border: 1px solid #dee2e6; +} + +.btn-secondary:hover { + background: #e9ecef; + border-color: #adb5bd; +} + +.btn-danger { + background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); + color: white; +} + +.btn-danger:hover { + background: linear-gradient(135deg, #e685f0 0%, #e4475c 100%); +} + +.btn-success { + background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); + color: white; +} + +.btn-success:hover { + background: linear-gradient(135deg, #3a9bf4 0%, #00d9e4 100%); +} + +/* 表格样式 */ +.table-container { + background: white; + border-radius: 10px; + overflow: hidden; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + margin-bottom: 30px; +} + +table { + width: 100%; + border-collapse: collapse; +} + +thead { + background: #f8f9fa; +} + +th { + padding: 15px 20px; + text-align: left; + font-weight: 600; + color: #495057; + border-bottom: 2px solid #dee2e6; +} + +td { + padding: 15px 20px; + border-bottom: 1px solid #dee2e6; +} + +tr:hover { + background: #f8f9fa; +} + +/* 表单样式 */ + + +/* 导航栏样式 */ +.navbar { + background: white; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); + padding: 15px 30px; + display: flex; + justify-content: space-between; + align-items: center; + border-radius: 0 0 15px 15px; + margin-bottom: 30px; +} + +.navbar-brand { + display: flex; + align-items: center; + gap: 10px; + font-size: 20px; + font-weight: 600; + color: #667eea; + text-decoration: none; +} + +.navbar-menu { + display: flex; + gap: 20px; + align-items: center; +} + +.navbar-user { + display: flex; + align-items: center; + gap: 10px; +} + +.user-avatar { + width: 40px; + height: 40px; + border-radius: 50%; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + display: flex; + align-items: center; + justify-content: center; + font-weight: 600; +} + +/* 响应式设计 */ +@media (max-width: 768px) { + .container { + padding: 0 15px; + } + + .card { + padding: 20px; + } + + .btn { + padding: 8px 16px; + font-size: 14px; + } + + .table-responsive { + overflow-x: auto; + } +} + +/* 统计卡片样式 */ +.stats-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 20px; + margin-bottom: 30px; +} + +.stat-card { + background: white; + border-radius: 10px; + padding: 25px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + transition: transform 0.3s ease, box-shadow 0.3s ease; + display: flex; + align-items: center; +} + +.stat-card:hover { + transform: translateY(-5px); + box-shadow: 0 8px 15px rgba(0, 0, 0, 0.15); +} + +.stat-icon { + width: 60px; + height: 60px; + border-radius: 10px; + display: flex; + align-items: center; + justify-content: center; + margin-right: 20px; + font-size: 24px; + color: white; +} + +.stat-icon.users, +.stat-icon.gpa { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); +} + +.stat-icon.students, +.stat-icon.courses { + background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); +} + +.stat-icon.teachers, +.stat-icon.credits { + background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); +} + +.stat-icon.courses, +.stat-icon.ranking { + background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%); +} + +.stat-content { + flex: 1; +} + +.stat-value { + font-size: 32px; + font-weight: 700; + color: #2d3748; + margin-bottom: 5px; +} + +.stat-label { + font-size: 14px; + color: #718096; + margin-bottom: 10px; +} + +.stat-change { + font-size: 12px; + display: flex; + align-items: center; +} + +.stat-change.positive { + color: #38a169; +} + +.stat-change i { + margin-right: 5px; +} + +/* 功能卡片样式 */ +.function-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 25px; + margin-bottom: 40px; +} + +.function-card { + background: white; + border-radius: 12px; + padding: 30px; + box-shadow: 0 6px 12px rgba(0, 0, 0, 0.08); + transition: all 0.3s ease; + cursor: pointer; + text-align: center; + border: 2px solid transparent; +} + +.function-card:hover { + transform: translateY(-8px); + box-shadow: 0 12px 20px rgba(0, 0, 0, 0.15); + border-color: #4a90e2; +} + +.function-icon { + width: 80px; + height: 80px; + border-radius: 50%; + background: linear-gradient(135deg, #4a90e2 0%, #357abd 100%); + display: flex; + align-items: center; + justify-content: center; + margin: 0 auto 20px; + font-size: 32px; + color: white; +} + +.function-title { + font-size: 20px; + font-weight: 600; + color: #2d3748; + margin-bottom: 15px; +} + +.function-description { + font-size: 14px; + color: #718096; + line-height: 1.6; + margin-bottom: 20px; +} + +/* 系统状态样式 */ +.system-status { + background: white; + border-radius: 12px; + padding: 30px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + margin-bottom: 40px; +} + +.section-title { + font-size: 20px; + font-weight: 600; + color: #2d3748; + margin-bottom: 25px; + display: flex; + align-items: center; +} + +.section-title i { + margin-right: 10px; + color: #4a90e2; +} + +.status-list { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 20px; +} + +.status-item { + display: flex; + align-items: center; + padding: 15px; + background: #f7fafc; + border-radius: 8px; + transition: background-color 0.3s ease; +} + +.status-item:hover { + background: #edf2f7; +} + +.status-indicator { + width: 12px; + height: 12px; + border-radius: 50%; + margin-right: 15px; +} + +.status-indicator.online { + background-color: #38a169; +} + +.status-indicator.warning { + background-color: #d69e2e; +} + +.status-indicator.offline { + background-color: #e53e3e; +} + +.status-label { + font-size: 14px; + font-weight: 600; + color: #2d3748; + margin-bottom: 5px; +} + +.status-value { + font-size: 12px; + color: #718096; +} + +/* 最近活动样式 */ +.recent-activities { + background: white; + border-radius: 12px; + padding: 30px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); +} + +.activity-list { + list-style: none; + padding: 0; +} + +.activity-item { + display: flex; + align-items: center; + padding: 15px 0; + border-bottom: 1px solid #e2e8f0; +} + +.activity-item:last-child { + border-bottom: none; +} + +.activity-icon { + width: 40px; + height: 40px; + border-radius: 50%; + background: #f7fafc; + display: flex; + align-items: center; + justify-content: center; + margin-right: 15px; + color: #4a90e2; +} + +.activity-content { + flex: 1; +} + +.activity-title { + font-size: 14px; + font-weight: 600; + color: #2d3748; + margin-bottom: 5px; +} + +.activity-time { + font-size: 12px; + color: #718096; +} + +/* 面包屑导航 */ +.breadcrumb { + display: flex; + align-items: center; + font-size: 14px; + color: #718096; + margin-top: 5px; +} + +.breadcrumb a { + color: #4a90e2; + text-decoration: none; + transition: color 0.3s ease; +} + +.breadcrumb a:hover { + color: #357abd; + text-decoration: underline; +} + +.breadcrumb i { + margin: 0 10px; + font-size: 12px; +} + +/* 当前时间显示 */ +.current-time { + font-size: 14px; + color: #718096; + background: #f7fafc; + padding: 8px 15px; + border-radius: 20px; +} + +/* 页面标题 */ +.page-title { + font-size: 24px; + font-weight: 700; + color: #2d3748; + margin-bottom: 5px; +} + +/* 内容头部 */ +.content-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 30px; +} + +/* ==================== 主页样式 ==================== */ +/* 英雄区域 */ +.hero-section { + min-height: 80vh; + display: flex; + align-items: center; + justify-content: center; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + position: relative; + overflow: hidden; +} + +.hero-section::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: url('../images/hero-bg.jpg') center/cover no-repeat; + opacity: 0.1; +} + +.hero-content { + max-width: 800px; + text-align: center; + position: relative; + z-index: 1; + padding: 40px; + background: rgba(255, 255, 255, 0.95); + border-radius: 20px; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); + margin: 20px; +} + +.hero-title { + font-size: 48px; + font-weight: 800; + margin-bottom: 20px; + color: #2c3e50; + text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.1); +} + +.hero-subtitle { + font-size: 20px; + margin-bottom: 30px; + color: #7f8c8d; + opacity: 0.9; + line-height: 1.6; +} + +/* 功能区域 */ +.features-section { + padding: 80px 20px; + background-color: #f8f9fa; + border-radius: 20px; + margin: 40px 20px; +} + +.section-title { + text-align: center; + font-size: 36px; + color: #2c3e50; + margin-bottom: 50px; +} + +.features-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 30px; + max-width: 1200px; + margin: 0 auto; +} + +.feature-card { + background: white; + border-radius: 15px; + padding: 40px 30px; + text-align: center; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); + transition: transform 0.3s ease, box-shadow 0.3s ease; +} + +.feature-card:hover { + transform: translateY(-10px); + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15); +} + +.feature-icon { + width: 80px; + height: 80px; + border-radius: 50%; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + display: flex; + align-items: center; + justify-content: center; + margin: 0 auto 25px; + font-size: 32px; + color: white; +} + +.feature-title { + font-size: 24px; + color: #2c3e50; + margin-bottom: 15px; +} + +.feature-description { + color: #7f8c8d; + line-height: 1.6; +} + +/* 行动号召区域 */ +.cta-section { + text-align: center; + padding: 80px 20px; +} + +.cta-title { + font-size: 36px; + color: #2c3e50; + margin-bottom: 30px; +} + +.cta-buttons { + display: flex; + justify-content: center; + gap: 20px; + flex-wrap: wrap; +} + +/* 页脚 */ +.footer { + background-color: #2c3e50; + color: white; + padding: 60px 20px; + text-align: center; + border-radius: 20px 20px 0 0; + margin-top: 80px; +} + +.footer-content { + max-width: 1200px; + margin: 0 auto; +} + +.footer-links { + display: flex; + justify-content: center; + gap: 30px; + margin: 30px 0; + flex-wrap: wrap; +} + +.footer-links a { + color: #bdc3c7; + text-decoration: none; + transition: color 0.3s ease; +} + +.footer-links a:hover { + color: white; +} + +.copyright { + margin-top: 30px; + color: #999; + font-size: 0.9rem; +} + +/* 主页响应式设计 */ +@media (max-width: 768px) { + .hero-title { + font-size: 32px; + } + + .hero-subtitle { + font-size: 16px; + } + + .section-title { + font-size: 28px; + } + + .cta-buttons { + flex-direction: column; + } + + .cta-buttons .btn { + width: 100%; + } +} + +/* ==================== 仪表板通用样式 ==================== */ +/* 仪表板布局 */ +.dashboard-container { + display: flex; + min-height: calc(100vh - 100px); +} + +/* 侧边栏 */ +.sidebar { + width: 250px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + position: sticky; + top: 0; + height: 100vh; + overflow-y: auto; +} + +.sidebar.admin-sidebar { + background: linear-gradient(135deg, #9b59b6 0%, #8e44ad 100%); +} + +.sidebar.teacher-sidebar { + background: linear-gradient(135deg, #3498db 0%, #2980b9 100%); +} + +.sidebar-header { + padding: 30px 20px; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + margin-bottom: 20px; +} + +.user-info { + display: flex; + align-items: center; + gap: 15px; +} + +.user-avatar { + width: 50px; + height: 50px; + border-radius: 50%; + background: rgba(255, 255, 255, 0.2); + display: flex; + align-items: center; + justify-content: center; + font-size: 20px; + color: white; + font-weight: 600; +} + +.user-details h3 { + color: white; + font-size: 16px; + margin-bottom: 5px; +} + +.user-details p { + color: rgba(255, 255, 255, 0.7); + font-size: 12px; +} + +.sidebar-menu { + list-style: none; + padding: 0 20px; +} + +.sidebar-menu li { + margin-bottom: 10px; +} + +.sidebar-menu a { + display: flex; + align-items: center; + gap: 15px; + padding: 15px; + color: rgba(255, 255, 255, 0.8); + text-decoration: none; + border-radius: 8px; + transition: all 0.3s ease; +} + +.sidebar-menu a:hover { + background: rgba(255, 255, 255, 0.1); + color: white; +} + +.sidebar-menu a.active { + background: rgba(255, 255, 255, 0.2); + color: white; +} + +.sidebar-menu i { + width: 20px; + text-align: center; +} + +/* 主内容区 */ +.main-content { + flex: 1; + padding: 30px; + background: #f8f9fa; + overflow-y: auto; +} + +.content-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 30px; +} + +.content-header h1 { + font-size: 28px; + color: #2c3e50; + margin: 0; +} + +.breadcrumb { + color: #7f8c8d; + font-size: 14px; + margin-top: 5px; +} + + + +/* 成绩表格 */ +.grades-table { + background: white; + border-radius: 15px; + overflow: hidden; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); +} + +.table-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 20px 25px; + border-bottom: 1px solid #eee; +} + +.table-title { + font-size: 1.3rem; + color: #333; + margin: 0; +} + +.table-actions { + display: flex; + gap: 10px; +} + +.table-container { + overflow-x: auto; +} + +.grades-table table { + width: 100%; + border-collapse: collapse; +} + +.grades-table th { + background: #f8f9ff; + padding: 15px 20px; + text-align: left; + font-weight: 600; + color: #333; + border-bottom: 1px solid #eee; +} + +.grades-table td { + padding: 15px 20px; + border-bottom: 1px solid #eee; +} + +.grades-table tr:hover { + background: #f8f9ff; +} + +/* 成绩徽章 */ +.grade-badge { + display: inline-block; + padding: 5px 15px; + border-radius: 20px; + font-size: 12px; + font-weight: 600; + color: white; +} + +.grade-badge.A { + background: #2ecc71; +} + +.grade-badge.B { + background: #3498db; +} + +.grade-badge.C { + background: #f39c12; +} + +.grade-badge.D { + background: #e74c3c; +} + +/* 查看按钮 */ +.view-btn { + background: #f8f9fa; + color: #495057; + border: 1px solid #dee2e6; + padding: 8px 16px; + border-radius: 5px; + font-size: 14px; + cursor: pointer; + transition: all 0.3s ease; +} + +.view-btn:hover { + background: #e9ecef; + border-color: #adb5bd; +} + +/* 仪表板响应式设计 */ +@media (max-width: 992px) { + .dashboard-container { + flex-direction: column; + } + + .sidebar { + width: 100%; + height: auto; + position: static; + } + + .sidebar-menu { + display: flex; + overflow-x: auto; + padding: 10px 20px; + } + + .sidebar-menu li { + margin-bottom: 0; + margin-right: 10px; + } + + .sidebar-menu a { + white-space: nowrap; + } +} + +@media (max-width: 768px) { + .main-content { + padding: 20px; + } + + .content-header { + flex-direction: column; + align-items: flex-start; + gap: 15px; + } + + .stats-grid { + grid-template-columns: 1fr; + } + + .table-actions { + flex-direction: column; + } + + .function-grid { + grid-template-columns: 1fr; + } + + .status-list { + grid-template-columns: 1fr; + } +} + +/* ==================== 管理页面通用样式 ==================== */ +/* 页面容器 */ +.page-container { + padding: 20px; +} + +/* 页面头部 */ +.page-header { + margin-bottom: 30px; +} + +.page-header h1 { + font-size: 28px; + color: #2c3e50; + margin-bottom: 10px; +} + +.page-header p { + color: #7f8c8d; + font-size: 16px; + margin: 0; +} + +/* 过滤区域 */ +.filter-section { + background: white; + border-radius: 10px; + padding: 20px; + margin-bottom: 30px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); +} + +.filter-title { + font-size: 18px; + font-weight: 600; + color: #2c3e50; + margin-bottom: 20px; + display: flex; + align-items: center; + gap: 10px; +} + +.filter-form { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 15px; +} + +.filter-row { + display: flex; + gap: 15px; + flex-wrap: wrap; +} + +.filter-group { + display: flex; + flex-direction: column; + min-width: 200px; +} + +.filter-group label { + font-size: 14px; + color: #666; + margin-bottom: 5px; +} + +/* 表单样式 */ +.entry-form { + background: white; + border-radius: 12px; + padding: 30px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); +} + +.form-row { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 20px; + margin-bottom: 20px; +} + +.form-group { + margin-bottom: 20px; +} + +.form-group label { + display: block; + font-size: 14px; + color: #666; + margin-bottom: 8px; +} + +.form-control { + width: 100%; + padding: 12px 15px; + border: 1px solid #ddd; + border-radius: 8px; + font-size: 14px; + transition: border-color 0.3s ease; +} + +.form-control:focus { + outline: none; + border-color: #667eea; + box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); +} + + + +/* 操作按钮 */ +.action-buttons { + display: flex; + gap: 10px; +} + +.btn-add { + background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%); + color: white; + border: none; + padding: 10px 20px; + border-radius: 8px; + font-size: 14px; + cursor: pointer; + transition: all 0.3s ease; +} + +.btn-add:hover { + transform: translateY(-2px); + box-shadow: 0 5px 15px rgba(67, 233, 123, 0.3); +} + +/* 管理页面响应式设计 */ +@media (max-width: 768px) { + .filter-form { + grid-template-columns: 1fr; + } + + .filter-row { + flex-direction: column; + } + + .filter-group { + min-width: 100%; + } + + .form-row { + grid-template-columns: 1fr; + } + + .action-buttons { + flex-direction: column; + } +} \ No newline at end of file diff --git a/frontend/html/admin_dashboard.html b/frontend/html/admin_dashboard.html new file mode 100644 index 0000000..a93f4c7 --- /dev/null +++ b/frontend/html/admin_dashboard.html @@ -0,0 +1,353 @@ + + + + + + 学生成绩管理系统 - 管理员仪表板 + + + + + + + + + +
+ + + + +
+
+
+

管理员仪表板

+ +
+
+
+ + +
+
+
+ +
+
+
1,248
+
总用户数
+
+ 12 本周新增 +
+
+
+ +
+
+ +
+
+
3,567
+
学生总数
+
+ 45 本周新增 +
+
+
+ +
+
+ +
+
+
128
+
教师总数
+
+ 无变化 +
+
+
+ +
+
+ +
+
+
89
+
课程总数
+
+ 3 本周新增 +
+
+
+
+ + +
+
+
+ +
+

用户管理

+

+ 管理所有用户账户,包括添加、编辑、删除用户,设置用户角色和权限。 +

+ +
+ +
+
+ +
+

学生管理

+

+ 管理学生信息,包括学籍管理、班级分配、信息维护和批量导入导出。 +

+ +
+ +
+
+ +
+

教师管理

+

+ 管理教师信息,包括教师分配、课程安排、权限设置和绩效考核。 +

+ +
+ +
+
+ +
+

成绩统计

+

+ 查看全校成绩统计,生成分析报告,支持图表展示和数据导出。 +

+ +
+
+ + +
+

+ + 系统状态 +

+
+
+
+
+
数据库服务
+
运行正常 | 响应时间: 12ms
+
+
+
+
+
+
Web服务器
+
运行正常 | 在线用户: 156
+
+
+
+
+
+
文件存储
+
使用率: 65% | 剩余: 35GB
+
+
+
+
+
+
备份服务
+
上次备份: 2天前 | 建议立即备份
+
+
+
+
+ + +
+

+ + 最近活动 +

+
    +
  • +
    + +
    +
    +
    新增学生用户
    +
    10分钟前 | 操作人: 管理员
    +
    +
  • +
  • +
    + +
    +
    +
    修改教师信息
    +
    1小时前 | 操作人: 管理员
    +
    +
  • +
  • +
    + +
    +
    +
    生成成绩统计报告
    +
    3小时前 | 操作人: 系统
    +
    +
  • +
  • +
    + +
    +
    +
    导出用户数据
    +
    5小时前 | 操作人: 管理员
    +
    +
  • +
  • +
    + +
    +
    +
    系统设置更新
    +
    1天前 | 操作人: 管理员
    +
    +
  • +
+
+
+
+ + + + \ No newline at end of file diff --git a/frontend/html/grade_entry.html b/frontend/html/grade_entry.html new file mode 100644 index 0000000..b86f505 --- /dev/null +++ b/frontend/html/grade_entry.html @@ -0,0 +1,459 @@ + + + + + + 成绩录入 - XX学校成绩管理系统 + + + + + + + + + +
+ +
+ + +
+
+

成绩录入

+

请选择班级和课程,然后为每个学生录入成绩

+
+ +
+
+
+ + +
+ +
+ + +
+
+ +
+ + +
+ +
+ + +
+ +
+

学生成绩录入

+

选择班级后,系统将自动加载该班级的学生列表

+ + + + + + + + + + + + + + + + + + + + +
+ +
+ + +
+
+ +
+

使用说明

+ +
+
+ + + + + \ No newline at end of file diff --git a/frontend/html/grade_management.html b/frontend/html/grade_management.html new file mode 100644 index 0000000..2ff3339 --- /dev/null +++ b/frontend/html/grade_management.html @@ -0,0 +1,584 @@ + + + + + + 成绩查询/管理 - 学生成绩管理系统 + + + + + + + + + +
+
+ + + + + + + +
+

+ + 筛选条件 +

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+

查询结果

+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + +
学号姓名班级课程平时成绩期中成绩期末成绩总评成绩等级操作
+ +

正在加载成绩数据...

+
+
+ + + +
+
+
+ + + + + + + \ No newline at end of file diff --git a/frontend/html/index.html b/frontend/html/index.html new file mode 100644 index 0000000..81173ea --- /dev/null +++ b/frontend/html/index.html @@ -0,0 +1,142 @@ + + + + + + 学生成绩管理系统 + + + + + + + + +
+ +
+
+

XX学校学生成绩管理系统

+

+ 高效、安全、智能的成绩管理平台,为学校师生提供全方位的成绩管理服务, + 实现成绩录入、查询、统计和分析的一体化解决方案。 +

+ +
+
+ + +
+

系统功能特色

+
+
+
+ +
+

学生成绩查询

+

+ 学生可随时查看个人成绩,包括各科成绩、平均分、排名等信息, + 支持成绩趋势分析和历史记录查看。 +

+
+ +
+
+ +
+

教师成绩管理

+

+ 教师可便捷录入、修改、查询学生成绩,支持批量操作和成绩统计分析, + 提供多种数据导出格式。 +

+
+ +
+
+ +
+

管理员权限控制

+

+ 管理员可管理用户账户、学生信息,查看系统统计报表, + 设置权限和系统参数,确保数据安全。 +

+
+ +
+
+ +
+

智能统计分析

+

+ 提供丰富的图表统计功能,包括成绩分布、趋势分析、对比图表等, + 帮助学校进行教学评估和决策支持。 +

+
+
+
+ + +
+

立即体验智能成绩管理

+

+ 加入XX学校成绩管理系统,体验高效、便捷的成绩管理服务。 +

+ +
+
+ + + + + + + \ No newline at end of file diff --git a/frontend/html/login.html b/frontend/html/login.html new file mode 100644 index 0000000..a66605d --- /dev/null +++ b/frontend/html/login.html @@ -0,0 +1,61 @@ + + + + + + 学生成绩管理系统 - 登录 + + + + +
+
+

学生成绩管理系统

+

请登录您的账户

+
+ +
+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ + +
+
+ + + + \ No newline at end of file diff --git a/frontend/html/register.html b/frontend/html/register.html new file mode 100644 index 0000000..2902e8d --- /dev/null +++ b/frontend/html/register.html @@ -0,0 +1,106 @@ + + + + + + 学生成绩管理系统 - 注册 + + + + +
+
+

学生成绩管理系统

+

创建新账户

+
+ +
+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + + +
+ +
+ + +
+
+ + + + + \ No newline at end of file diff --git a/frontend/html/student_dashboard.html b/frontend/html/student_dashboard.html new file mode 100644 index 0000000..baa2ba0 --- /dev/null +++ b/frontend/html/student_dashboard.html @@ -0,0 +1,195 @@ + + + + + + 学生成绩管理系统 - 学生仪表板 + + + + + + + + + +
+ + + + +
+
+
+

学生仪表板

+ +
+
+
+ + +
+
+
+ +
+
3.75
+
平均绩点
+
+ +
+
+ +
+
8
+
已修课程
+
+ +
+
+ +
+
24
+
总学分
+
+ +
+
+ +
+
5
+
班级排名
+
+
+ + +
+
+

本学期成绩

+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + +
课程名称课程代码学分平时成绩期末成绩总成绩绩点操作
+
+
+
+
+ + + + + + \ No newline at end of file diff --git a/frontend/html/student_management.html b/frontend/html/student_management.html new file mode 100644 index 0000000..100e700 --- /dev/null +++ b/frontend/html/student_management.html @@ -0,0 +1,451 @@ + + + + + + 学生管理 - XX学校成绩管理系统 + + + + + + + + + +
+
+
+ + + + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + +
学号姓名性别班级联系电话邮箱入学时间操作
+ +

正在加载学生数据...

+
+
+ + + +
+
+
+ + +
+
+

© 2023 XX学校成绩管理系统. 版权所有.

+

技术支持: 计算机科学与技术学院

+
+
+ + + + \ No newline at end of file diff --git a/frontend/html/teacher_dashboard.html b/frontend/html/teacher_dashboard.html new file mode 100644 index 0000000..e848a7d --- /dev/null +++ b/frontend/html/teacher_dashboard.html @@ -0,0 +1,515 @@ + + + + + + 学生成绩管理系统 - 教师仪表板 + + + + /* 仪表板布局 */ + .dashboard-container { + display: flex; + min-height: calc(100vh - 80px); + } + + /* 侧边栏 */ + .sidebar { + width: 250px; + background: linear-gradient(180deg, #43e97b 0%, #38f9d7 100%); + color: white; + padding: 30px 0; + position: sticky; + top: 80px; + height: calc(100vh - 80px); + overflow-y: auto; + } + + .sidebar-header { + padding: 0 25px 30px; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + margin-bottom: 20px; + } + + .user-info { + display: flex; + align-items: center; + gap: 15px; + } + + .user-avatar { + width: 50px; + height: 50px; + border-radius: 50%; + background: white; + display: flex; + align-items: center; + justify-content: center; + color: #43e97b; + font-size: 20px; + } + + .user-details h3 { + margin: 0 0 5px; + font-size: 1.1rem; + } + + .user-details p { + margin: 0; + font-size: 0.9rem; + opacity: 0.8; + } + + .sidebar-menu { + list-style: none; + padding: 0; + margin: 0; + } + + .sidebar-menu li { + margin: 5px 0; + } + + .sidebar-menu a { + display: flex; + align-items: center; + gap: 15px; + padding: 15px 25px; + color: white; + text-decoration: none; + transition: all 0.3s ease; + border-left: 3px solid transparent; + } + + .sidebar-menu a:hover { + background: rgba(255, 255, 255, 0.1); + border-left-color: white; + } + + .sidebar-menu a.active { + background: rgba(255, 255, 255, 0.2); + border-left-color: white; + } + + .sidebar-menu i { + width: 20px; + text-align: center; + } + + /* 主内容区 */ + .main-content { + flex: 1; + padding: 30px; + background: #f8f9ff; + } + + .content-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 30px; + } + + .page-title { + font-size: 1.8rem; + color: #333; + margin: 0; + } + + .breadcrumb { + display: flex; + align-items: center; + gap: 10px; + color: #666; + font-size: 0.9rem; + } + + .breadcrumb a { + color: #43e97b; + text-decoration: none; + } + + .breadcrumb i { + font-size: 0.8rem; + } + + /* 功能卡片 */ + .function-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 25px; + margin-bottom: 40px; + } + + .function-card { + background: white; + border-radius: 15px; + padding: 30px; + text-align: center; + box-shadow: 0 5px 20px rgba(0, 0, 0, 0.05); + transition: all 0.3s ease; + cursor: pointer; + border: 2px solid transparent; + } + + .function-card:hover { + transform: translateY(-5px); + box-shadow: 0 15px 30px rgba(0, 0, 0, 0.1); + border-color: #43e97b; + } + + .function-icon { + width: 80px; + height: 80px; + border-radius: 50%; + background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%); + display: flex; + align-items: center; + justify-content: center; + margin: 0 auto 20px; + font-size: 32px; + color: white; + } + + .function-title { + font-size: 1.3rem; + color: #333; + margin-bottom: 10px; + } + + .function-description { + color: #666; + line-height: 1.5; + margin-bottom: 20px; + } + + /* 快速操作 */ + .quick-actions { + background: white; + border-radius: 15px; + padding: 25px; + margin-bottom: 30px; + box-shadow: 0 5px 20px rgba(0, 0, 0, 0.05); + } + + .section-title { + font-size: 1.4rem; + color: #333; + margin-bottom: 20px; + display: flex; + align-items: center; + gap: 10px; + } + + .section-title i { + color: #43e97b; + } + + .action-buttons { + display: flex; + gap: 15px; + flex-wrap: wrap; + } + + .action-btn { + padding: 12px 25px; + background: #f8f9ff; + border: 1px solid #e0e0e0; + border-radius: 10px; + color: #333; + text-decoration: none; + display: flex; + align-items: center; + gap: 10px; + transition: all 0.3s ease; + } + + .action-btn:hover { + background: #43e97b; + color: white; + border-color: #43e97b; + } + + /* 最近活动 */ + .recent-activities { + background: white; + border-radius: 15px; + padding: 25px; + box-shadow: 0 5px 20px rgba(0, 0, 0, 0.05); + } + + .activity-list { + list-style: none; + padding: 0; + margin: 0; + } + + .activity-item { + display: flex; + align-items: center; + gap: 15px; + padding: 15px 0; + border-bottom: 1px solid #eee; + } + + .activity-item:last-child { + border-bottom: none; + } + + .activity-icon { + width: 40px; + height: 40px; + border-radius: 50%; + background: #f8f9ff; + display: flex; + align-items: center; + justify-content: center; + color: #43e97b; + } + + .activity-content { + flex: 1; + } + + .activity-title { + font-weight: 600; + color: #333; + margin-bottom: 5px; + } + + .activity-time { + font-size: 0.9rem; + color: #999; + } + + /* 响应式设计 */ + @media (max-width: 992px) { + .dashboard-container { + flex-direction: column; + } + + .sidebar { + width: 100%; + height: auto; + position: static; + padding: 20px 0; + } + + .sidebar-menu { + display: flex; + overflow-x: auto; + padding: 0 20px; + } + + .sidebar-menu li { + flex-shrink: 0; + } + + .sidebar-menu a { + padding: 10px 15px; + border-left: none; + border-bottom: 3px solid transparent; + } + + .sidebar-menu a:hover, + .sidebar-menu a.active { + border-left-color: transparent; + border-bottom-color: white; + } + } + + @media (max-width: 768px) { + .main-content { + padding: 20px; + } + + .content-header { + flex-direction: column; + align-items: flex-start; + gap: 15px; + } + + .function-grid { + grid-template-columns: 1fr; + } + + .action-buttons { + flex-direction: column; + } + + .action-btn { + justify-content: center; + } + } + + + + + + + +
+ + + + +
+
+
+

教师仪表板

+ +
+
+
+ + +
+
+
+ +
+

成绩录入

+

+ 为学生录入新的课程成绩,支持批量导入和单个录入。 +

+ +
+ +
+
+ +
+

成绩查询

+

+ 查询学生成绩,支持按班级、课程、学期等多维度筛选。 +

+ +
+ +
+
+ +
+

成绩管理

+

+ 修改或删除已录入的成绩,管理成绩记录和状态。 +

+ +
+ +
+
+ +
+

统计分析

+

+ 查看成绩统计图表,分析教学效果和学生表现。 +

+ +
+
+ + + + + + + + +
+
+
+ + + + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + + + +
+
+
+ + +
+ + + + + + + + + + + + + + + + + + + +
用户ID姓名角色班级联系电话邮箱注册时间操作
+ +

正在加载用户数据...

+
+
+ + + +
+
+
+ + +
+
+

© 2023 XX学校成绩管理系统. 版权所有.

+

技术支持: 信息技术中心 | 联系电话: 010-12345678

+
+
+ + + + + \ No newline at end of file diff --git a/frontend/js/admin.js b/frontend/js/admin.js new file mode 100644 index 0000000..a0c3944 --- /dev/null +++ b/frontend/js/admin.js @@ -0,0 +1,591 @@ +class AdminDashboard { + constructor() { + // 动态设置API基础URL,支持file:///协议和localhost:3000访问 + this.apiBase = window.location.protocol === 'file:' ? 'http://localhost:3000/api' : '/api'; + this.currentUser = null; + this.stats = {}; + this.users = []; + this.students = []; + this.teachers = []; + this.init(); + } + + async init() { + // 检查登录状态 + if (!await this.checkAuth()) { + window.location.href = '/frontend/html/login.html'; + return; + } + + // 加载用户信息 + await this.loadUserInfo(); + + // 加载统计数据 + await this.loadStats(); + + // 加载用户数据 + await this.loadUsers(); + + // 绑定事件 + this.bindEvents(); + + // 更新界面 + this.updateUI(); + + // 初始化图表 + this.initCharts(); + } + + async checkAuth() { + try { + const response = await fetch(`${this.apiBase}/auth/me`, { + credentials: 'include' + }); + + if (!response.ok) { + return false; + } + + const data = await response.json(); + return data.success && data.user.role === 'admin'; + } catch (error) { + console.error('认证检查失败:', error); + return false; + } + } + + async loadUserInfo() { + try { + const response = await fetch(`${this.apiBase}/auth/me`, { + credentials: 'include' + }); + + if (response.ok) { + const data = await response.json(); + if (data.success) { + this.currentUser = data.user; + } + } + } catch (error) { + console.error('加载用户信息失败:', error); + } + } + + async loadStats() { + try { + const response = await fetch(`${this.apiBase}/admin/stats`, { + credentials: 'include' + }); + + if (response.ok) { + const data = await response.json(); + if (data.success) { + this.stats = data.stats; + this.updateStatsUI(); + } + } + } catch (error) { + console.error('加载统计数据失败:', error); + this.showNotification('加载统计数据失败', 'error'); + } + } + + async loadUsers() { + try { + const response = await fetch(`${this.apiBase}/admin/users`, { + credentials: 'include' + }); + + if (response.ok) { + const data = await response.json(); + if (data.success) { + this.users = data.users; + this.renderUsersTable(); + } + } + } catch (error) { + console.error('加载用户数据失败:', error); + this.showNotification('加载用户数据失败', 'error'); + } + } + + updateStatsUI() { + // 更新统计卡片 + const statElements = { + 'totalUsers': 'totalUsers', + 'totalStudents': 'totalStudents', + 'totalTeachers': 'totalTeachers', + 'totalCourses': 'totalCourses', + 'totalGrades': 'totalGrades', + 'avgScore': 'avgScore' + }; + + Object.entries(statElements).forEach(([key, elementId]) => { + const element = document.getElementById(elementId); + if (element && this.stats[key] !== undefined) { + element.textContent = this.stats[key]; + } + }); + + // 更新时间 + const timeElement = document.getElementById('currentTime'); + if (timeElement) { + timeElement.textContent = new Date().toLocaleString(); + } + } + + renderUsersTable() { + const tableBody = document.getElementById('usersTableBody'); + if (!tableBody) return; + + if (this.users.length === 0) { + tableBody.innerHTML = ` + + +
+ +

暂无用户数据

+
+ + + `; + return; + } + + tableBody.innerHTML = this.users.map(user => { + const roleClass = this.getRoleClass(user.role); + return ` + + + ${user.user_id} + ${user.full_name} + ${user.role} + ${user.class_name || 'N/A'} + ${user.email || 'N/A'} + +
+ + +
+ + + `; + }).join(''); + } + + getRoleClass(role) { + switch (role) { + case 'admin': return 'role-admin'; + case 'teacher': return 'role-teacher'; + case 'student': return 'role-student'; + default: return 'role-default'; + } + } + + bindEvents() { + // 导航菜单点击 + document.querySelectorAll('.nav-link').forEach(link => { + link.addEventListener('click', (e) => { + e.preventDefault(); + const page = link.dataset.page; + this.loadPage(page); + }); + }); + + // 搜索按钮 + document.getElementById('searchBtn')?.addEventListener('click', () => { + this.handleSearch(); + }); + + // 重置按钮 + document.getElementById('resetBtn')?.addEventListener('click', () => { + this.resetFilters(); + }); + + // 添加用户按钮 + document.getElementById('addUserBtn')?.addEventListener('click', () => { + this.addUser(); + }); + + // 导出按钮 + document.getElementById('exportBtn')?.addEventListener('click', () => { + this.exportUsers(); + }); + + // 批量删除按钮 + document.getElementById('batchDeleteBtn')?.addEventListener('click', () => { + this.batchDeleteUsers(); + }); + + // 表格操作按钮事件委托 + document.addEventListener('click', (e) => { + if (e.target.closest('.btn-edit')) { + const userId = e.target.closest('.btn-edit').dataset.id; + this.editUser(userId); + } + + if (e.target.closest('.btn-delete')) { + const userId = e.target.closest('.btn-delete').dataset.id; + this.deleteUser(userId); + } + }); + + // 退出登录 + document.getElementById('logoutBtn')?.addEventListener('click', () => { + this.handleLogout(); + }); + + // 刷新按钮 + document.getElementById('refreshBtn')?.addEventListener('click', () => { + this.refreshData(); + }); + } + + async loadPage(page) { + // 这里可以实现页面切换逻辑 + // 暂时使用简单跳转 + switch (page) { + case 'users': + window.location.href = '/frontend/html/user_management.html'; + break; + case 'students': + window.location.href = '/frontend/html/student_management.html'; + break; + case 'teachers': + // 可以跳转到教师管理页面 + break; + case 'grades': + window.location.href = '/frontend/html/grade_management.html'; + break; + case 'settings': + // 可以跳转到系统设置页面 + break; + } + } + + handleSearch() { + const userId = document.getElementById('userIdFilter')?.value || ''; + const name = document.getElementById('nameFilter')?.value || ''; + const role = document.getElementById('roleFilter')?.value || ''; + const className = document.getElementById('classFilter')?.value || ''; + + // 这里可以实现搜索逻辑 + this.showNotification('搜索功能待实现', 'info'); + } + + resetFilters() { + document.getElementById('userIdFilter').value = ''; + document.getElementById('nameFilter').value = ''; + document.getElementById('roleFilter').value = ''; + document.getElementById('classFilter').value = ''; + + // 重新加载数据 + this.loadUsers(); + } + + async addUser() { + // 这里可以打开添加用户模态框 + const userData = { + user_id: prompt('请输入用户ID:'), + full_name: prompt('请输入姓名:'), + role: prompt('请输入角色 (admin/teacher/student):'), + email: prompt('请输入邮箱:'), + class_name: prompt('请输入班级 (学生/教师可选):') + }; + + if (!userData.user_id || !userData.full_name || !userData.role) { + this.showNotification('用户ID、姓名和角色为必填项', 'error'); + return; + } + + try { + const response = await fetch(`${this.apiBase}/admin/users`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify(userData) + }); + + const data = await response.json(); + if (data.success) { + this.showNotification('用户添加成功', 'success'); + await this.loadUsers(); + } else { + this.showNotification(data.message || '添加失败', 'error'); + } + } catch (error) { + console.error('添加用户失败:', error); + this.showNotification('添加用户失败', 'error'); + } + } + + async exportUsers() { + try { + const response = await fetch(`${this.apiBase}/admin/users/export`, { + credentials: 'include' + }); + + if (response.ok) { + const blob = await response.blob(); + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `用户列表_${new Date().toISOString().split('T')[0]}.xlsx`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + window.URL.revokeObjectURL(url); + } + } catch (error) { + console.error('导出失败:', error); + this.showNotification('导出失败', 'error'); + } + } + + async batchDeleteUsers() { + const checkboxes = document.querySelectorAll('.user-checkbox:checked'); + if (checkboxes.length === 0) { + this.showNotification('请选择要删除的用户', 'warning'); + return; + } + + if (!confirm(`确定要删除选中的 ${checkboxes.length} 个用户吗?`)) { + return; + } + + const userIds = Array.from(checkboxes).map(cb => cb.dataset.id); + + try { + const response = await fetch(`${this.apiBase}/admin/users/batch`, { + method: 'DELETE', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify({ userIds }) + }); + + const data = await response.json(); + if (data.success) { + this.showNotification(`成功删除 ${userIds.length} 个用户`, 'success'); + await this.loadUsers(); + } else { + this.showNotification(data.message || '删除失败', 'error'); + } + } catch (error) { + console.error('批量删除失败:', error); + this.showNotification('批量删除失败', 'error'); + } + } + + async editUser(userId) { + const user = this.users.find(u => u.id == userId); + if (!user) return; + + // 这里可以打开编辑模态框 + const newName = prompt('请输入新的姓名:', user.full_name); + if (newName === null) return; + + const newRole = prompt('请输入新的角色:', user.role); + if (newRole === null) return; + + try { + const response = await fetch(`${this.apiBase}/admin/users/${userId}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify({ + full_name: newName, + role: newRole, + email: user.email, + class_name: user.class_name + }) + }); + + const data = await response.json(); + if (data.success) { + this.showNotification('用户更新成功', 'success'); + await this.loadUsers(); + } else { + this.showNotification(data.message || '更新失败', 'error'); + } + } catch (error) { + console.error('更新用户失败:', error); + this.showNotification('更新用户失败', 'error'); + } + } + + async deleteUser(userId) { + if (!confirm('确定要删除这个用户吗?')) { + return; + } + + try { + const response = await fetch(`${this.apiBase}/admin/users/${userId}`, { + method: 'DELETE', + credentials: 'include' + }); + + const data = await response.json(); + if (data.success) { + this.showNotification('用户删除成功', 'success'); + await this.loadUsers(); + } else { + this.showNotification(data.message || '删除失败', 'error'); + } + } catch (error) { + console.error('删除用户失败:', error); + this.showNotification('删除用户失败', 'error'); + } + } + + async handleLogout() { + try { + const response = await fetch(`${this.apiBase}/auth/logout`, { + method: 'POST', + credentials: 'include' + }); + + if (response.ok) { + window.location.href = '/html/login.html'; + } + } catch (error) { + console.error('退出登录失败:', error); + } + } + + async refreshData() { + await this.loadStats(); + await this.loadUsers(); + this.showNotification('数据已刷新', 'success'); + } + + updateUI() { + // 更新用户信息 + if (this.currentUser) { + const userInfoElements = document.querySelectorAll('.user-info'); + userInfoElements.forEach(el => { + el.textContent = `${this.currentUser.full_name} (${this.currentUser.role})`; + }); + } + } + + async initCharts() { + // 加载Chart.js库 + await this.loadChartLibrary(); + + // 初始化用户分布饼图 + this.initUserDistributionChart(); + + // 初始化成绩分布柱状图 + this.initGradeDistributionChart(); + } + + showNotification(message, type = 'info') { + // 创建通知元素 + const notification = document.createElement('div'); + notification.className = `notification notification-${type}`; + notification.innerHTML = ` + + ${message} + + `; + + // 添加到页面 + document.body.appendChild(notification); + + // 添加关闭事件 + notification.querySelector('.notification-close').addEventListener('click', () => { + notification.remove(); + }); + + // 自动移除 + setTimeout(() => { + if (notification.parentNode) { + notification.remove(); + } + }, 5000); + } + + async loadChartLibrary() { + if (typeof Chart !== 'undefined') return; + + return new Promise((resolve, reject) => { + const script = document.createElement('script'); + script.src = 'https://cdn.jsdelivr.net/npm/chart.js@3.9.1/dist/chart.min.js'; + script.onload = resolve; + script.onerror = reject; + document.head.appendChild(script); + }); + } + + initUserDistributionChart() { + const ctx = document.getElementById('userDistributionChart'); + if (!ctx) return; + + // 模拟数据 + const data = { + labels: ['学生', '教师', '管理员'], + datasets: [{ + data: [this.stats.totalStudents || 100, this.stats.totalTeachers || 20, 1], + backgroundColor: [ + 'rgba(54, 162, 235, 0.8)', + 'rgba(255, 206, 86, 0.8)', + 'rgba(255, 99, 132, 0.8)' + ] + }] + }; + + new Chart(ctx, { + type: 'pie', + data: data, + options: { + responsive: true, + plugins: { + legend: { + position: 'bottom' + } + } + } + }); + } + + initGradeDistributionChart() { + const ctx = document.getElementById('gradeDistributionChart'); + if (!ctx) return; + + // 模拟数据 + const data = { + labels: ['A', 'B', 'C', 'D', 'F'], + datasets: [{ + label: '成绩分布', + data: [25, 35, 20, 15, 5], + backgroundColor: [ + 'rgba(75, 192, 192, 0.8)', + 'rgba(54, 162, 235, 0.8)', + 'rgba(255, 206, 86, 0.8)', + 'rgba(255, 159, 64, 0.8)', + 'rgba(255, 99, 132, 0.8)' + ] + }] + }; + + new Chart(ctx, { + type: 'bar', + data: data, + options: { + responsive: true, + scales: { + y: { + beginAtZero: true, + ticks: { + stepSize: 10 + } + } + } + } + }); + } +} diff --git a/frontend/js/auth.js b/frontend/js/auth.js new file mode 100644 index 0000000..a36daad --- /dev/null +++ b/frontend/js/auth.js @@ -0,0 +1,269 @@ +class AuthManager { + constructor() { + // 动态设置API基础URL,支持file:///协议和localhost:3000访问 + this.apiBase = window.location.protocol === 'file:' ? 'http://localhost:3000/api' : '/api'; + this.initEventListeners(); + this.checkAuthStatus(); + } + + async checkAuthStatus() { + try { + const response = await fetch(`${this.apiBase}/auth/me`); + const data = await response.json(); + + if (data.success && data.user) { + // 用户已登录,根据角色重定向到正确的仪表板 + const userRole = data.user.role; + let redirectUrl = '/dashboard'; + + if (userRole === 'student') { + redirectUrl = '/frontend/html/student_dashboard.html'; + } else if (userRole === 'teacher') { + redirectUrl = '/frontend/html/teacher_dashboard.html'; + } else if (userRole === 'admin') { + redirectUrl = '/frontend/html/admin_dashboard.html'; + } + + // 如果当前页面是登录或注册页面,则重定向到仪表板 + const currentPath = window.location.pathname; + if (currentPath.includes('login.html') || currentPath.includes('register.html')) { + window.location.href = redirectUrl; + } + // 如果当前页面不是正确的仪表板页面,则重定向 + else if (!currentPath.includes(redirectUrl) && + currentPath !== '/' && + !currentPath.includes('index.html')) { + window.location.href = redirectUrl; + } + } + } catch (error) { + console.log('用户未登录'); + } + } + + initEventListeners() { + // 登录表单提交 + const loginForm = document.getElementById('loginForm'); + if (loginForm) { + loginForm.addEventListener('submit', (e) => this.handleLogin(e)); + } + + // 注册表单提交 + const registerForm = document.getElementById('registerForm'); + if (registerForm) { + registerForm.addEventListener('submit', (e) => this.handleRegister(e)); + } + + // 登出按钮 + const logoutBtn = document.getElementById('logoutBtn'); + if (logoutBtn) { + logoutBtn.addEventListener('click', (e) => this.handleLogout(e)); + } + } + + async handleLogin(e) { + e.preventDefault(); + + const form = e.target; + const formData = new FormData(form); + const data = Object.fromEntries(formData.entries()); + + const submitBtn = form.querySelector('button[type="submit"]'); + const originalText = submitBtn.innerHTML; + submitBtn.innerHTML = ' 登录中...'; + submitBtn.disabled = true; + + try { + const response = await fetch(`${this.apiBase}/auth/login`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(data) + }); + + const result = await response.json(); + + if (result.success) { + this.showNotification('登录成功!正在跳转...', 'success'); + + // 根据用户角色跳转到不同的仪表板 + const userRole = result.user?.role; + let redirectUrl = '/dashboard'; + + if (userRole === 'student') { + redirectUrl = '/frontend/html/student_dashboard.html'; + } else if (userRole === 'teacher') { + redirectUrl = '/frontend/html/teacher_dashboard.html'; + } else if (userRole === 'admin') { + redirectUrl = '/frontend/html/admin_dashboard.html'; + } + + // 延迟跳转以显示通知 + setTimeout(() => { + window.location.href = redirectUrl; + }, 1500); + } else { + this.showNotification(result.message || '登录失败', 'error'); + submitBtn.innerHTML = originalText; + submitBtn.disabled = false; + } + } catch (error) { + console.error('登录错误:', error); + this.showNotification('网络错误,请重试', 'error'); + submitBtn.innerHTML = originalText; + submitBtn.disabled = false; + } + } + + async handleRegister(e) { + e.preventDefault(); + + const form = e.target; + const formData = new FormData(form); + const data = Object.fromEntries(formData.entries()); + + // 验证密码 + if (data.password !== data.confirmPassword) { + this.showNotification('两次输入的密码不一致', 'error'); + return; + } + + const submitBtn = form.querySelector('button[type="submit"]'); + const originalText = submitBtn.innerHTML; + submitBtn.innerHTML = ' 注册中...'; + submitBtn.disabled = true; + + try { + const response = await fetch(`${this.apiBase}/auth/register`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(data) + }); + + const result = await response.json(); + + if (result.success) { + this.showNotification('注册成功!正在跳转到登录页面...', 'success'); + + setTimeout(() => { + window.location.href = '/html/login.html'; + }, 1500); + } else { + this.showNotification(result.message || '注册失败', 'error'); + submitBtn.innerHTML = originalText; + submitBtn.disabled = false; + } + } catch (error) { + console.error('注册错误:', error); + this.showNotification('网络错误,请重试', 'error'); + submitBtn.innerHTML = originalText; + submitBtn.disabled = false; + } + } + + async handleLogout(e) { + e.preventDefault(); + + if (!confirm('确定要退出登录吗?')) { + return; + } + + try { + const response = await fetch(`${this.apiBase}/auth/logout`, { + method: 'POST' + }); + + const result = await response.json(); + + if (result.success) { + this.showNotification('已成功退出登录', 'success'); + setTimeout(() => { + window.location.href = '/'; + }, 1000); + } else { + this.showNotification('退出登录失败', 'error'); + } + } catch (error) { + console.error('退出登录错误:', error); + this.showNotification('网络错误,请重试', 'error'); + } + } + + showNotification(message, type = 'info') { + // 移除现有的通知 + const existingNotification = document.querySelector('.notification'); + if (existingNotification) { + existingNotification.remove(); + } + + // 创建新的通知 + const notification = document.createElement('div'); + notification.className = `notification notification-${type}`; + notification.innerHTML = ` + + ${message} + `; + + // 样式 + notification.style.cssText = ` + position: fixed; + top: 20px; + right: 20px; + background: ${type === 'success' ? '#10b981' : type === 'error' ? '#ef4444' : '#3b82f6'}; + color: white; + padding: 15px 20px; + border-radius: 8px; + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2); + display: flex; + align-items: center; + gap: 10px; + z-index: 1000; + animation: slideIn 0.3s ease; + `; + + document.body.appendChild(notification); + + // 3秒后自动移除 + setTimeout(() => { + notification.style.animation = 'slideOut 0.3s ease'; + setTimeout(() => notification.remove(), 300); + }, 3000); + + // 添加动画关键帧 + if (!document.querySelector('#notification-styles')) { + const style = document.createElement('style'); + style.id = 'notification-styles'; + style.textContent = ` + @keyframes slideIn { + from { + transform: translateX(100%); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } + } + @keyframes slideOut { + from { + transform: translateX(0); + opacity: 1; + } + to { + transform: translateX(100%); + opacity: 0; + } + } + `; + document.head.appendChild(style); + } + } +} + +// 初始化认证管理器 +document.addEventListener('DOMContentLoaded', () => { + window.authManager = new AuthManager(); +}); \ No newline at end of file diff --git a/frontend/js/main.js b/frontend/js/main.js new file mode 100644 index 0000000..0b11d50 --- /dev/null +++ b/frontend/js/main.js @@ -0,0 +1,222 @@ +// 首页通用JavaScript功能 +// 主要处理导航栏交互、页面滚动效果等通用功能 + +class MainPage { + constructor() { + this.init(); + } + + init() { + // 初始化所有功能 + this.initNavbar(); + this.initScrollEffects(); + this.initSmoothScroll(); + this.initBackToTop(); + this.initMobileMenu(); + this.initAuthButtons(); + } + + // 初始化导航栏交互 + initNavbar() { + const navbar = document.querySelector('.navbar'); + if (!navbar) return; + + // 滚动时改变导航栏样式 + window.addEventListener('scroll', () => { + if (window.scrollY > 50) { + navbar.classList.add('navbar-scrolled'); + } else { + navbar.classList.remove('navbar-scrolled'); + } + }); + + // 初始化当前页面高亮 + this.highlightCurrentPage(); + } + + // 高亮当前页面导航链接 + highlightCurrentPage() { + const currentPath = window.location.pathname; + const navLinks = document.querySelectorAll('.nav-link'); + + navLinks.forEach(link => { + const href = link.getAttribute('href'); + if (href && currentPath.includes(href.replace('.html', ''))) { + link.classList.add('active'); + } + }); + } + + // 初始化滚动效果 + initScrollEffects() { + // 滚动时显示/隐藏元素 + const observerOptions = { + root: null, + rootMargin: '0px', + threshold: 0.1 + }; + + const observer = new IntersectionObserver((entries) => { + entries.forEach(entry => { + if (entry.isIntersecting) { + entry.target.classList.add('animate-in'); + } + }); + }, observerOptions); + + // 观察需要动画的元素 + document.querySelectorAll('.feature-card, .hero-content').forEach(el => { + observer.observe(el); + }); + } + + // 初始化平滑滚动 + initSmoothScroll() { + document.querySelectorAll('a[href^="#"]').forEach(anchor => { + anchor.addEventListener('click', (e) => { + e.preventDefault(); + const targetId = anchor.getAttribute('href'); + if (targetId === '#') return; + + const targetElement = document.querySelector(targetId); + if (targetElement) { + targetElement.scrollIntoView({ + behavior: 'smooth', + block: 'start' + }); + } + }); + }); + } + + // 初始化返回顶部按钮 + initBackToTop() { + const backToTopBtn = document.createElement('button'); + backToTopBtn.id = 'backToTop'; + backToTopBtn.innerHTML = ''; + backToTopBtn.title = '返回顶部'; + document.body.appendChild(backToTopBtn); + + // 滚动时显示/隐藏按钮 + window.addEventListener('scroll', () => { + if (window.scrollY > 300) { + backToTopBtn.classList.add('show'); + } else { + backToTopBtn.classList.remove('show'); + } + }); + + // 点击返回顶部 + backToTopBtn.addEventListener('click', () => { + window.scrollTo({ + top: 0, + behavior: 'smooth' + }); + }); + } + + // 初始化移动端菜单 + initMobileMenu() { + const navbarToggler = document.querySelector('.navbar-toggler'); + const navbarCollapse = document.querySelector('.navbar-collapse'); + + if (!navbarToggler || !navbarCollapse) return; + + navbarToggler.addEventListener('click', () => { + navbarCollapse.classList.toggle('show'); + }); + + // 点击菜单项后自动关闭移动菜单 + document.querySelectorAll('.navbar-nav .nav-link').forEach(link => { + link.addEventListener('click', () => { + if (navbarCollapse.classList.contains('show')) { + navbarCollapse.classList.remove('show'); + } + }); + }); + } + + // 初始化认证按钮状态 + initAuthButtons() { + // 检查用户是否已登录 + this.checkLoginStatus().then(user => { + const loginBtn = document.getElementById('loginBtn'); + const registerBtn = document.getElementById('registerBtn'); + const heroLoginBtn = document.getElementById('heroLoginBtn'); + + if (user) { + // 用户已登录,显示仪表板按钮 + // 根据用户角色设置正确的仪表板路径 + let dashboardUrl = '/dashboard'; + if (user.role === 'student') { + dashboardUrl = '/frontend/html/student_dashboard.html'; + } else if (user.role === 'teacher') { + dashboardUrl = '/frontend/html/teacher_dashboard.html'; + } else if (user.role === 'admin') { + dashboardUrl = '/frontend/html/admin_dashboard.html'; + } + + if (loginBtn) { + loginBtn.textContent = '进入仪表板'; + loginBtn.href = dashboardUrl; + } + if (heroLoginBtn) { + heroLoginBtn.textContent = '进入仪表板'; + heroLoginBtn.href = dashboardUrl; + } + if (registerBtn) { + registerBtn.style.display = 'none'; + } + } + }); + } + + // 检查登录状态 + async checkLoginStatus() { + try { + const apiBase = window.location.protocol === 'file:' ? 'http://localhost:3000/api' : '/api'; + const response = await fetch(`${apiBase}/auth/me`); + const data = await response.json(); + return data.success && data.user; + } catch (error) { + console.log('用户未登录'); + return false; + } + } + + // 显示通知 + showNotification(message, type = 'info') { + // 创建通知元素 + const notification = document.createElement('div'); + notification.className = `notification notification-${type}`; + notification.innerHTML = ` +
+ + ${message} +
+ `; + + // 添加到页面 + document.body.appendChild(notification); + + // 显示通知 + setTimeout(() => { + notification.classList.add('show'); + }, 10); + + // 自动隐藏 + setTimeout(() => { + notification.classList.remove('show'); + setTimeout(() => { + if (notification.parentNode) { + notification.parentNode.removeChild(notification); + } + }, 300); + }, 3000); + } +} + +// 页面加载完成后初始化 +document.addEventListener('DOMContentLoaded', () => { + new MainPage(); +}); \ No newline at end of file diff --git a/frontend/js/student.js b/frontend/js/student.js new file mode 100644 index 0000000..8f750a4 --- /dev/null +++ b/frontend/js/student.js @@ -0,0 +1,438 @@ +class StudentManager { + constructor() { + // 动态设置API基础URL,支持file:///协议和localhost:3000访问 + this.apiBase = window.location.protocol === 'file:' ? 'http://localhost:3000/api' : '/api'; + this.initDashboard(); + this.initGradeDetails(); + this.loadProfile(); + } + + async initDashboard() { + const gradeList = document.getElementById('gradeList'); + const statisticsElement = document.getElementById('statistics'); + + if (!gradeList) return; + + try { + const response = await fetch(`${this.apiBase}/student/grades`, { + credentials: 'include' + }); + + if (response.status === 401) { + // 未登录,重定向到登录页 + this.showNotification('请先登录', 'error'); + setTimeout(() => { + window.location.href = '/html/login.html'; + }, 1500); + return; + } + + const data = await response.json(); + + if (data.success) { + this.renderGrades(data.grades); + this.renderStatistics(data.statistics); + this.updateChart(data.grades); + } else { + this.showNotification(data.message || '获取成绩失败', 'error'); + } + } catch (error) { + console.error('获取成绩错误:', error); + this.showNotification('网络错误,请重试', 'error'); + } + } + + renderGrades(grades) { + const gradeList = document.getElementById('gradeList'); + const gradeTable = document.getElementById('gradeTable'); + + if (!gradeTable) return; + + if (grades.length === 0) { + gradeList.innerHTML = ` +
+ +

暂无成绩记录

+

你还没有任何成绩记录

+
+ `; + return; + } + + const tbody = gradeTable.querySelector('tbody'); + tbody.innerHTML = ''; + + grades.forEach(grade => { + const row = document.createElement('tr'); + + // 根据分数设置颜色 + let scoreClass = ''; + if (grade.score >= 90) scoreClass = 'grade-excellent'; + else if (grade.score >= 80) scoreClass = 'grade-good'; + else if (grade.score >= 60) scoreClass = 'grade-pass'; + else scoreClass = 'grade-fail'; + + row.innerHTML = ` + ${grade.course_code} + ${grade.course_name} + ${grade.credit} + + ${grade.score} + + ${grade.grade_level || '-'} + ${grade.grade_point || '-'} + ${grade.teacher_name} + ${new Date(grade.exam_date).toLocaleDateString()} + + + 查看 + + + `; + + tbody.appendChild(row); + }); + } + + renderStatistics(statistics) { + const element = document.getElementById('statistics'); + if (!element) return; + + element.innerHTML = ` +
+
+
+ +
+
${statistics.totalCourses}
+
总课程数
+
+
+
+ +
+
${statistics.totalCredits}
+
总学分
+
+
+
+ +
+
${statistics.averageScore}
+
平均分
+
+
+
+ +
+
${statistics.gpa}
+
平均绩点
+
+
+ `; + } + + async loadProfile() { + const profileElement = document.getElementById('profileInfo'); + if (!profileElement) return; + + try { + const response = await fetch(`${this.apiBase}/student/profile`, { + credentials: 'include' + }); + + if (response.status === 401) { + // 未登录,重定向到登录页 + this.showNotification('请先登录', 'error'); + setTimeout(() => { + window.location.href = '/html/login.html'; + }, 1500); + return; + } + + const data = await response.json(); + + if (data.success) { + const profile = data.profile; + + // 更新学生仪表板顶部信息 + const userNameElement = document.getElementById('userName'); + const studentNameElement = document.getElementById('studentName'); + const studentClassElement = document.getElementById('studentClass'); + + if (userNameElement) { + userNameElement.textContent = profile.full_name || profile.username; + } + if (studentNameElement) { + studentNameElement.textContent = profile.full_name || profile.username; + } + if (studentClassElement) { + studentClassElement.textContent = profile.class_name || '未设置'; + } + + profileElement.innerHTML = ` +
+
+ +
+
+

${profile.full_name}

+

+ 学生 +

+
+
+
+
+ +
+

学号

+

${profile.student_id}

+
+
+
+ +
+

班级

+

${profile.class_name}

+
+
+
+ +
+

专业

+

${profile.major || '未设置'}

+
+
+
+ +
+

入学年份

+

${profile.enrollment_year || '未设置'}

+
+
+
+ `; + } else { + // API返回失败 + this.showNotification(data.message || '获取个人信息失败', 'error'); + } + } catch (error) { + console.error('加载个人信息错误:', error); + this.showNotification('网络错误,请重试', 'error'); + } + } + + async initGradeDetails() { + const urlParams = new URLSearchParams(window.location.search); + const gradeId = urlParams.get('id'); + + if (!gradeId) return; + + try { + const response = await fetch(`${this.apiBase}/student/grades/${gradeId}`, { + credentials: 'include' + }); + const data = await response.json(); + + if (data.success) { + this.renderGradeDetails(data.grade); + } else { + this.showNotification('获取成绩详情失败', 'error'); + setTimeout(() => window.history.back(), 1500); + } + } catch (error) { + console.error('获取成绩详情错误:', error); + this.showNotification('网络错误,请重试', 'error'); + } + } + + renderGradeDetails(grade) { + const container = document.getElementById('gradeDetails'); + if (!container) return; + + // 计算绩点描述 + let gradeDescription = ''; + if (grade.score >= 90) gradeDescription = '优秀'; + else if (grade.score >= 80) gradeDescription = '良好'; + else if (grade.score >= 70) gradeDescription = '中等'; + else if (grade.score >= 60) gradeDescription = '及格'; + else gradeDescription = '不及格'; + + container.innerHTML = ` +
+
+

${grade.course_name} (${grade.course_code})

+
+ ${grade.score} 分 + ${gradeDescription} +
+
+ +
+
+

基本信息

+
+ 学分: + ${grade.credit} +
+
+ 学期: + ${grade.semester} +
+
+ 考试日期: + ${new Date(grade.exam_date).toLocaleDateString()} +
+
+ 等级: + ${grade.grade_level || '-'} +
+
+ 绩点: + ${grade.grade_point || '-'} +
+
+ +
+

学生信息

+
+ 姓名: + ${grade.full_name} +
+
+ 学号: + ${grade.student_number} +
+
+ 班级: + ${grade.class_name} +
+
+ 专业: + ${grade.major || '未设置'} +
+
+ +
+

教师信息

+
+ 任课教师: + ${grade.teacher_name} +
+
+ 教师邮箱: + ${grade.teacher_email} +
+
+
+ + ${grade.remark ? ` +
+

备注

+

${grade.remark}

+
+ ` : ''} + +
+ + +
+
+ `; + } + + updateChart(grades) { + const ctx = document.getElementById('gradeChart'); + if (!ctx) return; + + if (typeof Chart === 'undefined') { + // 如果没有Chart.js,延迟加载 + this.loadChartLibrary().then(() => this.updateChart(grades)); + return; + } + + const courseNames = grades.map(g => g.course_name); + const scores = grades.map(g => g.score); + + // 销毁现有图表实例 + if (window.gradeChart instanceof Chart) { + window.gradeChart.destroy(); + } + + window.gradeChart = new Chart(ctx, { + type: 'bar', + data: { + labels: courseNames, + datasets: [{ + label: '分数', + data: scores, + backgroundColor: scores.map(score => { + if (score >= 90) return 'rgba(75, 192, 192, 0.7)'; + if (score >= 80) return 'rgba(54, 162, 235, 0.7)'; + if (score >= 60) return 'rgba(255, 206, 86, 0.7)'; + return 'rgba(255, 99, 132, 0.7)'; + }), + borderColor: scores.map(score => { + if (score >= 90) return 'rgb(75, 192, 192)'; + if (score >= 80) return 'rgb(54, 162, 235)'; + if (score >= 60) return 'rgb(255, 206, 86)'; + return 'rgb(255, 99, 132)'; + }), + borderWidth: 1 + }] + }, + options: { + responsive: true, + plugins: { + title: { + display: true, + text: '各科成绩分布' + } + }, + scales: { + y: { + beginAtZero: true, + max: 100 + } + } + } + }); + } + + async loadChartLibrary() { + return new Promise((resolve, reject) => { + if (typeof Chart !== 'undefined') { + resolve(); + return; + } + + const script = document.createElement('script'); + script.src = 'https://cdn.jsdelivr.net/npm/chart.js'; + script.onload = resolve; + script.onerror = reject; + document.head.appendChild(script); + }); + } + + showNotification(message, type = 'info') { + // 使用AuthManager的通知系统或自己实现 + if (window.authManager && window.authManager.showNotification) { + window.authManager.showNotification(message, type); + } else { + alert(message); + } + } +} + +// 初始化学生管理器 +document.addEventListener('DOMContentLoaded', () => { + if (window.location.pathname.includes('/student/')) { + window.studentManager = new StudentManager(); + } +}); \ No newline at end of file diff --git a/frontend/js/teacher.js b/frontend/js/teacher.js new file mode 100644 index 0000000..2df8457 --- /dev/null +++ b/frontend/js/teacher.js @@ -0,0 +1,409 @@ +class TeacherDashboard { + constructor() { + // 动态设置API基础URL,支持file:///协议和localhost:3000访问 + this.apiBase = window.location.protocol === 'file:' ? 'http://localhost:3000/api' : '/api'; + this.currentUser = null; + this.courses = []; + this.grades = []; + this.init(); + } + + async init() { + // 检查登录状态 + if (!await this.checkAuth()) { + window.location.href = '/frontend/html/login.html'; + return; + } + + // 加载用户信息 + await this.loadUserInfo(); + + // 加载课程数据 + await this.loadCourses(); + + // 加载成绩数据 + await this.loadGrades(); + + // 绑定事件 + this.bindEvents(); + + // 更新界面 + this.updateUI(); + } + + async checkAuth() { + try { + const response = await fetch(`${this.apiBase}/auth/check`, { + credentials: 'include' + }); + + if (!response.ok) { + return false; + } + + const data = await response.json(); + return data.success && data.user.role === 'teacher'; + } catch (error) { + console.error('认证检查失败:', error); + return false; + } + } + + async loadUserInfo() { + try { + const response = await fetch(`${this.apiBase}/auth/me`, { + credentials: 'include' + }); + + if (response.ok) { + const data = await response.json(); + if (data.success) { + this.currentUser = data.user; + } + } + } catch (error) { + console.error('加载用户信息失败:', error); + } + } + + async loadCourses() { + try { + const response = await fetch(`${this.apiBase}/teacher/courses`, { + credentials: 'include' + }); + + if (response.ok) { + const data = await response.json(); + if (data.success) { + this.courses = data.courses; + this.populateCourseSelectors(); + } + } + } catch (error) { + console.error('加载课程失败:', error); + this.showNotification('加载课程失败', 'error'); + } + } + + async loadGrades(filters = {}) { + try { + const queryParams = new URLSearchParams(filters).toString(); + const url = `${this.apiBase}/teacher/grades${queryParams ? '?' + queryParams : ''}`; + + const response = await fetch(url, { + credentials: 'include' + }); + + if (response.ok) { + const data = await response.json(); + if (data.success) { + this.grades = data.grades; + this.renderGradesTable(); + } + } + } catch (error) { + console.error('加载成绩失败:', error); + this.showNotification('加载成绩失败', 'error'); + } + } + + populateCourseSelectors() { + // 填充课程选择器 + const courseSelectors = document.querySelectorAll('.course-selector'); + courseSelectors.forEach(select => { + select.innerHTML = ''; + this.courses.forEach(course => { + const option = document.createElement('option'); + option.value = course.id; + option.textContent = `${course.course_code} - ${course.course_name}`; + select.appendChild(option); + }); + }); + } + + renderGradesTable() { + const tableBody = document.getElementById('gradesTableBody'); + if (!tableBody) return; + + if (this.grades.length === 0) { + tableBody.innerHTML = ` + + +
+ +

暂无成绩数据

+
+ + + `; + return; + } + + tableBody.innerHTML = this.grades.map(grade => { + const gradeClass = this.getGradeClass(grade.score); + return ` + + + ${grade.student_id} + ${grade.full_name} + ${grade.class_name} + ${grade.course_code} + ${grade.course_name} + + ${grade.score} + ${grade.grade_level} + + ${grade.exam_date ? new Date(grade.exam_date).toLocaleDateString() : '未设置'} + +
+ + +
+ + + `; + }).join(''); + + // 更新统计信息 + this.updateStats(); + } + + getGradeClass(score) { + if (score >= 90) return 'grade-excellent'; + if (score >= 80) return 'grade-good'; + if (score >= 70) return 'grade-medium'; + if (score >= 60) return 'grade-pass'; + return 'grade-fail'; + } + + updateStats() { + if (this.grades.length === 0) return; + + const totalStudents = new Set(this.grades.map(g => g.student_id)).size; + const avgScore = this.grades.reduce((sum, g) => sum + g.score, 0) / this.grades.length; + const passRate = (this.grades.filter(g => g.score >= 60).length / this.grades.length * 100).toFixed(1); + + document.getElementById('totalStudents').textContent = totalStudents; + document.getElementById('avgScore').textContent = avgScore.toFixed(1); + document.getElementById('passRate').textContent = `${passRate}%`; + } + + bindEvents() { + // 搜索按钮 + document.getElementById('searchBtn')?.addEventListener('click', () => { + this.handleSearch(); + }); + + // 重置按钮 + document.getElementById('resetBtn')?.addEventListener('click', () => { + this.resetFilters(); + }); + + // 导出按钮 + document.getElementById('exportBtn')?.addEventListener('click', () => { + this.exportGrades(); + }); + + // 批量删除按钮 + document.getElementById('batchDeleteBtn')?.addEventListener('click', () => { + this.batchDeleteGrades(); + }); + + // 表格操作按钮事件委托 + document.addEventListener('click', (e) => { + if (e.target.closest('.btn-edit')) { + const gradeId = e.target.closest('.btn-edit').dataset.id; + this.editGrade(gradeId); + } + + if (e.target.closest('.btn-delete')) { + const gradeId = e.target.closest('.btn-delete').dataset.id; + this.deleteGrade(gradeId); + } + }); + + // 退出登录 + document.getElementById('logoutBtn')?.addEventListener('click', () => { + this.handleLogout(); + }); + } + + handleSearch() { + const className = document.getElementById('classFilter')?.value || ''; + const courseId = document.getElementById('courseFilter')?.value || ''; + + this.loadGrades({ class_name: className, course_id: courseId }); + } + + resetFilters() { + document.getElementById('classFilter').value = ''; + document.getElementById('courseFilter').value = ''; + this.loadGrades(); + } + + async exportGrades() { + try { + const response = await fetch(`${this.apiBase}/teacher/grades/export`, { + credentials: 'include' + }); + + if (response.ok) { + const blob = await response.blob(); + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `成绩报表_${new Date().toISOString().split('T')[0]}.xlsx`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + window.URL.revokeObjectURL(url); + } + } catch (error) { + console.error('导出失败:', error); + this.showNotification('导出失败', 'error'); + } + } + + async batchDeleteGrades() { + const checkboxes = document.querySelectorAll('.grade-checkbox:checked'); + if (checkboxes.length === 0) { + this.showNotification('请选择要删除的成绩', 'warning'); + return; + } + + if (!confirm(`确定要删除选中的 ${checkboxes.length} 条成绩记录吗?`)) { + return; + } + + const gradeIds = Array.from(checkboxes).map(cb => cb.dataset.id); + + try { + const response = await fetch(`${this.apiBase}/teacher/grades/batch`, { + method: 'DELETE', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify({ gradeIds }) + }); + + const data = await response.json(); + if (data.success) { + this.showNotification(`成功删除 ${gradeIds.length} 条成绩记录`, 'success'); + await this.loadGrades(); + } else { + this.showNotification(data.message || '删除失败', 'error'); + } + } catch (error) { + console.error('批量删除失败:', error); + this.showNotification('批量删除失败', 'error'); + } + } + + async editGrade(gradeId) { + const grade = this.grades.find(g => g.id == gradeId); + if (!grade) return; + + // 这里可以打开编辑模态框 + // 暂时使用简单提示框 + const newScore = prompt('请输入新的分数:', grade.score); + if (newScore === null) return; + + const numericScore = parseFloat(newScore); + if (isNaN(numericScore) || numericScore < 0 || numericScore > 100) { + this.showNotification('请输入0-100之间的有效分数', 'error'); + return; + } + + try { + const response = await fetch(`${this.apiBase}/teacher/grades/${gradeId}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify({ + score: numericScore, + examDate: grade.exam_date, + remark: grade.remark + }) + }); + + const data = await response.json(); + if (data.success) { + this.showNotification('成绩更新成功', 'success'); + await this.loadGrades(); + } else { + this.showNotification(data.message || '更新失败', 'error'); + } + } catch (error) { + console.error('更新成绩失败:', error); + this.showNotification('更新成绩失败', 'error'); + } + } + + async deleteGrade(gradeId) { + if (!confirm('确定要删除这条成绩记录吗?')) { + return; + } + + try { + const response = await fetch(`${this.apiBase}/teacher/grades/${gradeId}`, { + method: 'DELETE', + credentials: 'include' + }); + + const data = await response.json(); + if (data.success) { + this.showNotification('成绩删除成功', 'success'); + await this.loadGrades(); + } else { + this.showNotification(data.message || '删除失败', 'error'); + } + } catch (error) { + console.error('删除成绩失败:', error); + this.showNotification('删除成绩失败', 'error'); + } + } + + async handleLogout() { + try { + const response = await fetch(`${this.apiBase}/auth/logout`, { + method: 'POST', + credentials: 'include' + }); + + if (response.ok) { + window.location.href = '/frontend/html/login.html'; + } + } catch (error) { + console.error('退出登录失败:', error); + } + } + + updateUI() { + // 更新用户信息 + if (this.currentUser) { + const userInfoElements = document.querySelectorAll('.user-info'); + userInfoElements.forEach(el => { + el.textContent = `${this.currentUser.full_name} (${this.currentUser.role})`; + }); + } + } + + showNotification(message, type = 'info') { + // 使用AuthManager的通知系统或简单alert + if (typeof AuthManager !== 'undefined' && AuthManager.showNotification) { + AuthManager.showNotification(message, type); + } else { + alert(`${type}: ${message}`); + } + } +} + +// 页面加载完成后初始化 +document.addEventListener('DOMContentLoaded', () => { + if (window.location.pathname.includes('/teacher/')) { + new TeacherDashboard(); + } +}); \ No newline at end of file