feat: 多班级版班级管理系统 v2.0
技术栈:Go (Gin + GORM) + PHP + MySQL 5.7 + Redis 主要功能: - 多班级完全隔离(class_id 贯穿全系统) - 后端 Go Gin(端口 56789),Nginx 反代 - 超级管理员独立登录(env 配置,默认账密 admin/Admin123) - bcrypt 密码加密(无 PASSWORD_SALT) - 科任老师/课代表新角色 - 课代表作业管理页面 - 排行榜分项排行(操行分/考勤/作业) - 角色加减分上下限由班主任配置 - 家长改密功能(可开关) - 班级角色按需开关 - 宿舍号格式:南0-000 - 周度/月度重置功能 - MySQL 5.7 兼容 - 43 轮代码审查 + 全部修复 开发者: Canglan 版权归属: Sea Network Technology Studio 许可证: Apache License 2.0
This commit is contained in:
265
frontend/assets/css/admin.css
Normal file
265
frontend/assets/css/admin.css
Normal file
@@ -0,0 +1,265 @@
|
||||
/**
|
||||
* 多班级版班级管理系统 - 管理端样式
|
||||
*
|
||||
* 开发者: Canglan
|
||||
* 联系方式: admin@sea-studio.top
|
||||
* 版权归属: Sea Network Technology Studio
|
||||
* 许可证: Apache License 2.0
|
||||
*
|
||||
* 版权所有 © 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: var(--color-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 导入区域 */
|
||||
.import-area {
|
||||
border: 2px dashed var(--color-border);
|
||||
border-radius: 12px;
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
transition: border-color 0.3s;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.import-area:hover {
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.import-area input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.import-label {
|
||||
color: var(--color-primary);
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* 预览表格 */
|
||||
.preview-table {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
/* 筛选栏 */
|
||||
.filter-bar {
|
||||
background: var(--color-hover);
|
||||
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: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.filter-group input,
|
||||
.filter-group select {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
border: 1px solid var(--color-border);
|
||||
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: var(--color-text);
|
||||
}
|
||||
|
||||
.assignment-meta {
|
||||
color: var(--color-text-muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* 状态选择器 */
|
||||
.status-select {
|
||||
padding: 4px 8px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* 复选框 */
|
||||
.student-checkbox {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
cursor: pointer;
|
||||
}
|
||||
/* 扣分类型按钮组 */
|
||||
.deduction-types {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* 考勤学生方格网格 */
|
||||
.student-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
.student-cell {
|
||||
width: calc(100% / 7 - 10px);
|
||||
min-height: 60px;
|
||||
border: 2px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease;
|
||||
padding: 8px 4px;
|
||||
text-align: center;
|
||||
word-break: break-all;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.student-cell:hover {
|
||||
background: #f3f4f6;
|
||||
border-color: #9ca3af;
|
||||
}
|
||||
|
||||
.student-cell.selected {
|
||||
background: #fee2e2;
|
||||
border-color: #ef4444;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.student-cell.has-record {
|
||||
border: 2px dashed #9ca3af;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.attendance-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin: 15px 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.toolbar-field {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
background: var(--color-hover);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 4px;
|
||||
padding: 6px 10px;
|
||||
}
|
||||
|
||||
.toolbar-field .toolbar-label {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-secondary);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.toolbar-field input,
|
||||
.toolbar-field select {
|
||||
border: none;
|
||||
background: transparent;
|
||||
outline: none;
|
||||
font-size: 13px;
|
||||
padding: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.attendance-toolbar .status-group {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.attendance-toolbar .status-btn {
|
||||
padding: 6px 16px;
|
||||
border: 2px solid #e5e7eb;
|
||||
border-radius: 6px;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.attendance-toolbar .status-btn.active {
|
||||
border-color: var(--color-primary);
|
||||
background: var(--color-primary-light);
|
||||
color: #4338ca;
|
||||
}
|
||||
|
||||
.student-tag {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
background: #e8f4f8;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
margin: 2px;
|
||||
color: #2c3e50;
|
||||
}
|
||||
.student-tags-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.student-cell {
|
||||
width: calc(100% / 4 - 10px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.student-cell {
|
||||
width: calc(100% / 3 - 10px);
|
||||
}
|
||||
}
|
||||
|
||||
.preserve-newlines {
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
}
|
||||
998
frontend/assets/css/style.css
Normal file
998
frontend/assets/css/style.css
Normal file
@@ -0,0 +1,998 @@
|
||||
/**
|
||||
* 多班级版班级管理系统 - 全局样式
|
||||
*
|
||||
* 开发者: Canglan
|
||||
* 联系方式: admin@sea-studio.top
|
||||
* 版权归属: Sea Network Technology Studio
|
||||
* 许可证: Apache License 2.0
|
||||
*
|
||||
* 版权所有 © Sea Network Technology Studio
|
||||
*/
|
||||
|
||||
:root {
|
||||
/* 主色调 */
|
||||
--color-primary: #4361ee;
|
||||
--color-primary-light: #eef0ff;
|
||||
--color-primary-dark: #3651d4;
|
||||
--color-primary-hover: #3a56d4;
|
||||
|
||||
/* 语义色 */
|
||||
--color-danger: #e53e3e;
|
||||
--color-danger-light: #fff5f5;
|
||||
--color-danger-dark: #c53030;
|
||||
--color-success: #38a169;
|
||||
--color-success-light: #f0fff4;
|
||||
--color-warning: #d69e2e;
|
||||
--color-warning-light: #fffff0;
|
||||
|
||||
/* 灰度 */
|
||||
--color-text: #1a202c;
|
||||
--color-text-secondary: #4a5568;
|
||||
--color-text-muted: #a0aec0;
|
||||
--color-bg: #f5f7fb;
|
||||
--color-card: #ffffff;
|
||||
--color-border: #e2e8f0;
|
||||
--color-border-light: #edf2f7;
|
||||
--color-hover: #f7fafc;
|
||||
|
||||
/* 按钮 */
|
||||
--btn-primary-bg: var(--color-primary);
|
||||
--btn-primary-text: #ffffff;
|
||||
--btn-outline-bg: transparent;
|
||||
--btn-outline-border: var(--color-primary);
|
||||
--btn-outline-text: var(--color-primary);
|
||||
--btn-danger-bg: var(--color-danger);
|
||||
--btn-danger-text: #ffffff;
|
||||
--btn-ghost-text: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
background: var(--color-bg);
|
||||
min-height: 100vh;
|
||||
font-size: 14px;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
/* ========== 登录页面 ========== */
|
||||
.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: var(--color-text);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.login-header p {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.login-form .form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.login-form label {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
color: var(--color-text-secondary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.login-form input {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
|
||||
.login-form input:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.btn-login {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
background: linear-gradient(135deg, var(--color-primary) 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: var(--color-danger-light);
|
||||
color: var(--color-danger);
|
||||
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 var(--color-border-light);
|
||||
color: var(--color-text-muted);
|
||||
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: var(--color-text);
|
||||
}
|
||||
|
||||
.header-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
color: var(--color-text-secondary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.user-role {
|
||||
background: var(--color-primary);
|
||||
color: white;
|
||||
padding: 2px 8px;
|
||||
border-radius: 20px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.btn-logout {
|
||||
background: var(--color-danger);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 6px 16px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
.btn-logout:hover {
|
||||
background: var(--color-danger-dark);
|
||||
}
|
||||
|
||||
/* ========== 导航菜单 ========== */
|
||||
.nav {
|
||||
background: white;
|
||||
padding: 0 24px;
|
||||
border-bottom: 1px solid var(--color-border-light);
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
padding: 12px 20px;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--color-text-secondary);
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: all 0.3s;
|
||||
border-bottom: 2px solid transparent;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.nav-item:hover {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.nav-item.active {
|
||||
color: var(--color-primary);
|
||||
border-bottom-color: var(--color-primary);
|
||||
}
|
||||
|
||||
/* ========== 容器 ========== */
|
||||
.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 var(--color-primary);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
/* ========== 统计卡片网格 ========== */
|
||||
.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: var(--color-primary);
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* ========== 表格 ========== */
|
||||
.table-wrapper {
|
||||
overflow-x: auto;
|
||||
overflow-y: visible;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--color-border-light);
|
||||
}
|
||||
|
||||
th {
|
||||
background: var(--color-hover);
|
||||
font-weight: 600;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
tr:hover {
|
||||
background: var(--color-hover);
|
||||
}
|
||||
|
||||
/* ========== 状态标签 ========== */
|
||||
.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: var(--btn-primary-bg);
|
||||
color: var(--btn-primary-text);
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--color-primary-hover);
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: var(--btn-danger-bg);
|
||||
color: var(--btn-danger-text);
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: var(--color-danger-dark);
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: var(--color-success-light);
|
||||
color: var(--color-success);
|
||||
border: 1px solid #c6f6d5;
|
||||
}
|
||||
|
||||
.btn-success:hover {
|
||||
background: #c6f6d5;
|
||||
}
|
||||
|
||||
.btn-warning {
|
||||
background: var(--color-warning-light);
|
||||
color: var(--color-warning);
|
||||
border: 1px solid #fefcbf;
|
||||
}
|
||||
|
||||
.btn-warning:hover {
|
||||
background: #fefcbf;
|
||||
}
|
||||
|
||||
.btn-info {
|
||||
background: #e3f2fd;
|
||||
color: #1565c0;
|
||||
border: 1px solid #bbdefb;
|
||||
}
|
||||
|
||||
.btn-info:hover {
|
||||
background: #bbdefb;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--color-hover);
|
||||
color: var(--color-text-secondary);
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: var(--color-border-light);
|
||||
}
|
||||
|
||||
.btn-outline {
|
||||
background: transparent;
|
||||
color: var(--color-primary);
|
||||
border: 1px solid var(--color-primary);
|
||||
}
|
||||
|
||||
.btn-outline:hover {
|
||||
background: var(--color-primary-light);
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
background: transparent;
|
||||
color: var(--color-text-secondary);
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.btn-ghost:hover {
|
||||
background: var(--color-hover);
|
||||
border-color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.btn-outline-danger {
|
||||
background: transparent;
|
||||
color: var(--color-danger);
|
||||
border: 1px solid var(--color-danger);
|
||||
}
|
||||
|
||||
.btn-outline-danger:hover {
|
||||
background: var(--color-danger-light);
|
||||
}
|
||||
|
||||
.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 var(--color-border-light);
|
||||
}
|
||||
|
||||
.modal-header h3 {
|
||||
font-size: 18px;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
margin-top: 20px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid var(--color-border-light);
|
||||
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: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group select,
|
||||
.form-group textarea {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.form-group input:focus,
|
||||
.form-group select:focus,
|
||||
.form-group textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.form-group small {
|
||||
display: block;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 12px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.form-group textarea {
|
||||
min-height: 60px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
/* ========== 复选框组 ========== */
|
||||
.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 var(--color-border);
|
||||
border-radius: 6px;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
/* ========== 分页 ========== */
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-top: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.pagination a, .pagination span {
|
||||
padding: 6px 12px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 4px;
|
||||
text-decoration: none;
|
||||
color: var(--color-text-secondary);
|
||||
cursor: pointer;
|
||||
min-width: 36px;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.pagination a:hover {
|
||||
background: var(--color-primary-light);
|
||||
border-color: var(--color-primary);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.pagination .active {
|
||||
background: var(--color-primary);
|
||||
color: white;
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.pagination .ellipsis {
|
||||
border: none;
|
||||
cursor: default;
|
||||
padding: 6px 4px;
|
||||
color: var(--color-text-muted);
|
||||
min-width: auto;
|
||||
}
|
||||
|
||||
.pagination .page-jump {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-left: 8px;
|
||||
font-size: 13px;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.pagination .page-jump input {
|
||||
width: 50px;
|
||||
padding: 5px 8px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.pagination .page-jump input:focus {
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 0 0 0 2px rgba(67, 97, 238, 0.15);
|
||||
}
|
||||
|
||||
.pagination .page-nav {
|
||||
padding: 6px 10px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* ========== 提示消息 ========== */
|
||||
.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: var(--color-success);
|
||||
}
|
||||
|
||||
.toast-error {
|
||||
background: var(--color-danger);
|
||||
}
|
||||
|
||||
.toast-warning {
|
||||
background: #ed8936;
|
||||
}
|
||||
|
||||
.toast-info {
|
||||
background: var(--color-primary);
|
||||
}
|
||||
|
||||
@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 var(--color-border);
|
||||
border-top-color: var(--color-primary);
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* ========== 底部 ========== */
|
||||
.footer {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* ========== 记录项 ========== */
|
||||
.record-item {
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid var(--color-border-light);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.record-points {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.record-points.plus {
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.record-points.minus {
|
||||
color: var(--color-danger);
|
||||
}
|
||||
|
||||
.record-reason {
|
||||
flex: 1;
|
||||
margin: 0 15px;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.record-time {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.view-more {
|
||||
text-align: center;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.view-more a {
|
||||
color: var(--color-primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.conduct-score {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.score-number {
|
||||
font-size: 64px;
|
||||
font-weight: bold;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
/* ========== 响应式 ========== */
|
||||
@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;
|
||||
}
|
||||
}
|
||||
|
||||
/* ========== 操作列下拉菜单 ========== */
|
||||
.action-dropdown {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.action-dropdown-toggle {
|
||||
background: var(--color-hover);
|
||||
color: var(--color-text-secondary);
|
||||
border: 1px solid var(--color-border);
|
||||
padding: 4px 10px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.action-dropdown-toggle:hover {
|
||||
background: var(--color-border-light);
|
||||
border-color: #cbd5e0;
|
||||
}
|
||||
|
||||
.action-dropdown-toggle.open {
|
||||
background: var(--color-border-light);
|
||||
border-color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.action-dropdown-menu {
|
||||
display: none;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
|
||||
border: 1px solid var(--color-border);
|
||||
min-width: 120px;
|
||||
z-index: 9999;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.action-dropdown-menu.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.action-dropdown-menu a {
|
||||
display: block;
|
||||
padding: 8px 14px;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
transition: background 0.15s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.action-dropdown-menu a:hover {
|
||||
background: var(--color-hover);
|
||||
color: #2d3748;
|
||||
}
|
||||
|
||||
.action-dropdown-menu a.danger {
|
||||
color: var(--color-danger);
|
||||
border-top: 1px solid var(--color-border-light);
|
||||
margin-top: 4px;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.action-dropdown-menu a.danger:hover {
|
||||
background: var(--color-danger-light);
|
||||
color: var(--color-danger-dark);
|
||||
}
|
||||
|
||||
|
||||
/* ========== 链接 ========== */
|
||||
.link {
|
||||
color: var(--color-primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* ========== 文本工具类 ========== */
|
||||
.text-danger { color: var(--color-danger); }
|
||||
.text-success { color: var(--color-success); }
|
||||
.text-muted { color: var(--color-text-muted); }
|
||||
|
||||
/* ========== 标签 ========== */
|
||||
.tag { padding: 2px 8px; border-radius: 10px; font-size: 12px; }
|
||||
.tag-success { background: #e8f5e9; color: #2e7d32; }
|
||||
.tag-danger { background: #ffebee; color: #c62828; }
|
||||
.tag-warning { background: #fff3e0; color: #e65100; }
|
||||
.tag-info { background: #e3f2fd; color: #1565c0; }
|
||||
|
||||
/* ========== 历史记录页优化 ========== */
|
||||
/* 时间列:确保分两行显示(日期+时间) */
|
||||
.history-time {
|
||||
white-space: nowrap;
|
||||
min-width: 80px;
|
||||
line-height: 1.5;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
/* 原因列:每行最少7个字,自动换行(使用td前缀提升优先级,防止被preserve-newlines覆盖) */
|
||||
td.history-reason {
|
||||
min-width: 7em;
|
||||
max-width: 200px;
|
||||
white-space: normal !important;
|
||||
word-break: break-word;
|
||||
line-height: 1.5;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
/* 学生名列:允许换行 */
|
||||
.history-students {
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
min-width: 60px;
|
||||
max-width: 120px;
|
||||
line-height: 1.5;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
/* 合并记录复选框样式 */
|
||||
.history-grouped-label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 6px;
|
||||
background: var(--color-hover);
|
||||
transition: all 0.2s;
|
||||
white-space: nowrap;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.history-grouped-label:hover {
|
||||
border-color: var(--color-primary);
|
||||
background: var(--color-primary-light);
|
||||
}
|
||||
|
||||
.history-grouped-label input[type="checkbox"] {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* 合并记录按钮样式 */
|
||||
.btn-outline-danger {
|
||||
background: transparent;
|
||||
color: var(--color-danger);
|
||||
border: 1px solid var(--color-danger);
|
||||
padding: 4px 10px;
|
||||
font-size: 12px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.btn-outline-danger:hover {
|
||||
background: var(--color-danger-light);
|
||||
color: var(--color-danger-dark);
|
||||
border-color: var(--color-danger-dark);
|
||||
}
|
||||
|
||||
14
frontend/assets/js/admin.js
Normal file
14
frontend/assets/js/admin.js
Normal file
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* admin.js - 管理端公共函数库
|
||||
*
|
||||
* 此文件已拆分为独立模块,各模块文件位于 /assets/js/modules/ 目录
|
||||
* 各页面通过引用对应模块获取所需功能
|
||||
*
|
||||
* 模块列表:
|
||||
* - modules/modal-utils.js - 模态框工具函数
|
||||
* - modules/utils.js - 通用工具函数(escapeHtml, toggleSelectAll等)
|
||||
* - modules/student-mgmt.js - 学生管理函数
|
||||
* - modules/admin-mgmt.js - 管理员管理函数
|
||||
* - modules/subject-mgmt.js - 科目管理函数
|
||||
* - modules/points-mgmt.js - 加减分管理函数
|
||||
*/
|
||||
146
frontend/assets/js/admins.js
Normal file
146
frontend/assets/js/admins.js
Normal file
@@ -0,0 +1,146 @@
|
||||
/**
|
||||
* 多班级版班级管理系统 - 管理员管理页JS
|
||||
*
|
||||
* 开发者: Canglan
|
||||
* 版权归属: Sea Network Technology Studio
|
||||
*
|
||||
* 版权所有 © Sea Network Technology Studio
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
let currentEditUserId = null;
|
||||
let currentResetUserId = null;
|
||||
|
||||
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>
|
||||
<td>
|
||||
<div class="action-dropdown">
|
||||
<button class="btn btn-sm action-dropdown-toggle" onclick="toggleActionDropdown(this)">操作 ▼</button>
|
||||
<div class="action-dropdown-menu">
|
||||
<a onclick="showEditAdminModal(${admin.user_id}, '${escapeHtml(admin.username)}', '${escapeHtml(admin.real_name)}', '${escapeHtml(admin.role_type)}')">编辑</a>
|
||||
<a onclick="resetAdminPassword(${admin.user_id}, '${escapeHtml(admin.real_name)}')">重置密码</a>
|
||||
<a onclick="unlockUser('${escapeHtml(admin.username)}', '${escapeHtml(admin.real_name)}')">解锁</a>
|
||||
<a class="danger" onclick="deleteAdmin(${admin.user_id}, '${escapeHtml(admin.real_name)}')">删除</a>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>`;
|
||||
});
|
||||
if (res.data.admins.length === 0) {
|
||||
html = '<tr><td colspan="4" style="text-align:center;">暂无管理员</td></tr>';
|
||||
}
|
||||
document.getElementById('adminList').innerHTML = html;
|
||||
}
|
||||
}
|
||||
|
||||
function showEditAdminModal(userId, username, realName, roleType) {
|
||||
currentEditUserId = userId;
|
||||
document.getElementById('editAdminUserId').value = userId;
|
||||
document.getElementById('editAdminUsername').value = username;
|
||||
document.getElementById('editAdminRealName').value = realName;
|
||||
document.getElementById('editAdminRole').value = roleType;
|
||||
document.getElementById('editAdminModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
async function submitEditAdmin() {
|
||||
if (!currentEditUserId) return;
|
||||
|
||||
const roleType = document.getElementById('editAdminRole').value;
|
||||
if (!roleType) {
|
||||
showToast('请选择角色', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await apiPut(`/api/admin/update/${currentEditUserId}`, {
|
||||
real_name: document.getElementById('editAdminRealName').value,
|
||||
role_type: roleType
|
||||
});
|
||||
|
||||
if (res && res.success) {
|
||||
showToast('管理员更新成功');
|
||||
closeModal('editAdminModal');
|
||||
loadAdmins();
|
||||
} else {
|
||||
showToast(res?.message || '更新失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteAdmin(userId, realName) {
|
||||
if (!confirm(`确定要删除管理员 "${realName}" 吗?此操作不可恢复。`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await apiDelete(`/api/admin/delete/${userId}`);
|
||||
if (res && res.success) {
|
||||
showToast('管理员删除成功');
|
||||
loadAdmins();
|
||||
} else {
|
||||
showToast(res?.message || '删除失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function resetAdminPassword(userId, realName) {
|
||||
currentResetUserId = userId;
|
||||
document.getElementById('resetPasswordUserId').value = userId;
|
||||
document.getElementById('resetPasswordAdminName').value = realName;
|
||||
document.getElementById('newPassword').value = '';
|
||||
document.getElementById('resetPasswordModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
async function unlockUser(username, realName) {
|
||||
if (!confirm(`确定要解除用户 "${realName}" 的登录锁定吗?\n(适用于多次登录失败被禁止登录的情况)`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await apiPost('/api/admin/unlock-user', {
|
||||
username: username
|
||||
});
|
||||
|
||||
if (res && res.success) {
|
||||
showToast(res.message || '解锁成功');
|
||||
} else {
|
||||
showToast(res?.message || '解锁失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function submitResetPassword() {
|
||||
if (!currentResetUserId) return;
|
||||
|
||||
const newPassword = document.getElementById('newPassword').value;
|
||||
if (!newPassword || newPassword.length < 6) {
|
||||
showToast('密码长度至少6位', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await apiPost(`/api/admin/reset-password/${currentResetUserId}`, {
|
||||
new_password: newPassword
|
||||
});
|
||||
|
||||
if (res && res.success) {
|
||||
showToast('密码重置成功');
|
||||
closeModal('resetPasswordModal');
|
||||
} else {
|
||||
showToast(res?.message || '密码重置失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
loadAdmins();
|
||||
|
||||
window.loadAdmins = loadAdmins;
|
||||
window.showEditAdminModal = showEditAdminModal;
|
||||
window.submitEditAdmin = submitEditAdmin;
|
||||
window.deleteAdmin = deleteAdmin;
|
||||
window.resetAdminPassword = resetAdminPassword;
|
||||
window.unlockUser = unlockUser;
|
||||
window.submitResetPassword = submitResetPassword;
|
||||
|
||||
})();
|
||||
195
frontend/assets/js/attendance-manage.js
Normal file
195
frontend/assets/js/attendance-manage.js
Normal file
@@ -0,0 +1,195 @@
|
||||
/**
|
||||
* 多班级版班级管理系统 - 考勤管理页JS
|
||||
*
|
||||
* 开发者: Canglan
|
||||
* 版权归属: Sea Network Technology Studio
|
||||
*
|
||||
* 版权所有 © Sea Network Technology Studio
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
let currentStatus = 'absent';
|
||||
let studentsData = [];
|
||||
let existingRecords = [];
|
||||
|
||||
// 考勤扣分配置映射(从后端配置注入)
|
||||
const attendanceDeductionMap = {
|
||||
absent: window.DEDUCTION_ATTENDANCE_ABSENT || 3,
|
||||
late: window.DEDUCTION_ATTENDANCE_LATE || 1,
|
||||
leave: window.DEDUCTION_ATTENDANCE_LEAVE || 0
|
||||
};
|
||||
|
||||
// 初始化按钮文字
|
||||
function initAttendanceButtons() {
|
||||
const btnAbsent = document.getElementById('btnAbsent');
|
||||
const btnLate = document.getElementById('btnLate');
|
||||
const btnLeave = document.getElementById('btnLeave');
|
||||
if (btnAbsent) btnAbsent.textContent = '缺勤(' + attendanceDeductionMap.absent + '分)';
|
||||
if (btnLate) btnLate.textContent = '迟到(' + attendanceDeductionMap.late + '分)';
|
||||
if (btnLeave) btnLeave.textContent = '请假(' + (attendanceDeductionMap.leave > 0 ? attendanceDeductionMap.leave + '分' : '不扣分') + ')';
|
||||
if (attendanceDeductionMap.absent > 0) {
|
||||
document.getElementById('customDeduction').value = attendanceDeductionMap.absent;
|
||||
}
|
||||
}
|
||||
|
||||
function selectStatus(btn) {
|
||||
document.querySelectorAll('.status-btn').forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
currentStatus = btn.dataset.status;
|
||||
const defaultDeduction = attendanceDeductionMap[currentStatus] || 0;
|
||||
if (defaultDeduction > 0) {
|
||||
document.getElementById('customDeduction').value = defaultDeduction;
|
||||
} else {
|
||||
document.getElementById('customDeduction').value = '';
|
||||
}
|
||||
}
|
||||
|
||||
async function loadStudents() {
|
||||
const res = await apiGet('/api/admin/students', {page_size: 1000});
|
||||
if (res && res.success) {
|
||||
studentsData = res.data.students;
|
||||
renderStudentGrid();
|
||||
await loadExistingRecords();
|
||||
} else {
|
||||
document.getElementById('studentGrid').innerHTML = '<div style="text-align:center;padding:20px;color:#999;">加载学生列表失败</div>';
|
||||
}
|
||||
}
|
||||
|
||||
function renderStudentGrid() {
|
||||
const currentSlot = document.getElementById('attendanceSlot').value;
|
||||
let html = '';
|
||||
studentsData.forEach(student => {
|
||||
const hasRecord = existingRecords.some(r => r.student_id === student.student_id && r.slot === currentSlot);
|
||||
html += `<div class="student-cell${hasRecord ? ' has-record' : ''}"
|
||||
data-id="${student.student_id}"
|
||||
data-name="${escapeHtml(student.name)}"
|
||||
onclick="toggleStudent(this)">
|
||||
<span class="student-cell-name">${escapeHtml(student.name)}</span>
|
||||
<span class="student-cell-no">${escapeHtml(student.student_no)}</span>
|
||||
</div>`;
|
||||
});
|
||||
if (studentsData.length === 0) {
|
||||
html = '<div style="text-align:center;padding:20px;color:#999;width:100%;">暂无学生数据</div>';
|
||||
}
|
||||
document.getElementById('studentGrid').innerHTML = html;
|
||||
}
|
||||
|
||||
function toggleStudent(cell) {
|
||||
cell.classList.toggle('selected');
|
||||
}
|
||||
|
||||
function selectAllStudents() {
|
||||
document.querySelectorAll('.student-cell:not(.has-record)').forEach(cell => {
|
||||
cell.classList.add('selected');
|
||||
});
|
||||
}
|
||||
|
||||
function deselectAllStudents() {
|
||||
document.querySelectorAll('.student-cell').forEach(cell => {
|
||||
cell.classList.remove('selected');
|
||||
});
|
||||
}
|
||||
|
||||
async function loadExistingRecords() {
|
||||
const date = document.getElementById('attendanceDate').value;
|
||||
const slot = document.getElementById('attendanceSlot').value;
|
||||
const res = await apiGet('/api/admin/attendance/records', { date, slot });
|
||||
if (res && res.success) {
|
||||
existingRecords = res.data.records || [];
|
||||
renderStudentGrid();
|
||||
}
|
||||
}
|
||||
|
||||
async function submitAttendance() {
|
||||
const selectedCells = document.querySelectorAll('.student-cell.selected');
|
||||
if (selectedCells.length === 0) {
|
||||
showToast('请先选择有考勤异常的学生', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const date = document.getElementById('attendanceDate').value;
|
||||
const slot = document.getElementById('attendanceSlot').value;
|
||||
const reason = document.getElementById('attendanceReason').value;
|
||||
const customDeduction = document.getElementById('customDeduction').value;
|
||||
const customDeductionValue = customDeduction ? parseInt(customDeduction) : null;
|
||||
|
||||
const promises = [];
|
||||
selectedCells.forEach(cell => {
|
||||
const studentId = parseInt(cell.dataset.id);
|
||||
const payload = {
|
||||
student_id: studentId,
|
||||
date: date,
|
||||
slot: slot,
|
||||
status: currentStatus,
|
||||
reason: reason,
|
||||
apply_deduction: true
|
||||
};
|
||||
if (customDeductionValue !== null && customDeductionValue > 0) {
|
||||
payload.custom_deduction = customDeductionValue;
|
||||
}
|
||||
promises.push(apiPost('/api/admin/attendance', payload));
|
||||
});
|
||||
|
||||
const results = await Promise.allSettled(promises);
|
||||
const succeeded = results.filter(r => r.status === 'fulfilled' && r.value?.success).length;
|
||||
const failed = results.length - succeeded;
|
||||
|
||||
if (failed === 0) {
|
||||
showToast(`考勤提交成功(${succeeded}条)`);
|
||||
} else {
|
||||
showToast(`提交完成:成功${succeeded}条,失败${failed}条`, 'error');
|
||||
}
|
||||
|
||||
deselectAllStudents();
|
||||
await loadExistingRecords();
|
||||
loadAttendanceRecords();
|
||||
}
|
||||
|
||||
async function loadAttendanceRecords() {
|
||||
const date = document.getElementById('attendanceDate').value;
|
||||
const res = await apiGet('/api/admin/attendance/records', { date });
|
||||
if (res && res.success) {
|
||||
let html = '';
|
||||
const records = res.data.records || [];
|
||||
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 (records.length === 0) {
|
||||
html = '<tr><td colspan="6" style="text-align:center;">暂无考勤记录</td></tr>';
|
||||
}
|
||||
document.getElementById('attendanceList').innerHTML = html;
|
||||
}
|
||||
}
|
||||
|
||||
// 日期或时段变化时重新加载
|
||||
document.getElementById('attendanceDate').addEventListener('change', function() {
|
||||
loadExistingRecords();
|
||||
loadAttendanceRecords();
|
||||
});
|
||||
document.getElementById('attendanceSlot').addEventListener('change', function() {
|
||||
loadExistingRecords();
|
||||
});
|
||||
|
||||
// 页面初始化
|
||||
initAttendanceButtons();
|
||||
loadStudents();
|
||||
loadAttendanceRecords();
|
||||
|
||||
window.selectStatus = selectStatus;
|
||||
window.loadStudents = loadStudents;
|
||||
window.toggleStudent = toggleStudent;
|
||||
window.selectAllStudents = selectAllStudents;
|
||||
window.deselectAllStudents = deselectAllStudents;
|
||||
window.submitAttendance = submitAttendance;
|
||||
window.loadAttendanceRecords = loadAttendanceRecords;
|
||||
|
||||
})();
|
||||
159
frontend/assets/js/cadre-homework.js
Normal file
159
frontend/assets/js/cadre-homework.js
Normal file
@@ -0,0 +1,159 @@
|
||||
/**
|
||||
* 多班级版班级管理系统 - 课代表作业管理JS
|
||||
*
|
||||
* 开发者: Canglan
|
||||
* 版权归属: Sea Network Technology Studio
|
||||
*
|
||||
* 版权所有 © Sea Network Technology Studio
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
var currentPage = 1;
|
||||
var pageSize = 20;
|
||||
var currentAssignmentId = null;
|
||||
|
||||
async function loadHomework(page) {
|
||||
var res = await apiGet('/api/cadre/homework', { page: page, page_size: pageSize });
|
||||
if (res && res.success && res.data) {
|
||||
var items = res.data.items || res.data.records || [];
|
||||
var total = res.data.total || 0;
|
||||
var html = '';
|
||||
if (items.length === 0) {
|
||||
html = '<tr><td colspan="5" style="text-align:center;">暂无作业记录</td></tr>';
|
||||
} else {
|
||||
items.forEach(function(item) {
|
||||
html += '<tr>' +
|
||||
'<td>' + escapeHtml(item.title || '-') + '</td>' +
|
||||
'<td>' + escapeHtml(item.subject_name || '-') + '</td>' +
|
||||
'<td>' + formatDate(item.deadline) + '</td>' +
|
||||
'<td>' + escapeHtml(item.description || '-') + '</td>' +
|
||||
'<td><button class="btn btn-sm btn-outline" onclick="showAbsentModal(' + item.assignment_id + ')">登记缺交</button></td>' +
|
||||
'</tr>';
|
||||
});
|
||||
}
|
||||
document.getElementById('homeworkList').innerHTML = html;
|
||||
|
||||
var totalPages = Math.ceil(total / pageSize);
|
||||
if (totalPages > 1) {
|
||||
document.getElementById('pagination').style.display = 'flex';
|
||||
document.getElementById('pageInfo').textContent = page + ' / ' + totalPages;
|
||||
document.getElementById('prevBtn').disabled = page <= 1;
|
||||
document.getElementById('nextBtn').disabled = page >= totalPages;
|
||||
} else {
|
||||
document.getElementById('pagination').style.display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.changePage = function(delta) {
|
||||
currentPage += delta;
|
||||
loadHomework(currentPage);
|
||||
};
|
||||
|
||||
window.showPublishModal = function() {
|
||||
document.getElementById('publishForm').reset();
|
||||
document.getElementById('hwDeadline').value = new Date().toISOString().split('T')[0];
|
||||
document.getElementById('publishModal').style.display = 'flex';
|
||||
};
|
||||
|
||||
window.submitHomework = async function() {
|
||||
var title = document.getElementById('hwTitle').value.trim();
|
||||
var deadline = document.getElementById('hwDeadline').value;
|
||||
var description = document.getElementById('hwDescription').value.trim();
|
||||
|
||||
if (!title) {
|
||||
showToast('请填写作业标题', 'error');
|
||||
return;
|
||||
}
|
||||
if (!deadline) {
|
||||
showToast('请选择截止日期', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
var res = await apiPost('/api/cadre/homework', {
|
||||
title: title,
|
||||
deadline: deadline,
|
||||
description: description
|
||||
});
|
||||
|
||||
if (res && res.success) {
|
||||
showToast('作业发布成功');
|
||||
closeModal('publishModal');
|
||||
loadHomework(currentPage);
|
||||
} else {
|
||||
showToast(res && res.message ? res.message : '发布失败', 'error');
|
||||
}
|
||||
};
|
||||
|
||||
window.showAbsentModal = async function(assignmentId) {
|
||||
currentAssignmentId = assignmentId;
|
||||
var res = await apiGet('/api/admin/students', { page_size: 1000 });
|
||||
if (res && res.success && res.data) {
|
||||
var students = res.data.students || res.data.items || [];
|
||||
var html = '<div class="form-group"><label>选择缺交学生</label></div>';
|
||||
if (students.length === 0) {
|
||||
html += '<p style="text-align:center;padding:20px;">暂无学生数据</p>';
|
||||
} else {
|
||||
html += '<div class="table-wrapper"><table class="table"><thead><tr>' +
|
||||
'<th><input type="checkbox" id="selectAllAbsent" onchange="toggleAllAbsent(this)"></th>' +
|
||||
'<th>学号</th><th>姓名</th></tr></thead><tbody>';
|
||||
students.forEach(function(s) {
|
||||
html += '<tr>' +
|
||||
'<td><input type="checkbox" class="absent-checkbox" data-id="' + s.student_id + '"></td>' +
|
||||
'<td>' + escapeHtml(s.student_no) + '</td>' +
|
||||
'<td>' + escapeHtml(s.name) + '</td>' +
|
||||
'</tr>';
|
||||
});
|
||||
html += '</tbody></table></div>';
|
||||
}
|
||||
document.getElementById('absentStudentList').innerHTML = html;
|
||||
document.getElementById('absentModal').style.display = 'flex';
|
||||
} else {
|
||||
showToast('获取学生列表失败', 'error');
|
||||
}
|
||||
};
|
||||
|
||||
window.toggleAllAbsent = function(el) {
|
||||
var checkboxes = document.querySelectorAll('.absent-checkbox');
|
||||
checkboxes.forEach(function(cb) { cb.checked = el.checked; });
|
||||
};
|
||||
|
||||
window.submitAbsent = async function() {
|
||||
var checkboxes = document.querySelectorAll('.absent-checkbox:checked');
|
||||
if (checkboxes.length === 0) {
|
||||
showToast('请选择至少一名缺交学生', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
var studentIds = [];
|
||||
checkboxes.forEach(function(cb) {
|
||||
studentIds.push(parseInt(cb.getAttribute('data-id')));
|
||||
});
|
||||
|
||||
var hwDeduct = window.DEDUCTION_HOMEWORK_NOT_SUBMIT || 2;
|
||||
var res = await apiPost('/api/cadre/conduct/add', {
|
||||
student_ids: studentIds,
|
||||
points_change: -hwDeduct,
|
||||
reason: '作业未提交',
|
||||
related_type: 'homework'
|
||||
});
|
||||
|
||||
if (res && res.success) {
|
||||
showToast('已登记 ' + studentIds.length + ' 名学生缺交');
|
||||
closeModal('absentModal');
|
||||
} else {
|
||||
showToast(res && res.message ? res.message : '提交失败', 'error');
|
||||
}
|
||||
};
|
||||
|
||||
window.closeModal = function(id) {
|
||||
document.getElementById(id).style.display = 'none';
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
loadHomework(currentPage);
|
||||
});
|
||||
|
||||
})();
|
||||
425
frontend/assets/js/common.js
Normal file
425
frontend/assets/js/common.js
Normal file
@@ -0,0 +1,425 @@
|
||||
/**
|
||||
* 多班级版班级管理系统 - 公共JS
|
||||
*
|
||||
* 开发者: Canglan
|
||||
* 联系方式: admin@sea-studio.top
|
||||
* 版权归属: Sea Network Technology Studio
|
||||
* 许可证: Apache License 2.0
|
||||
*
|
||||
* 版权所有 © Sea Network Technology Studio
|
||||
*/
|
||||
|
||||
function getToken() {
|
||||
return localStorage.getItem(window.JWT_STORAGE_KEY || 'class_system_token');
|
||||
}
|
||||
|
||||
function getUserInfo() {
|
||||
const userStr = localStorage.getItem(window.USER_STORAGE_KEY || 'class_system_user');
|
||||
if (!userStr) return null;
|
||||
try {
|
||||
return JSON.parse(userStr);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function setUserInfo(user) {
|
||||
localStorage.setItem(window.USER_STORAGE_KEY || 'class_system_user', JSON.stringify(user));
|
||||
}
|
||||
|
||||
function clearAuth() {
|
||||
localStorage.removeItem(window.JWT_STORAGE_KEY || 'class_system_token');
|
||||
localStorage.removeItem(window.USER_STORAGE_KEY || 'class_system_user');
|
||||
}
|
||||
|
||||
async function apiRequest(url, options = {}) {
|
||||
const token = getToken();
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers
|
||||
};
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
const baseUrl = window.API_BASE_URL;
|
||||
const fullUrl = `${baseUrl}${url}`;
|
||||
|
||||
try {
|
||||
const response = await fetch(fullUrl, { ...options, headers });
|
||||
const data = await response.json();
|
||||
|
||||
if (response.status === 401) {
|
||||
clearAuth();
|
||||
|
||||
// 同步清除 PHP Session,防止 index.php 302 重定向循环
|
||||
try {
|
||||
await fetch('/api/clear_session.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn('[Auth] 清除PHP Session失败:', e);
|
||||
}
|
||||
|
||||
// 防循环机制:检查是否已在登录页
|
||||
if (window.location.pathname === '/index.php' || window.location.pathname === '/') {
|
||||
console.warn('[Auth] 已在登录页收到401,停止重定向');
|
||||
return null;
|
||||
}
|
||||
|
||||
// 防循环机制:5秒内重复401则停止重定向
|
||||
const now = Date.now();
|
||||
const lastRedirect = parseInt(sessionStorage.getItem('_last_401_redirect') || '0');
|
||||
if (now - lastRedirect < 5000) {
|
||||
console.warn('[Auth] 5秒内重复401,停止重定向。请检查Token是否有效。');
|
||||
return null;
|
||||
}
|
||||
sessionStorage.setItem('_last_401_redirect', now.toString());
|
||||
|
||||
window.location.href = '/index.php';
|
||||
return null;
|
||||
}
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('API请求错误:', error);
|
||||
showToast('网络错误,请稍后重试', 'error');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function apiGet(url, params = {}) {
|
||||
const queryString = new URLSearchParams(params).toString();
|
||||
const fullUrl = queryString ? `${url}?${queryString}` : url;
|
||||
return apiRequest(fullUrl, { method: 'GET' });
|
||||
}
|
||||
|
||||
function apiPost(url, data = {}) {
|
||||
return apiRequest(url, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
}
|
||||
|
||||
function apiPut(url, data = {}) {
|
||||
return apiRequest(url, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
}
|
||||
|
||||
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')}`;
|
||||
}
|
||||
|
||||
function getStatusBadge(status, type = 'attendance') {
|
||||
const statusMap = {
|
||||
attendance: {
|
||||
'present': '出勤',
|
||||
'absent': '缺勤',
|
||||
'late': '迟到',
|
||||
'leave': '请假'
|
||||
}
|
||||
};
|
||||
const texts = statusMap[type] || statusMap.attendance;
|
||||
const text = texts[status] || status;
|
||||
let className = 'status-badge ';
|
||||
switch (status) {
|
||||
case 'present':
|
||||
className += 'status-submitted';
|
||||
break;
|
||||
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() {
|
||||
// 清除 PHP Session
|
||||
try {
|
||||
await fetch('/api/clear_session.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn('清除Session失败', e);
|
||||
}
|
||||
|
||||
// 清除后端 Token
|
||||
try {
|
||||
await apiPost('/api/auth/logout');
|
||||
} catch (e) {
|
||||
console.warn('后端登出失败', e);
|
||||
}
|
||||
|
||||
// 清除 localStorage
|
||||
clearAuth();
|
||||
|
||||
// 跳转回登录页
|
||||
window.location.href = '/index.php';
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
if (!str) return '';
|
||||
return String(str)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
.replace(/`/g, '`')
|
||||
.replace(/\//g, '/');
|
||||
}
|
||||
|
||||
/**
|
||||
* 智能分页渲染(最多显示7个页码 + 跳转输入框)
|
||||
* @param {string|HTMLElement} container - 分页容器ID或DOM元素
|
||||
* @param {number} currentPage - 当前页码
|
||||
* @param {number} totalPages - 总页数
|
||||
* @param {function} onPageChange - 页码变化回调函数,参数为新的页码
|
||||
*/
|
||||
function renderSmartPagination(container, currentPage, totalPages, onPageChange) {
|
||||
if (typeof container === 'string') {
|
||||
container = document.getElementById(container);
|
||||
}
|
||||
if (!container || totalPages <= 1) {
|
||||
if (container) container.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const MAX_VISIBLE = 7;
|
||||
let html = '';
|
||||
|
||||
// 上一页按钮
|
||||
if (currentPage > 1) {
|
||||
html += `<a href="#" class="page-nav" data-page="${currentPage - 1}">« 上一页</a>`;
|
||||
}
|
||||
|
||||
if (totalPages <= MAX_VISIBLE) {
|
||||
// 总页数不超过最大显示数,全部显示
|
||||
for (let i = 1; i <= totalPages; i++) {
|
||||
if (i === currentPage) {
|
||||
html += `<span class="active">${i}</span>`;
|
||||
} else {
|
||||
html += `<a href="#" data-page="${i}">${i}</a>`;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 需要省略号
|
||||
// 始终显示第1页
|
||||
if (currentPage === 1) {
|
||||
html += `<span class="active">1</span>`;
|
||||
} else {
|
||||
html += `<a href="#" data-page="1">1</a>`;
|
||||
}
|
||||
|
||||
// 计算中间页码范围
|
||||
let start = Math.max(2, currentPage - 2);
|
||||
let end = Math.min(totalPages - 1, currentPage + 2);
|
||||
|
||||
// 调整确保中间至少有3个页码(加上首尾共5-7个)
|
||||
if (currentPage <= 3) {
|
||||
end = Math.min(5, totalPages - 1);
|
||||
}
|
||||
if (currentPage >= totalPages - 2) {
|
||||
start = Math.max(2, totalPages - 4);
|
||||
}
|
||||
|
||||
// 前省略号
|
||||
if (start > 2) {
|
||||
html += `<span class="ellipsis">...</span>`;
|
||||
}
|
||||
|
||||
// 中间页码
|
||||
for (let i = start; i <= end; i++) {
|
||||
if (i === currentPage) {
|
||||
html += `<span class="active">${i}</span>`;
|
||||
} else {
|
||||
html += `<a href="#" data-page="${i}">${i}</a>`;
|
||||
}
|
||||
}
|
||||
|
||||
// 后省略号
|
||||
if (end < totalPages - 1) {
|
||||
html += `<span class="ellipsis">...</span>`;
|
||||
}
|
||||
|
||||
// 始终显示最后一页
|
||||
if (currentPage === totalPages) {
|
||||
html += `<span class="active">${totalPages}</span>`;
|
||||
} else {
|
||||
html += `<a href="#" data-page="${totalPages}">${totalPages}</a>`;
|
||||
}
|
||||
}
|
||||
|
||||
// 下一页按钮
|
||||
if (currentPage < totalPages) {
|
||||
html += `<a href="#" class="page-nav" data-page="${currentPage + 1}">下一页 »</a>`;
|
||||
}
|
||||
|
||||
// 页码跳转
|
||||
html += `<span class="page-jump">跳至 <input type="number" min="1" max="${totalPages}" placeholder="页码"> / ${totalPages}页</span>`;
|
||||
|
||||
container.innerHTML = html;
|
||||
|
||||
// 绑定页码点击事件
|
||||
container.querySelectorAll('a[data-page]').forEach(link => {
|
||||
link.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
const page = parseInt(this.dataset.page);
|
||||
if (page && page !== currentPage && page >= 1 && page <= totalPages) {
|
||||
onPageChange(page);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 绑定跳转输入框事件
|
||||
const jumpInput = container.querySelector('.page-jump input');
|
||||
if (jumpInput) {
|
||||
jumpInput.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
const page = parseInt(this.value);
|
||||
if (page && page >= 1 && page <= totalPages) {
|
||||
onPageChange(page);
|
||||
} else {
|
||||
showToast(`请输入1-${totalPages}之间的页码`, 'warning');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const user = getUserInfo();
|
||||
const userNameSpan = document.getElementById('userName');
|
||||
if (userNameSpan && user) {
|
||||
userNameSpan.textContent = user.real_name || user.username;
|
||||
}
|
||||
const logoutBtn = document.getElementById('logoutBtn');
|
||||
if (logoutBtn) {
|
||||
logoutBtn.addEventListener('click', logout);
|
||||
}
|
||||
});
|
||||
|
||||
function toggleActionDropdown(el) {
|
||||
var dropdown = el.closest('.action-dropdown');
|
||||
if (!dropdown) return;
|
||||
var menu = dropdown.querySelector('.action-dropdown-menu');
|
||||
if (!menu) return;
|
||||
|
||||
var isOpen = menu.classList.contains('show');
|
||||
// 先关闭所有
|
||||
closeAllDropdowns();
|
||||
|
||||
if (!isOpen) {
|
||||
// 使用 fixed 定位,避免被 overflow 容器裁剪
|
||||
var rect = el.getBoundingClientRect();
|
||||
// 先显示以便测量高度
|
||||
menu.classList.add('show');
|
||||
menu.style.position = 'fixed';
|
||||
menu.style.bottom = 'auto';
|
||||
menu.style.right = 'auto';
|
||||
menu.style.transform = 'none';
|
||||
var menuHeight = menu.offsetHeight;
|
||||
// 智能判断:按钮在上半部分则菜单显示在下方,否则显示在上方
|
||||
if (rect.top < window.innerHeight / 2) {
|
||||
menu.style.top = rect.bottom + 'px';
|
||||
} else {
|
||||
menu.style.top = (rect.top - menuHeight) + 'px';
|
||||
}
|
||||
menu.style.left = Math.min(rect.left, window.innerWidth - 130) + 'px';
|
||||
el.classList.add('open');
|
||||
}
|
||||
}
|
||||
|
||||
function closeAllDropdowns() {
|
||||
document.querySelectorAll('.action-dropdown-menu.show').forEach(function(m) {
|
||||
m.classList.remove('show');
|
||||
m.style.position = '';
|
||||
m.style.left = '';
|
||||
m.style.top = '';
|
||||
m.style.bottom = '';
|
||||
m.style.right = '';
|
||||
m.style.transform = '';
|
||||
var toggle = m.closest('.action-dropdown');
|
||||
if (toggle) {
|
||||
var btn = toggle.querySelector('.action-dropdown-toggle');
|
||||
if (btn) btn.classList.remove('open');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('click', function(e) {
|
||||
if (!e.target.closest('.action-dropdown')) {
|
||||
closeAllDropdowns();
|
||||
}
|
||||
});
|
||||
|
||||
// 全局textarea键盘事件:Ctrl+Enter提交表单,Enter换行(默认行为)
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.target.tagName !== 'TEXTAREA') return;
|
||||
|
||||
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
|
||||
// Ctrl+Enter / Cmd+Enter 提交表单
|
||||
e.preventDefault();
|
||||
var form = e.target.closest('form');
|
||||
if (form) {
|
||||
var submitEvent = new Event('submit', { cancelable: true, bubbles: true });
|
||||
form.dispatchEvent(submitEvent);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
window.selectDeductionType = function(points, reason) {
|
||||
var pointsEl = document.getElementById('pointsChange');
|
||||
var reasonEl = document.getElementById('pointsReason');
|
||||
if (points === 0 && reason === '') {
|
||||
// 自定义模式 - 清空分值和原因,聚焦原因输入框
|
||||
if (pointsEl) pointsEl.value = '';
|
||||
if (reasonEl) {
|
||||
reasonEl.value = '';
|
||||
reasonEl.focus();
|
||||
}
|
||||
} else if (points === null || points === undefined) {
|
||||
// 类别模式 - 仅填充原因,聚焦分值输入框
|
||||
if (reasonEl) reasonEl.value = reason;
|
||||
if (pointsEl) {
|
||||
pointsEl.value = '';
|
||||
pointsEl.focus();
|
||||
}
|
||||
} else {
|
||||
// 预设模式 - 同时填充分值和原因
|
||||
if (pointsEl) pointsEl.value = points;
|
||||
if (reasonEl) reasonEl.value = reason;
|
||||
}
|
||||
};
|
||||
253
frontend/assets/js/conduct.js
Normal file
253
frontend/assets/js/conduct.js
Normal file
@@ -0,0 +1,253 @@
|
||||
/**
|
||||
* 多班级版班级管理系统 - 操行分管理页JS
|
||||
*
|
||||
* 开发者: Canglan
|
||||
* 版权归属: Sea Network Technology Studio
|
||||
*
|
||||
* 版权所有 © Sea Network Technology Studio
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
async function loadStudents() {
|
||||
const res = await apiGet('/api/admin/students', {page_size: 1000});
|
||||
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><a href="/admin/history.php?student_id=${student.student_id}" class="link">${escapeHtml(student.name)}</a></td>
|
||||
<td>${student.total_points}</td>
|
||||
<td><button class="btn btn-sm btn-outline js-single-points" data-student-id="${student.student_id}" data-student-name="${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 showSinglePointsModal(studentId, studentName) {
|
||||
window.selectedStudentIds = [studentId];
|
||||
document.getElementById('selectedStudentsCount').innerHTML = `${studentName} (1人)`;
|
||||
document.getElementById('pointsChange').value = '';
|
||||
document.getElementById('pointsReason').value = '';
|
||||
document.getElementById('batchPointsModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
async function exportMoralityRecords() {
|
||||
showToast('正在导出操行分记录...', 'info');
|
||||
|
||||
try {
|
||||
const studentsRes = await apiGet('/api/admin/students', { page_size: 1000 });
|
||||
if (!studentsRes || !studentsRes.success) {
|
||||
showToast('获取学生列表失败', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const students = studentsRes.data.students;
|
||||
if (students.length === 0) {
|
||||
showToast('没有找到学生', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const allRecords = [];
|
||||
let page = 1;
|
||||
let totalPages = 1;
|
||||
do {
|
||||
const historyRes = await apiGet('/api/admin/conduct/history', { page: page, page_size: 500 });
|
||||
if (!historyRes || !historyRes.success) {
|
||||
showToast('获取历史记录失败', 'error');
|
||||
return;
|
||||
}
|
||||
const records = historyRes.data.records || [];
|
||||
allRecords.push(...records);
|
||||
totalPages = historyRes.data.total_pages || 1;
|
||||
page++;
|
||||
} while (page <= totalPages);
|
||||
|
||||
const recordsByStudent = {};
|
||||
allRecords.forEach(record => {
|
||||
const sid = record.student_id;
|
||||
if (!recordsByStudent[sid]) {
|
||||
recordsByStudent[sid] = [];
|
||||
}
|
||||
recordsByStudent[sid].push(record);
|
||||
});
|
||||
|
||||
const studentRecords = [];
|
||||
for (const student of students) {
|
||||
const studentRecords_list = recordsByStudent[student.student_id] || [];
|
||||
const positiveRecords = studentRecords_list.filter(r => r.points_change > 0).map(r => `${r.reason}(+${r.points_change})`);
|
||||
const negativeRecords = studentRecords_list.filter(r => r.points_change < 0).map(r => `${r.reason}(${r.points_change})`);
|
||||
|
||||
studentRecords.push({
|
||||
student_no: student.student_no,
|
||||
name: student.name,
|
||||
total_points: student.total_points || 0,
|
||||
positive_history: positiveRecords.join('; '),
|
||||
negative_history: negativeRecords.join('; ')
|
||||
});
|
||||
}
|
||||
|
||||
function escapeCsvField(field) {
|
||||
if (field === null || field === undefined) return '';
|
||||
let str = String(field).replace(/[\r\n]+/g, ' ');
|
||||
str = str.replace(/"/g, '""');
|
||||
if (/[\,\"\s]/.test(str)) {
|
||||
str = '"' + str + '"';
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
let csv = '\uFEFF';
|
||||
csv += '学号,姓名,分数,加分历史,减分记录\n';
|
||||
studentRecords.forEach(s => {
|
||||
csv += `${escapeCsvField(s.student_no)},${escapeCsvField(s.name)},${escapeCsvField(s.total_points)},${escapeCsvField(s.positive_history)},${escapeCsvField(s.negative_history)}\n`;
|
||||
});
|
||||
|
||||
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `操行分记录_${new Date().toISOString().slice(0,10)}.csv`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
showToast(`导出成功,共${studentRecords.length}名学生`);
|
||||
} catch (err) {
|
||||
showToast('导出失败:' + err.message, 'error');
|
||||
console.error('导出失败:', err);
|
||||
}
|
||||
}
|
||||
// 宿舍集体加分相关
|
||||
let dormitoryStudentIds = [];
|
||||
|
||||
async function showDormitoryPointsModal() {
|
||||
dormitoryStudentIds = [];
|
||||
document.getElementById('dormitorySelect').innerHTML = '<option value="">-- 请选择宿舍 --</option>';
|
||||
document.getElementById('dormitoryStudentsGroup').style.display = 'none';
|
||||
document.getElementById('dormitoryStudentsList').innerHTML = '';
|
||||
document.getElementById('dormitoryPointsChange').value = '';
|
||||
document.getElementById('dormitoryPointsReason').value = '';
|
||||
|
||||
// 加载宿舍列表
|
||||
const res = await apiGet('/api/admin/students/dormitories');
|
||||
if (res && res.success && res.data.dormitories) {
|
||||
const select = document.getElementById('dormitorySelect');
|
||||
res.data.dormitories.forEach(d => {
|
||||
const option = document.createElement('option');
|
||||
option.value = d;
|
||||
option.textContent = d;
|
||||
select.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('dormitoryPointsModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
async function onDormitorySelected() {
|
||||
const dormitory = document.getElementById('dormitorySelect').value;
|
||||
const studentsGroup = document.getElementById('dormitoryStudentsGroup');
|
||||
const studentsList = document.getElementById('dormitoryStudentsList');
|
||||
const studentsCount = document.getElementById('dormitoryStudentsCount');
|
||||
|
||||
dormitoryStudentIds = [];
|
||||
studentsList.innerHTML = '';
|
||||
|
||||
if (!dormitory) {
|
||||
studentsGroup.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
// 加载该宿舍的学生
|
||||
const res = await apiGet('/api/admin/students', { dormitory_number: dormitory, page_size: 1000 });
|
||||
if (res && res.success && res.data.students) {
|
||||
const students = res.data.students;
|
||||
if (students.length === 0) {
|
||||
studentsList.innerHTML = '<p style="color: var(--text-secondary);">该宿舍暂无学生</p>';
|
||||
studentsCount.textContent = '';
|
||||
} else {
|
||||
students.forEach(s => {
|
||||
dormitoryStudentIds.push(s.student_id);
|
||||
const div = document.createElement('div');
|
||||
div.style.cssText = 'display: flex; justify-content: space-between; padding: 4px 0; border-bottom: 1px solid var(--border-color);';
|
||||
div.innerHTML = `<span>${escapeHtml(s.name)}</span><span style="color: var(--text-secondary);">${escapeHtml(s.student_no)}</span>`;
|
||||
studentsList.appendChild(div);
|
||||
});
|
||||
studentsCount.textContent = `共 ${students.length} 人`;
|
||||
}
|
||||
studentsGroup.style.display = 'block';
|
||||
} else {
|
||||
studentsList.innerHTML = '<p style="color: var(--text-secondary);">加载失败</p>';
|
||||
studentsGroup.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
async function submitDormitoryPoints() {
|
||||
if (dormitoryStudentIds.length === 0) {
|
||||
showToast('该宿舍没有学生', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const pointsChange = parseInt(document.getElementById('dormitoryPointsChange').value);
|
||||
const reason = document.getElementById('dormitoryPointsReason').value;
|
||||
|
||||
if (isNaN(pointsChange) || pointsChange === 0) {
|
||||
showToast('分值不能为0', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (Math.abs(pointsChange) > 100) {
|
||||
showToast('分值绝对值不能超过100', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!reason.trim()) {
|
||||
showToast('请填写原因', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const data = {
|
||||
student_ids: dormitoryStudentIds,
|
||||
points_change: pointsChange,
|
||||
reason: reason,
|
||||
related_type: 'manual'
|
||||
};
|
||||
|
||||
const res = await apiPost('/api/admin/conduct/add', data);
|
||||
|
||||
if (res && res.success) {
|
||||
showToast(`操作成功: ${res.data.success_count} 人成功`);
|
||||
closeModal('dormitoryPointsModal');
|
||||
loadStudents();
|
||||
} else {
|
||||
showToast(res?.message || '操作失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
loadStudents();
|
||||
|
||||
document.getElementById('studentList').addEventListener('click', function(e) {
|
||||
const btn = e.target.closest('.js-single-points');
|
||||
if (btn) {
|
||||
showSinglePointsModal(
|
||||
parseInt(btn.dataset.studentId, 10),
|
||||
btn.dataset.studentName
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
window.loadStudents = loadStudents;
|
||||
window.showSinglePointsModal = showSinglePointsModal;
|
||||
window.exportMoralityRecords = exportMoralityRecords;
|
||||
window.showDormitoryPointsModal = showDormitoryPointsModal;
|
||||
window.onDormitorySelected = onDormitorySelected;
|
||||
window.submitDormitoryPoints = submitDormitoryPoints;
|
||||
|
||||
})();
|
||||
120
frontend/assets/js/dashboard.js
Normal file
120
frontend/assets/js/dashboard.js
Normal file
@@ -0,0 +1,120 @@
|
||||
/**
|
||||
* 多班级版班级管理系统 - 管理端首页JS
|
||||
*
|
||||
* 开发者: Canglan
|
||||
* 版权归属: Sea Network Technology Studio
|
||||
*
|
||||
* 版权所有 © Sea Network Technology Studio
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
const role = window.PAGE_CONFIG.role;
|
||||
let totalStudents = 0;
|
||||
|
||||
async function loadDashboard() {
|
||||
// 并行加载学生数据和学期信息
|
||||
const [studentsRes, semesterRes] = await Promise.all([
|
||||
apiGet('/api/admin/students'),
|
||||
apiGet('/api/semester/active')
|
||||
]);
|
||||
|
||||
let statsHtml = '';
|
||||
if (studentsRes && studentsRes.success) {
|
||||
statsHtml += `
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">学生总数</div>
|
||||
<div class="stat-value">${studentsRes.data.total || 0}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// 显示学期信息和当前周数
|
||||
if (semesterRes && semesterRes.success && semesterRes.data) {
|
||||
const sem = semesterRes.data;
|
||||
const weekNum = sem.current_week;
|
||||
let semesterInfo = escapeHtml(sem.semester_name);
|
||||
if (weekNum && weekNum > 0) {
|
||||
semesterInfo += ` · 第${weekNum}周`;
|
||||
}
|
||||
statsHtml += `
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">当前学期</div>
|
||||
<div class="stat-value" style="font-size:20px;">${semesterInfo}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
document.getElementById('dashboardStats').innerHTML = statsHtml;
|
||||
|
||||
let quickActions = '';
|
||||
if (role === '班主任' || role === '班长' || role === '学习委员' || role === '劳动委员' || role === '志愿委员') {
|
||||
quickActions += '<button class="btn btn-primary" onclick="location.href=\'/admin/conduct.php\'">操行分管理</button>';
|
||||
}
|
||||
if (role === '班主任' || role === '学习委员') {
|
||||
quickActions += '<button class="btn btn-primary" onclick="location.href=\'/admin/homework.php\'">作业扣分</button>';
|
||||
}
|
||||
if (role === '班主任' || role === '考勤委员') {
|
||||
quickActions += '<button class="btn btn-primary" onclick="location.href=\'/admin/attendance.php\'">考勤管理</button>';
|
||||
}
|
||||
if (role === '班主任') {
|
||||
quickActions += '<button class="btn btn-outline" onclick="location.href=\'/admin/students.php\'">导入学生</button>';
|
||||
quickActions += '<button class="btn btn-secondary" onclick="location.href=\'/admin/conduct.php\'">导出德育分记录</button>';
|
||||
}
|
||||
document.getElementById('quickActions').innerHTML = quickActions || '<p>暂无快捷操作</p>';
|
||||
|
||||
const rankingRes = await apiGet('/api/student/ranking', { limit: 100 });
|
||||
if (rankingRes && rankingRes.success) {
|
||||
totalStudents = rankingRes.data.total_students || 0;
|
||||
let html = '';
|
||||
rankingRes.data.ranking.forEach((student, index) => {
|
||||
const rank = index + 1;
|
||||
html += `<tr>
|
||||
<td>${rank}</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;
|
||||
}
|
||||
}
|
||||
|
||||
function applyPercentileFilter() {
|
||||
const input = document.getElementById('percentileFilter');
|
||||
const percentile = parseInt(input.value);
|
||||
if (isNaN(percentile) || percentile < 1 || percentile > 100) {
|
||||
showToast('请输入 1-100 之间的整数', 'error');
|
||||
return;
|
||||
}
|
||||
const rows = document.getElementById('rankingList').querySelectorAll('tr');
|
||||
if (rows.length === 0) return;
|
||||
const showCount = Math.max(1, Math.floor(totalStudents * (percentile / 100)));
|
||||
rows.forEach(function(row, index) {
|
||||
row.style.display = index < showCount ? '' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
function resetPercentileFilter() {
|
||||
document.getElementById('percentileFilter').value = 100;
|
||||
const rows = document.getElementById('rankingList').querySelectorAll('tr');
|
||||
rows.forEach(function(row) {
|
||||
row.style.display = '';
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('percentileFilter').addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') applyPercentileFilter();
|
||||
});
|
||||
|
||||
loadDashboard();
|
||||
|
||||
window.loadDashboard = loadDashboard;
|
||||
window.applyPercentileFilter = applyPercentileFilter;
|
||||
window.resetPercentileFilter = resetPercentileFilter;
|
||||
|
||||
})();
|
||||
345
frontend/assets/js/history.js
Normal file
345
frontend/assets/js/history.js
Normal file
@@ -0,0 +1,345 @@
|
||||
/**
|
||||
* 多班级版班级管理系统 - 历史记录页JS
|
||||
*
|
||||
* 开发者: Canglan
|
||||
* 版权归属: Sea Network Technology Studio
|
||||
*
|
||||
* 版权所有 © Sea Network Technology Studio
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
const role = window.PAGE_CONFIG.role;
|
||||
const currentUserId = window.PAGE_CONFIG.userId;
|
||||
let currentHistoryPage = 1;
|
||||
let totalHistoryPages = 1;
|
||||
|
||||
function getTypeLabel(relatedType) {
|
||||
if (!relatedType) return '操行';
|
||||
switch (relatedType) {
|
||||
case 'conduct': return '操行';
|
||||
case 'homework': return '作业';
|
||||
case 'attendance': return '考勤';
|
||||
default: return relatedType;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadStudentsForSelect() {
|
||||
const res = await apiGet('/api/admin/students', {page_size: 1000});
|
||||
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 loadSubjectsForFilter() {
|
||||
let subjectSelect = document.getElementById('historySubjectFilter');
|
||||
if (!subjectSelect) return;
|
||||
let res = await apiGet('/api/subject/list', { is_active: true });
|
||||
if (res && res.success && res.data && res.data.subjects) {
|
||||
let html = '<option value="">全部科目</option>';
|
||||
res.data.subjects.forEach(s => {
|
||||
html += '<option value="' + escapeHtml(s.subject_name) + '">' + escapeHtml(s.subject_name) + '</option>';
|
||||
});
|
||||
subjectSelect.innerHTML = html;
|
||||
}
|
||||
}
|
||||
|
||||
// 筛选学生时自动取消合并记录
|
||||
function onStudentFilterChange() {
|
||||
let studentId = document.getElementById('historyStudentId').value;
|
||||
if (studentId) {
|
||||
let grouped = document.getElementById('historyGrouped');
|
||||
if (grouped) grouped.checked = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 科目筛选变化时,取消扣分类型筛选(互斥)
|
||||
function onSubjectFilterChange() {
|
||||
let subjectVal = document.getElementById('historySubjectFilter').value;
|
||||
if (subjectVal) {
|
||||
document.getElementById('historyReasonFilter').value = '';
|
||||
}
|
||||
}
|
||||
|
||||
// 折叠/展开筛选面板
|
||||
function toggleFilterPanel() {
|
||||
let panel = document.getElementById('advancedFilters');
|
||||
let btn = document.getElementById('filterToggleBtn');
|
||||
if (!panel || !btn) return;
|
||||
if (panel.style.display === 'none') {
|
||||
panel.style.display = 'block';
|
||||
btn.textContent = '收起筛选 ▲';
|
||||
} else {
|
||||
panel.style.display = 'none';
|
||||
btn.textContent = '展开筛选 ▼';
|
||||
}
|
||||
}
|
||||
|
||||
async function loadHistory(page) {
|
||||
page = page || 1;
|
||||
currentHistoryPage = page;
|
||||
let startDate = document.getElementById('historyStartDate').value;
|
||||
let endDate = document.getElementById('historyEndDate').value;
|
||||
let studentId = document.getElementById('historyStudentId').value;
|
||||
let reasonFilter = document.getElementById('historyReasonFilter').value;
|
||||
let subjectFilter = document.getElementById('historySubjectFilter').value;
|
||||
let reasonSearch = document.getElementById('historyReasonSearch').value.trim();
|
||||
let isGrouped = document.getElementById('historyGrouped').checked;
|
||||
let statusFilter = document.getElementById('historyStatusFilter') ? document.getElementById('historyStatusFilter').value : '';
|
||||
|
||||
// 筛选学生时强制取消合并
|
||||
if (studentId) isGrouped = false;
|
||||
|
||||
let params = {
|
||||
page: page, page_size: 20,
|
||||
start_date: startDate,
|
||||
end_date: endDate
|
||||
};
|
||||
if (studentId) params.student_id = studentId;
|
||||
|
||||
// 科目筛选优先于扣分类型筛选
|
||||
if (subjectFilter) {
|
||||
params.reason_prefix = '[' + subjectFilter + ']';
|
||||
} else if (reasonFilter) {
|
||||
params.reason_prefix = reasonFilter;
|
||||
}
|
||||
|
||||
if (reasonSearch) params.reason_search = reasonSearch;
|
||||
if (isGrouped) params.grouped = true;
|
||||
if (statusFilter !== '') params.is_revoked = parseInt(statusFilter);
|
||||
|
||||
let res = await apiGet('/api/admin/conduct/history', params);
|
||||
|
||||
if (res && res.success) {
|
||||
let nowrapStyle = ' style="white-space:nowrap;min-width:80px;"';
|
||||
let headHtml = '';
|
||||
if (isGrouped) {
|
||||
headHtml = '<th>类型</th><th>分值</th><th>原因</th><th>学生名单</th><th' + nowrapStyle + '>操作人</th><th>时间</th>';
|
||||
if (role === '班主任' || role === '班长') {
|
||||
headHtml += '<th>操作</th>';
|
||||
}
|
||||
} else {
|
||||
headHtml = '<th>类型</th><th>分值</th><th>原因</th><th>学生</th><th' + nowrapStyle + '>操作人</th><th>时间</th>';
|
||||
if (role === '班主任' || role === '班长' || role === '考勤委员') {
|
||||
headHtml += '<th>操作</th>';
|
||||
}
|
||||
}
|
||||
document.getElementById('historyTableHead').innerHTML = headHtml;
|
||||
|
||||
let html = '';
|
||||
if (isGrouped) {
|
||||
res.data.records.forEach(function(record) {
|
||||
let pointsClass = record.points_change > 0 ? 'plus' : 'minus';
|
||||
let names = record.student_names || '';
|
||||
let allRevoked = record.all_revoked;
|
||||
let revokedStyle = allRevoked ? ' style="opacity:0.5;text-decoration:line-through;"' : '';
|
||||
html += '<tr' + revokedStyle + '>' +
|
||||
'<td>' + escapeHtml(getTypeLabel(record.related_type)) + '</td>' +
|
||||
'<td class="' + pointsClass + '">' + (record.points_change > 0 ? '+' : '') + record.points_change + '×' + record.student_count + '</td>' +
|
||||
'<td class="history-reason">' + escapeHtml(record.reason) + '</td>' +
|
||||
'<td class="history-students">' + escapeHtml(names) + '</td>' +
|
||||
'<td>' + escapeHtml(record.recorder_name || '') + '</td>' +
|
||||
'<td class="history-time">' + formatDateTime(record.created_at) + '</td>';
|
||||
if (role === '班主任' || role === '班长') {
|
||||
if (allRevoked) {
|
||||
html += '<td><span class="text-muted">已撤销</span></td>';
|
||||
} else {
|
||||
html += '<td><button class="btn btn-sm btn-outline" onclick="batchRevokeGrouped(\'' + escapeHtml(record.reason).replace(/'/g, "\\'") + '\',' + record.points_change + ',\'' + escapeHtml(record.recorder_name || '').replace(/'/g, "\\'") + '\',\'' + formatDateTime(record.created_at) + '\')">批量撤销</button></td>';
|
||||
}
|
||||
}
|
||||
html += '</tr>';
|
||||
});
|
||||
if (res.data.records.length === 0) {
|
||||
let colSpan = (role === '班主任' || role === '班长') ? 7 : 6;
|
||||
html = '<tr><td colspan="' + colSpan + '" style="text-align:center;">暂无记录</td></tr>';
|
||||
}
|
||||
} else {
|
||||
res.data.records.forEach(function(record) {
|
||||
let pointsClass = record.points_change > 0 ? 'plus' : 'minus';
|
||||
let revokedStyle = record.is_revoked == 1 ? ' style="opacity:0.5;text-decoration:line-through;"' : '';
|
||||
html += '<tr' + revokedStyle + '>' +
|
||||
'<td>' + escapeHtml(getTypeLabel(record.related_type)) + '</td>' +
|
||||
'<td class="' + pointsClass + '">' + (record.points_change > 0 ? '+' : '') + record.points_change + '</td>' +
|
||||
'<td class="history-reason">' + escapeHtml(record.reason) + '</td>' +
|
||||
'<td>' + escapeHtml(record.student_name) + '</td>' +
|
||||
'<td>' + escapeHtml(record.recorder_name) + '</td>' +
|
||||
'<td class="history-time">' + formatDateTime(record.created_at) + '</td>';
|
||||
if (role === '班主任') {
|
||||
if (record.is_revoked == 1) {
|
||||
let revokerInfo = record.revoker_name ? '由 ' + escapeHtml(record.revoker_name) + ' 撤销' : '已撤销';
|
||||
html += '<td><span class="text-muted" style="margin-right:4px;">' + revokerInfo + '</span><button class="btn btn-sm btn-outline" onclick="restoreRecord(' + record.record_id + ')">反撤销</button></td>';
|
||||
} else {
|
||||
html += '<td><button class="btn btn-sm btn-outline" onclick="revokeRecord(' + record.record_id + ')">撤销</button></td>';
|
||||
}
|
||||
} else if (role === '班长') {
|
||||
if (record.is_revoked == 1) {
|
||||
let revokerInfo = record.revoker_name ? '由 ' + escapeHtml(record.revoker_name) + ' 撤销' : '已撤销';
|
||||
html += '<td><span class="text-muted">' + revokerInfo + '</span></td>';
|
||||
} else {
|
||||
html += '<td><button class="btn btn-sm btn-outline" onclick="revokeRecord(' + record.record_id + ')">撤销</button></td>';
|
||||
}
|
||||
} else if (role === '考勤委员') {
|
||||
if (record.is_revoked == 1) {
|
||||
html += '<td><span class="text-muted">已撤销</span></td>';
|
||||
} else if (record.recorder_id == currentUserId) {
|
||||
html += '<td><button class="btn btn-sm btn-outline" onclick="revokeRecord(' + record.record_id + ')">撤销</button></td>';
|
||||
} else {
|
||||
html += '<td><span class="text-muted">-</span></td>';
|
||||
}
|
||||
}
|
||||
html += '</tr>';
|
||||
});
|
||||
|
||||
if (res.data.records.length === 0) {
|
||||
let colSpan = (role === '班主任' || role === '班长' || role === '考勤委员') ? 7 : 6;
|
||||
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() {
|
||||
renderSmartPagination('historyPagination', currentHistoryPage, totalHistoryPages, function(page) {
|
||||
loadHistory(page);
|
||||
});
|
||||
}
|
||||
|
||||
async function exportHistoryRecords() {
|
||||
let startDate = document.getElementById('historyStartDate').value;
|
||||
let endDate = document.getElementById('historyEndDate').value;
|
||||
let studentId = document.getElementById('historyStudentId').value;
|
||||
|
||||
showToast('正在导出历史记录...', 'info');
|
||||
|
||||
try {
|
||||
let reasonFilter = document.getElementById('historyReasonFilter').value;
|
||||
let subjectFilter = document.getElementById('historySubjectFilter').value;
|
||||
let reasonSearch = document.getElementById('historyReasonSearch').value.trim();
|
||||
let params = { page: 1, page_size: 1000 };
|
||||
if (startDate) params.start_date = startDate;
|
||||
if (endDate) params.end_date = endDate;
|
||||
if (studentId) params.student_id = studentId;
|
||||
if (subjectFilter) {
|
||||
params.reason_prefix = '[' + subjectFilter + ']';
|
||||
} else if (reasonFilter) {
|
||||
params.reason_prefix = reasonFilter;
|
||||
}
|
||||
if (reasonSearch) params.reason_search = reasonSearch;
|
||||
|
||||
let res = await apiGet('/api/admin/conduct/history', params);
|
||||
if (res && res.success && res.data.records) {
|
||||
let records = res.data.records;
|
||||
if (records.length === 0) {
|
||||
showToast('没有找到记录', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
function csvField(val) {
|
||||
let s = String(val == null ? '' : val);
|
||||
if (s.indexOf(',') >= 0 || s.indexOf('"') >= 0 || s.indexOf('\n') >= 0) {
|
||||
return '"' + s.replace(/"/g, '""') + '"';
|
||||
}
|
||||
return s;
|
||||
}
|
||||
let csv = '\uFEFF';
|
||||
csv += '时间,学号,姓名,分数变动,原因,操作人\n';
|
||||
records.forEach(function(r) {
|
||||
if (r.is_revoked == 1) return;
|
||||
csv += csvField(r.created_at) + ',' + csvField(r.student_no) + ',' + csvField(r.student_name) + ',' + csvField((r.points_change > 0 ? '+' : '') + r.points_change) + ',' + csvField(r.reason) + ',' + csvField(r.recorder_name) + '\n';
|
||||
});
|
||||
|
||||
let blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
||||
let url = URL.createObjectURL(blob);
|
||||
let link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = '历史记录_' + new Date().toISOString().slice(0,10) + '.csv';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
showToast('导出成功,共' + records.length + '条记录');
|
||||
} else {
|
||||
showToast('导出失败:' + (res && res.message || '未知错误'), 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
showToast('导出失败:' + err.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 批量撤销合并记录(按条件查找并撤销)
|
||||
async function batchRevokeGrouped(reason, pointsChange, recorderName, createdAt) {
|
||||
if (!confirm('确定要撤销所有"' + reason + '"(' + (pointsChange > 0 ? '+' : '') + pointsChange + '分)的记录吗?')) return;
|
||||
|
||||
showToast('正在批量撤销...', 'info');
|
||||
|
||||
try {
|
||||
let params = {
|
||||
page: 1, page_size: 1000,
|
||||
start_date: document.getElementById('historyStartDate').value,
|
||||
end_date: document.getElementById('historyEndDate').value,
|
||||
reason_prefix: reason,
|
||||
grouped: false
|
||||
};
|
||||
|
||||
let res = await apiGet('/api/admin/conduct/history', params);
|
||||
if (!res || !res.success || !res.data.records) {
|
||||
showToast('查询记录失败', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
let matchedIds = [];
|
||||
res.data.records.forEach(function(r) {
|
||||
if (r.reason === reason && r.points_change === pointsChange && r.is_revoked == 0) {
|
||||
matchedIds.push(r.record_id);
|
||||
}
|
||||
});
|
||||
|
||||
if (matchedIds.length === 0) {
|
||||
showToast('没有找到可撤销的记录', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
let revokeRes = await apiPost('/api/admin/conduct/batch-revoke', { record_ids: matchedIds });
|
||||
if (revokeRes && revokeRes.success) {
|
||||
showToast('批量撤销完成: ' + (revokeRes.data ? revokeRes.data.success_count : 0) + '条成功');
|
||||
loadHistory(currentHistoryPage);
|
||||
} else {
|
||||
showToast(revokeRes && revokeRes.message || '批量撤销失败', 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
showToast('批量撤销失败: ' + err.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化:并行加载学生和科目列表,然后加载历史记录
|
||||
Promise.all([loadStudentsForSelect(), loadSubjectsForFilter()]).then(function() {
|
||||
let urlParams = new URLSearchParams(window.location.search);
|
||||
let preStudentId = urlParams.get('student_id');
|
||||
if (preStudentId) {
|
||||
document.getElementById('historyStudentId').value = preStudentId;
|
||||
onStudentFilterChange();
|
||||
}
|
||||
loadHistory();
|
||||
});
|
||||
|
||||
window.loadHistory = loadHistory;
|
||||
window.loadStudentsForSelect = loadStudentsForSelect;
|
||||
window.exportHistoryRecords = exportHistoryRecords;
|
||||
window.batchRevokeGrouped = batchRevokeGrouped;
|
||||
window.onStudentFilterChange = onStudentFilterChange;
|
||||
window.onSubjectFilterChange = onSubjectFilterChange;
|
||||
window.toggleFilterPanel = toggleFilterPanel;
|
||||
|
||||
})();
|
||||
266
frontend/assets/js/homework-manage.js
Normal file
266
frontend/assets/js/homework-manage.js
Normal file
@@ -0,0 +1,266 @@
|
||||
/**
|
||||
* 多班级版班级管理系统 - 作业扣分页JS
|
||||
*
|
||||
* 开发者: Canglan
|
||||
* 版权归属: Sea Network Technology Studio
|
||||
*
|
||||
* 版权所有 © Sea Network Technology Studio
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
const hwRole = window.PAGE_CONFIG.role;
|
||||
|
||||
// 初始化扣分配置
|
||||
const hwMaxPoints = hwRole === '班主任' ? 100 : 5;
|
||||
const hwNotSubmit = window.DEDUCTION_HOMEWORK_NOT_SUBMIT || 2;
|
||||
const hwLate = window.DEDUCTION_HOMEWORK_LATE || 1;
|
||||
|
||||
// 更新页面中的配置值显示
|
||||
document.querySelectorAll('.hw-not-submit').forEach(el => el.textContent = hwNotSubmit);
|
||||
document.querySelectorAll('.hw-late').forEach(el => el.textContent = hwLate);
|
||||
document.querySelectorAll('.hw-max').forEach(el => el.textContent = hwMaxPoints);
|
||||
|
||||
// 更新输入框的 min/max
|
||||
document.getElementById('pointsChange').setAttribute('min', -hwMaxPoints);
|
||||
document.getElementById('pointsChange').setAttribute('max', hwMaxPoints);
|
||||
|
||||
// 加载科目列表(学习委员)
|
||||
async function loadSubjectsForHomework() {
|
||||
const subjectSelect = document.getElementById('hwSubjectSelect');
|
||||
if (!subjectSelect) return;
|
||||
const res = await apiGet('/api/subject/list');
|
||||
if (res && res.success && res.data && res.data.subjects) {
|
||||
let html = '<option value="">不选择科目</option>';
|
||||
res.data.subjects.forEach(s => {
|
||||
html += `<option value="${escapeHtml(s.subject_name)}">${escapeHtml(s.subject_name)}</option>`;
|
||||
});
|
||||
subjectSelect.innerHTML = html;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadStudents() {
|
||||
const res = await apiGet('/api/admin/students', {page_size: 1000});
|
||||
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-outline" 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 showSinglePointsModal(studentId, studentName) {
|
||||
window.selectedStudentIds = [studentId];
|
||||
document.getElementById('selectedStudentsCount').innerHTML = `${studentName} (1人)`;
|
||||
document.getElementById('pointsChange').value = '';
|
||||
document.getElementById('pointsReason').value = '';
|
||||
document.getElementById('batchPointsModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
function handleSubmitPoints() {
|
||||
const pointsChange = parseInt(document.getElementById('pointsChange').value);
|
||||
if (isNaN(pointsChange) || pointsChange === 0) {
|
||||
showToast('请输入有效的加减分值', 'warning');
|
||||
return;
|
||||
}
|
||||
if (Math.abs(pointsChange) > hwMaxPoints) {
|
||||
showToast(`每次加减分不超过${hwMaxPoints}分`, 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
// 学习委员附加科目前缀、具体作业和缴交时间
|
||||
if (hwRole === '学习委员' || hwRole === '班主任') {
|
||||
const subjectSelect = document.getElementById('hwSubjectSelect');
|
||||
const subjectName = subjectSelect ? subjectSelect.value : '';
|
||||
const hwTitle = document.getElementById('hwTitle').value.trim();
|
||||
const hwDeadline = document.getElementById('hwDeadline').value;
|
||||
const reasonEl = document.getElementById('pointsReason');
|
||||
|
||||
let prefix = '';
|
||||
if (subjectName) {
|
||||
prefix = `[${subjectName}]`;
|
||||
}
|
||||
if (hwTitle) {
|
||||
prefix += `[${hwTitle}]`;
|
||||
}
|
||||
if (hwDeadline) {
|
||||
prefix += ` 缴交:${hwDeadline}`;
|
||||
}
|
||||
if (prefix) {
|
||||
reasonEl.value = prefix + ' ' + reasonEl.value;
|
||||
}
|
||||
}
|
||||
|
||||
submitBatchPoints({ related_type: 'homework' });
|
||||
}
|
||||
|
||||
// ========== 科目管理功能 ==========
|
||||
|
||||
function toggleSubjectPanel() {
|
||||
const content = document.getElementById('subjectPanelContent');
|
||||
const toggle = document.getElementById('subjectPanelToggle');
|
||||
if (!content || !toggle) return;
|
||||
|
||||
const isExpanded = content.classList.contains('expanded');
|
||||
if (isExpanded) {
|
||||
content.classList.remove('expanded');
|
||||
toggle.classList.remove('expanded');
|
||||
toggle.textContent = '▶ 展开';
|
||||
} else {
|
||||
content.classList.add('expanded');
|
||||
toggle.classList.add('expanded');
|
||||
toggle.textContent = '▼ 收起';
|
||||
loadSubjectList();
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSubjectList() {
|
||||
const res = await apiGet('/api/subject/list', { is_active: true });
|
||||
if (res && res.success && res.data) {
|
||||
let html = '';
|
||||
const subjects = res.data.subjects || [];
|
||||
subjects.forEach(sub => {
|
||||
const safeName = escapeHtml(sub.subject_name || '');
|
||||
const safeCode = escapeHtml(sub.subject_code || '');
|
||||
const sortOrder = sub.sort_order || 0;
|
||||
html += `
|
||||
<div class="subject-item">
|
||||
<span class="subject-name">${safeName}</span>
|
||||
<span class="subject-code">${safeCode}</span>
|
||||
<span class="subject-status ${sub.is_active ? 'subject-status-active' : 'subject-status-inactive'}">
|
||||
${sub.is_active ? '启用' : '禁用'}
|
||||
</span>
|
||||
<button class="btn btn-sm btn-outline" onclick="showEditSubjectModal(${sub.subject_id}, '${safeName.replace(/'/g, "\\'")}', '${safeCode.replace(/'/g, "\\'")}', ${sortOrder})">编辑</button>
|
||||
<button class="btn btn-sm btn-ghost" onclick="toggleSubjectStatus(${sub.subject_id}, ${sub.is_active ? 'false' : 'true'})">
|
||||
${sub.is_active ? '禁用' : '启用'}
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-danger" onclick="deleteSubject(${sub.subject_id})">删除</button>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
if (subjects.length === 0) {
|
||||
html = '<p style="text-align:center;padding:40px;">暂无科目,请点击"添加科目"</p>';
|
||||
}
|
||||
document.getElementById('subjectList').innerHTML = html;
|
||||
}
|
||||
}
|
||||
|
||||
function showAddSubjectModal() {
|
||||
const form = document.getElementById('addSubjectFormInHw');
|
||||
if (form) form.reset();
|
||||
document.getElementById('addSubjectModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
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');
|
||||
loadSubjectList();
|
||||
loadSubjectsForHomework();
|
||||
} else {
|
||||
showToast(res?.message || '添加失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleSubjectStatus(subjectId, enable) {
|
||||
const res = await apiPut(`/api/subject/toggle/${subjectId}`, { is_active: enable });
|
||||
if (res && res.success) {
|
||||
showToast(enable ? '科目已启用' : '科目已禁用');
|
||||
loadSubjectList();
|
||||
loadSubjectsForHomework();
|
||||
} else {
|
||||
showToast(res?.message || '操作失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteSubject(subjectId) {
|
||||
if (!confirm('确定要删除该科目吗?如果科目下有作业数据将无法删除。')) return;
|
||||
const res = await apiDelete('/api/subject/delete/' + subjectId);
|
||||
if (res && res.success) {
|
||||
showToast('科目删除成功');
|
||||
loadSubjectList();
|
||||
loadSubjectsForHomework();
|
||||
} else {
|
||||
showToast(res?.message || '删除失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function showEditSubjectModal(subjectId, name, code, sortOrder) {
|
||||
document.getElementById('editSubjectId').value = subjectId;
|
||||
document.getElementById('editSubjectName').value = name;
|
||||
document.getElementById('editSubjectCode').value = code;
|
||||
document.getElementById('editSubjectSortOrder').value = sortOrder;
|
||||
document.getElementById('editSubjectModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
async function submitEditSubject() {
|
||||
const subjectId = document.getElementById('editSubjectId').value;
|
||||
const subjectName = document.getElementById('editSubjectName').value.trim();
|
||||
const subjectCode = document.getElementById('editSubjectCode').value.trim();
|
||||
const sortOrder = document.getElementById('editSubjectSortOrder').value;
|
||||
|
||||
if (!subjectName) {
|
||||
showToast('请填写科目名称', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const data = { subject_name: subjectName };
|
||||
if (subjectCode) data.subject_code = subjectCode;
|
||||
if (sortOrder !== '') data.sort_order = parseInt(sortOrder);
|
||||
|
||||
const res = await apiPut(`/api/subject/update/${subjectId}`, data);
|
||||
if (res && res.success) {
|
||||
showToast('科目更新成功');
|
||||
closeModal('editSubjectModal');
|
||||
loadSubjectList();
|
||||
loadSubjectsForHomework();
|
||||
} else {
|
||||
showToast(res?.message || '更新失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 绑定科目管理折叠面板
|
||||
var subjectHeader = document.getElementById('subjectPanelHeader');
|
||||
if (subjectHeader) {
|
||||
subjectHeader.addEventListener('click', toggleSubjectPanel);
|
||||
}
|
||||
|
||||
loadStudents();
|
||||
loadSubjectsForHomework();
|
||||
|
||||
window.loadStudents = loadStudents;
|
||||
window.showSinglePointsModal = showSinglePointsModal;
|
||||
window.handleSubmitPoints = handleSubmitPoints;
|
||||
window.toggleSubjectPanel = toggleSubjectPanel;
|
||||
window.showAddSubjectModal = showAddSubjectModal;
|
||||
window.submitAddSubject = submitAddSubject;
|
||||
window.toggleSubjectStatus = toggleSubjectStatus;
|
||||
window.deleteSubject = deleteSubject;
|
||||
window.showEditSubjectModal = showEditSubjectModal;
|
||||
window.submitEditSubject = submitEditSubject;
|
||||
|
||||
})();
|
||||
53
frontend/assets/js/modules/admin-mgmt.js
Normal file
53
frontend/assets/js/modules/admin-mgmt.js
Normal file
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* 多班级版班级管理系统 - 管理员管理函数
|
||||
*
|
||||
* 开发者: Canglan
|
||||
* 联系方式: admin@sea-studio.top
|
||||
* 版权归属: Sea Network Technology Studio
|
||||
* 许可证: Apache License 2.0
|
||||
*
|
||||
* 版权所有 © Sea Network Technology Studio
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// 显示添加管理员模态框
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
window.showAddAdminModal = showAddAdminModal;
|
||||
window.submitAddAdmin = submitAddAdmin;
|
||||
})();
|
||||
24
frontend/assets/js/modules/modal-utils.js
Normal file
24
frontend/assets/js/modules/modal-utils.js
Normal file
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* 多班级版班级管理系统 - 模态框工具函数
|
||||
*
|
||||
* 开发者: Canglan
|
||||
* 联系方式: admin@sea-studio.top
|
||||
* 版权归属: Sea Network Technology Studio
|
||||
* 许可证: Apache License 2.0
|
||||
*
|
||||
* 版权所有 © Sea Network Technology Studio
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// 关闭模态框
|
||||
function closeModal(modalId) {
|
||||
const modal = document.getElementById(modalId);
|
||||
if (modal) {
|
||||
modal.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
window.closeModal = closeModal;
|
||||
})();
|
||||
102
frontend/assets/js/modules/points-mgmt.js
Normal file
102
frontend/assets/js/modules/points-mgmt.js
Normal file
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* 多班级版班级管理系统 - 加减分管理函数
|
||||
*
|
||||
* 开发者: Canglan
|
||||
* 联系方式: admin@sea-studio.top
|
||||
* 版权归属: Sea Network Technology Studio
|
||||
* 许可证: Apache License 2.0
|
||||
*
|
||||
* 版权所有 © Sea Network Technology Studio
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// 全局变量
|
||||
var selectedStudentIds = [];
|
||||
var 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(options = {}) {
|
||||
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 data = {
|
||||
student_ids: selectedStudentIds,
|
||||
points_change: pointsChange,
|
||||
reason: reason
|
||||
};
|
||||
if (options.related_type) {
|
||||
data.related_type = options.related_type;
|
||||
}
|
||||
const res = await apiPost('/api/admin/conduct/add', data);
|
||||
|
||||
if (res && res.success) {
|
||||
showToast(`操作成功: ${res.data.success_count} 人成功`);
|
||||
closeModal('batchPointsModal');
|
||||
loadStudents();
|
||||
if (typeof loadConductStudents === 'function') loadConductStudents();
|
||||
} 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');
|
||||
}
|
||||
}
|
||||
|
||||
// 反撤销(恢复)记录
|
||||
async function restoreRecord(recordId) {
|
||||
if (!confirm('确定要反撤销这条记录吗?分数变动将重新生效。')) return;
|
||||
|
||||
const res = await apiPost('/api/admin/conduct/restore', { record_id: recordId });
|
||||
if (res && res.success) {
|
||||
showToast('反撤销成功');
|
||||
loadHistory(currentHistoryPage);
|
||||
} else {
|
||||
showToast(res?.message || '反撤销失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
window.showBatchPointsModal = showBatchPointsModal;
|
||||
window.submitBatchPoints = submitBatchPoints;
|
||||
window.revokeRecord = revokeRecord;
|
||||
window.restoreRecord = restoreRecord;
|
||||
})();
|
||||
234
frontend/assets/js/modules/student-mgmt.js
Normal file
234
frontend/assets/js/modules/student-mgmt.js
Normal file
@@ -0,0 +1,234 @@
|
||||
/**
|
||||
* 多班级版班级管理系统 - 学生管理函数
|
||||
*
|
||||
* 开发者: Canglan
|
||||
* 联系方式: admin@sea-studio.top
|
||||
* 版权归属: Sea Network Technology Studio
|
||||
* 许可证: Apache License 2.0
|
||||
*
|
||||
* 版权所有 © Sea Network Technology Studio
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// 显示新增学生模态框
|
||||
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_account: parentPhone,
|
||||
dormitory_number: document.getElementById('addDormitoryNumber').value.trim()
|
||||
});
|
||||
|
||||
if (res && res.success) {
|
||||
showToast('学生添加成功');
|
||||
closeModal('addStudentModal');
|
||||
loadStudents();
|
||||
} else {
|
||||
showToast(res?.message || '添加失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 显示编辑学生模态框
|
||||
function showEditStudentModal(studentId, studentNo, name, phone, dormitoryNumber) {
|
||||
document.getElementById('editStudentId').value = studentId;
|
||||
document.getElementById('editStudentNo').value = studentNo;
|
||||
document.getElementById('editStudentName').value = name;
|
||||
document.getElementById('editStudentPhone').value = phone || '';
|
||||
document.getElementById('editDormitoryNumber').value = dormitoryNumber || '';
|
||||
document.getElementById('editStudentModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
// 提交编辑学生
|
||||
async function submitEditStudent() {
|
||||
const studentId = document.getElementById('editStudentId').value;
|
||||
const name = document.getElementById('editStudentName').value.trim();
|
||||
const phone = document.getElementById('editStudentPhone').value.trim();
|
||||
|
||||
if (!name) {
|
||||
showToast('请输入姓名', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await apiPut(`/api/admin/students/${studentId}`, {
|
||||
name: name,
|
||||
parent_account: phone || null,
|
||||
dormitory_number: document.getElementById('editDormitoryNumber').value.trim()
|
||||
});
|
||||
|
||||
if (res && res.success) {
|
||||
showToast('学生信息更新成功');
|
||||
closeModal('editStudentModal');
|
||||
location.reload();
|
||||
} else {
|
||||
showToast(res?.message || '更新失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 显示重置学生密码模态框
|
||||
function showResetStudentPasswordModal(studentId, name) {
|
||||
document.getElementById('resetStudentId').value = studentId;
|
||||
document.getElementById('resetStudentInfo').textContent = `正在重置学生 "${name}" 的密码`;
|
||||
document.getElementById('newStudentPassword').value = '';
|
||||
document.getElementById('resetStudentPasswordModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
// 提交重置学生密码
|
||||
async function submitResetStudentPassword() {
|
||||
const studentId = document.getElementById('resetStudentId').value;
|
||||
const newPassword = document.getElementById('newStudentPassword').value;
|
||||
|
||||
if (!newPassword || newPassword.length < 6) {
|
||||
showToast('密码至少6位', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await apiPost(`/api/admin/students/reset-password/${studentId}`, {
|
||||
new_password: newPassword
|
||||
});
|
||||
|
||||
if (res && res.success) {
|
||||
showToast('密码重置成功');
|
||||
closeModal('resetStudentPasswordModal');
|
||||
} else {
|
||||
showToast(res?.message || '重置失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 删除学生
|
||||
async function deleteStudent(studentId, name) {
|
||||
if (!confirm(`确定要删除学生 "${name}" 吗?删除后学生账号将被禁用。`)) return;
|
||||
|
||||
const res = await apiDelete(`/api/admin/students/${studentId}`);
|
||||
|
||||
if (res && res.success) {
|
||||
showToast('学生删除成功');
|
||||
location.reload();
|
||||
} 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><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_account || '')}</td>
|
||||
<td>${escapeHtml(s.dormitory_number || '-')}</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();
|
||||
|
||||
// 显示详细导入结果
|
||||
if (result.data && result.data.results) {
|
||||
const failedList = result.data.results.filter(r => !r.success);
|
||||
if (failedList.length > 0) {
|
||||
let detail = '失败详情:\n';
|
||||
failedList.forEach(r => {
|
||||
detail += `${r.student_no || '未知'}: ${r.error}\n`;
|
||||
});
|
||||
alert(detail);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
showToast(result.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 绑定文件选择事件
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const fileInput = document.getElementById('importFile');
|
||||
if (fileInput) {
|
||||
fileInput.addEventListener('change', previewImportFile);
|
||||
}
|
||||
});
|
||||
|
||||
window.showAddStudentModal = showAddStudentModal;
|
||||
window.submitAddStudent = submitAddStudent;
|
||||
window.showEditStudentModal = showEditStudentModal;
|
||||
window.submitEditStudent = submitEditStudent;
|
||||
window.showResetStudentPasswordModal = showResetStudentPasswordModal;
|
||||
window.submitResetStudentPassword = submitResetStudentPassword;
|
||||
window.deleteStudent = deleteStudent;
|
||||
window.showImportModal = showImportModal;
|
||||
window.previewImportFile = previewImportFile;
|
||||
window.doImport = doImport;
|
||||
})();
|
||||
47
frontend/assets/js/modules/subject-mgmt.js
Normal file
47
frontend/assets/js/modules/subject-mgmt.js
Normal file
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* 多班级版班级管理系统 - 科目管理函数
|
||||
*
|
||||
* 开发者: Canglan
|
||||
* 联系方式: admin@sea-studio.top
|
||||
* 版权归属: Sea Network Technology Studio
|
||||
* 许可证: Apache License 2.0
|
||||
*
|
||||
* 版权所有 © Sea Network Technology Studio
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// 显示添加科目模态框
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
window.showAddSubjectModal = showAddSubjectModal;
|
||||
window.submitAddSubject = submitAddSubject;
|
||||
})();
|
||||
35
frontend/assets/js/modules/utils.js
Normal file
35
frontend/assets/js/modules/utils.js
Normal file
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* 多班级版班级管理系统 - 通用工具函数
|
||||
*
|
||||
* 开发者: Canglan
|
||||
* 联系方式: admin@sea-studio.top
|
||||
* 版权归属: Sea Network Technology Studio
|
||||
* 许可证: Apache License 2.0
|
||||
*
|
||||
* 版权所有 © Sea Network Technology Studio
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// HTML转义
|
||||
function escapeHtml(str) {
|
||||
if (!str) return '';
|
||||
var el = document.createElement('span');
|
||||
el.appendChild(document.createTextNode(str));
|
||||
return el.innerHTML;
|
||||
}
|
||||
|
||||
// 全选功能
|
||||
function toggleSelectAll() {
|
||||
const selectAll = document.getElementById('selectAll');
|
||||
if (selectAll) {
|
||||
document.querySelectorAll('.student-checkbox').forEach(cb => {
|
||||
cb.checked = selectAll.checked;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
window.escapeHtml = escapeHtml;
|
||||
window.toggleSelectAll = toggleSelectAll;
|
||||
})();
|
||||
13
frontend/assets/js/parent.js
Normal file
13
frontend/assets/js/parent.js
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* 多班级版班级管理系统 - 家长端JS
|
||||
*
|
||||
* 开发者: Canglan
|
||||
* 联系方式: admin@sea-studio.top
|
||||
* 版权归属: Sea Network Technology Studio
|
||||
* 许可证: Apache License 2.0
|
||||
*
|
||||
* 版权所有 © Sea Network Technology Studio
|
||||
*/
|
||||
|
||||
// 家长端专用功能
|
||||
console.log('家长端已加载');
|
||||
59
frontend/assets/js/rankings.js
Normal file
59
frontend/assets/js/rankings.js
Normal file
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* 多班级版班级管理系统 - 排行榜JS
|
||||
*
|
||||
* 开发者: Canglan
|
||||
* 版权归属: Sea Network Technology Studio
|
||||
*
|
||||
* 版权所有 © Sea Network Technology Studio
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
let currentType = 'conduct';
|
||||
|
||||
async function loadRankings(type) {
|
||||
const res = await apiGet('/api/admin/rankings', { type: type, limit: 50 });
|
||||
if (res && res.success && res.data) {
|
||||
const rankings = res.data.ranking || [];
|
||||
let html = '';
|
||||
if (rankings.length === 0) {
|
||||
html = '<tr><td colspan="4" style="text-align:center;">暂无排行数据</td></tr>';
|
||||
} else {
|
||||
rankings.forEach(function(item, index) {
|
||||
let rankClass = '';
|
||||
if (index === 0) rankClass = 'rank-gold';
|
||||
else if (index === 1) rankClass = 'rank-silver';
|
||||
else if (index === 2) rankClass = 'rank-bronze';
|
||||
|
||||
let pointsText = Number(item.points !== undefined ? item.points : (item.total_points || 0));
|
||||
if (pointsText > 0) {
|
||||
pointsText = '+' + pointsText;
|
||||
}
|
||||
|
||||
html += '<tr>' +
|
||||
'<td><span class="rank-badge ' + rankClass + '">' + (index + 1) + '</span></td>' +
|
||||
'<td>' + escapeHtml(item.student_no || '-') + '</td>' +
|
||||
'<td>' + escapeHtml(item.name || '-') + '</td>' +
|
||||
'<td><span class="record-points ' + (pointsText > 0 ? 'plus' : (pointsText < 0 ? 'minus' : '')) + '">' + pointsText + '</span></td>' +
|
||||
'</tr>';
|
||||
});
|
||||
}
|
||||
document.getElementById('rankingList').innerHTML = html;
|
||||
} else {
|
||||
document.getElementById('rankingList').innerHTML = '<tr><td colspan="4" style="text-align:center;">加载失败</td></tr>';
|
||||
}
|
||||
}
|
||||
|
||||
window.switchTab = function(type, btn) {
|
||||
currentType = type;
|
||||
document.querySelectorAll('.tab-btn').forEach(function(b) { b.classList.remove('active'); });
|
||||
btn.classList.add('active');
|
||||
loadRankings(type);
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
loadRankings(currentType);
|
||||
});
|
||||
|
||||
})();
|
||||
383
frontend/assets/js/semesters.js
Normal file
383
frontend/assets/js/semesters.js
Normal file
@@ -0,0 +1,383 @@
|
||||
/**
|
||||
* 多班级版班级管理系统 - 学期管理页JS
|
||||
*
|
||||
* 开发者: Canglan
|
||||
* 版权归属: Sea Network Technology Studio
|
||||
*
|
||||
* 版权所有 © Sea Network Technology Studio
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
let archiveSemesterId = null;
|
||||
let archivePage = 1;
|
||||
let archiveTotalPages = 1;
|
||||
let associateSemesterId = null;
|
||||
|
||||
function fillSemesterDates(type) {
|
||||
const now = new Date();
|
||||
const currentYear = now.getFullYear();
|
||||
const currentMonth = now.getMonth() + 1;
|
||||
const startDateInput = document.getElementById('semesterStartDate');
|
||||
const endDateInput = document.getElementById('semesterEndDate');
|
||||
|
||||
if (type === 'upper') {
|
||||
const year = currentMonth >= 6 ? currentYear : currentYear - 1;
|
||||
const endYear = year + 1;
|
||||
let febDay = 28;
|
||||
if ((endYear % 4 === 0 && endYear % 100 !== 0) || endYear % 400 === 0) {
|
||||
febDay = 29;
|
||||
}
|
||||
startDateInput.value = year + '-09-01';
|
||||
endDateInput.value = endYear + '-02-' + febDay;
|
||||
} else if (type === 'lower') {
|
||||
startDateInput.value = currentYear + '-03-01';
|
||||
endDateInput.value = currentYear + '-07-15';
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSemesters() {
|
||||
const res = await apiGet('/api/semester/list');
|
||||
if (res && res.success) {
|
||||
let html = '';
|
||||
const semesters = res.data.semesters || [];
|
||||
semesters.forEach(sem => {
|
||||
let statusText = '';
|
||||
let statusClass = '';
|
||||
if (sem.is_archived) {
|
||||
statusText = '已归档';
|
||||
statusClass = 'status-badge status-not_submitted';
|
||||
} else if (sem.is_active) {
|
||||
statusText = '当前学期';
|
||||
statusClass = 'status-badge status-submitted';
|
||||
} else {
|
||||
statusText = '未激活';
|
||||
statusClass = 'status-badge status-late';
|
||||
}
|
||||
|
||||
let actions = '';
|
||||
const startDate = sem.start_date || '';
|
||||
const endDate = sem.end_date || '';
|
||||
if (!sem.is_archived) {
|
||||
actions += `<div class="action-dropdown">
|
||||
<button class="btn btn-sm action-dropdown-toggle" onclick="toggleActionDropdown(this)">操作 ▼</button>
|
||||
<div class="action-dropdown-menu">
|
||||
<a onclick="showEditSemesterModal(${sem.semester_id}, '${escapeHtml(sem.semester_name)}', '${startDate}', '${endDate}')">编辑</a>
|
||||
${!sem.is_active ? `<a onclick="activateSemester(${sem.semester_id})">激活</a>` : ''}
|
||||
<a onclick="showAssociateConfirm(${sem.semester_id}, '${escapeHtml(sem.semester_name)}', '${startDate}', '${endDate}')">关联数据</a>
|
||||
<a class="danger" onclick="showArchiveConfirm(${sem.semester_id}, '${escapeHtml(sem.semester_name)}')">归档</a>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
if (sem.is_archived) {
|
||||
actions += `<button class="btn btn-sm btn-secondary" onclick="viewArchiveData(${sem.semester_id}, '${escapeHtml(sem.semester_name)}')">查看归档</button>`;
|
||||
}
|
||||
|
||||
const conductCount = sem.conduct_count || 0;
|
||||
const attendanceCount = sem.attendance_count || 0;
|
||||
let recordText = '-';
|
||||
if (conductCount > 0 || attendanceCount > 0) {
|
||||
recordText = `${conductCount}条操行分 / ${attendanceCount}条考勤`;
|
||||
}
|
||||
|
||||
const weekText = sem.current_week ? `第${sem.current_week}周` : '-';
|
||||
html += `<tr>
|
||||
<td>${escapeHtml(sem.semester_name)}</td>
|
||||
<td>${formatDate(sem.start_date)}</td>
|
||||
<td>${formatDate(sem.end_date)}</td>
|
||||
<td>${weekText}</td>
|
||||
<td><span class="${statusClass}">${statusText}</span></td>
|
||||
<td>${recordText}</td>
|
||||
<td>${formatDateTime(sem.created_at)}</td>
|
||||
<td>${actions}</td>
|
||||
</tr>`;
|
||||
});
|
||||
if (semesters.length === 0) {
|
||||
html = '<tr><td colspan="8" style="text-align:center;">暂无学期,请点击上方按钮创建新学期</td></tr>';
|
||||
}
|
||||
document.getElementById('semesterList').innerHTML = html;
|
||||
}
|
||||
}
|
||||
|
||||
function showCreateSemesterModal() {
|
||||
document.getElementById('semesterName').value = '';
|
||||
document.getElementById('semesterStartDate').value = '';
|
||||
document.getElementById('semesterEndDate').value = '';
|
||||
document.getElementById('createSemesterModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
async function submitCreateSemester() {
|
||||
const name = document.getElementById('semesterName').value.trim();
|
||||
const startDate = document.getElementById('semesterStartDate').value;
|
||||
const endDate = document.getElementById('semesterEndDate').value;
|
||||
|
||||
if (!name) {
|
||||
showToast('请输入学期名称', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await apiPost('/api/semester/create', {
|
||||
semester_name: name,
|
||||
start_date: startDate || null,
|
||||
end_date: endDate || null
|
||||
});
|
||||
|
||||
if (res && res.success) {
|
||||
showToast(res.message || '学期创建成功');
|
||||
closeModal('createSemesterModal');
|
||||
loadSemesters();
|
||||
} else {
|
||||
showToast(res?.message || '创建失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function activateSemester(semesterId) {
|
||||
if (!confirm('确认将此学期设为当前活跃学期?其他学期将被设为非活跃。')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await apiPut(`/api/semester/activate/${semesterId}`);
|
||||
if (res && res.success) {
|
||||
showToast(res.message || '已设为当前学期');
|
||||
loadSemesters();
|
||||
} else {
|
||||
showToast(res?.message || '操作失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function showEditSemesterModal(id, name, startDate, endDate) {
|
||||
document.getElementById('editSemesterId').value = id;
|
||||
document.getElementById('editSemesterName').value = name;
|
||||
document.getElementById('editSemesterStartDate').value = startDate || '';
|
||||
document.getElementById('editSemesterEndDate').value = endDate || '';
|
||||
document.getElementById('editSemesterModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
async function submitEditSemester() {
|
||||
const id = document.getElementById('editSemesterId').value;
|
||||
const name = document.getElementById('editSemesterName').value.trim();
|
||||
const startDate = document.getElementById('editSemesterStartDate').value;
|
||||
const endDate = document.getElementById('editSemesterEndDate').value;
|
||||
|
||||
if (!name) {
|
||||
showToast('请输入学期名称', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const data = { semester_name: name };
|
||||
if (startDate) data.start_date = startDate;
|
||||
if (endDate) data.end_date = endDate;
|
||||
|
||||
const res = await apiPut(`/api/semester/update/${id}`, data);
|
||||
if (res && res.success) {
|
||||
showToast(res.message || '更新成功');
|
||||
closeModal('editSemesterModal');
|
||||
loadSemesters();
|
||||
} else {
|
||||
showToast(res?.message || '更新失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteSemester() {
|
||||
const id = document.getElementById('editSemesterId').value;
|
||||
if (!confirm('确定要删除该学期吗?如果学期已有归档数据则无法删除。')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await apiDelete(`/api/semester/delete/${id}`);
|
||||
if (res && res.success) {
|
||||
showToast(res.message || '删除成功');
|
||||
closeModal('editSemesterModal');
|
||||
loadSemesters();
|
||||
} else {
|
||||
showToast(res?.message || '删除失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function showAssociateConfirm(id, name, startDate, endDate) {
|
||||
associateSemesterId = id;
|
||||
const dateRange = startDate ? `${startDate} ~ ${endDate || '至今'}` : '未设置日期范围';
|
||||
document.getElementById('associateConfirmText').innerHTML =
|
||||
`即将关联 <strong>${dateRange}</strong> 内的所有未分配学期的操行分记录和考勤记录到学期 "<strong>${name}</strong>"。`;
|
||||
document.getElementById('associateConfirmModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
async function confirmAssociate() {
|
||||
if (!associateSemesterId) return;
|
||||
|
||||
const res = await apiPost(`/api/semester/${associateSemesterId}/associate`);
|
||||
if (res && res.success) {
|
||||
showToast(res.message || '关联成功');
|
||||
closeModal('associateConfirmModal');
|
||||
associateSemesterId = null;
|
||||
} else {
|
||||
showToast(res?.message || '关联失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function showArchiveConfirm(semesterId, semesterName) {
|
||||
archiveSemesterId = semesterId;
|
||||
document.getElementById('archiveResetScores').checked = false;
|
||||
document.getElementById('archiveConfirmText').innerHTML =
|
||||
`确定要归档学期 "<strong>${semesterName}</strong>" 吗?<br>归档后将保存所有学生的当前操行分快照,该学期数据将变为只读。`;
|
||||
document.getElementById('archiveConfirmModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
async function confirmArchive() {
|
||||
if (!archiveSemesterId) return;
|
||||
|
||||
const resetScores = document.getElementById('archiveResetScores').checked;
|
||||
const url = `/api/semester/archive/${archiveSemesterId}?reset_scores=${resetScores}`;
|
||||
|
||||
const res = await apiPost(url);
|
||||
if (res && res.success) {
|
||||
showToast(res.message || '归档成功');
|
||||
closeModal('archiveConfirmModal');
|
||||
archiveSemesterId = null;
|
||||
loadSemesters();
|
||||
} else {
|
||||
showToast(res?.message || '归档失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function viewArchiveData(semesterId, semesterName, page) {
|
||||
page = page || 1;
|
||||
archivePage = page;
|
||||
document.getElementById('archiveDataTitle').textContent = `归档数据 - ${semesterName}`;
|
||||
|
||||
const res = await apiGet(`/api/semester/archive/${semesterId}/records`, {
|
||||
page: page, page_size: 50
|
||||
});
|
||||
|
||||
if (res && res.success) {
|
||||
const data = res.data || {};
|
||||
const archives = data.items || [];
|
||||
let html = '';
|
||||
archives.forEach(a => {
|
||||
html += `<tr>
|
||||
<td>${a.rank_position || '-'}</td>
|
||||
<td>${escapeHtml(a.student_no)}</td>
|
||||
<td>${escapeHtml(a.student_name)}</td>
|
||||
<td>${a.final_points}</td>
|
||||
<td>${a.attendance_present || 0}</td>
|
||||
<td>${a.attendance_absent || 0}</td>
|
||||
<td>${a.attendance_late || 0}</td>
|
||||
<td>${a.attendance_leave || 0}</td>
|
||||
<td>${a.homework_submitted || 0}</td>
|
||||
<td>${a.homework_not_submitted || 0}</td>
|
||||
<td>${a.homework_late || 0}</td>
|
||||
</tr>`;
|
||||
});
|
||||
if (archives.length === 0) {
|
||||
html = '<tr><td colspan="11" style="text-align:center;">暂无归档数据</td></tr>';
|
||||
}
|
||||
document.getElementById('archiveDataList').innerHTML = html;
|
||||
|
||||
archiveTotalPages = data.total_pages || 1;
|
||||
renderArchivePagination(semesterId, semesterName);
|
||||
document.getElementById('archiveDataModal').style.display = 'flex';
|
||||
} else {
|
||||
showToast(res?.message || '获取归档数据失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function renderArchivePagination(semesterId, semesterName) {
|
||||
renderSmartPagination('archivePagination', archivePage, archiveTotalPages, function(page) {
|
||||
viewArchiveData(semesterId, semesterName, page);
|
||||
});
|
||||
}
|
||||
|
||||
// ========== 周期重置功能 ==========
|
||||
|
||||
let pendingPeriodType = null;
|
||||
let periodArchivesType = null;
|
||||
let periodArchivesPage = 1;
|
||||
let periodArchivesTotalPages = 1;
|
||||
|
||||
function confirmPeriodReset(periodType) {
|
||||
pendingPeriodType = periodType;
|
||||
const label = periodType === 'weekly' ? '本周' : '本月';
|
||||
document.getElementById('periodResetText').innerHTML =
|
||||
`确定要执行 <strong>${label}重置</strong> 吗?<br>将保存当前所有学生的操行分快照,然后将所有学生操行分重置为初始值。`;
|
||||
document.getElementById('periodResetModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
async function executePeriodReset() {
|
||||
if (!pendingPeriodType) return;
|
||||
|
||||
const res = await apiPost('/api/semester/period-reset', { period: pendingPeriodType });
|
||||
if (res && res.success) {
|
||||
showToast(res.message || '重置成功');
|
||||
closeModal('periodResetModal');
|
||||
pendingPeriodType = null;
|
||||
} else {
|
||||
showToast(res?.message || '重置失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function showPeriodArchives(type, page) {
|
||||
var periodType = type || periodArchivesType;
|
||||
page = page || 1;
|
||||
periodArchivesType = periodType;
|
||||
periodArchivesPage = page;
|
||||
|
||||
const label = periodType === 'weekly' ? '周' : '月';
|
||||
document.getElementById('periodArchivesTitle').textContent = label + '归档数据';
|
||||
|
||||
const res = await apiGet('/api/semester/period-archives', {
|
||||
period: periodType,
|
||||
page: page,
|
||||
page_size: 50
|
||||
});
|
||||
|
||||
if (res && res.success) {
|
||||
const data = res.data || {};
|
||||
const archives = data.items || [];
|
||||
let html = '';
|
||||
archives.forEach(function(a) {
|
||||
const resetByLabel = a.reset_by === 'auto' ? '自动' : '手动';
|
||||
html += '<tr>' +
|
||||
'<td>' + escapeHtml(a.period_label) + '</td>' +
|
||||
'<td>' + (a.rank_position || '-') + '</td>' +
|
||||
'<td>' + escapeHtml(a.student_no) + '</td>' +
|
||||
'<td>' + escapeHtml(a.student_name) + '</td>' +
|
||||
'<td>' + a.final_points + '</td>' +
|
||||
'<td>' + resetByLabel + '</td>' +
|
||||
'<td>' + formatDateTime(a.archived_at) + '</td>' +
|
||||
'</tr>';
|
||||
});
|
||||
if (archives.length === 0) {
|
||||
html = '<tr><td colspan="7" style="text-align:center;">暂无归档数据</td></tr>';
|
||||
}
|
||||
document.getElementById('periodArchivesList').innerHTML = html;
|
||||
|
||||
periodArchivesTotalPages = data.total_pages || 1;
|
||||
renderSmartPagination('periodArchivePagination', periodArchivesPage, periodArchivesTotalPages, function(p) {
|
||||
showPeriodArchives(periodArchivesType, p);
|
||||
});
|
||||
document.getElementById('periodArchivesModal').style.display = 'flex';
|
||||
} else {
|
||||
showToast(res?.message || '获取归档数据失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
loadSemesters();
|
||||
|
||||
window.fillSemesterDates = fillSemesterDates;
|
||||
window.showCreateSemesterModal = showCreateSemesterModal;
|
||||
window.submitCreateSemester = submitCreateSemester;
|
||||
window.activateSemester = activateSemester;
|
||||
window.showEditSemesterModal = showEditSemesterModal;
|
||||
window.submitEditSemester = submitEditSemester;
|
||||
window.deleteSemester = deleteSemester;
|
||||
window.showAssociateConfirm = showAssociateConfirm;
|
||||
window.confirmAssociate = confirmAssociate;
|
||||
window.showArchiveConfirm = showArchiveConfirm;
|
||||
window.confirmArchive = confirmArchive;
|
||||
window.viewArchiveData = viewArchiveData;
|
||||
window.confirmPeriodReset = confirmPeriodReset;
|
||||
window.executePeriodReset = executePeriodReset;
|
||||
window.showPeriodArchives = showPeriodArchives;
|
||||
|
||||
})();
|
||||
38
frontend/assets/js/student-homework.js
Normal file
38
frontend/assets/js/student-homework.js
Normal file
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* 多班级版班级管理系统 - 学生端作业情况JS
|
||||
*
|
||||
* 开发者: Canglan
|
||||
* 版权归属: Sea Network Technology Studio
|
||||
*
|
||||
* 版权所有 © Sea Network Technology Studio
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
const STUDENT_ID = window.PAGE_CONFIG.studentId;
|
||||
|
||||
async function loadHomework() {
|
||||
const res = await apiGet(`/api/student/homework/${STUDENT_ID}`);
|
||||
if (res && res.success) {
|
||||
let html = '';
|
||||
res.data.homework.forEach(record => {
|
||||
const pointsClass = record.points_change > 0 ? 'plus' : 'minus';
|
||||
const pointsColor = record.points_change > 0 ? '#38a169' : '#e53e3e';
|
||||
html += `<tr>
|
||||
<td>${formatDateTime(record.created_at)}</td>
|
||||
<td style="color: ${pointsColor}; font-weight: bold;">${record.points_change > 0 ? '+' : ''}${record.points_change}</td>
|
||||
<td>${escapeHtml(record.reason)}</td>
|
||||
<td>${escapeHtml(record.recorder_name || '-')}</td>
|
||||
</tr>`;
|
||||
});
|
||||
if (res.data.homework.length === 0) {
|
||||
html = '<tr><td colspan="4" style="text-align:center; padding: 40px; color: #999;">📝 暂无作业扣分记录</td></tr>';
|
||||
}
|
||||
document.getElementById('homeworkList').innerHTML = html;
|
||||
}
|
||||
}
|
||||
|
||||
loadHomework();
|
||||
|
||||
})();
|
||||
13
frontend/assets/js/student.js
Normal file
13
frontend/assets/js/student.js
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* 多班级版班级管理系统 - 学生端JS
|
||||
*
|
||||
* 开发者: Canglan
|
||||
* 联系方式: admin@sea-studio.top
|
||||
* 版权归属: Sea Network Technology Studio
|
||||
* 许可证: Apache License 2.0
|
||||
*
|
||||
* 版权所有 © Sea Network Technology Studio
|
||||
*/
|
||||
|
||||
// 学生端专用功能
|
||||
console.log('学生端已加载');
|
||||
100
frontend/assets/js/students-manage.js
Normal file
100
frontend/assets/js/students-manage.js
Normal file
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* 多班级版班级管理系统 - 学生管理页JS
|
||||
*
|
||||
* 开发者: Canglan
|
||||
* 版权归属: Sea Network Technology Studio
|
||||
*
|
||||
* 版权所有 © Sea Network Technology Studio
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
const userRole = window.PAGE_CONFIG.role;
|
||||
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><a href="/admin/history.php?student_id=${student.student_id}" class="link">${escapeHtml(student.name)}</a></td>
|
||||
<td>${escapeHtml(student.dormitory_number || '-')}</td>
|
||||
<td>${student.total_points}</td>
|
||||
${userRole === '班主任' ? `<td>${student.parent_account ? student.parent_account.slice(0,3) + '******' + student.parent_account.slice(-2) : '-'}</td>` : ''}
|
||||
<td>
|
||||
<div class="action-dropdown">
|
||||
<button class="btn btn-sm btn-outline" onclick="showSinglePointsModal(${student.student_id}, '${escapeHtml(student.name)}')">加减分</button>
|
||||
${userRole === '班主任' ? `<button class="btn btn-sm action-dropdown-toggle" onclick="toggleActionDropdown(this)">更多 ▼</button>
|
||||
<div class="action-dropdown-menu">
|
||||
<a onclick="showEditStudentModal(${student.student_id}, '${escapeHtml(student.student_no)}', '${escapeHtml(student.name)}', '${escapeHtml(student.parent_account || '')}', '${escapeHtml(student.dormitory_number || '')}')">编辑</a>
|
||||
<a onclick="showResetStudentPasswordModal(${student.student_id}, '${escapeHtml(student.name)}')">重置密码</a>
|
||||
<a onclick="unlockStudent('${escapeHtml(student.student_no)}', '${escapeHtml(student.name)}')">解锁</a>
|
||||
<a class="danger" onclick="deleteStudent(${student.student_id}, '${escapeHtml(student.name)}')">删除</a>
|
||||
</div>` : ''}
|
||||
</div>
|
||||
</td>
|
||||
</tr>`;
|
||||
});
|
||||
|
||||
if (res.data.students.length === 0) {
|
||||
html = `<tr><td colspan="${userRole === '班主任' ? '7' : '6'}" style="text-align:center;">暂无学生数据</td></tr>`;
|
||||
}
|
||||
|
||||
document.getElementById('studentList').innerHTML = html;
|
||||
|
||||
totalPages = res.data.total_pages || 1;
|
||||
renderPagination();
|
||||
}
|
||||
}
|
||||
|
||||
function renderPagination() {
|
||||
renderSmartPagination('pagination', currentPage, totalPages, function(page) {
|
||||
loadStudents(page);
|
||||
});
|
||||
}
|
||||
|
||||
function showSinglePointsModal(studentId, studentName) {
|
||||
window.selectedStudentIds = [studentId];
|
||||
document.getElementById('selectedStudentsCount').innerHTML = `${studentName} (1人)`;
|
||||
document.getElementById('pointsChange').value = '';
|
||||
document.getElementById('pointsReason').value = '';
|
||||
document.getElementById('batchPointsModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
async function unlockStudent(studentNo, studentName) {
|
||||
if (!confirm(`确定要解除学生 "${studentName}" 的登录锁定吗?\n(适用于多次登录失败被禁止登录的情况)`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await apiPost('/api/admin/unlock-user', {
|
||||
username: studentNo
|
||||
});
|
||||
|
||||
if (res && res.success) {
|
||||
showToast(res.message || '解锁成功');
|
||||
} else {
|
||||
showToast(res?.message || '解锁失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
loadStudents();
|
||||
|
||||
let searchTimeout;
|
||||
document.getElementById('searchInput').addEventListener('input', () => {
|
||||
clearTimeout(searchTimeout);
|
||||
searchTimeout = setTimeout(() => loadStudents(1), 500);
|
||||
});
|
||||
|
||||
window.loadStudents = loadStudents;
|
||||
window.showSinglePointsModal = showSinglePointsModal;
|
||||
window.unlockStudent = unlockStudent;
|
||||
|
||||
})();
|
||||
23
frontend/assets/uploads/sample_import.json
Normal file
23
frontend/assets/uploads/sample_import.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"students": [
|
||||
{
|
||||
"student_no": "2025001",
|
||||
"name": "张三",
|
||||
"parent_account": "13800138001",
|
||||
"dormitory_number": "A301",
|
||||
"password": "123456"
|
||||
},
|
||||
{
|
||||
"student_no": "2025002",
|
||||
"name": "李四",
|
||||
"parent_account": "13800138002",
|
||||
"dormitory_number": "A302"
|
||||
},
|
||||
{
|
||||
"student_no": "2025003",
|
||||
"name": "王五",
|
||||
"parent_account": "",
|
||||
"dormitory_number": "B101"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user