v0.1测试

This commit is contained in:
2026-04-07 17:07:13 +08:00
parent 593973f598
commit 6b1b586fe3
80 changed files with 9073 additions and 32 deletions

28
frontend/.env.example Normal file
View File

@@ -0,0 +1,28 @@
# ===========================================
# 班级操行分管理系统 - 前端配置
#
# 开发者: Canglan
# 联系方式: admin@sea-studio.top
# 版权归属: Sea Network Technology Studio
# 许可证: MIT License
#
# 版权所有 © Sea Network Technology Studio
# ===========================================
# 后端API地址
API_BASE_URL=https://api.your-domain.com
# API超时时间
API_TIMEOUT=30
# JWT存储Key
JWT_STORAGE_KEY=class_system_token
# 用户信息存储Key
USER_STORAGE_KEY=class_system_user
# 站点名称
SITE_NAME=班级操行分管理系统
# 会话超时时间(分钟)
SESSION_TIMEOUT=30

126
frontend/admin/admins.php Normal file
View File

@@ -0,0 +1,126 @@
<?php
/**
* 班级操行分管理系统 - 管理端管理员管理
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: MIT License
*
* 版权所有 © Sea Network Technology Studio
*/
require_once __DIR__ . '/../config.php';
if (!isset($_SESSION['user_id']) || $_SESSION['user_type'] !== 'admin') {
header('Location: /index.php');
exit();
}
$page_title = '管理员管理';
$role = $_SESSION['role'] ?? '';
if ($role !== '班主任') {
header('Location: /admin/dashboard.php');
exit();
}
include __DIR__ . '/../includes/header.php';
?>
<div class="nav">
<a href="/admin/dashboard.php" class="nav-item">首页</a>
<a href="/admin/students.php" class="nav-item">学生管理</a>
<a href="/admin/conduct.php" class="nav-item">操行分管理</a>
<a href="/admin/homework.php" class="nav-item">作业管理</a>
<a href="/admin/attendance.php" class="nav-item">考勤管理</a>
<a href="/admin/subjects.php" class="nav-item">科目管理</a>
<a href="/admin/admins.php" class="nav-item active">管理员管理</a>
<a href="/admin/history.php" class="nav-item">历史记录</a>
<a href="/admin/password.php" class="nav-item">修改密码</a>
</div>
<div class="container">
<div class="card">
<div class="action-bar">
<button class="btn btn-primary" onclick="showAddAdminModal()">添加管理员</button>
</div>
<div class="table-wrapper">
<table class="table">
<thead>
<tr>
<th>用户名</th>
<th>姓名</th>
<th>角色</th>
</tr>
</thead>
<tbody id="adminList"></tbody>
</table>
</div>
</div>
</div>
<!-- 添加管理员模态框 -->
<div id="addAdminModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3>添加管理员</h3>
<button class="modal-close" onclick="closeModal('addAdminModal')">&times;</button>
</div>
<form onsubmit="event.preventDefault(); submitAddAdmin()">
<div class="form-group">
<label>用户名</label>
<input type="text" id="adminUsername" required placeholder="登录账号">
</div>
<div class="form-group">
<label>姓名</label>
<input type="text" id="adminRealName" required placeholder="真实姓名">
</div>
<div class="form-group">
<label>密码</label>
<input type="text" id="adminPassword" placeholder="留空则自动生成">
<small>自动生成8位随机密码</small>
</div>
<div class="form-group">
<label>角色</label>
<select id="adminRole" required>
<option value="">请选择角色</option>
<option value="班长">班长</option>
<option value="科代表">科代表</option>
<option value="考勤委员">考勤委员</option>
<option value="劳动委员">劳动委员</option>
</select>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary">添加</button>
<button type="button" class="btn" onclick="closeModal('addAdminModal')">取消</button>
</div>
</form>
</div>
</div>
<script>
async function loadAdmins() {
const res = await apiGet('/api/admin/list');
if (res && res.success) {
let html = '';
res.data.admins.forEach(admin => {
html += `<tr>
<td>${escapeHtml(admin.username)}</td>
<td>${escapeHtml(admin.real_name)}</td>
<td>${escapeHtml(admin.role_type)}</td>
</tr>`;
});
if (res.data.admins.length === 0) {
html = '<tr><td colspan="3" style="text-align:center;">暂无管理员</td></tr>';
}
document.getElementById('adminList').innerHTML = html;
}
}
loadAdmins();
</script>
<script src="/assets/js/admin.js"></script>
<?php include __DIR__ . '/../includes/footer.php'; ?>

View File

@@ -0,0 +1,184 @@
<?php
/**
* 班级操行分管理系统 - 管理端考勤管理
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: MIT License
*
* 版权所有 © Sea Network Technology Studio
*/
require_once __DIR__ . '/../config.php';
if (!isset($_SESSION['user_id']) || $_SESSION['user_type'] !== 'admin') {
header('Location: /index.php');
exit();
}
$page_title = '考勤管理';
$role = $_SESSION['role'] ?? '';
if (!in_array($role, ['班主任', '考勤委员'])) {
header('Location: /admin/dashboard.php');
exit();
}
include __DIR__ . '/../includes/header.php';
?>
<div class="nav">
<a href="/admin/dashboard.php" class="nav-item">首页</a>
<a href="/admin/students.php" class="nav-item">学生管理</a>
<?php if ($role === '班主任' || $role === '班长'): ?>
<a href="/admin/conduct.php" class="nav-item">操行分管理</a>
<?php endif; ?>
<?php if ($role === '班主任' || $role === '科代表'): ?>
<a href="/admin/homework.php" class="nav-item">作业管理</a>
<?php endif; ?>
<a href="/admin/attendance.php" class="nav-item active">考勤管理</a>
<?php if ($role === '班主任'): ?>
<a href="/admin/subjects.php" class="nav-item">科目管理</a>
<a href="/admin/admins.php" class="nav-item">管理员管理</a>
<?php endif; ?>
<a href="/admin/history.php" class="nav-item">历史记录</a>
<a href="/admin/password.php" class="nav-item">修改密码</a>
</div>
<div class="container">
<div class="card">
<div class="action-bar">
<button class="btn btn-primary" onclick="showAddAttendanceModal()">添加考勤</button>
<div class="search-bar">
<input type="date" id="attendanceDate" value="<?php echo date('Y-m-d'); ?>">
<button class="btn btn-primary" onclick="loadAttendanceRecords()">查询</button>
</div>
</div>
<div class="table-wrapper">
<table class="table">
<thead>
<tr><th>学号</th><th>姓名</th><th>状态</th><th>原因</th><th>记录人</th><th>扣分</th></tr>
</thead>
<tbody id="attendanceList"></tbody>
</table>
</div>
</div>
</div>
<!-- 添加考勤模态框 -->
<div id="addAttendanceModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3>添加考勤记录</h3>
<button class="modal-close" onclick="closeModal('addAttendanceModal')">&times;</button>
</div>
<form onsubmit="event.preventDefault(); submitAddAttendance()">
<div class="form-group">
<label>学生</label>
<select id="attendanceStudentId" required></select>
</div>
<div class="form-group">
<label>日期</label>
<input type="date" id="attAttendanceDate" required>
</div>
<div class="form-group">
<label>状态</label>
<select id="attendanceStatus" required>
<option value="present">出勤</option>
<option value="absent">缺勤</option>
<option value="late">迟到</option>
<option value="leave">请假</option>
</select>
</div>
<div class="form-group">
<label>原因</label>
<input type="text" id="attendanceReason" placeholder="缺勤/迟到/请假原因">
</div>
<div class="form-group">
<label><input type="checkbox" id="attendanceDeduct"> 同时扣分</label>
<small>扣分规则:缺勤-5分迟到-2分请假-1分</small>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary">提交</button>
<button type="button" class="btn" onclick="closeModal('addAttendanceModal')">取消</button>
</div>
</form>
</div>
</div>
<script>
async function loadAttendanceRecords() {
const date = document.getElementById('attendanceDate').value;
const res = await apiGet('/api/admin/attendance/records', { date });
if (res && res.success) {
let html = '';
res.data.records.forEach(record => {
html += `<tr>
<td>${escapeHtml(record.student_no)}</td>
<td>${escapeHtml(record.student_name)}</td>
<td>${getStatusBadge(record.status, 'attendance')}</td>
<td>${escapeHtml(record.reason || '-')}</td>
<td>${escapeHtml(record.recorder_name || '-')}</td>
<td>${record.deduction_applied ? '已扣分' : '-'}</td>
</tr>`;
});
if (res.data.records.length === 0) {
html = '<tr><td colspan="6" style="text-align:center;">暂无考勤记录</td></tr>';
}
document.getElementById('attendanceList').innerHTML = html;
}
}
async function loadStudentsForSelect() {
const res = await apiGet('/api/admin/students');
if (res && res.success) {
let html = '<option value="">请选择学生</option>';
res.data.students.forEach(s => {
html += `<option value="${s.student_id}">${escapeHtml(s.student_no)} - ${escapeHtml(s.name)}</option>`;
});
document.getElementById('attendanceStudentId').innerHTML = html;
}
}
async function showAddAttendanceModal() {
await loadStudentsForSelect();
document.getElementById('addAttendanceModal').style.display = 'flex';
document.getElementById('attAttendanceDate').value = new Date().toISOString().split('T')[0];
}
async function submitAddAttendance() {
const studentId = document.getElementById('attendanceStudentId').value;
const date = document.getElementById('attAttendanceDate').value;
const status = document.getElementById('attendanceStatus').value;
const reason = document.getElementById('attendanceReason').value;
const applyDeduction = document.getElementById('attendanceDeduct').checked;
if (!studentId || !date || !status) {
showToast('请填写完整信息', 'warning');
return;
}
const res = await apiPost('/api/admin/attendance', {
student_id: parseInt(studentId),
date: date,
status: status,
reason: reason,
apply_deduction: applyDeduction
});
if (res && res.success) {
showToast('考勤记录添加成功');
closeModal('addAttendanceModal');
loadAttendanceRecords();
} else {
showToast(res?.message || '添加失败', 'error');
}
}
loadAttendanceRecords();
</script>
<script src="/assets/js/admin.js"></script>
<?php include __DIR__ . '/../includes/footer.php'; ?>

145
frontend/admin/conduct.php Normal file
View File

@@ -0,0 +1,145 @@
<?php
/**
* 班级操行分管理系统 - 管理端操行分管理
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: MIT License
*
* 版权所有 © Sea Network Technology Studio
*/
require_once __DIR__ . '/../config.php';
if (!isset($_SESSION['user_id']) || $_SESSION['user_type'] !== 'admin') {
header('Location: /index.php');
exit();
}
$page_title = '操行分管理';
$role = $_SESSION['role'] ?? '';
if (!in_array($role, ['班主任', '班长'])) {
header('Location: /admin/dashboard.php');
exit();
}
include __DIR__ . '/../includes/header.php';
?>
<div class="nav">
<a href="/admin/dashboard.php" class="nav-item">首页</a>
<a href="/admin/students.php" class="nav-item">学生管理</a>
<a href="/admin/conduct.php" class="nav-item active">操行分管理</a>
<?php if ($role === '班主任' || $role === '科代表'): ?>
<a href="/admin/homework.php" class="nav-item">作业管理</a>
<?php endif; ?>
<?php if ($role === '班主任' || $role === '考勤委员'): ?>
<a href="/admin/attendance.php" class="nav-item">考勤管理</a>
<?php endif; ?>
<?php if ($role === '班主任'): ?>
<a href="/admin/subjects.php" class="nav-item">科目管理</a>
<a href="/admin/admins.php" class="nav-item">管理员管理</a>
<?php endif; ?>
<a href="/admin/history.php" class="nav-item">历史记录</a>
<a href="/admin/password.php" class="nav-item">修改密码</a>
</div>
<div class="container">
<div class="card">
<div class="action-bar">
<div class="action-buttons">
<button class="btn btn-primary" onclick="showBatchPointsModal()">批量加减分</button>
</div>
</div>
<div class="table-wrapper">
<table class="table">
<thead>
<tr>
<th><input type="checkbox" id="selectAll" onclick="toggleSelectAll()"></th>
<th>学号</th>
<th>姓名</th>
<th>当前操行分</th>
<th>操作</th>
</tr>
</thead>
<tbody id="studentList"></tbody>
</table>
</div>
</div>
</div>
<script>
let selectedStudentIds = [];
async function loadStudents() {
const res = await apiGet('/api/admin/students');
if (res && res.success) {
let html = '';
res.data.students.forEach(student => {
html += `<tr>
<td><input type="checkbox" class="student-checkbox" data-id="${student.student_id}"></td>
<td>${escapeHtml(student.student_no)}</td>
<td>${escapeHtml(student.name)}</td>
<td>${student.total_points}</td>
<td><button class="btn btn-sm btn-primary" onclick="showSinglePointsModal(${student.student_id}, '${escapeHtml(student.name)}')">加减分</button></td>
</tr>`;
});
if (res.data.students.length === 0) {
html = '<tr><td colspan="5" style="text-align:center;">暂无学生数据</td></tr>';
}
document.getElementById('studentList').innerHTML = html;
}
}
function toggleSelectAll() {
const selectAll = document.getElementById('selectAll');
document.querySelectorAll('.student-checkbox').forEach(cb => {
cb.checked = selectAll.checked;
});
}
function showSinglePointsModal(studentId, studentName) {
selectedStudentIds = [studentId];
document.getElementById('selectedStudentsCount').innerHTML = `${studentName} (1人)`;
document.getElementById('pointsChange').value = '';
document.getElementById('pointsReason').value = '';
document.getElementById('batchPointsModal').style.display = 'flex';
}
loadStudents();
</script>
<script src="/assets/js/admin.js"></script>
<!-- 批量加减分模态框 -->
<div id="batchPointsModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3>批量加减分</h3>
<button class="modal-close" onclick="closeModal('batchPointsModal')">&times;</button>
</div>
<form onsubmit="event.preventDefault(); submitBatchPoints()">
<div class="form-group">
<label>选中学生</label>
<div id="selectedStudentsCount">0 人</div>
</div>
<div class="form-group">
<label>分数变动</label>
<input type="number" id="pointsChange" required placeholder="正数为加分,负数为扣分">
<small><?php echo $role === '班长' ? '班长单次±5分以内' : '班主任无限制'; ?></small>
</div>
<div class="form-group">
<label>原因</label>
<textarea id="pointsReason" required rows="3" placeholder="请填写加减分原因"></textarea>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary">确认提交</button>
<button type="button" class="btn" onclick="closeModal('batchPointsModal')">取消</button>
</div>
</form>
</div>
</div>
<?php include __DIR__ . '/../includes/footer.php'; ?>

View File

@@ -0,0 +1,114 @@
<?php
/**
* 班级操行分管理系统 - 管理端首页
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: MIT License
*
* 版权所有 © Sea Network Technology Studio
*/
require_once __DIR__ . '/../config.php';
if (!isset($_SESSION['user_id']) || $_SESSION['user_type'] !== 'admin') {
header('Location: /index.php');
exit();
}
$page_title = '首页';
$role = $_SESSION['role'] ?? '';
include __DIR__ . '/../includes/header.php';
?>
<div class="nav">
<a href="/admin/dashboard.php" class="nav-item active">首页</a>
<a href="/admin/students.php" class="nav-item">学生管理</a>
<?php if ($role === '班主任' || $role === '班长'): ?>
<a href="/admin/conduct.php" class="nav-item">操行分管理</a>
<?php endif; ?>
<?php if ($role === '班主任' || $role === '科代表'): ?>
<a href="/admin/homework.php" class="nav-item">作业管理</a>
<?php endif; ?>
<?php if ($role === '班主任' || $role === '考勤委员'): ?>
<a href="/admin/attendance.php" class="nav-item">考勤管理</a>
<?php endif; ?>
<?php if ($role === '班主任'): ?>
<a href="/admin/subjects.php" class="nav-item">科目管理</a>
<a href="/admin/admins.php" class="nav-item">管理员管理</a>
<?php endif; ?>
<a href="/admin/history.php" class="nav-item">历史记录</a>
<a href="/admin/password.php" class="nav-item">修改密码</a>
</div>
<div class="container">
<div class="stats-grid" id="dashboardStats"></div>
<div class="card">
<div class="card-title">快捷操作</div>
<div class="action-buttons" id="quickActions"></div>
</div>
<div class="card">
<div class="card-title">操行分排行榜 (Top 10)</div>
<div class="table-wrapper">
<table class="table">
<thead>
<tr><th>排名</th><th>学号</th><th>姓名</th><th>操行分</th></tr>
</thead>
<tbody id="rankingList"></tbody>
</table>
</div>
</div>
</div>
<script>
async function loadDashboard() {
// 加载学生统计
const studentsRes = await apiGet('/api/admin/students');
if (studentsRes && studentsRes.success) {
document.getElementById('dashboardStats').innerHTML = `
<div class="stat-card">
<div class="stat-label">班级学生数</div>
<div class="stat-value">${studentsRes.data.total || 0}</div>
</div>
`;
}
// 快捷操作按钮
let quickActions = '';
if ('<?php echo $role; ?>' === '班主任' || '<?php echo $role; ?>' === '班长') {
quickActions += '<button class="btn btn-primary" onclick="location.href=\'/admin/conduct.php\'">操行分管理</button>';
}
if ('<?php echo $role; ?>' === '班主任') {
quickActions += '<button class="btn btn-success" onclick="location.href=\'/admin/students.php\'">导入学生</button>';
}
document.getElementById('quickActions').innerHTML = quickActions || '<p>暂无快捷操作</p>';
// 加载排行榜
const rankingRes = await apiGet('/api/student/ranking', { limit: 10 });
if (rankingRes && rankingRes.success) {
let html = '';
rankingRes.data.ranking.forEach((student, index) => {
html += `
<tr>
<td>${index + 1}</td>
<td>${escapeHtml(student.student_no)}</td>
<td>${escapeHtml(student.name)}</td>
<td>${student.total_points}</td>
</tr>
`;
});
if (rankingRes.data.ranking.length === 0) {
html = '<tr><td colspan="4" style="text-align:center;">暂无数据</td></tr>';
}
document.getElementById('rankingList').innerHTML = html;
}
}
loadDashboard();
</script>
<script src="/assets/js/admin.js"></script>
<?php include __DIR__ . '/../includes/footer.php'; ?>

166
frontend/admin/history.php Normal file
View File

@@ -0,0 +1,166 @@
<?php
/**
* 班级操行分管理系统 - 管理端历史记录
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: MIT License
*
* 版权所有 © Sea Network Technology Studio
*/
require_once __DIR__ . '/../config.php';
if (!isset($_SESSION['user_id']) || $_SESSION['user_type'] !== 'admin') {
header('Location: /index.php');
exit();
}
$page_title = '历史记录';
$role = $_SESSION['role'] ?? '';
include __DIR__ . '/../includes/header.php';
?>
<div class="nav">
<a href="/admin/dashboard.php" class="nav-item">首页</a>
<a href="/admin/students.php" class="nav-item">学生管理</a>
<?php if ($role === '班主任' || $role === '班长'): ?>
<a href="/admin/conduct.php" class="nav-item">操行分管理</a>
<?php endif; ?>
<?php if ($role === '班主任' || $role === '科代表'): ?>
<a href="/admin/homework.php" class="nav-item">作业管理</a>
<?php endif; ?>
<?php if ($role === '班主任' || $role === '考勤委员'): ?>
<a href="/admin/attendance.php" class="nav-item">考勤管理</a>
<?php endif; ?>
<?php if ($role === '班主任'): ?>
<a href="/admin/subjects.php" class="nav-item">科目管理</a>
<a href="/admin/admins.php" class="nav-item">管理员管理</a>
<?php endif; ?>
<a href="/admin/history.php" class="nav-item active">历史记录</a>
<a href="/admin/password.php" class="nav-item">修改密码</a>
</div>
<div class="container">
<div class="card">
<div class="filter-bar">
<div class="filter-group">
<label>开始日期</label>
<input type="date" id="historyStartDate">
</div>
<div class="filter-group">
<label>结束日期</label>
<input type="date" id="historyEndDate">
</div>
<div class="filter-group">
<label>学生</label>
<select id="historyStudentId">
<option value="">全部</option>
</select>
</div>
<button class="btn btn-primary" onclick="loadHistory(1)">查询</button>
</div>
<div class="table-wrapper">
<table class="table">
<thead>
<tr>
<th>时间</th>
<th>学生</th>
<th>分数变动</th>
<th>原因</th>
<th>操作人</th>
<?php if ($role === '班主任' || $role === '班长'): ?>
<th>操作</th>
<?php endif; ?>
</tr>
</thead>
<tbody id="historyList"></tbody>
</table>
</div>
<div class="pagination" id="historyPagination"></div>
</div>
</div>
<script>
let currentHistoryPage = 1;
let totalHistoryPages = 1;
async function loadStudentsForSelect() {
const res = await apiGet('/api/admin/students');
if (res && res.success) {
let html = '<option value="">全部</option>';
res.data.students.forEach(s => {
html += `<option value="${s.student_id}">${escapeHtml(s.student_no)} - ${escapeHtml(s.name)}</option>`;
});
document.getElementById('historyStudentId').innerHTML = html;
}
}
async function loadHistory(page = 1) {
currentHistoryPage = page;
const startDate = document.getElementById('historyStartDate').value;
const endDate = document.getElementById('historyEndDate').value;
const studentId = document.getElementById('historyStudentId').value;
const res = await apiGet('/api/admin/conduct/history', {
page, page_size: 20,
start_date: startDate,
end_date: endDate,
student_id: studentId
});
if (res && res.success) {
let html = '';
res.data.records.forEach(record => {
const pointsClass = record.points_change > 0 ? 'plus' : 'minus';
html += `<tr>
<td>${formatDateTime(record.created_at)}</td>
<td>${escapeHtml(record.student_name)}</td>
<td class="${pointsClass}">${record.points_change > 0 ? '+' : ''}${record.points_change}</td>
<td>${escapeHtml(record.reason)}</td>
<td>${escapeHtml(record.recorder_name)}</td>`;
<?php if ($role === '班主任' || $role === '班长'): ?>
html += `<td><button class="btn btn-sm btn-danger" onclick="revokeRecord(${record.record_id})">撤销</button></td>`;
<?php endif; ?>
html += `</tr>`;
});
if (res.data.records.length === 0) {
const colSpan = <?php echo ($role === '班主任' || $role === '班长') ? '6' : '5'; ?>;
html = `<tr><td colspan="${colSpan}" style="text-align:center;">暂无记录</td></tr>`;
}
document.getElementById('historyList').innerHTML = html;
totalHistoryPages = res.data.total_pages || 1;
renderHistoryPagination();
}
}
function renderHistoryPagination() {
const container = document.getElementById('historyPagination');
if (!container) return;
if (totalHistoryPages <= 1) {
container.innerHTML = '';
return;
}
let html = '';
for (let i = 1; i <= totalHistoryPages; i++) {
if (i === currentHistoryPage) {
html += `<span class="active">${i}</span>`;
} else {
html += `<a href="#" onclick="loadHistory(${i}); return false;">${i}</a>`;
}
}
container.innerHTML = html;
}
loadStudentsForSelect();
loadHistory();
</script>
<script src="/assets/js/admin.js"></script>
<?php include __DIR__ . '/../includes/footer.php'; ?>

170
frontend/admin/homework.php Normal file
View File

@@ -0,0 +1,170 @@
<?php
/**
* 班级操行分管理系统 - 管理端作业管理
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: MIT License
*
* 版权所有 © Sea Network Technology Studio
*/
require_once __DIR__ . '/../config.php';
if (!isset($_SESSION['user_id']) || $_SESSION['user_type'] !== 'admin') {
header('Location: /index.php');
exit();
}
$page_title = '作业管理';
$role = $_SESSION['role'] ?? '';
if (!in_array($role, ['班主任', '科代表'])) {
header('Location: /admin/dashboard.php');
exit();
}
include __DIR__ . '/../includes/header.php';
?>
<div class="nav">
<a href="/admin/dashboard.php" class="nav-item">首页</a>
<a href="/admin/students.php" class="nav-item">学生管理</a>
<?php if ($role === '班主任' || $role === '班长'): ?>
<a href="/admin/conduct.php" class="nav-item">操行分管理</a>
<?php endif; ?>
<a href="/admin/homework.php" class="nav-item active">作业管理</a>
<?php if ($role === '班主任' || $role === '考勤委员'): ?>
<a href="/admin/attendance.php" class="nav-item">考勤管理</a>
<?php endif; ?>
<?php if ($role === '班主任'): ?>
<a href="/admin/subjects.php" class="nav-item">科目管理</a>
<a href="/admin/admins.php" class="nav-item">管理员管理</a>
<?php endif; ?>
<a href="/admin/history.php" class="nav-item">历史记录</a>
<a href="/admin/password.php" class="nav-item">修改密码</a>
</div>
<div class="container">
<div class="card">
<div class="action-bar">
<?php if ($role === '班主任'): ?>
<button class="btn btn-primary" onclick="showAddAssignmentModal()">发布作业</button>
<?php endif; ?>
</div>
<div id="assignmentsList"></div>
</div>
</div>
<!-- 发布作业模态框 -->
<div id="addAssignmentModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3>发布作业</h3>
<button class="modal-close" onclick="closeModal('addAssignmentModal')">&times;</button>
</div>
<form onsubmit="event.preventDefault(); submitAddAssignment()">
<div class="form-group">
<label>科目</label>
<select id="assignmentSubjectId" required></select>
</div>
<div class="form-group">
<label>作业标题</label>
<input type="text" id="assignmentTitle" required>
</div>
<div class="form-group">
<label>作业描述</label>
<textarea id="assignmentDescription" rows="3"></textarea>
</div>
<div class="form-group">
<label>截止日期</label>
<input type="date" id="assignmentDeadline" required>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary">发布</button>
<button type="button" class="btn" onclick="closeModal('addAssignmentModal')">取消</button>
</div>
</form>
</div>
</div>
<script>
async function loadAssignments() {
const res = await apiGet('/api/admin/homework/assignments');
if (res && res.success) {
let html = '';
for (const assignment of res.data.assignments) {
html += `
<div class="card assignment-card">
<div class="assignment-header">
<div><span class="assignment-title">${escapeHtml(assignment.title)}</span> <span class="assignment-meta">(${escapeHtml(assignment.subject_name)})</span></div>
<div class="assignment-meta">截止: ${assignment.deadline}</div>
</div>
<div class="table-wrapper">
<table class="table">
<thead><tr><th>学生</th><th>状态</th><th>备注</th><th>操作</th></tr></thead>
<tbody id="submissions-${assignment.assignment_id}"><tr><td colspan="4" class="loading">加载中...</td></tr></tbody>
</table>
</div>
</div>
`;
}
if (res.data.assignments.length === 0) {
html = '<div style="text-align:center;padding:40px;">暂无作业</div>';
}
document.getElementById('assignmentsList').innerHTML = html;
for (const assignment of res.data.assignments) {
await loadSubmissions(assignment.assignment_id);
}
}
}
async function loadSubmissions(assignmentId) {
const res = await apiGet(`/api/admin/homework/submissions/${assignmentId}`);
if (res && res.success) {
let html = '';
res.data.submissions.forEach(sub => {
html += `<tr>
<td>${escapeHtml(sub.student_name)}</td>
<td>${getStatusBadge(sub.status, 'homework')}</td>
<td>${escapeHtml(sub.comments || '-')}</td>
<td>
<select id="status-${sub.submission_id}" class="status-select">
<option value="submitted" ${sub.status === 'submitted' ? 'selected' : ''}>已提交</option>
<option value="not_submitted" ${sub.status === 'not_submitted' ? 'selected' : ''}>未提交</option>
<option value="late" ${sub.status === 'late' ? 'selected' : ''}>迟交</option>
</select>
<label><input type="checkbox" id="deduct-${sub.submission_id}"> 同时扣分</label>
<button class="btn btn-sm btn-primary" onclick="updateSubmission(${sub.submission_id})">更新</button>
</td>
</tr>`;
});
document.getElementById(`submissions-${assignmentId}`).innerHTML = html;
}
}
async function updateSubmission(submissionId) {
const status = document.getElementById(`status-${submissionId}`).value;
const applyDeduction = document.getElementById(`deduct-${submissionId}`).checked;
const res = await apiPut('/api/admin/homework/submission', {
submission_id: submissionId,
status: status,
apply_deduction: applyDeduction
});
if (res && res.success) {
showToast('更新成功');
loadAssignments();
} else {
showToast(res?.message || '更新失败', 'error');
}
}
loadAssignments();
</script>
<script src="/assets/js/admin.js"></script>
<?php include __DIR__ . '/../includes/footer.php'; ?>

100
frontend/admin/passwork.php Normal file
View File

@@ -0,0 +1,100 @@
<?php
/**
* 班级操行分管理系统 - 管理端修改密码
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: MIT License
*
* 版权所有 © Sea Network Technology Studio
*/
require_once __DIR__ . '/../config.php';
if (!isset($_SESSION['user_id']) || $_SESSION['user_type'] !== 'admin') {
header('Location: /index.php');
exit();
}
$page_title = '修改密码';
$role = $_SESSION['role'] ?? '';
include __DIR__ . '/../includes/header.php';
?>
<div class="nav">
<a href="/admin/dashboard.php" class="nav-item">首页</a>
<a href="/admin/students.php" class="nav-item">学生管理</a>
<?php if ($role === '班主任' || $role === '班长'): ?>
<a href="/admin/conduct.php" class="nav-item">操行分管理</a>
<?php endif; ?>
<?php if ($role === '班主任' || $role === '科代表'): ?>
<a href="/admin/homework.php" class="nav-item">作业管理</a>
<?php endif; ?>
<?php if ($role === '班主任' || $role === '考勤委员'): ?>
<a href="/admin/attendance.php" class="nav-item">考勤管理</a>
<?php endif; ?>
<?php if ($role === '班主任'): ?>
<a href="/admin/subjects.php" class="nav-item">科目管理</a>
<a href="/admin/admins.php" class="nav-item">管理员管理</a>
<?php endif; ?>
<a href="/admin/history.php" class="nav-item">历史记录</a>
<a href="/admin/password.php" class="nav-item active">修改密码</a>
</div>
<div class="container">
<div class="card">
<div class="card-title">修改密码</div>
<form id="passwordForm">
<div class="form-group">
<label>原密码 <span style="color:red;">*</span></label>
<input type="password" id="oldPassword" required>
</div>
<div class="form-group">
<label>新密码 <span style="color:red;">*</span></label>
<input type="password" id="newPassword" required>
<small>密码长度6-20位需包含字母和数字</small>
</div>
<div class="form-group">
<label>确认新密码 <span style="color:red;">*</span></label>
<input type="password" id="confirmPassword" required>
</div>
<button type="submit" class="btn btn-primary">确认修改</button>
</form>
</div>
</div>
<script>
document.getElementById('passwordForm').addEventListener('submit', async (e) => {
e.preventDefault();
const oldPassword = document.getElementById('oldPassword').value;
const newPassword = document.getElementById('newPassword').value;
const confirmPassword = document.getElementById('confirmPassword').value;
if (newPassword !== confirmPassword) {
showToast('两次输入的新密码不一致', 'error');
return;
}
if (newPassword.length < 6 || newPassword.length > 20) {
showToast('密码长度需为6-20位', 'error');
return;
}
const res = await apiPost('/api/auth/change-password', {
old_password: oldPassword,
new_password: newPassword
});
if (res && res.success) {
showToast('密码修改成功,请重新登录');
setTimeout(() => logout(), 1500);
} else {
showToast(res?.message || '密码修改失败', 'error');
}
});
</script>
<script src="/assets/js/admin.js"></script>
<?php include __DIR__ . '/../includes/footer.php'; ?>

243
frontend/admin/students.php Normal file
View File

@@ -0,0 +1,243 @@
<?php
/**
* 班级操行分管理系统 - 管理端学生管理
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: MIT License
*
* 版权所有 © Sea Network Technology Studio
*/
require_once __DIR__ . '/../config.php';
if (!isset($_SESSION['user_id']) || $_SESSION['user_type'] !== 'admin') {
header('Location: /index.php');
exit();
}
$page_title = '学生管理';
$role = $_SESSION['role'] ?? '';
include __DIR__ . '/../includes/header.php';
?>
<div class="nav">
<a href="/admin/dashboard.php" class="nav-item">首页</a>
<a href="/admin/students.php" class="nav-item active">学生管理</a>
<?php if ($role === '班主任' || $role === '班长'): ?>
<a href="/admin/conduct.php" class="nav-item">操行分管理</a>
<?php endif; ?>
<?php if ($role === '班主任' || $role === '科代表'): ?>
<a href="/admin/homework.php" class="nav-item">作业管理</a>
<?php endif; ?>
<?php if ($role === '班主任' || $role === '考勤委员'): ?>
<a href="/admin/attendance.php" class="nav-item">考勤管理</a>
<?php endif; ?>
<?php if ($role === '班主任'): ?>
<a href="/admin/subjects.php" class="nav-item">科目管理</a>
<a href="/admin/admins.php" class="nav-item">管理员管理</a>
<?php endif; ?>
<a href="/admin/history.php" class="nav-item">历史记录</a>
<a href="/admin/password.php" class="nav-item">修改密码</a>
</div>
<div class="container">
<div class="card">
<div class="action-bar">
<div class="action-buttons">
<?php if ($role === '班主任'): ?>
<button class="btn btn-primary" onclick="showImportModal()">导入学生</button>
<button class="btn btn-success" onclick="showAddStudentModal()">新增学生</button>
<?php endif; ?>
</div>
<div class="search-bar">
<input type="text" id="searchInput" placeholder="搜索姓名/学号">
<button class="btn btn-primary" onclick="loadStudents(1)">搜索</button>
</div>
</div>
<div class="table-wrapper">
<table class="table">
<thead>
<tr>
<th><input type="checkbox" id="selectAll" onclick="toggleSelectAll()"></th>
<th>学号</th>
<th>姓名</th>
<th>操行分</th>
<th>家长手机号</th>
<th>操作</th>
</tr>
</thead>
<tbody id="studentList"></tbody>
</table>
</div>
<div class="pagination" id="pagination"></div>
</div>
</div>
<!-- 导入学生模态框 -->
<div id="importModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3>导入学生</h3>
<button class="modal-close" onclick="closeModal('importModal')">&times;</button>
</div>
<div class="import-area" onclick="document.getElementById('importFile').click()">
<p>点击选择JSON文件</p>
<p class="import-label">或点击此处上传</p>
<input type="file" id="importFile" accept=".json">
<p style="margin-top: 10px; font-size: 12px; color: #999;">
<a href="/assets/uploads/sample_import.json" download style="color: #667eea;">下载示例文件</a>
</p>
</div>
<div id="importPreview" class="preview-table" style="display: none;"></div>
<div class="modal-footer">
<button class="btn btn-primary" onclick="doImport()" id="importBtn" style="display: none;">确认导入</button>
</div>
</div>
</div>
<!-- 新增学生模态框 -->
<div id="addStudentModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3>新增学生</h3>
<button class="modal-close" onclick="closeModal('addStudentModal')">&times;</button>
</div>
<form onsubmit="event.preventDefault(); submitAddStudent()">
<div class="form-group">
<label>学号 <span style="color:red;">*</span></label>
<input type="text" id="studentNo" required placeholder="4-20位字母数字组合">
<small>学号将作为学生登录账号</small>
</div>
<div class="form-group">
<label>姓名 <span style="color:red;">*</span></label>
<input type="text" id="studentName" required>
</div>
<div class="form-group">
<label>家长手机号</label>
<input type="tel" id="parentPhone" placeholder="11位手机号">
<small>填写后将自动创建家长账号密码同学生初始密码123456</small>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary">确认添加</button>
<button type="button" class="btn" onclick="closeModal('addStudentModal')">取消</button>
</div>
</form>
</div>
</div>
<script>
let currentPage = 1;
let totalPages = 1;
async function loadStudents(page = 1) {
currentPage = page;
const search = document.getElementById('searchInput').value;
const res = await apiGet('/api/admin/students', { page, page_size: 20, search });
if (res && res.success) {
let html = '';
res.data.students.forEach(student => {
html += `<tr>
<td><input type="checkbox" class="student-checkbox" data-id="${student.student_id}"></td>
<td>${escapeHtml(student.student_no)}</td>
<td>${escapeHtml(student.name)}</td>
<td>${student.total_points}</td>
<td>${student.parent_phone || '-'}</td>
<td>
<button class="btn btn-sm btn-primary" onclick="showSinglePointsModal(${student.student_id}, '${escapeHtml(student.name)}')">加减分</button>
</td>
</tr>`;
});
if (res.data.students.length === 0) {
html = '<tr><td colspan="6" style="text-align:center;">暂无学生数据</td></tr>';
}
document.getElementById('studentList').innerHTML = html;
totalPages = res.data.total_pages || 1;
renderPagination();
}
}
function renderPagination() {
const container = document.getElementById('pagination');
if (!container) return;
if (totalPages <= 1) {
container.innerHTML = '';
return;
}
let html = '';
for (let i = 1; i <= totalPages; i++) {
if (i === currentPage) {
html += `<span class="active">${i}</span>`;
} else {
html += `<a href="#" onclick="loadStudents(${i}); return false;">${i}</a>`;
}
}
container.innerHTML = html;
}
function showSinglePointsModal(studentId, studentName) {
selectedStudentIds = [studentId];
document.getElementById('selectedStudentsCount').innerHTML = `${studentName} (1人)`;
document.getElementById('pointsChange').value = '';
document.getElementById('pointsReason').value = '';
document.getElementById('batchPointsModal').style.display = 'flex';
}
function toggleSelectAll() {
const selectAll = document.getElementById('selectAll');
if (selectAll) {
document.querySelectorAll('.student-checkbox').forEach(cb => {
cb.checked = selectAll.checked;
});
}
}
// 页面加载
loadStudents();
// 搜索防抖
let searchTimeout;
document.getElementById('searchInput').addEventListener('input', () => {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => loadStudents(1), 500);
});
</script>
<script src="/assets/js/admin.js"></script>
<!-- 批量加减分模态框(共用) -->
<div id="batchPointsModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3>批量加减分</h3>
<button class="modal-close" onclick="closeModal('batchPointsModal')">&times;</button>
</div>
<form onsubmit="event.preventDefault(); submitBatchPoints()">
<div class="form-group">
<label>选中学生</label>
<div id="selectedStudentsCount">0 人</div>
</div>
<div class="form-group">
<label>分数变动</label>
<input type="number" id="pointsChange" required placeholder="正数为加分,负数为扣分">
<small><?php echo $role === '班长' ? '班长单次±5分以内' : '班主任无限制'; ?></small>
</div>
<div class="form-group">
<label>原因</label>
<textarea id="pointsReason" required rows="3" placeholder="请填写加减分原因"></textarea>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary">确认提交</button>
<button type="button" class="btn" onclick="closeModal('batchPointsModal')">取消</button>
</div>
</form>
</div>
</div>
<?php include __DIR__ . '/../includes/footer.php'; ?>

157
frontend/admin/subjects.php Normal file
View File

@@ -0,0 +1,157 @@
<?php
/**
* 班级操行分管理系统 - 管理端科目管理
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: MIT License
*
* 版权所有 © Sea Network Technology Studio
*/
require_once __DIR__ . '/../config.php';
if (!isset($_SESSION['user_id']) || $_SESSION['user_type'] !== 'admin') {
header('Location: /index.php');
exit();
}
$page_title = '科目管理';
$role = $_SESSION['role'] ?? '';
if ($role !== '班主任') {
header('Location: /admin/dashboard.php');
exit();
}
include __DIR__ . '/../includes/header.php';
?>
<div class="nav">
<a href="/admin/dashboard.php" class="nav-item">首页</a>
<a href="/admin/students.php" class="nav-item">学生管理</a>
<a href="/admin/conduct.php" class="nav-item">操行分管理</a>
<a href="/admin/homework.php" class="nav-item">作业管理</a>
<a href="/admin/attendance.php" class="nav-item">考勤管理</a>
<a href="/admin/subjects.php" class="nav-item active">科目管理</a>
<a href="/admin/admins.php" class="nav-item">管理员管理</a>
<a href="/admin/history.php" class="nav-item">历史记录</a>
<a href="/admin/password.php" class="nav-item">修改密码</a>
</div>
<div class="container">
<div class="card">
<div class="action-bar">
<button class="btn btn-primary" onclick="showAddSubjectModal()">添加科目</button>
</div>
<div id="subjectList" class="subject-list"></div>
</div>
</div>
<!-- 添加科目模态框 -->
<div id="addSubjectModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3>添加科目</h3>
<button class="modal-close" onclick="closeModal('addSubjectModal')">&times;</button>
</div>
<form onsubmit="event.preventDefault(); submitAddSubject()">
<div class="form-group">
<label>科目名称</label>
<input type="text" id="subjectName" required placeholder="例如:语文、数学">
</div>
<div class="form-group">
<label>科目代码</label>
<input type="text" id="subjectCode" placeholder="例如CHI、MATH">
<small>可选,用于排序和标识</small>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary">添加</button>
<button type="button" class="btn" onclick="closeModal('addSubjectModal')">取消</button>
</div>
</form>
</div>
</div>
<style>
.subject-list {
display: flex;
flex-wrap: wrap;
gap: 12px;
}
.subject-item {
background: #f8f9fa;
padding: 12px 20px;
border-radius: 8px;
display: flex;
align-items: center;
gap: 15px;
}
.subject-name {
font-weight: 500;
font-size: 16px;
}
.subject-code {
color: #999;
font-size: 12px;
}
.subject-status {
font-size: 11px;
padding: 2px 8px;
border-radius: 12px;
}
.subject-status-active {
background: #c6f6d5;
color: #22543d;
}
.subject-status-inactive {
background: #fed7d7;
color: #742a2a;
}
</style>
<script>
async function loadSubjects() {
const res = await apiGet('/api/subject/list');
if (res && res.success) {
let html = '';
res.data.subjects.forEach(sub => {
html += `
<div class="subject-item">
<span class="subject-name">${escapeHtml(sub.subject_name)}</span>
<span class="subject-code">${escapeHtml(sub.subject_code || '')}</span>
<span class="subject-status ${sub.is_active ? 'subject-status-active' : 'subject-status-inactive'}">
${sub.is_active ? '启用' : '禁用'}
</span>
<button class="btn btn-sm" onclick="toggleSubject(${sub.subject_id}, ${!sub.is_active})">
${sub.is_active ? '禁用' : '启用'}
</button>
</div>
`;
});
if (res.data.subjects.length === 0) {
html = '<p style="text-align:center;padding:40px;">暂无科目,请点击"添加科目"</p>';
}
document.getElementById('subjectList').innerHTML = html;
}
}
async function toggleSubject(subjectId, enable) {
const res = await apiPut(`/api/subject/update/${subjectId}`, {
is_active: enable
});
if (res && res.success) {
showToast(enable ? '科目已启用' : '科目已禁用');
loadSubjects();
} else {
showToast(res?.message || '操作失败', 'error');
}
}
loadSubjects();
</script>
<script src="/assets/js/admin.js"></script>
<?php include __DIR__ . '/../includes/footer.php'; ?>

View File

@@ -0,0 +1,130 @@
/**
* 班级操行分管理系统 - 管理端样式
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: MIT License
*
* 版权所有 © Sea Network Technology Studio
*/
/* 批量操作栏 */
.batch-bar {
background: #f0f4ff;
padding: 12px 16px;
border-radius: 8px;
margin-bottom: 16px;
display: flex;
justify-content: space-between;
align-items: center;
}
.batch-info {
color: #667eea;
font-weight: 500;
}
/* 导入区域 */
.import-area {
border: 2px dashed #ddd;
border-radius: 12px;
padding: 30px;
text-align: center;
margin-bottom: 20px;
transition: border-color 0.3s;
cursor: pointer;
}
.import-area:hover {
border-color: #667eea;
}
.import-area input {
display: none;
}
.import-label {
color: #667eea;
text-decoration: underline;
cursor: pointer;
}
/* 预览表格 */
.preview-table {
max-height: 300px;
overflow-y: auto;
margin-top: 16px;
}
/* 筛选栏 */
.filter-bar {
background: #f8f9fa;
padding: 16px;
border-radius: 8px;
margin-bottom: 20px;
display: flex;
flex-wrap: wrap;
gap: 16px;
align-items: flex-end;
}
.filter-group {
flex: 1;
min-width: 150px;
}
.filter-group label {
display: block;
margin-bottom: 4px;
font-size: 12px;
color: #666;
}
.filter-group input,
.filter-group select {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
/* 作业卡片 */
.assignment-card {
margin-bottom: 20px;
}
.assignment-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
flex-wrap: wrap;
gap: 10px;
}
.assignment-title {
font-size: 16px;
font-weight: bold;
color: #333;
}
.assignment-meta {
color: #999;
font-size: 12px;
}
/* 状态选择器 */
.status-select {
padding: 4px 8px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 12px;
}
/* 复选框 */
.student-checkbox {
width: 18px;
height: 18px;
cursor: pointer;
}

View File

@@ -0,0 +1,666 @@
/**
* 班级操行分管理系统 - 全局样式
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: MIT License
*
* 版权所有 © Sea Network Technology Studio
*/
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: #f5f7fb;
min-height: 100vh;
font-size: 14px;
color: #333;
}
/* ========== 登录页面 ========== */
.login-container {
background: white;
border-radius: 16px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
padding: 40px;
width: 400px;
max-width: 90%;
margin: 100px auto;
}
.login-header {
text-align: center;
margin-bottom: 30px;
}
.login-header h1 {
font-size: 24px;
color: #333;
margin-bottom: 8px;
}
.login-header p {
color: #666;
font-size: 14px;
}
.login-form .form-group {
margin-bottom: 20px;
}
.login-form label {
display: block;
margin-bottom: 6px;
color: #555;
font-weight: 500;
}
.login-form input {
width: 100%;
padding: 12px;
border: 1px solid #ddd;
border-radius: 8px;
font-size: 14px;
transition: border-color 0.3s;
}
.login-form input:focus {
outline: none;
border-color: #667eea;
}
.btn-login {
width: 100%;
padding: 12px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: bold;
cursor: pointer;
transition: opacity 0.3s;
}
.btn-login:hover {
opacity: 0.9;
}
.error-msg {
background: #fee;
color: #c33;
padding: 10px;
border-radius: 8px;
margin-top: 15px;
text-align: center;
font-size: 13px;
}
.login-footer {
text-align: center;
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid #eee;
color: #999;
font-size: 12px;
}
/* ========== 公共头部 ========== */
.header {
background: white;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
padding: 12px 24px;
display: flex;
justify-content: space-between;
align-items: center;
position: sticky;
top: 0;
z-index: 100;
}
.header h1 {
font-size: 18px;
color: #333;
}
.header-info {
display: flex;
align-items: center;
gap: 16px;
}
.user-name {
color: #666;
font-weight: 500;
}
.user-role {
background: #667eea;
color: white;
padding: 2px 8px;
border-radius: 20px;
font-size: 11px;
}
.btn-logout {
background: #e53e3e;
color: white;
border: none;
padding: 6px 16px;
border-radius: 6px;
cursor: pointer;
font-size: 13px;
transition: background 0.3s;
}
.btn-logout:hover {
background: #c53030;
}
/* ========== 导航菜单 ========== */
.nav {
background: white;
padding: 0 24px;
border-bottom: 1px solid #eee;
display: flex;
gap: 4px;
overflow-x: auto;
}
.nav-item {
padding: 12px 20px;
background: none;
border: none;
color: #666;
cursor: pointer;
font-size: 14px;
transition: all 0.3s;
border-bottom: 2px solid transparent;
text-decoration: none;
display: inline-block;
}
.nav-item:hover {
color: #667eea;
}
.nav-item.active {
color: #667eea;
border-bottom-color: #667eea;
}
/* ========== 容器 ========== */
.container {
max-width: 1200px;
margin: 24px auto;
padding: 0 24px;
}
/* ========== 卡片 ========== */
.card {
background: white;
border-radius: 12px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.card-title {
font-size: 18px;
font-weight: bold;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 2px solid #667eea;
color: #333;
}
/* ========== 统计卡片网格 ========== */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 16px;
margin-bottom: 24px;
}
.stat-card {
background: white;
border-radius: 12px;
padding: 20px;
text-align: center;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.stat-value {
font-size: 32px;
font-weight: bold;
color: #667eea;
margin: 10px 0;
}
.stat-label {
color: #666;
font-size: 13px;
}
/* ========== 表格 ========== */
.table-wrapper {
overflow-x: auto;
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #eee;
}
th {
background: #f8f9fa;
font-weight: 600;
color: #555;
}
tr:hover {
background: #f8f9fa;
}
/* ========== 状态标签 ========== */
.status-badge {
display: inline-block;
padding: 4px 10px;
border-radius: 20px;
font-size: 12px;
font-weight: 500;
}
.status-submitted {
background: #c6f6d5;
color: #22543d;
}
.status-not_submitted {
background: #fed7d7;
color: #742a2a;
}
.status-late {
background: #feebc8;
color: #7c2d12;
}
.status-present {
background: #c6f6d5;
color: #22543d;
}
.status-absent {
background: #fed7d7;
color: #742a2a;
}
.status-leave {
background: #e9d8fd;
color: #553c9a;
}
/* ========== 按钮 ========== */
.btn {
padding: 8px 16px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 13px;
transition: all 0.3s;
}
.btn-primary {
background: #667eea;
color: white;
}
.btn-primary:hover {
background: #5a67d8;
}
.btn-danger {
background: #e53e3e;
color: white;
}
.btn-danger:hover {
background: #c53030;
}
.btn-success {
background: #38a169;
color: white;
}
.btn-success:hover {
background: #2f855a;
}
.btn-sm {
padding: 4px 10px;
font-size: 12px;
}
/* ========== 模态框 ========== */
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal-content {
background: white;
border-radius: 12px;
padding: 24px;
width: 500px;
max-width: 90%;
max-height: 80vh;
overflow-y: auto;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 12px;
border-bottom: 1px solid #eee;
}
.modal-header h3 {
font-size: 18px;
color: #333;
}
.modal-close {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #999;
}
.modal-footer {
margin-top: 20px;
padding-top: 16px;
border-top: 1px solid #eee;
display: flex;
justify-content: flex-end;
gap: 12px;
}
/* ========== 表单 ========== */
.form-group {
margin-bottom: 16px;
}
.form-group label {
display: block;
margin-bottom: 6px;
font-weight: 500;
color: #555;
}
.form-group input,
.form-group select,
.form-group textarea {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
}
.form-group input:focus,
.form-group select:focus,
.form-group textarea:focus {
outline: none;
border-color: #667eea;
}
.form-group small {
display: block;
color: #999;
font-size: 12px;
margin-top: 4px;
}
/* ========== 复选框组 ========== */
.checkbox-group {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
}
.checkbox-group input {
width: auto;
}
/* ========== 操作栏 ========== */
.action-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
flex-wrap: wrap;
gap: 12px;
}
.action-buttons {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.search-bar {
display: flex;
gap: 10px;
}
.search-bar input {
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 6px;
width: 200px;
}
/* ========== 分页 ========== */
.pagination {
display: flex;
justify-content: center;
gap: 8px;
margin-top: 20px;
}
.pagination a, .pagination span {
padding: 6px 12px;
border: 1px solid #ddd;
border-radius: 4px;
text-decoration: none;
color: #666;
cursor: pointer;
}
.pagination .active {
background: #667eea;
color: white;
border-color: #667eea;
}
/* ========== 提示消息 ========== */
.toast {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
padding: 12px 24px;
border-radius: 8px;
color: white;
font-size: 14px;
z-index: 1100;
animation: fadeInUp 0.3s ease;
}
.toast-success {
background: #38a169;
}
.toast-error {
background: #e53e3e;
}
.toast-warning {
background: #ed8936;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateX(-50%) translateY(20px);
}
to {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
}
/* ========== 加载动画 ========== */
.loading {
display: inline-block;
width: 20px;
height: 20px;
border: 2px solid #ddd;
border-top-color: #667eea;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* ========== 底部 ========== */
.footer {
text-align: center;
padding: 20px;
color: #999;
font-size: 12px;
}
/* ========== 记录项 ========== */
.record-item {
padding: 12px 0;
border-bottom: 1px solid #eee;
display: flex;
justify-content: space-between;
align-items: center;
}
.record-points {
font-weight: bold;
}
.record-points.plus {
color: #38a169;
}
.record-points.minus {
color: #e53e3e;
}
.record-reason {
flex: 1;
margin: 0 15px;
color: #555;
}
.record-time {
font-size: 12px;
color: #999;
}
.view-more {
text-align: center;
margin-top: 15px;
}
.view-more a {
color: #667eea;
text-decoration: none;
}
.conduct-score {
text-align: center;
padding: 20px;
}
.score-number {
font-size: 64px;
font-weight: bold;
color: #667eea;
}
/* ========== 响应式 ========== */
@media (max-width: 768px) {
.container {
padding: 0 16px;
}
.stats-grid {
grid-template-columns: repeat(2, 1fr);
gap: 12px;
}
th, td {
padding: 8px;
font-size: 12px;
}
.card {
padding: 16px;
}
.nav {
padding: 0 16px;
}
.nav-item {
padding: 10px 14px;
font-size: 13px;
}
.action-bar {
flex-direction: column;
align-items: stretch;
}
.search-bar {
width: 100%;
}
.search-bar input {
flex: 1;
}
}

385
frontend/assets/js/admin.js Normal file
View File

@@ -0,0 +1,385 @@
/**
* 班级操行分管理系统 - 管理端JS
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: MIT License
*
* 版权所有 © Sea Network Technology Studio
*/
// 全局变量
let selectedStudentIds = [];
let currentPage = 1;
let totalPages = 1;
let currentHistoryPage = 1;
// 显示批量加减分模态框
function showBatchPointsModal() {
selectedStudentIds = [];
document.querySelectorAll('.student-checkbox:checked').forEach(cb => {
selectedStudentIds.push(parseInt(cb.dataset.id));
});
if (selectedStudentIds.length === 0) {
showToast('请先选择学生', 'warning');
return;
}
document.getElementById('selectedStudentsCount').innerHTML = `${selectedStudentIds.length}`;
document.getElementById('pointsChange').value = '';
document.getElementById('pointsReason').value = '';
document.getElementById('batchPointsModal').style.display = 'flex';
}
// 提交批量加减分
async function submitBatchPoints() {
const pointsChange = parseInt(document.getElementById('pointsChange').value);
const reason = document.getElementById('pointsReason').value;
if (isNaN(pointsChange) || pointsChange === 0) {
showToast('分值不能为0', 'error');
return;
}
if (!reason.trim()) {
showToast('请填写原因', 'error');
return;
}
const res = await apiPost('/api/admin/conduct/add', {
student_ids: selectedStudentIds,
points_change: pointsChange,
reason: reason
});
if (res && res.success) {
showToast(`操作成功: ${res.data.success_count} 人成功`);
closeModal('batchPointsModal');
loadStudents();
if (typeof loadConductStudents === 'function') loadConductStudents();
} else {
showToast(res?.message || '操作失败', 'error');
}
}
// 显示导入模态框
function showImportModal() {
document.getElementById('importModal').style.display = 'flex';
document.getElementById('importPreview').style.display = 'none';
document.getElementById('importPreview').innerHTML = '';
document.getElementById('importBtn').style.display = 'none';
document.getElementById('importFile').value = '';
}
// 预览导入文件
function previewImportFile() {
const file = document.getElementById('importFile').files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function(e) {
try {
const data = JSON.parse(e.target.result);
const students = data.students || [];
let html = '<h4>预览数据</h4><div class="table-wrapper"><table><thead><tr>';
html += '<th>学号</th><th>姓名</th><th>家长手机号</th><th>初始密码</th>';
html += '</tr></thead><tbody>';
students.forEach(s => {
html += `<tr>
<td>${escapeHtml(s.student_no || '')}</td>
<td>${escapeHtml(s.name || '')}</td>
<td>${escapeHtml(s.parent_phone || '')}</td>
<td>${escapeHtml(s.password || '123456')}</td>
</tr>`;
});
html += `</tbody></table></div><p>共 ${students.length} 条记录初始操行分默认为60分</p>`;
document.getElementById('importPreview').innerHTML = html;
document.getElementById('importPreview').style.display = 'block';
document.getElementById('importBtn').style.display = 'inline-block';
} catch (error) {
showToast('JSON格式错误', 'error');
}
};
reader.readAsText(file);
}
// 执行导入
async function doImport() {
const file = document.getElementById('importFile').files[0];
if (!file) {
showToast('请选择文件', 'warning');
return;
}
const formData = new FormData();
formData.append('file', file);
const token = getToken();
const response = await fetch(`${API_BASE_URL}/api/admin/students/import`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`
},
body: formData
});
const result = await response.json();
if (result.success) {
showToast(result.message);
closeModal('importModal');
loadStudents();
} else {
showToast(result.message, 'error');
}
}
// 显示新增学生模态框
function showAddStudentModal() {
document.getElementById('addStudentModal').style.display = 'flex';
document.getElementById('addStudentForm').reset();
}
// 提交新增学生
async function submitAddStudent() {
const studentNo = document.getElementById('studentNo').value.trim();
const name = document.getElementById('studentName').value.trim();
const parentPhone = document.getElementById('parentPhone').value.trim();
if (!studentNo || !name) {
showToast('请填写学号和姓名', 'warning');
return;
}
const res = await apiPost('/api/admin/students', {
student_no: studentNo,
name: name,
parent_phone: parentPhone
});
if (res && res.success) {
showToast('学生添加成功');
closeModal('addStudentModal');
loadStudents();
} else {
showToast(res?.message || '添加失败', 'error');
}
}
// 显示添加作业模态框
function showAddAssignmentModal() {
document.getElementById('addAssignmentModal').style.display = 'flex';
loadSubjectsForSelect();
}
// 加载科目下拉框
async function loadSubjectsForSelect() {
const res = await apiGet('/api/subject/list');
if (res && res.success) {
let html = '<option value="">请选择科目</option>';
res.data.subjects.forEach(s => {
if (s.is_active) {
html += `<option value="${s.subject_id}">${s.subject_name}</option>`;
}
});
document.getElementById('assignmentSubjectId').innerHTML = html;
}
}
// 提交添加作业
async function submitAddAssignment() {
const subjectId = document.getElementById('assignmentSubjectId').value;
const title = document.getElementById('assignmentTitle').value.trim();
const description = document.getElementById('assignmentDescription').value;
const deadline = document.getElementById('assignmentDeadline').value;
if (!subjectId || !title || !deadline) {
showToast('请填写完整信息', 'warning');
return;
}
const res = await apiPost('/api/admin/homework/assignment', {
subject_id: parseInt(subjectId),
title: title,
description: description,
deadline: deadline
});
if (res && res.success) {
showToast('作业发布成功');
closeModal('addAssignmentModal');
loadAssignments();
} else {
showToast(res?.message || '发布失败', 'error');
}
}
// 显示添加考勤模态框
async function showAddAttendanceModal() {
await loadStudentsForSelect();
document.getElementById('addAttendanceModal').style.display = 'flex';
document.getElementById('attendanceDate').value = new Date().toISOString().split('T')[0];
}
// 加载学生下拉框
async function loadStudentsForSelect() {
const res = await apiGet('/api/admin/students');
if (res && res.success) {
let html = '<option value="">请选择学生</option>';
res.data.students.forEach(s => {
html += `<option value="${s.student_id}">${s.student_no} - ${s.name}</option>`;
});
document.getElementById('attendanceStudentId').innerHTML = html;
}
}
// 提交添加考勤
async function submitAddAttendance() {
const studentId = document.getElementById('attendanceStudentId').value;
const date = document.getElementById('attendanceDate').value;
const status = document.getElementById('attendanceStatus').value;
const reason = document.getElementById('attendanceReason').value;
const applyDeduction = document.getElementById('attendanceDeduct').checked;
if (!studentId || !date || !status) {
showToast('请填写完整信息', 'warning');
return;
}
const res = await apiPost('/api/admin/attendance', {
student_id: parseInt(studentId),
date: date,
status: status,
reason: reason,
apply_deduction: applyDeduction
});
if (res && res.success) {
showToast('考勤记录添加成功');
closeModal('addAttendanceModal');
loadAttendanceRecords();
} else {
showToast(res?.message || '添加失败', 'error');
}
}
// 显示添加管理员模态框
function showAddAdminModal() {
document.getElementById('addAdminModal').style.display = 'flex';
document.getElementById('addAdminForm').reset();
}
// 提交添加管理员
async function submitAddAdmin() {
const username = document.getElementById('adminUsername').value.trim();
const realName = document.getElementById('adminRealName').value.trim();
const password = document.getElementById('adminPassword').value;
const roleType = document.getElementById('adminRole').value;
if (!username || !realName || !roleType) {
showToast('请填写完整信息', 'warning');
return;
}
const res = await apiPost('/api/admin/add', {
username: username,
real_name: realName,
password: password || undefined,
role_type: roleType
});
if (res && res.success) {
let msg = `管理员 ${res.data.username} 添加成功`;
if (res.data.password) msg += `,密码: ${res.data.password}`;
showToast(msg);
closeModal('addAdminModal');
loadAdmins();
} else {
showToast(res?.message || '添加失败', 'error');
}
}
// 显示添加科目模态框
function showAddSubjectModal() {
document.getElementById('addSubjectModal').style.display = 'flex';
document.getElementById('addSubjectForm').reset();
}
// 提交添加科目
async function submitAddSubject() {
const subjectName = document.getElementById('subjectName').value.trim();
const subjectCode = document.getElementById('subjectCode').value.trim();
if (!subjectName) {
showToast('请填写科目名称', 'warning');
return;
}
const res = await apiPost('/api/subject/create', {
subject_name: subjectName,
subject_code: subjectCode
});
if (res && res.success) {
showToast('科目添加成功');
closeModal('addSubjectModal');
loadSubjects();
} else {
showToast(res?.message || '添加失败', 'error');
}
}
// 撤销扣分记录
async function revokeRecord(recordId) {
if (!confirm('确定要撤销这条扣分记录吗?')) return;
const res = await apiPost('/api/admin/conduct/revoke', { record_id: recordId });
if (res && res.success) {
showToast('撤销成功');
loadHistory(currentHistoryPage);
} else {
showToast(res?.message || '撤销失败', 'error');
}
}
// 关闭模态框
function closeModal(modalId) {
const modal = document.getElementById(modalId);
if (modal) {
modal.style.display = 'none';
}
}
// HTML转义
function escapeHtml(str) {
if (!str) return '';
return str.replace(/[&<>]/g, function(m) {
if (m === '&') return '&amp;';
if (m === '<') return '&lt;';
if (m === '>') return '&gt;';
return m;
});
}
// 全选功能
function toggleSelectAll() {
const selectAll = document.getElementById('selectAll');
if (selectAll) {
document.querySelectorAll('.student-checkbox').forEach(cb => {
cb.checked = selectAll.checked;
});
}
}
// 绑定文件选择事件
document.addEventListener('DOMContentLoaded', () => {
const fileInput = document.getElementById('importFile');
if (fileInput) {
fileInput.addEventListener('change', previewImportFile);
}
});

View File

@@ -0,0 +1,242 @@
/**
* 班级操行分管理系统 - 公共JS
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: MIT License
*
* 版权所有 © Sea Network Technology Studio
*/
// API基础地址
const API_BASE_URL = window.API_BASE_URL || 'http://localhost:8000';
const JWT_STORAGE_KEY = 'class_system_token';
const USER_STORAGE_KEY = 'class_system_user';
// 获取Token
function getToken() {
return localStorage.getItem(JWT_STORAGE_KEY);
}
// 获取用户信息
function getUserInfo() {
const userStr = localStorage.getItem(USER_STORAGE_KEY);
if (!userStr) return null;
try {
return JSON.parse(userStr);
} catch {
return null;
}
}
// 保存用户信息
function setUserInfo(user) {
localStorage.setItem(USER_STORAGE_KEY, JSON.stringify(user));
}
// 清除登录信息
function clearAuth() {
localStorage.removeItem(JWT_STORAGE_KEY);
localStorage.removeItem(USER_STORAGE_KEY);
}
// 检查登录状态
function checkAuth() {
const token = getToken();
if (!token) {
window.location.href = '/index.php';
return false;
}
return true;
}
// API请求封装
async function apiRequest(url, options = {}) {
const token = getToken();
const headers = {
'Content-Type': 'application/json',
...options.headers
};
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
const config = {
...options,
headers
};
try {
const response = await fetch(`${API_BASE_URL}${url}`, config);
const data = await response.json();
if (response.status === 401) {
clearAuth();
window.location.href = '/index.php';
return null;
}
return data;
} catch (error) {
console.error('API请求错误:', error);
showToast('网络错误,请稍后重试', 'error');
return null;
}
}
// GET请求
async function apiGet(url, params = {}) {
const queryString = new URLSearchParams(params).toString();
const fullUrl = queryString ? `${url}?${queryString}` : url;
return apiRequest(fullUrl, { method: 'GET' });
}
// POST请求
async function apiPost(url, data = {}) {
return apiRequest(url, {
method: 'POST',
body: JSON.stringify(data)
});
}
// PUT请求
async function apiPut(url, data = {}) {
return apiRequest(url, {
method: 'PUT',
body: JSON.stringify(data)
});
}
// DELETE请求
async function apiDelete(url) {
return apiRequest(url, { method: 'DELETE' });
}
// 显示提示消息
function showToast(message, type = 'success') {
const toast = document.createElement('div');
toast.className = `toast toast-${type}`;
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => {
toast.remove();
}, 3000);
}
// 格式化日期
function formatDate(dateStr) {
if (!dateStr) return '-';
const date = new Date(dateStr);
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
}
// 格式化日期时间
function formatDateTime(dateStr) {
if (!dateStr) return '-';
const date = new Date(dateStr);
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`;
}
// 获取状态标签HTML
function getStatusBadge(status, type = 'homework') {
const statusMap = {
homework: {
'submitted': '已提交',
'not_submitted': '未提交',
'late': '迟交'
},
attendance: {
'present': '出勤',
'absent': '缺勤',
'late': '迟到',
'leave': '请假'
}
};
const texts = statusMap[type] || statusMap.homework;
const text = texts[status] || status;
let className = 'status-badge ';
switch (status) {
case 'submitted':
case 'present':
className += 'status-submitted';
break;
case 'not_submitted':
case 'absent':
className += 'status-not_submitted';
break;
case 'late':
className += 'status-late';
break;
case 'leave':
className += 'status-leave';
break;
default:
className += 'status-not_submitted';
}
return `<span class="${className}">${text}</span>`;
}
// 退出登录
async function logout() {
await apiPost('/api/auth/logout');
clearAuth();
window.location.href = '/index.php';
}
// 加载用户信息
function loadUserInfo() {
const user = getUserInfo();
const userNameSpan = document.getElementById('userName');
if (userNameSpan && user) {
userNameSpan.textContent = user.real_name || user.username;
}
}
// 检查是否需要修改密码
function checkNeedChangePassword() {
const user = getUserInfo();
if (user && user.need_change_password) {
const newPassword = prompt('首次登录请设置新密码6-20位需包含字母和数字');
if (newPassword) {
changePassword(newPassword);
}
}
}
// 修改密码
async function changePassword(newPassword) {
const res = await apiPost('/api/auth/change-password', {
old_password: newPassword,
new_password: newPassword
});
if (res && res.success) {
showToast('密码修改成功,请重新登录');
setTimeout(() => logout(), 1500);
} else {
showToast(res?.message || '密码修改失败', 'error');
checkNeedChangePassword();
}
}
// 页面加载时初始化
document.addEventListener('DOMContentLoaded', () => {
loadUserInfo();
const logoutBtn = document.getElementById('logoutBtn');
if (logoutBtn) {
logoutBtn.addEventListener('click', logout);
}
// 学生端检查强制修改密码
if (window.location.pathname.includes('/student/')) {
checkNeedChangePassword();
}
});

View File

@@ -0,0 +1,13 @@
/**
* 班级操行分管理系统 - 家长端JS
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: MIT License
*
* 版权所有 © Sea Network Technology Studio
*/
// 家长端专用功能
console.log('家长端已加载');

View File

@@ -0,0 +1,13 @@
/**
* 班级操行分管理系统 - 学生端JS
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: MIT License
*
* 版权所有 © Sea Network Technology Studio
*/
// 学生端专用功能
console.log('学生端已加载');

View File

@@ -0,0 +1,44 @@
{
"_comment1": "================================================",
"_comment2": "班级操行分管理系统 - 学生批量导入模板",
"_comment3": "开发者: Canglan | 版权: Sea Network Technology Studio",
"_comment4": "================================================",
"_comment5": "字段说明:",
"_comment6": " student_no - 必填,学生学号,唯一标识",
"_comment7": " name - 必填,学生姓名",
"_comment8": " parent_phone - 可选家长手机号11位手机号",
"_comment9": " password - 可选,初始密码,不填则默认 123456",
"_comment10": "================================================",
"_comment11": "导入规则:",
"_comment12": " 1. 学生操行分初始值 = 60分",
"_comment13": " 2. 学生账号 = 学号,密码 = 指定的password或123456",
"_comment14": " 3. 家长账号 = 手机号若parent_phone有值密码 = 指定的password或123456",
"_comment15": " 4. 家长姓名默认显示为 '学生姓名家长'",
"_comment16": "================================================",
"students": [
{
"student_no": "20240001",
"name": "张三",
"parent_phone": "13800138001",
"password": "123456"
},
{
"student_no": "20240002",
"name": "李四",
"parent_phone": "13800138002",
"password": "123456"
},
{
"student_no": "20240003",
"name": "王五",
"parent_phone": "",
"password": ""
},
{
"student_no": "20240004",
"name": "赵六",
"parent_phone": "13800138004",
"password": ""
}
]
}

53
frontend/config.php Normal file
View File

@@ -0,0 +1,53 @@
<?php
/**
* 班级操行分管理系统 - 前端配置
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: MIT License
*
* 版权所有 © Sea Network Technology Studio
*/
// 加载环境变量
$envFile = __DIR__ . '/.env';
if (file_exists($envFile)) {
$lines = file($envFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
if (strpos(trim($line), '#') === 0) {
continue;
}
if (strpos($line, '=') !== false) {
list($key, $value) = explode('=', $line, 2);
putenv(trim($key) . '=' . trim($value));
}
}
}
// 定义常量
define('API_BASE_URL', getenv('API_BASE_URL') ?: 'http://localhost:8000');
define('API_TIMEOUT', (int)(getenv('API_TIMEOUT') ?: 30));
define('JWT_STORAGE_KEY', getenv('JWT_STORAGE_KEY') ?: 'class_system_token');
define('USER_STORAGE_KEY', getenv('USER_STORAGE_KEY') ?: 'class_system_user');
define('SITE_NAME', getenv('SITE_NAME') ?: '班级操行分管理系统');
define('SESSION_TIMEOUT', (int)(getenv('SESSION_TIMEOUT') ?: 30));
// 会话配置
ini_set('session.cookie_httponly', 1);
ini_set('session.use_only_cookies', 1);
ini_set('session.cookie_secure', 1);
session_start();
// 时区设置
date_default_timezone_set('Asia/Shanghai');
// 错误报告(生产环境关闭)
if (getenv('APP_ENV') === 'production') {
error_reporting(0);
ini_set('display_errors', 0);
} else {
error_reporting(E_ALL);
ini_set('display_errors', 1);
}
?>

View File

@@ -0,0 +1,17 @@
<?php
/**
* 班级操行分管理系统 - 公共底部
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: MIT License
*
* 版权所有 © Sea Network Technology Studio
*/
?>
<div class="footer">
<p>&copy; <?php echo date('Y'); ?> Sea Network Technology Studio</p>
</div>
</body>
</html>

View File

@@ -0,0 +1,49 @@
<?php
/**
* 班级操行分管理系统 - 公共头部
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: MIT License
*
* 版权所有 © Sea Network Technology Studio
*/
if (!isset($_SESSION)) {
session_start();
}
$current_page = basename($_SERVER['PHP_SELF'], '.php');
$user_type = $_SESSION['user_type'] ?? '';
$role = $_SESSION['role'] ?? '';
$page_title = $page_title ?? '首页';
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title><?php echo SITE_NAME; ?> - <?php echo $page_title; ?></title>
<link rel="stylesheet" href="/assets/css/style.css">
<?php if ($user_type === 'admin'): ?>
<link rel="stylesheet" href="/assets/css/admin.css">
<?php endif; ?>
</head>
<body>
<div class="header">
<h1><?php echo SITE_NAME; ?></h1>
<div class="header-info">
<span class="user-name" id="userName"><?php echo htmlspecialchars($_SESSION['real_name'] ?? ''); ?></span>
<?php if ($role): ?>
<span class="user-role">(<?php echo htmlspecialchars($role); ?>)</span>
<?php endif; ?>
<button class="btn-logout" id="logoutBtn">退出登录</button>
</div>
</div>
<script>
const API_BASE_URL = '<?php echo API_BASE_URL; ?>';
const JWT_STORAGE_KEY = '<?php echo JWT_STORAGE_KEY; ?>';
const USER_STORAGE_KEY = '<?php echo USER_STORAGE_KEY; ?>';
</script>
<script src="/assets/js/common.js"></script>

112
frontend/index.php Normal file
View File

@@ -0,0 +1,112 @@
<?php
/**
* 班级操行分管理系统 - 登录入口
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: MIT License
*
* 版权所有 © Sea Network Technology Studio
*/
require_once __DIR__ . '/config.php';
// 如果已登录,跳转到对应页面
if (isset($_SESSION['user_id']) && isset($_SESSION['user_type'])) {
$redirect = [
'student' => '/student/dashboard.php',
'parent' => '/parent/dashboard.php',
'admin' => '/admin/dashboard.php'
];
$target = $redirect[$_SESSION['user_type']] ?? '/index.php';
header("Location: $target");
exit();
}
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title><?php echo SITE_NAME; ?> - 登录</title>
<link rel="stylesheet" href="/assets/css/style.css">
</head>
<body>
<div class="login-container">
<div class="login-header">
<h1><?php echo SITE_NAME; ?></h1>
<p>学生 / 家长 / 管理端 统一登录</p>
</div>
<form id="loginForm" class="login-form">
<div class="form-group">
<label>用户名</label>
<input type="text" id="username" name="username" required autocomplete="off" placeholder="请输入用户名">
</div>
<div class="form-group">
<label>密码</label>
<input type="password" id="password" name="password" required placeholder="请输入密码">
</div>
<button type="submit" class="btn-login">登 录</button>
<div id="errorMsg" class="error-msg" style="display: none;"></div>
</form>
<div class="login-footer">
<p>© Sea Network Technology Studio</p>
</div>
</div>
<script>
const API_BASE_URL = '<?php echo API_BASE_URL; ?>';
const JWT_STORAGE_KEY = '<?php echo JWT_STORAGE_KEY; ?>';
const USER_STORAGE_KEY = '<?php echo USER_STORAGE_KEY; ?>';
document.getElementById('loginForm').addEventListener('submit', async (e) => {
e.preventDefault();
const username = document.getElementById('username').value.trim();
const password = document.getElementById('password').value;
const errorMsg = document.getElementById('errorMsg');
if (!username || !password) {
showError('请填写用户名和密码');
return;
}
try {
const response = await fetch(`${API_BASE_URL}/api/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});
const data = await response.json();
if (data.success && data.data) {
// 存储Token和用户信息
localStorage.setItem(JWT_STORAGE_KEY, data.data.token);
localStorage.setItem(USER_STORAGE_KEY, JSON.stringify(data.data));
// 跳转
window.location.href = data.data.redirect;
} else {
showError(data.message || '登录失败');
}
} catch (error) {
console.error('登录错误:', error);
showError('网络错误,请检查后端服务是否启动');
}
});
function showError(msg) {
const errorMsg = document.getElementById('errorMsg');
errorMsg.textContent = msg;
errorMsg.style.display = 'block';
setTimeout(() => {
errorMsg.style.display = 'none';
}, 3000);
}
</script>
</body>
</html>

View File

@@ -0,0 +1,83 @@
<?php
/**
* 班级操行分管理系统 - 家长端考勤记录
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: MIT License
*
* 版权所有 © Sea Network Technology Studio
*/
require_once __DIR__ . '/../config.php';
if (!isset($_SESSION['user_id']) || $_SESSION['user_type'] !== 'parent') {
header('Location: /index.php');
exit();
}
$page_title = '考勤记录';
include __DIR__ . '/../includes/header.php';
?>
<div class="nav">
<a href="/parent/dashboard.php" class="nav-item">首页</a>
<a href="/parent/homework.php" class="nav-item">作业情况</a>
<a href="/parent/attendance.php" class="nav-item active">考勤记录</a>
</div>
<div class="container">
<div class="stats-grid">
<div class="stat-card"><div class="stat-label">出勤</div><div class="stat-value" id="attPresent">0</div></div>
<div class="stat-card"><div class="stat-label">缺勤</div><div class="stat-value" id="attAbsent">0</div></div>
<div class="stat-card"><div class="stat-label">迟到</div><div class="stat-value" id="attLate">0</div></div>
<div class="stat-card"><div class="stat-label">请假</div><div class="stat-value" id="attLeave">0</div></div>
</div>
<div class="card">
<div class="card-title">考勤记录明细</div>
<div class="table-wrapper">
<table class="table">
<thead><tr><th>日期</th><th>状态</th><th>原因</th></tr></thead>
<tbody id="attendanceList"></tbody>
</table>
</div>
</div>
</div>
<script>
async function loadAttendance() {
const res = await apiGet('/api/parent/child/attendance');
if (res && res.success) {
let present = 0, absent = 0, late = 0, leave = 0;
let html = '';
res.data.records.forEach(record => {
html += `<tr>
<td>${record.date}</td>
<td>${getStatusBadge(record.status, 'attendance')}</td>
<td>${escapeHtml(record.reason || '-')}</td>
</tr>`;
switch(record.status) {
case 'present': present++; break;
case 'absent': absent++; break;
case 'late': late++; break;
case 'leave': leave++; break;
}
});
document.getElementById('attPresent').textContent = present;
document.getElementById('attAbsent').textContent = absent;
document.getElementById('attLate').textContent = late;
document.getElementById('attLeave').textContent = leave;
if (res.data.records.length === 0) {
html = '<tr><td colspan="3" style="text-align:center;">暂无记录</td></tr>';
}
document.getElementById('attendanceList').innerHTML = html;
}
}
loadAttendance();
</script>
<script src="/assets/js/parent.js"></script>
<?php include __DIR__ . '/../includes/footer.php'; ?>

View File

@@ -0,0 +1,272 @@
<?php
/**
* 班级操行分管理系统 - 家长端主页
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: MIT License
*
* 版权所有 © Sea Network Technology Studio
*/
require_once __DIR__ . '/../config.php';
// 检查登录状态
if (!isset($_SESSION['user_id']) || $_SESSION['user_type'] !== 'parent') {
header('Location: /index.php');
exit();
}
$student_id = $_SESSION['student_id'];
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?php echo SITE_NAME; ?> - 家长端</title>
<link rel="stylesheet" href="/assets/css/style.css">
<style>
.child-info {
text-align: center;
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 12px;
color: white;
margin-bottom: 20px;
}
.child-name {
font-size: 24px;
font-weight: bold;
margin-bottom: 8px;
}
.child-no {
font-size: 14px;
opacity: 0.9;
}
.conduct-score {
text-align: center;
padding: 30px;
}
.score-number {
font-size: 72px;
font-weight: bold;
color: #667eea;
}
</style>
</head>
<body>
<div class="header">
<h1><?php echo SITE_NAME; ?> - 家长端</h1>
<div class="header-info">
<span class="user-name" id="userName"></span>
<button class="btn-logout" id="logoutBtn">退出登录</button>
</div>
</div>
<div class="nav">
<button class="nav-item active" data-page="dashboard">首页</button>
<button class="nav-item" data-page="homework">作业情况</button>
<button class="nav-item" data-page="attendance">考勤记录</button>
</div>
<div class="container" id="pageContainer">
<!-- 首页内容 -->
<div id="page-dashboard" class="page-content">
<div class="child-info" id="childInfo">
<div class="child-name" id="childName">--</div>
<div class="child-no" id="childNo">--</div>
</div>
<div class="card">
<div class="conduct-score">
<div class="score-number" id="totalPoints">--</div>
<div class="score-label">当前操行分</div>
</div>
</div>
</div>
<!-- 作业情况页 -->
<div id="page-homework" class="page-content" style="display: none;">
<div class="card">
<div class="card-title">作业列表</div>
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>科目</th>
<th>作业标题</th>
<th>截止日期</th>
<th>状态</th>
<th>备注</th>
</tr>
</thead>
<tbody id="homeworkList"></tbody>
</table>
</div>
</div>
</div>
<!-- 考勤记录页 -->
<div id="page-attendance" class="page-content" style="display: none;">
<div class="stats-grid">
<div class="stat-card">
<div class="stat-label">出勤</div>
<div class="stat-value" id="attPresent">0</div>
</div>
<div class="stat-card">
<div class="stat-label">缺勤</div>
<div class="stat-value" id="attAbsent">0</div>
</div>
<div class="stat-card">
<div class="stat-label">迟到</div>
<div class="stat-value" id="attLate">0</div>
</div>
<div class="stat-card">
<div class="stat-label">请假</div>
<div class="stat-value" id="attLeave">0</div>
</div>
</div>
<div class="card">
<div class="card-title">考勤记录明细</div>
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>日期</th>
<th>状态</th>
<th>原因</th>
</tr>
</thead>
<tbody id="attendanceList"></tbody>
</table>
</div>
</div>
</div>
</div>
<script>
const API_BASE_URL = '<?php echo API_BASE_URL; ?>';
const STUDENT_ID = <?php echo $student_id ?: 0; ?>;
// 页面切换
function showPage(pageName) {
document.querySelectorAll('.page-content').forEach(page => {
page.style.display = 'none';
});
document.getElementById(`page-${pageName}`).style.display = 'block';
document.querySelectorAll('.nav-item').forEach(item => {
item.classList.remove('active');
if (item.dataset.page === pageName) {
item.classList.add('active');
}
});
switch(pageName) {
case 'dashboard':
loadDashboard();
break;
case 'homework':
loadHomework();
break;
case 'attendance':
loadAttendance();
break;
}
}
// 加载首页
async function loadDashboard() {
try {
// 获取子女信息
const childRes = await apiGet(`/api/parent/child/conduct`);
if (childRes && childRes.success) {
document.getElementById('childName').textContent = childRes.data.student_name;
document.getElementById('childNo').textContent = childRes.data.student_no;
document.getElementById('totalPoints').textContent = childRes.data.total_points;
}
} catch (error) {
console.error('加载首页失败:', error);
}
}
// 加载作业
async function loadHomework() {
try {
const res = await apiGet(`/api/parent/child/homework`);
if (res && res.success) {
let html = '';
res.data.homework.forEach(hw => {
html += `
<tr>
<td>${hw.subject}</td>
<td>${hw.title}</td>
<td>${hw.deadline}</td>
<td>${getStatusBadge(hw.status, 'homework')}</td>
<td>${hw.comments || '-'}</td>
</tr>
`;
});
if (res.data.homework.length === 0) {
html = '<tr><td colspan="5" style="text-align:center;">暂无作业</td></tr>';
}
document.getElementById('homeworkList').innerHTML = html;
}
} catch (error) {
console.error('加载作业失败:', error);
}
}
// 加载考勤
async function loadAttendance() {
try {
const res = await apiGet(`/api/parent/child/attendance`);
if (res && res.success) {
const records = res.data.records;
let present = 0, absent = 0, late = 0, leave = 0;
let html = '';
records.forEach(record => {
html += `
<tr>
<td>${record.date}</td>
<td>${getStatusBadge(record.status, 'attendance')}</td>
<td>${record.reason || '-'}</td>
</tr>
`;
switch(record.status) {
case 'present': present++; break;
case 'absent': absent++; break;
case 'late': late++; break;
case 'leave': leave++; break;
}
});
document.getElementById('attPresent').textContent = present;
document.getElementById('attAbsent').textContent = absent;
document.getElementById('attLate').textContent = late;
document.getElementById('attLeave').textContent = leave;
if (records.length === 0) {
html = '<tr><td colspan="3" style="text-align:center;">暂无考勤记录</td></tr>';
}
document.getElementById('attendanceList').innerHTML = html;
}
} catch (error) {
console.error('加载考勤失败:', error);
}
}
// 初始化
document.querySelectorAll('.nav-item').forEach(btn => {
btn.addEventListener('click', () => {
showPage(btn.dataset.page);
});
});
loadDashboard();
</script>
<script src="/assets/js/common.js"></script>
</body>
</html>

View File

@@ -0,0 +1,66 @@
<?php
/**
* 班级操行分管理系统 - 家长端作业情况
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: MIT License
*
* 版权所有 © Sea Network Technology Studio
*/
require_once __DIR__ . '/../config.php';
if (!isset($_SESSION['user_id']) || $_SESSION['user_type'] !== 'parent') {
header('Location: /index.php');
exit();
}
$page_title = '作业情况';
include __DIR__ . '/../includes/header.php';
?>
<div class="nav">
<a href="/parent/dashboard.php" class="nav-item">首页</a>
<a href="/parent/homework.php" class="nav-item active">作业情况</a>
<a href="/parent/attendance.php" class="nav-item">考勤记录</a>
</div>
<div class="container">
<div class="card">
<div class="card-title">作业列表</div>
<div class="table-wrapper">
<table class="table">
<thead><tr><th>科目</th><th>作业标题</th><th>截止日期</th><th>状态</th><th>备注</th></tr></thead>
<tbody id="homeworkList"></tbody>
</table>
</div>
</div>
</div>
<script>
async function loadHomework() {
const res = await apiGet('/api/parent/child/homework');
if (res && res.success) {
let html = '';
res.data.homework.forEach(hw => {
html += `<tr>
<td>${escapeHtml(hw.subject)}</td>
<td>${escapeHtml(hw.title)}</td>
<td>${hw.deadline}</td>
<td>${getStatusBadge(hw.status, 'homework')}</td>
<td>${escapeHtml(hw.comments || '-')}</td>
</tr>`;
});
if (res.data.homework.length === 0) {
html = '<tr><td colspan="5" style="text-align:center;">暂无作业</td></tr>';
}
document.getElementById('homeworkList').innerHTML = html;
}
}
loadHomework();
</script>
<script src="/assets/js/parent.js"></script>
<?php include __DIR__ . '/../includes/footer.php'; ?>

View File

@@ -0,0 +1,83 @@
<?php
/**
* 班级操行分管理系统 - 学生端考勤记录
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: MIT License
*
* 版权所有 © Sea Network Technology Studio
*/
require_once __DIR__ . '/../config.php';
if (!isset($_SESSION['user_id']) || $_SESSION['user_type'] !== 'student') {
header('Location: /index.php');
exit();
}
$page_title = '考勤记录';
$student_id = $_SESSION['student_id'];
include __DIR__ . '/../includes/header.php';
?>
<div class="nav">
<a href="/student/dashboard.php" class="nav-item">首页</a>
<a href="/student/conduct.php" class="nav-item">操行分</a>
<a href="/student/homework.php" class="nav-item">作业</a>
<a href="/student/attendance.php" class="nav-item active">考勤</a>
<a href="/student/password.php" class="nav-item">修改密码</a>
</div>
<div class="container">
<div class="stats-grid">
<div class="stat-card"><div class="stat-label">出勤</div><div class="stat-value" id="attPresent">0</div></div>
<div class="stat-card"><div class="stat-label">缺勤</div><div class="stat-value" id="attAbsent">0</div></div>
<div class="stat-card"><div class="stat-label">迟到</div><div class="stat-value" id="attLate">0</div></div>
<div class="stat-card"><div class="stat-label">请假</div><div class="stat-value" id="attLeave">0</div></div>
</div>
<div class="card">
<div class="card-title">考勤记录明细</div>
<div class="table-wrapper">
<table class="table">
<thead><tr><th>日期</th><th>状态</th><th>原因</th></tr></thead>
<tbody id="attendanceList"></tbody>
</table>
</div>
</div>
</div>
<script>
const STUDENT_ID = <?php echo $student_id; ?>;
async function loadAttendance() {
const res = await apiGet(`/api/student/attendance/${STUDENT_ID}`);
if (res && res.success) {
const stats = res.data.statistics;
document.getElementById('attPresent').textContent = stats.present || 0;
document.getElementById('attAbsent').textContent = stats.absent || 0;
document.getElementById('attLate').textContent = stats.late || 0;
document.getElementById('attLeave').textContent = stats.leave || 0;
let html = '';
res.data.records.forEach(record => {
html += `<tr>
<td>${record.date}</td>
<td>${getStatusBadge(record.status, 'attendance')}</td>
<td>${escapeHtml(record.reason || '-')}</td>
</tr>`;
});
if (res.data.records.length === 0) {
html = '<tr><td colspan="3" style="text-align:center;">暂无记录</td></tr>';
}
document.getElementById('attendanceList').innerHTML = html;
}
}
loadAttendance();
</script>
<script src="/assets/js/student.js"></script>
<?php include __DIR__ . '/../includes/footer.php'; ?>

View File

View File

@@ -0,0 +1,513 @@
<?php
/**
* 班级操行分管理系统 - 学生端主页
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: MIT License
*
* 版权所有 © Sea Network Technology Studio
*/
require_once __DIR__ . '/../config.php';
// 检查登录状态
if (!isset($_SESSION['user_id']) || $_SESSION['user_type'] !== 'student') {
header('Location: /index.php');
exit();
}
$student_id = $_SESSION['student_id'];
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?php echo SITE_NAME; ?> - 学生端</title>
<link rel="stylesheet" href="/assets/css/style.css">
<style>
.conduct-score {
text-align: center;
padding: 20px;
}
.score-number {
font-size: 64px;
font-weight: bold;
color: #667eea;
}
.score-label {
color: #666;
margin-top: 8px;
}
.record-item {
padding: 12px 0;
border-bottom: 1px solid #eee;
display: flex;
justify-content: space-between;
align-items: center;
}
.record-points {
font-weight: bold;
}
.record-points.plus {
color: #38a169;
}
.record-points.minus {
color: #e53e3e;
}
.record-reason {
flex: 1;
margin: 0 15px;
color: #555;
}
.record-time {
font-size: 12px;
color: #999;
}
.view-more {
text-align: center;
margin-top: 15px;
}
.view-more a {
color: #667eea;
text-decoration: none;
}
</style>
</head>
<body>
<div class="header">
<h1><?php echo SITE_NAME; ?> - 学生端</h1>
<div class="header-info">
<span class="user-name" id="userName"></span>
<button class="btn-logout" id="logoutBtn">退出登录</button>
</div>
</div>
<div class="nav">
<button class="nav-item active" data-page="dashboard">首页</button>
<button class="nav-item" data-page="conduct">操行分详情</button>
<button class="nav-item" data-page="homework">作业情况</button>
<button class="nav-item" data-page="attendance">考勤记录</button>
<button class="nav-item" data-page="password">修改密码</button>
</div>
<div class="container" id="pageContainer">
<!-- 首页内容 -->
<div id="page-dashboard" class="page-content">
<div class="stats-grid">
<div class="stat-card">
<div class="stat-label">当前操行分</div>
<div class="stat-value" id="totalPoints">--</div>
</div>
<div class="stat-card">
<div class="stat-label">作业完成率</div>
<div class="stat-value" id="homeworkRate">--%</div>
</div>
<div class="stat-card">
<div class="stat-label">本月出勤率</div>
<div class="stat-value" id="attendanceRate">--%</div>
</div>
</div>
<div class="card">
<div class="card-title">最新操行分记录</div>
<div id="recentRecords"></div>
<div class="view-more">
<a href="#" onclick="showPage('conduct'); return false;">查看更多 &gt;</a>
</div>
</div>
</div>
<!-- 操行分详情页 -->
<div id="page-conduct" class="page-content" style="display: none;">
<div class="card">
<div class="conduct-score">
<div class="score-number" id="conductTotalPoints">--</div>
<div class="score-label">当前操行分</div>
</div>
</div>
<div class="card">
<div class="card-title">历史记录</div>
<div id="conductRecords"></div>
<div class="pagination" id="conductPagination"></div>
</div>
</div>
<!-- 作业情况页 -->
<div id="page-homework" class="page-content" style="display: none;">
<div class="card">
<div class="card-title">作业列表</div>
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>科目</th>
<th>作业标题</th>
<th>截止日期</th>
<th>状态</th>
<th>备注</th>
</tr>
</thead>
<tbody id="homeworkList"></tbody>
</table>
</div>
</div>
</div>
<!-- 考勤记录页 -->
<div id="page-attendance" class="page-content" style="display: none;">
<div class="stats-grid">
<div class="stat-card">
<div class="stat-label">出勤</div>
<div class="stat-value" id="attPresent">0</div>
</div>
<div class="stat-card">
<div class="stat-label">缺勤</div>
<div class="stat-value" id="attAbsent">0</div>
</div>
<div class="stat-card">
<div class="stat-label">迟到</div>
<div class="stat-value" id="attLate">0</div>
</div>
<div class="stat-card">
<div class="stat-label">请假</div>
<div class="stat-value" id="attLeave">0</div>
</div>
</div>
<div class="card">
<div class="card-title">考勤记录明细</div>
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>日期</th>
<th>状态</th>
<th>原因</th>
</tr>
</thead>
<tbody id="attendanceList"></tbody>
</table>
</div>
</div>
</div>
<!-- 修改密码页 -->
<div id="page-password" class="page-content" style="display: none;">
<div class="card">
<div class="card-title">修改密码</div>
<form id="passwordForm">
<div class="form-group">
<label>原密码</label>
<input type="password" id="oldPassword" required>
</div>
<div class="form-group">
<label>新密码</label>
<input type="password" id="newPassword" required>
<small>密码长度6-20位需包含字母和数字</small>
</div>
<div class="form-group">
<label>确认新密码</label>
<input type="password" id="confirmPassword" required>
</div>
<button type="submit" class="btn btn-primary">确认修改</button>
</form>
</div>
</div>
</div>
<!-- 修改密码模态框(首次登录强制) -->
<div id="forceChangePasswordModal" class="modal" style="display: none;">
<div class="modal-content">
<div class="modal-header">
<h3>首次登录,请修改密码</h3>
</div>
<form id="forcePasswordForm">
<div class="form-group">
<label>新密码</label>
<input type="password" id="forceNewPassword" required>
<small>密码长度6-20位需包含字母和数字</small>
</div>
<div class="form-group">
<label>确认新密码</label>
<input type="password" id="forceConfirmPassword" required>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary">确认修改</button>
</div>
</form>
</div>
</div>
<script>
const API_BASE_URL = '<?php echo API_BASE_URL; ?>';
const STUDENT_ID = <?php echo $student_id; ?>;
let conductPage = 1;
let conductTotalPages = 1;
// 页面切换
function showPage(pageName) {
document.querySelectorAll('.page-content').forEach(page => {
page.style.display = 'none';
});
document.getElementById(`page-${pageName}`).style.display = 'block';
document.querySelectorAll('.nav-item').forEach(item => {
item.classList.remove('active');
if (item.dataset.page === pageName) {
item.classList.add('active');
}
});
// 加载对应页面数据
switch(pageName) {
case 'dashboard':
loadDashboard();
break;
case 'conduct':
loadConductHistory();
break;
case 'homework':
loadHomework();
break;
case 'attendance':
loadAttendance();
break;
}
}
// 加载首页
async function loadDashboard() {
try {
// 获取操行分
const conductRes = await apiGet(`/api/student/conduct/${STUDENT_ID}`);
if (conductRes && conductRes.success) {
document.getElementById('totalPoints').textContent = conductRes.data.total_points;
// 显示最近5条记录
const records = conductRes.data.records.slice(0, 5);
let html = '';
records.forEach(record => {
const pointsClass = record.points_change > 0 ? 'plus' : 'minus';
html += `
<div class="record-item">
<span class="record-points ${pointsClass}">${record.points_change > 0 ? '+' : ''}${record.points_change}</span>
<span class="record-reason">${record.reason}</span>
<span class="record-time">${formatDate(record.created_at)}</span>
</div>
`;
});
if (records.length === 0) {
html = '<div style="text-align:center;padding:20px;color:#999;">暂无记录</div>';
}
document.getElementById('recentRecords').innerHTML = html;
}
// 获取作业统计
const homeworkRes = await apiGet(`/api/student/homework/${STUDENT_ID}`);
if (homeworkRes && homeworkRes.success) {
const stats = homeworkRes.data.statistics;
const rate = stats.total > 0 ? Math.round(stats.submitted / stats.total * 100) : 0;
document.getElementById('homeworkRate').textContent = `${rate}%`;
}
// 获取考勤统计
const attendanceRes = await apiGet(`/api/student/attendance/${STUDENT_ID}`);
if (attendanceRes && attendanceRes.success) {
const stats = attendanceRes.data.statistics;
const total = stats.present + stats.absent + stats.late + stats.leave;
const rate = total > 0 ? Math.round(stats.present / total * 100) : 100;
document.getElementById('attendanceRate').textContent = `${rate}%`;
}
} catch (error) {
console.error('加载首页失败:', error);
}
}
// 加载操行分历史
async function loadConductHistory(page = 1) {
conductPage = page;
try {
const res = await apiGet(`/api/student/conduct/${STUDENT_ID}`, {
limit: 20,
offset: (page - 1) * 20
});
if (res && res.success) {
document.getElementById('conductTotalPoints').textContent = res.data.total_points;
let html = '<div class="table-wrapper"><table><thead><tr><th>时间</th><th>分数变动</th><th>原因</th><th>操作人</th></tr></thead><tbody>';
res.data.records.forEach(record => {
const pointsClass = record.points_change > 0 ? 'plus' : 'minus';
html += `
<tr>
<td>${formatDateTime(record.created_at)}</td>
<td class="record-points ${pointsClass}">${record.points_change > 0 ? '+' : ''}${record.points_change}</td>
<td>${record.reason}</td>
<td>${record.recorder_name}</td>
</tr>
`;
});
if (res.data.records.length === 0) {
html += '<tr><td colspan="4" style="text-align:center;">暂无记录</td></tr>';
}
html += '</tbody></table></div>';
document.getElementById('conductRecords').innerHTML = html;
// 分页
const total = res.data.total || res.data.records.length;
conductTotalPages = Math.ceil(total / 20);
renderConductPagination();
}
} catch (error) {
console.error('加载操行分历史失败:', error);
}
}
function renderConductPagination() {
const container = document.getElementById('conductPagination');
if (!container) return;
if (conductTotalPages <= 1) {
container.innerHTML = '';
return;
}
let html = '';
for (let i = 1; i <= conductTotalPages; i++) {
if (i === conductPage) {
html += `<span class="active">${i}</span>`;
} else {
html += `<a href="#" onclick="loadConductHistory(${i}); return false;">${i}</a>`;
}
}
container.innerHTML = html;
}
// 加载作业
async function loadHomework() {
try {
const res = await apiGet(`/api/student/homework/${STUDENT_ID}`);
if (res && res.success) {
let html = '';
res.data.homework.forEach(hw => {
html += `
<tr>
<td>${hw.subject}</td>
<td>${hw.title}</td>
<td>${hw.deadline}</td>
<td>${getStatusBadge(hw.status, 'homework')}</td>
<td>${hw.comments || '-'}</td>
</tr>
`;
});
if (res.data.homework.length === 0) {
html = '<tr><td colspan="5" style="text-align:center;">暂无作业</td></tr>';
}
document.getElementById('homeworkList').innerHTML = html;
}
} catch (error) {
console.error('加载作业失败:', error);
}
}
// 加载考勤
async function loadAttendance() {
try {
const res = await apiGet(`/api/student/attendance/${STUDENT_ID}`);
if (res && res.success) {
const stats = res.data.statistics;
document.getElementById('attPresent').textContent = stats.present || 0;
document.getElementById('attAbsent').textContent = stats.absent || 0;
document.getElementById('attLate').textContent = stats.late || 0;
document.getElementById('attLeave').textContent = stats.leave || 0;
let html = '';
res.data.records.forEach(record => {
html += `
<tr>
<td>${record.date}</td>
<td>${getStatusBadge(record.status, 'attendance')}</td>
<td>${record.reason || '-'}</td>
</tr>
`;
});
if (res.data.records.length === 0) {
html = '<tr><td colspan="3" style="text-align:center;">暂无考勤记录</td></tr>';
}
document.getElementById('attendanceList').innerHTML = html;
}
} catch (error) {
console.error('加载考勤失败:', error);
}
}
// 修改密码
document.getElementById('passwordForm')?.addEventListener('submit', async (e) => {
e.preventDefault();
const oldPassword = document.getElementById('oldPassword').value;
const newPassword = document.getElementById('newPassword').value;
const confirmPassword = document.getElementById('confirmPassword').value;
if (newPassword !== confirmPassword) {
showToast('两次输入的新密码不一致', 'error');
return;
}
const res = await apiPost('/api/auth/change-password', {
old_password: oldPassword,
new_password: newPassword
});
if (res && res.success) {
showToast('密码修改成功,请重新登录');
setTimeout(() => logout(), 1500);
} else {
showToast(res?.message || '密码修改失败', 'error');
}
});
// 强制修改密码
document.getElementById('forcePasswordForm')?.addEventListener('submit', async (e) => {
e.preventDefault();
const newPassword = document.getElementById('forceNewPassword').value;
const confirmPassword = document.getElementById('forceConfirmPassword').value;
if (newPassword !== confirmPassword) {
alert('两次输入的新密码不一致');
return;
}
const res = await apiPost('/api/auth/change-password', {
old_password: newPassword,
new_password: newPassword
});
if (res && res.success) {
showToast('密码修改成功,请重新登录');
setTimeout(() => logout(), 1500);
} else {
alert(res?.message || '密码修改失败');
}
});
// 检查是否需要强制修改密码
function checkForceChangePassword() {
const user = getUserInfo();
if (user && user.need_change_password) {
document.getElementById('forceChangePasswordModal').style.display = 'flex';
}
}
// 初始化
document.querySelectorAll('.nav-item').forEach(btn => {
btn.addEventListener('click', () => {
showPage(btn.dataset.page);
});
});
loadDashboard();
checkForceChangePassword();
</script>
<script src="/assets/js/common.js"></script>
</body>
</html>

View File

@@ -0,0 +1,74 @@
<?php
/**
* 班级操行分管理系统 - 学生端作业情况
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: MIT License
*
* 版权所有 © Sea Network Technology Studio
*/
require_once __DIR__ . '/../config.php';
if (!isset($_SESSION['user_id']) || $_SESSION['user_type'] !== 'student') {
header('Location: /index.php');
exit();
}
$page_title = '作业情况';
$student_id = $_SESSION['student_id'];
include __DIR__ . '/../includes/header.php';
?>
<div class="nav">
<a href="/student/dashboard.php" class="nav-item">首页</a>
<a href="/student/conduct.php" class="nav-item">操行分</a>
<a href="/student/homework.php" class="nav-item active">作业</a>
<a href="/student/attendance.php" class="nav-item">考勤</a>
<a href="/student/password.php" class="nav-item">修改密码</a>
</div>
<div class="container">
<div class="card">
<div class="card-title">作业列表</div>
<div class="table-wrapper">
<table class="table">
<thead>
<tr><th>科目</th><th>作业标题</th><th>截止日期</th><th>状态</th><th>备注</th></tr>
</thead>
<tbody id="homeworkList"></tbody>
</table>
</div>
</div>
</div>
<script>
const STUDENT_ID = <?php echo $student_id; ?>;
async function loadHomework() {
const res = await apiGet(`/api/student/homework/${STUDENT_ID}`);
if (res && res.success) {
let html = '';
res.data.homework.forEach(hw => {
html += `<tr>
<td>${escapeHtml(hw.subject)}</td>
<td>${escapeHtml(hw.title)}</td>
<td>${hw.deadline}</td>
<td>${getStatusBadge(hw.status, 'homework')}</td>
<td>${escapeHtml(hw.comments || '-')}</td>
</tr>`;
});
if (res.data.homework.length === 0) {
html = '<tr><td colspan="5" style="text-align:center;">暂无作业</td></tr>';
}
document.getElementById('homeworkList').innerHTML = html;
}
}
loadHomework();
</script>
<script src="/assets/js/student.js"></script>
<?php include __DIR__ . '/../includes/footer.php'; ?>

View File

@@ -0,0 +1,81 @@
<?php
/**
* 班级操行分管理系统 - 学生端修改密码
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: MIT License
*
* 版权所有 © Sea Network Technology Studio
*/
require_once __DIR__ . '/../config.php';
if (!isset($_SESSION['user_id']) || $_SESSION['user_type'] !== 'student') {
header('Location: /index.php');
exit();
}
$page_title = '修改密码';
include __DIR__ . '/../includes/header.php';
?>
<div class="nav">
<a href="/student/dashboard.php" class="nav-item">首页</a>
<a href="/student/conduct.php" class="nav-item">操行分</a>
<a href="/student/homework.php" class="nav-item">作业</a>
<a href="/student/attendance.php" class="nav-item">考勤</a>
<a href="/student/password.php" class="nav-item active">修改密码</a>
</div>
<div class="container">
<div class="card">
<div class="card-title">修改密码</div>
<form id="passwordForm">
<div class="form-group">
<label>原密码</label>
<input type="password" id="oldPassword" required>
</div>
<div class="form-group">
<label>新密码</label>
<input type="password" id="newPassword" required>
<small>密码长度6-20位需包含字母和数字</small>
</div>
<div class="form-group">
<label>确认新密码</label>
<input type="password" id="confirmPassword" required>
</div>
<button type="submit" class="btn btn-primary">确认修改</button>
</form>
</div>
</div>
<script>
document.getElementById('passwordForm').addEventListener('submit', async (e) => {
e.preventDefault();
const oldPassword = document.getElementById('oldPassword').value;
const newPassword = document.getElementById('newPassword').value;
const confirmPassword = document.getElementById('confirmPassword').value;
if (newPassword !== confirmPassword) {
showToast('两次输入的新密码不一致', 'error');
return;
}
const res = await apiPost('/api/auth/change-password', {
old_password: oldPassword,
new_password: newPassword
});
if (res && res.success) {
showToast('密码修改成功,请重新登录');
setTimeout(() => logout(), 1500);
} else {
showToast(res?.message || '密码修改失败', 'error');
}
});
</script>
<script src="/assets/js/student.js"></script>
<?php include __DIR__ . '/../includes/footer.php'; ?>