0914
This commit is contained in:
parent
fd27daa012
commit
77b57a9876
@ -6,7 +6,7 @@ class WeeklyAttendance(db.Model):
|
|||||||
__tablename__ = 'weekly_attendance'
|
__tablename__ = 'weekly_attendance'
|
||||||
|
|
||||||
record_id = db.Column(db.Integer, primary_key=True)
|
record_id = db.Column(db.Integer, primary_key=True)
|
||||||
student_number = db.Column(db.String(20), db.ForeignKey('students.student_number'), nullable=False)
|
student_number = db.Column(db.String(20), db.ForeignKey('students.student_number', ondelete='CASCADE'), nullable=False)
|
||||||
name = db.Column(db.String(50), nullable=False)
|
name = db.Column(db.String(50), nullable=False)
|
||||||
week_start_date = db.Column(db.Date, nullable=False, index=True)
|
week_start_date = db.Column(db.Date, nullable=False, index=True)
|
||||||
week_end_date = db.Column(db.Date, nullable=False, index=True)
|
week_end_date = db.Column(db.Date, nullable=False, index=True)
|
||||||
@ -30,8 +30,8 @@ class DailyAttendanceDetail(db.Model):
|
|||||||
__tablename__ = 'daily_attendance_details'
|
__tablename__ = 'daily_attendance_details'
|
||||||
|
|
||||||
detail_id = db.Column(db.Integer, primary_key=True)
|
detail_id = db.Column(db.Integer, primary_key=True)
|
||||||
weekly_record_id = db.Column(db.Integer, db.ForeignKey('weekly_attendance.record_id'))
|
weekly_record_id = db.Column(db.Integer, db.ForeignKey('weekly_attendance.record_id', ondelete='CASCADE'))
|
||||||
student_number = db.Column(db.String(20), db.ForeignKey('students.student_number'), nullable=False)
|
student_number = db.Column(db.String(20), db.ForeignKey('students.student_number', ondelete='CASCADE'), nullable=False)
|
||||||
attendance_date = db.Column(db.Date, nullable=False, index=True)
|
attendance_date = db.Column(db.Date, nullable=False, index=True)
|
||||||
status = db.Column(db.String(20), default='正常')
|
status = db.Column(db.String(20), default='正常')
|
||||||
check_in_time = db.Column(db.Time)
|
check_in_time = db.Column(db.Time)
|
||||||
@ -53,7 +53,7 @@ class LeaveRecord(db.Model):
|
|||||||
__tablename__ = 'leave_records'
|
__tablename__ = 'leave_records'
|
||||||
|
|
||||||
leave_id = db.Column(db.Integer, primary_key=True)
|
leave_id = db.Column(db.Integer, primary_key=True)
|
||||||
student_number = db.Column(db.String(20), db.ForeignKey('students.student_number'), nullable=False)
|
student_number = db.Column(db.String(20), db.ForeignKey('students.student_number', ondelete='CASCADE'), nullable=False)
|
||||||
leave_start_date = db.Column(db.Date, nullable=False)
|
leave_start_date = db.Column(db.Date, nullable=False)
|
||||||
leave_end_date = db.Column(db.Date, nullable=False)
|
leave_end_date = db.Column(db.Date, nullable=False)
|
||||||
leave_reason = db.Column(db.Text)
|
leave_reason = db.Column(db.Text)
|
||||||
|
@ -6,7 +6,7 @@ class Student(db.Model):
|
|||||||
__tablename__ = 'students'
|
__tablename__ = 'students'
|
||||||
|
|
||||||
student_id = db.Column(db.Integer, primary_key=True)
|
student_id = db.Column(db.Integer, primary_key=True)
|
||||||
student_number = db.Column(db.String(20), db.ForeignKey('users.student_number'), unique=True, nullable=False)
|
student_number = db.Column(db.String(20), db.ForeignKey('users.student_number', ondelete='CASCADE'), unique=True, nullable=False)
|
||||||
name = db.Column(db.String(50), nullable=False, index=True)
|
name = db.Column(db.String(50), nullable=False, index=True)
|
||||||
gender = db.Column(db.Enum('男', '女'), nullable=False)
|
gender = db.Column(db.Enum('男', '女'), nullable=False)
|
||||||
grade = db.Column(db.Integer, nullable=False, index=True)
|
grade = db.Column(db.Integer, nullable=False, index=True)
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,70 @@
|
|||||||
|
|
||||||
|
/* 高级搜索样式 */
|
||||||
|
.supervisor-checkboxes, .grade-checkboxes {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.supervisor-checkboxes::-webkit-scrollbar,
|
||||||
|
.grade-checkboxes::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.supervisor-checkboxes::-webkit-scrollbar-track,
|
||||||
|
.grade-checkboxes::-webkit-scrollbar-track {
|
||||||
|
background: #f1f1f1;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.supervisor-checkboxes::-webkit-scrollbar-thumb,
|
||||||
|
.grade-checkboxes::-webkit-scrollbar-thumb {
|
||||||
|
background: #c1c1c1;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.supervisor-checkboxes::-webkit-scrollbar-thumb:hover,
|
||||||
|
.grade-checkboxes::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #a8a8a8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-check {
|
||||||
|
padding: 2px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-check-label {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-check-input:checked + .form-check-label {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #0066cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 高级搜索区域动画 */
|
||||||
|
#advancedSearchArea {
|
||||||
|
transition: all 0.3s ease-in-out;
|
||||||
|
opacity: 0;
|
||||||
|
max-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#advancedSearchArea[style*="block"] {
|
||||||
|
opacity: 1;
|
||||||
|
max-height: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 搜索标题样式 */
|
||||||
|
.text-primary {
|
||||||
|
font-weight: 600 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式调整 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.supervisor-checkboxes, .grade-checkboxes {
|
||||||
|
max-height: 150px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-md-6 {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,206 @@
|
|||||||
|
// 高级搜索功能
|
||||||
|
let isAdvancedSearchVisible = false;
|
||||||
|
|
||||||
|
function toggleAdvancedSearch() {
|
||||||
|
console.log('toggleAdvancedSearch called'); // 调试信息
|
||||||
|
|
||||||
|
const advancedArea = document.getElementById('advancedSearchArea');
|
||||||
|
const toggleButton = document.querySelector('button[onclick="toggleAdvancedSearch()"]');
|
||||||
|
|
||||||
|
if (!advancedArea) {
|
||||||
|
console.error('找不到高级搜索区域元素');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!toggleButton) {
|
||||||
|
console.error('找不到高级搜索切换按钮');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isAdvancedSearchVisible) {
|
||||||
|
// 隐藏高级搜索
|
||||||
|
advancedArea.style.display = 'none';
|
||||||
|
toggleButton.innerHTML = '<i class="fas fa-cog"></i>';
|
||||||
|
toggleButton.title = '高级搜索';
|
||||||
|
toggleButton.classList.remove('btn-info');
|
||||||
|
toggleButton.classList.add('btn-outline-info');
|
||||||
|
isAdvancedSearchVisible = false;
|
||||||
|
console.log('高级搜索已隐藏');
|
||||||
|
} else {
|
||||||
|
// 显示高级搜索
|
||||||
|
advancedArea.style.display = 'block';
|
||||||
|
toggleButton.innerHTML = '<i class="fas fa-times"></i>';
|
||||||
|
toggleButton.title = '关闭高级搜索';
|
||||||
|
toggleButton.classList.remove('btn-outline-info');
|
||||||
|
toggleButton.classList.add('btn-info');
|
||||||
|
isAdvancedSearchVisible = true;
|
||||||
|
console.log('高级搜索已显示');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导师选择功能
|
||||||
|
function selectAllSupervisors() {
|
||||||
|
const checkboxes = document.querySelectorAll('.supervisor-checkboxes input[type="checkbox"]');
|
||||||
|
checkboxes.forEach(checkbox => {
|
||||||
|
checkbox.checked = true;
|
||||||
|
});
|
||||||
|
updateSupervisorCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearAllSupervisors() {
|
||||||
|
const checkboxes = document.querySelectorAll('.supervisor-checkboxes input[type="checkbox"]');
|
||||||
|
checkboxes.forEach(checkbox => {
|
||||||
|
checkbox.checked = false;
|
||||||
|
});
|
||||||
|
updateSupervisorCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 年级选择功能
|
||||||
|
function selectAllGrades() {
|
||||||
|
const checkboxes = document.querySelectorAll('.grade-checkboxes input[type="checkbox"]');
|
||||||
|
checkboxes.forEach(checkbox => {
|
||||||
|
checkbox.checked = true;
|
||||||
|
});
|
||||||
|
updateGradeCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearAllGrades() {
|
||||||
|
const checkboxes = document.querySelectorAll('.grade-checkboxes input[type="checkbox"]');
|
||||||
|
checkboxes.forEach(checkbox => {
|
||||||
|
checkbox.checked = false;
|
||||||
|
});
|
||||||
|
updateGradeCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新导师选择计数
|
||||||
|
function updateSupervisorCount() {
|
||||||
|
const checkedCount = document.querySelectorAll('.supervisor-checkboxes input[type="checkbox"]:checked').length;
|
||||||
|
const label = document.querySelector('.supervisor-checkboxes').previousElementSibling;
|
||||||
|
if (label) {
|
||||||
|
if (checkedCount > 0) {
|
||||||
|
label.innerHTML = `导师(已选${checkedCount}个)`;
|
||||||
|
label.classList.add('text-primary');
|
||||||
|
} else {
|
||||||
|
label.innerHTML = '导师(多选)';
|
||||||
|
label.classList.remove('text-primary');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新年级选择计数
|
||||||
|
function updateGradeCount() {
|
||||||
|
const checkedCount = document.querySelectorAll('.grade-checkboxes input[type="checkbox"]:checked').length;
|
||||||
|
const label = document.querySelector('.grade-checkboxes').previousElementSibling;
|
||||||
|
if (label) {
|
||||||
|
if (checkedCount > 0) {
|
||||||
|
label.innerHTML = `年级(已选${checkedCount}个)`;
|
||||||
|
label.classList.add('text-primary');
|
||||||
|
} else {
|
||||||
|
label.innerHTML = '年级(多选)';
|
||||||
|
label.classList.remove('text-primary');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否有高级搜索条件被选中
|
||||||
|
function hasAdvancedSearchFilters() {
|
||||||
|
const supervisorChecked = document.querySelectorAll('.supervisor-checkboxes input[type="checkbox"]:checked').length > 0;
|
||||||
|
const gradeChecked = document.querySelectorAll('.grade-checkboxes input[type="checkbox"]:checked').length > 0;
|
||||||
|
return supervisorChecked || gradeChecked;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 智能搜索提示
|
||||||
|
function setupSearchHints() {
|
||||||
|
const supervisorCheckboxes = document.querySelectorAll('.supervisor-checkboxes input[type="checkbox"]');
|
||||||
|
const gradeCheckboxes = document.querySelectorAll('.grade-checkboxes input[type="checkbox"]');
|
||||||
|
|
||||||
|
// 导师搜索计数提示
|
||||||
|
supervisorCheckboxes.forEach(checkbox => {
|
||||||
|
checkbox.addEventListener('change', updateSupervisorCount);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 年级搜索计数提示
|
||||||
|
gradeCheckboxes.forEach(checkbox => {
|
||||||
|
checkbox.addEventListener('change', updateGradeCount);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空所有筛选条件
|
||||||
|
function clearAllFilters() {
|
||||||
|
// 清空基本搜索
|
||||||
|
const startDate = document.getElementById('start_date');
|
||||||
|
const endDate = document.getElementById('end_date');
|
||||||
|
const studentSearch = document.getElementById('student_search');
|
||||||
|
const sortBy = document.getElementById('sort_by');
|
||||||
|
|
||||||
|
if (startDate) startDate.value = '';
|
||||||
|
if (endDate) endDate.value = '';
|
||||||
|
if (studentSearch) studentSearch.value = '';
|
||||||
|
if (sortBy) sortBy.value = 'week_start_date_desc';
|
||||||
|
|
||||||
|
// 清空高级搜索
|
||||||
|
clearAllSupervisors();
|
||||||
|
clearAllGrades();
|
||||||
|
|
||||||
|
// 提交表单
|
||||||
|
const form = document.getElementById('searchForm');
|
||||||
|
if (form) {
|
||||||
|
form.submit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出当前筛选结果
|
||||||
|
function exportCurrentFilter() {
|
||||||
|
const form = document.getElementById('searchForm');
|
||||||
|
if (!form) {
|
||||||
|
console.error('找不到搜索表单');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = new FormData(form);
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
|
||||||
|
for (let [key, value] of formData.entries()) {
|
||||||
|
params.append(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
params.set('export', 'excel');
|
||||||
|
|
||||||
|
const exportUrl = window.location.pathname + '?' + params.toString();
|
||||||
|
console.log('导出URL:', exportUrl);
|
||||||
|
|
||||||
|
window.location.href = exportUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面加载完成后的初始化
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
console.log('admin.js: 页面加载完成,开始初始化');
|
||||||
|
|
||||||
|
// 检查高级搜索区域是否存在
|
||||||
|
const advancedArea = document.getElementById('advancedSearchArea');
|
||||||
|
if (!advancedArea) {
|
||||||
|
console.warn('高级搜索区域不存在,可能模板未正确更新');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置搜索提示
|
||||||
|
setupSearchHints();
|
||||||
|
|
||||||
|
// 检查URL参数,如果有高级搜索参数则自动展开
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const hasSupervisorParam = urlParams.getAll('supervisor').length > 0;
|
||||||
|
const hasGradeParam = urlParams.getAll('grade').length > 0;
|
||||||
|
|
||||||
|
if (hasSupervisorParam || hasGradeParam || hasAdvancedSearchFilters()) {
|
||||||
|
console.log('检测到高级搜索参数,自动展开');
|
||||||
|
toggleAdvancedSearch();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化计数显示
|
||||||
|
setTimeout(() => {
|
||||||
|
updateSupervisorCount();
|
||||||
|
updateGradeCount();
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
console.log('admin.js: 初始化完成');
|
||||||
|
});
|
@ -14,141 +14,257 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 添加学生表单 -->
|
<!-- 选项卡导航 -->
|
||||||
<div class="row justify-content-center">
|
<ul class="nav nav-tabs mb-4" id="addStudentTabs" role="tablist">
|
||||||
<div class="col-lg-8">
|
<li class="nav-item" role="presentation">
|
||||||
<div class="card shadow">
|
<button class="nav-link active" id="single-tab" data-bs-toggle="tab" data-bs-target="#single"
|
||||||
<div class="card-header py-3">
|
type="button" role="tab" aria-controls="single" aria-selected="true">
|
||||||
<h6 class="m-0 font-weight-bold text-primary">
|
<i class="fas fa-user me-2"></i>单个添加
|
||||||
<i class="fas fa-info-circle me-2"></i>学生基本信息
|
</button>
|
||||||
</h6>
|
</li>
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link" id="batch-tab" data-bs-toggle="tab" data-bs-target="#batch"
|
||||||
|
type="button" role="tab" aria-controls="batch" aria-selected="false">
|
||||||
|
<i class="fas fa-users me-2"></i>批量导入
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<!-- 选项卡内容 -->
|
||||||
|
<div class="tab-content" id="addStudentTabsContent">
|
||||||
|
<!-- 单个添加选项卡 -->
|
||||||
|
<div class="tab-pane fade show active" id="single" role="tabpanel" aria-labelledby="single-tab">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<div class="card shadow">
|
||||||
|
<div class="card-header py-3">
|
||||||
|
<h6 class="m-0 font-weight-bold text-primary">
|
||||||
|
<i class="fas fa-info-circle me-2"></i>学生基本信息
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form id="addStudentForm" method="POST">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">学号 <span class="text-danger">*</span></label>
|
||||||
|
<input type="text" class="form-control" name="student_number" id="student_number" required
|
||||||
|
placeholder="请输入学号">
|
||||||
|
<div class="form-text">学号将作为登录账号使用</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">姓名 <span class="text-danger">*</span></label>
|
||||||
|
<input type="text" class="form-control" name="name" required
|
||||||
|
placeholder="请输入姓名">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">性别 <span class="text-danger">*</span></label>
|
||||||
|
<select class="form-select" name="gender" required>
|
||||||
|
<option value="">请选择性别</option>
|
||||||
|
<option value="男">男</option>
|
||||||
|
<option value="女">女</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">年级 <span class="text-danger">*</span></label>
|
||||||
|
<input type="number" class="form-control" name="grade" required
|
||||||
|
min="20" max="30" placeholder="如:23">
|
||||||
|
<div class="form-text">如:20级、21级、22级</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">手机号</label>
|
||||||
|
<input type="tel" class="form-control" name="phone" maxlength="11"
|
||||||
|
pattern="1[3-9]\d{9}" placeholder="请输入11位手机号">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">学院</label>
|
||||||
|
<input type="text" class="form-control" name="college"
|
||||||
|
placeholder="请输入学院名称">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">专业</label>
|
||||||
|
<input type="text" class="form-control" name="major"
|
||||||
|
placeholder="请输入专业名称">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">导师</label>
|
||||||
|
<input type="text" class="form-control" name="supervisor"
|
||||||
|
placeholder="请输入导师姓名">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">学位类型</label>
|
||||||
|
<select class="form-select" name="degree_type">
|
||||||
|
<option value="">请选择学位类型</option>
|
||||||
|
<option value="专硕">专硕</option>
|
||||||
|
<option value="学硕">学硕</option>
|
||||||
|
<option value="学博">学博</option>
|
||||||
|
<option value="专博">专博</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">入学日期</label>
|
||||||
|
<input type="date" class="form-control" name="enrollment_date">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">状态</label>
|
||||||
|
<select class="form-select" name="status">
|
||||||
|
<option value="在读" selected>在读</option>
|
||||||
|
<option value="毕业">毕业</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">登录密码</label>
|
||||||
|
<input type="password" class="form-control" name="password" id="password" readonly
|
||||||
|
placeholder="将自动设置为学号">
|
||||||
|
<div class="form-text">
|
||||||
|
<i class="fas fa-info-circle text-info me-1"></i>
|
||||||
|
密码将自动设置为学号,学生可在登录后自行修改
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="mb-3 d-flex align-items-end">
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" id="custom_password">
|
||||||
|
<label class="form-check-label" for="custom_password">
|
||||||
|
自定义密码
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="my-4">
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-end gap-2">
|
||||||
|
<a href="{{ url_for('admin.student_list') }}" class="btn btn-secondary">
|
||||||
|
<i class="fas fa-times me-1"></i>取消
|
||||||
|
</a>
|
||||||
|
<button type="submit" class="btn btn-success">
|
||||||
|
<i class="fas fa-save me-1"></i>保存学生信息
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
</div>
|
||||||
<form id="addStudentForm" method="POST">
|
</div>
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-6">
|
<!-- 批量导入选项卡 -->
|
||||||
<div class="mb-3">
|
<div class="tab-pane fade" id="batch" role="tabpanel" aria-labelledby="batch-tab">
|
||||||
<label class="form-label">学号 <span class="text-danger">*</span></label>
|
<div class="row justify-content-center">
|
||||||
<input type="text" class="form-control" name="student_number" required
|
<div class="col-lg-10">
|
||||||
placeholder="请输入学号">
|
<div class="card shadow">
|
||||||
<div class="form-text">学号将作为登录账号使用</div>
|
<div class="card-header py-3">
|
||||||
|
<h6 class="m-0 font-weight-bold text-primary">
|
||||||
|
<i class="fas fa-upload me-2"></i>批量导入学生信息
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<!-- 使用说明 -->
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<h6><i class="fas fa-info-circle me-2"></i>使用说明</h6>
|
||||||
|
<ul class="mb-2">
|
||||||
|
<li>请上传Excel文件(.xlsx或.xls格式)</li>
|
||||||
|
<li>Excel文件应包含以下列:姓名、性别、年级、学号、手机号、导师、学院、专业、学位类型</li>
|
||||||
|
<li>第一行应为表头,数据从第二行开始</li>
|
||||||
|
<li>学号不能重复,如有重复将跳过该记录</li>
|
||||||
|
<li>年级格式:20、21、22等(不要写成2020、2021)</li>
|
||||||
|
</ul>
|
||||||
|
<div class="alert alert-warning mb-0">
|
||||||
|
<i class="fas fa-key me-2"></i>
|
||||||
|
<strong>密码设置:</strong>系统将自动将每个学生的密码设置为其学号,学生可在登录后自行修改
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
|
||||||
|
<!-- 模板下载 -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<h6>📥 下载Excel模板</h6>
|
||||||
|
<a href="{{ url_for('admin.download_student_template') }}" class="btn btn-outline-primary">
|
||||||
|
<i class="fas fa-download me-1"></i>下载Excel模板
|
||||||
|
</a>
|
||||||
|
<small class="text-muted ms-2">建议先下载模板,按照格式填写学生信息</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 文件上传表单 -->
|
||||||
|
<form id="batchImportForm" enctype="multipart/form-data">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">姓名 <span class="text-danger">*</span></label>
|
<label class="form-label">选择Excel文件 <span class="text-danger">*</span></label>
|
||||||
<input type="text" class="form-control" name="name" required
|
<input type="file" class="form-control" name="excel_file" accept=".xlsx,.xls" required>
|
||||||
placeholder="请输入姓名">
|
<div class="form-text">支持 .xlsx 和 .xls 格式</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" id="overwrite_existing" name="overwrite_existing">
|
||||||
|
<label class="form-check-label" for="overwrite_existing">
|
||||||
|
覆盖已存在的学生信息(默认跳过重复学号)
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="my-4">
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-end gap-2">
|
||||||
|
<button type="button" class="btn btn-secondary" onclick="clearBatchForm()">
|
||||||
|
<i class="fas fa-times me-1"></i>清空
|
||||||
|
</button>
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="fas fa-upload me-1"></i>开始导入
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- 导入结果显示区域 -->
|
||||||
|
<div id="importResult" class="mt-4" style="display: none;">
|
||||||
|
<h6>导入结果</h6>
|
||||||
|
<div id="importProgress" class="progress mb-3" style="display: none;">
|
||||||
|
<div class="progress-bar progress-bar-striped progress-bar-animated"
|
||||||
|
role="progressbar" style="width: 0%"></div>
|
||||||
|
</div>
|
||||||
|
<div id="importSummary"></div>
|
||||||
|
<div id="importDetails"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-4">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label">性别 <span class="text-danger">*</span></label>
|
|
||||||
<select class="form-select" name="gender" required>
|
|
||||||
<option value="">请选择性别</option>
|
|
||||||
<option value="男">男</option>
|
|
||||||
<option value="女">女</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label">年级 <span class="text-danger">*</span></label>
|
|
||||||
<input type="number" class="form-control" name="grade" required
|
|
||||||
min="2020" max="2030" placeholder="如:2023">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label">手机号</label>
|
|
||||||
<input type="tel" class="form-control" name="phone" maxlength="11"
|
|
||||||
pattern="1[3-9]\d{9}" placeholder="请输入11位手机号">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-6">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label">学院</label>
|
|
||||||
<input type="text" class="form-control" name="college"
|
|
||||||
placeholder="请输入学院名称">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label">专业</label>
|
|
||||||
<input type="text" class="form-control" name="major"
|
|
||||||
placeholder="请输入专业名称">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-6">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label">导师</label>
|
|
||||||
<input type="text" class="form-control" name="supervisor"
|
|
||||||
placeholder="请输入导师姓名">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label">学位类型</label>
|
|
||||||
<select class="form-select" name="degree_type">
|
|
||||||
<option value="">请选择学位类型</option>
|
|
||||||
<option value="专硕">专硕</option>
|
|
||||||
<option value="学硕">学硕</option>
|
|
||||||
<option value="学博">学博</option>
|
|
||||||
<option value="专博">专博</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-6">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label">入学日期</label>
|
|
||||||
<input type="date" class="form-control" name="enrollment_date">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label">状态</label>
|
|
||||||
<select class="form-select" name="status">
|
|
||||||
<option value="在读" selected>在读</option>
|
|
||||||
<option value="毕业">毕业</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-6">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label">初始密码</label>
|
|
||||||
<input type="password" class="form-control" name="password"
|
|
||||||
placeholder="默认为123456" value="123456">
|
|
||||||
<div class="form-text">学生可在登录后自行修改密码</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr class="my-4">
|
|
||||||
|
|
||||||
<div class="d-flex justify-content-end gap-2">
|
|
||||||
<a href="{{ url_for('admin.student_list') }}" class="btn btn-secondary">
|
|
||||||
<i class="fas fa-times me-1"></i>取消
|
|
||||||
</a>
|
|
||||||
<button type="submit" class="btn btn-success">
|
|
||||||
<i class="fas fa-save me-1"></i>保存学生信息
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -158,11 +274,44 @@
|
|||||||
|
|
||||||
{% block extra_js %}
|
{% block extra_js %}
|
||||||
<script>
|
<script>
|
||||||
|
// 学号输入框变化时自动同步到密码字段
|
||||||
|
document.getElementById('student_number').addEventListener('input', function() {
|
||||||
|
const passwordField = document.getElementById('password');
|
||||||
|
const customPasswordCheckbox = document.getElementById('custom_password');
|
||||||
|
|
||||||
|
if (!customPasswordCheckbox.checked) {
|
||||||
|
passwordField.value = this.value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 自定义密码复选框处理
|
||||||
|
document.getElementById('custom_password').addEventListener('change', function() {
|
||||||
|
const passwordField = document.getElementById('password');
|
||||||
|
const studentNumberField = document.getElementById('student_number');
|
||||||
|
|
||||||
|
if (this.checked) {
|
||||||
|
passwordField.removeAttribute('readonly');
|
||||||
|
passwordField.placeholder = '请输入自定义密码';
|
||||||
|
passwordField.focus();
|
||||||
|
} else {
|
||||||
|
passwordField.setAttribute('readonly', true);
|
||||||
|
passwordField.placeholder = '将自动设置为学号';
|
||||||
|
passwordField.value = studentNumberField.value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 单个添加表单处理
|
||||||
document.getElementById('addStudentForm').addEventListener('submit', function(e) {
|
document.getElementById('addStudentForm').addEventListener('submit', function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const formData = new FormData(this);
|
const formData = new FormData(this);
|
||||||
const data = Object.fromEntries(formData);
|
const data = Object.fromEntries(formData);
|
||||||
|
|
||||||
|
// 如果没有自定义密码,使用学号作为密码
|
||||||
|
const customPasswordCheckbox = document.getElementById('custom_password');
|
||||||
|
if (!customPasswordCheckbox.checked) {
|
||||||
|
data.password = data.student_number;
|
||||||
|
}
|
||||||
|
|
||||||
// 表单验证
|
// 表单验证
|
||||||
if (!data.student_number || !data.name || !data.gender || !data.grade) {
|
if (!data.student_number || !data.name || !data.gender || !data.grade) {
|
||||||
@ -176,6 +325,12 @@ document.getElementById('addStudentForm').addEventListener('submit', function(e)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 验证密码不能为空
|
||||||
|
if (!data.password || data.password.trim() === '') {
|
||||||
|
alert('密码不能为空');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 禁用提交按钮
|
// 禁用提交按钮
|
||||||
const submitBtn = this.querySelector('button[type="submit"]');
|
const submitBtn = this.querySelector('button[type="submit"]');
|
||||||
const originalText = submitBtn.innerHTML;
|
const originalText = submitBtn.innerHTML;
|
||||||
@ -193,33 +348,14 @@ document.getElementById('addStudentForm').addEventListener('submit', function(e)
|
|||||||
.then(result => {
|
.then(result => {
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
// 显示成功消息
|
// 显示成功消息
|
||||||
const alertDiv = document.createElement('div');
|
showAlert('success', result.message + `(密码:${data.password})`);
|
||||||
alertDiv.className = 'alert alert-success alert-dismissible fade show';
|
|
||||||
alertDiv.innerHTML = `
|
|
||||||
<i class="fas fa-check-circle me-2"></i>${result.message}
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
|
||||||
`;
|
|
||||||
|
|
||||||
// 插入到表单前面
|
|
||||||
const card = document.querySelector('.card-body');
|
|
||||||
card.insertBefore(alertDiv, card.firstChild);
|
|
||||||
|
|
||||||
// 3秒后跳转到学生列表
|
// 3秒后跳转到学生列表
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.location.href = '/admin/students';
|
window.location.href = '/admin/students';
|
||||||
}, 2000);
|
}, 3000);
|
||||||
} else {
|
} else {
|
||||||
// 显示错误消息
|
// 显示错误消息
|
||||||
const alertDiv = document.createElement('div');
|
showAlert('danger', result.message);
|
||||||
alertDiv.className = 'alert alert-danger alert-dismissible fade show';
|
|
||||||
alertDiv.innerHTML = `
|
|
||||||
<i class="fas fa-exclamation-triangle me-2"></i>${result.message}
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
|
||||||
`;
|
|
||||||
|
|
||||||
const card = document.querySelector('.card-body');
|
|
||||||
card.insertBefore(alertDiv, card.firstChild);
|
|
||||||
|
|
||||||
// 恢复提交按钮
|
// 恢复提交按钮
|
||||||
submitBtn.disabled = false;
|
submitBtn.disabled = false;
|
||||||
submitBtn.innerHTML = originalText;
|
submitBtn.innerHTML = originalText;
|
||||||
@ -227,14 +363,158 @@ document.getElementById('addStudentForm').addEventListener('submit', function(e)
|
|||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error('Error:', error);
|
console.error('Error:', error);
|
||||||
alert('网络错误,请稍后重试');
|
showAlert('danger', '网络错误,请稍后重试');
|
||||||
|
|
||||||
// 恢复提交按钮
|
// 恢复提交按钮
|
||||||
submitBtn.disabled = false;
|
submitBtn.disabled = false;
|
||||||
submitBtn.innerHTML = originalText;
|
submitBtn.innerHTML = originalText;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 批量导入表单处理
|
||||||
|
document.getElementById('batchImportForm').addEventListener('submit', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const formData = new FormData(this);
|
||||||
|
const fileInput = this.querySelector('input[name="excel_file"]');
|
||||||
|
|
||||||
|
if (!fileInput.files[0]) {
|
||||||
|
alert('请选择要上传的Excel文件');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证文件格式
|
||||||
|
const fileName = fileInput.files[0].name;
|
||||||
|
if (!fileName.match(/\.(xlsx|xls)$/i)) {
|
||||||
|
alert('请上传Excel文件(.xlsx或.xls格式)');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示进度条
|
||||||
|
const importResult = document.getElementById('importResult');
|
||||||
|
const importProgress = document.getElementById('importProgress');
|
||||||
|
const progressBar = importProgress.querySelector('.progress-bar');
|
||||||
|
|
||||||
|
importResult.style.display = 'block';
|
||||||
|
importProgress.style.display = 'block';
|
||||||
|
progressBar.style.width = '10%';
|
||||||
|
|
||||||
|
// 禁用提交按钮
|
||||||
|
const submitBtn = this.querySelector('button[type="submit"]');
|
||||||
|
const originalText = submitBtn.innerHTML;
|
||||||
|
submitBtn.disabled = true;
|
||||||
|
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>导入中...';
|
||||||
|
|
||||||
|
// 发送文件
|
||||||
|
fetch('/admin/students/batch_import', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
progressBar.style.width = '50%';
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(result => {
|
||||||
|
progressBar.style.width = '100%';
|
||||||
|
|
||||||
|
// 隐藏进度条
|
||||||
|
setTimeout(() => {
|
||||||
|
importProgress.style.display = 'none';
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
// 显示结果
|
||||||
|
displayImportResult(result);
|
||||||
|
|
||||||
|
// 恢复提交按钮
|
||||||
|
submitBtn.disabled = false;
|
||||||
|
submitBtn.innerHTML = originalText;
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
progressBar.style.width = '100%';
|
||||||
|
progressBar.classList.add('bg-danger');
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
importProgress.style.display = 'none';
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
showAlert('danger', '导入失败,请稍后重试');
|
||||||
|
|
||||||
|
// 恢复提交按钮
|
||||||
|
submitBtn.disabled = false;
|
||||||
|
submitBtn.innerHTML = originalText;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 显示导入结果
|
||||||
|
function displayImportResult(result) {
|
||||||
|
const summaryDiv = document.getElementById('importSummary');
|
||||||
|
const detailsDiv = document.getElementById('importDetails');
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
summaryDiv.innerHTML = `
|
||||||
|
<div class="alert alert-success">
|
||||||
|
<h6><i class="fas fa-check-circle me-2"></i>导入完成</h6>
|
||||||
|
<p class="mb-1">成功导入 <strong>${result.success_count}</strong> 个学生,
|
||||||
|
跳过 <strong>${result.skip_count || 0}</strong> 个,
|
||||||
|
失败 <strong>${result.error_count || 0}</strong> 个</p>
|
||||||
|
<small class="text-muted">
|
||||||
|
<i class="fas fa-key me-1"></i>所有学生的初始密码均为其学号
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// 显示详细信息
|
||||||
|
if (result.details && result.details.length > 0) {
|
||||||
|
let detailsHtml = '<div class="mt-3"><h6>详细信息</h6><ul class="list-group">';
|
||||||
|
result.details.forEach(detail => {
|
||||||
|
const iconClass = detail.status === 'success' ? 'text-success fas fa-check' :
|
||||||
|
detail.status === 'skip' ? 'text-warning fas fa-exclamation-triangle' :
|
||||||
|
'text-danger fas fa-times';
|
||||||
|
detailsHtml += `
|
||||||
|
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||||
|
<span>${detail.name} (${detail.student_number})</span>
|
||||||
|
<span class="${iconClass.includes('success') ? 'text-success' : iconClass.includes('warning') ? 'text-warning' : 'text-danger'}">
|
||||||
|
<i class="${iconClass}"></i> ${detail.message}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
detailsHtml += '</ul></div>';
|
||||||
|
detailsDiv.innerHTML = detailsHtml;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
summaryDiv.innerHTML = `
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
<h6><i class="fas fa-exclamation-triangle me-2"></i>导入失败</h6>
|
||||||
|
<p class="mb-0">${result.message}</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空批量导入表单
|
||||||
|
function clearBatchForm() {
|
||||||
|
document.getElementById('batchImportForm').reset();
|
||||||
|
document.getElementById('importResult').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示提示消息
|
||||||
|
function showAlert(type, message) {
|
||||||
|
const alertDiv = document.createElement('div');
|
||||||
|
alertDiv.className = `alert alert-${type} alert-dismissible fade show`;
|
||||||
|
alertDiv.innerHTML = `
|
||||||
|
<i class="fas fa-${type === 'success' ? 'check-circle' : 'exclamation-triangle'} me-2"></i>${message}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// 插入到当前活动的选项卡中
|
||||||
|
const activeTab = document.querySelector('.tab-pane.active .card-body');
|
||||||
|
activeTab.insertBefore(alertDiv, activeTab.firstChild);
|
||||||
|
|
||||||
|
// 滚动到顶部
|
||||||
|
window.scrollTo(0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
// 学号输入框失焦时检查是否已存在
|
// 学号输入框失焦时检查是否已存在
|
||||||
document.querySelector('input[name="student_number"]').addEventListener('blur', function() {
|
document.querySelector('input[name="student_number"]').addEventListener('blur', function() {
|
||||||
const studentNumber = this.value.trim();
|
const studentNumber = this.value.trim();
|
||||||
|
@ -123,10 +123,11 @@
|
|||||||
<div class="row no-gutters align-items-center">
|
<div class="row no-gutters align-items-center">
|
||||||
<div class="col mr-2">
|
<div class="col mr-2">
|
||||||
<div class="text-xs font-weight-bold text-warning text-uppercase mb-1">
|
<div class="text-xs font-weight-bold text-warning text-uppercase mb-1">
|
||||||
旷工天数
|
缺勤次数
|
||||||
</div>
|
</div>
|
||||||
<div class="h5 mb-0 font-weight-bold text-gray-800">
|
<div class="h5 mb-0 font-weight-bold text-gray-800">
|
||||||
{{ weekly_record.absent_days }}天
|
{# 🔥 修改:显示缺勤次数而不是天数 #}
|
||||||
|
{{ weekly_record.absent_count if weekly_record.absent_count is defined else weekly_record.absent_days }}次
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
@ -319,7 +320,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th>周期</th>
|
<th>周期</th>
|
||||||
<th>出勤时长</th>
|
<th>出勤时长</th>
|
||||||
<th>旷工天数</th>
|
<th>缺勤次数</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@ -332,10 +333,11 @@
|
|||||||
<span class="badge bg-primary">{{ "%.1f"|format(record.actual_work_hours) }}h</span>
|
<span class="badge bg-primary">{{ "%.1f"|format(record.actual_work_hours) }}h</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
{# 🔥 修改:显示缺勤次数而不是天数 #}
|
||||||
{% if record.absent_days > 0 %}
|
{% if record.absent_days > 0 %}
|
||||||
<span class="badge bg-warning">{{ record.absent_days }}</span>
|
<span class="badge bg-warning">{{ record.absent_days }}次</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="badge bg-success">0</span>
|
<span class="badge bg-success">0次</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -52,8 +52,8 @@
|
|||||||
<option value="actual_work_hours_asc" {{ 'selected' if sort_by == 'actual_work_hours_asc' else '' }}>出勤时长↑</option>
|
<option value="actual_work_hours_asc" {{ 'selected' if sort_by == 'actual_work_hours_asc' else '' }}>出勤时长↑</option>
|
||||||
<option value="class_work_hours_desc" {{ 'selected' if sort_by == 'class_work_hours_desc' else '' }}>班内工作↓</option>
|
<option value="class_work_hours_desc" {{ 'selected' if sort_by == 'class_work_hours_desc' else '' }}>班内工作↓</option>
|
||||||
<option value="class_work_hours_asc" {{ 'selected' if sort_by == 'class_work_hours_asc' else '' }}>班内工作↑</option>
|
<option value="class_work_hours_asc" {{ 'selected' if sort_by == 'class_work_hours_asc' else '' }}>班内工作↑</option>
|
||||||
<option value="absent_days_desc" {{ 'selected' if sort_by == 'absent_days_desc' else '' }}>旷工天数↓</option>
|
<option value="absent_count_desc" {{ 'selected' if sort_by == 'absent_count_desc' or sort_by == 'absent_days_desc' else '' }}>缺勤次数↓</option>
|
||||||
<option value="absent_days_asc" {{ 'selected' if sort_by == 'absent_days_asc' else '' }}>旷工天数↑</option>
|
<option value="absent_count_asc" {{ 'selected' if sort_by == 'absent_count_asc' or sort_by == 'absent_days_asc' else '' }}>缺勤次数↑</option>
|
||||||
<option value="late_count_desc" {{ 'selected' if sort_by == 'late_count_desc' else '' }}>迟到次数↓</option>
|
<option value="late_count_desc" {{ 'selected' if sort_by == 'late_count_desc' else '' }}>迟到次数↓</option>
|
||||||
<option value="late_count_asc" {{ 'selected' if sort_by == 'late_count_asc' else '' }}>迟到次数↑</option>
|
<option value="late_count_asc" {{ 'selected' if sort_by == 'late_count_asc' else '' }}>迟到次数↑</option>
|
||||||
<option value="overtime_hours_desc" {{ 'selected' if sort_by == 'overtime_hours_desc' else '' }}>加班时长↓</option>
|
<option value="overtime_hours_desc" {{ 'selected' if sort_by == 'overtime_hours_desc' else '' }}>加班时长↓</option>
|
||||||
@ -71,6 +71,60 @@
|
|||||||
<i class="fas fa-cog"></i>
|
<i class="fas fa-cog"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 🔥 新增:高级搜索区域 -->
|
||||||
|
<div id="advancedSearchArea" class="col-12 mt-3" style="display: none;">
|
||||||
|
<div class="border-top pt-3">
|
||||||
|
<h6 class="text-muted mb-3">
|
||||||
|
<i class="fas fa-filter me-2"></i>高级搜索
|
||||||
|
</h6>
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">导师(多选)</label>
|
||||||
|
<div class="supervisor-checkboxes" style="max-height: 200px; overflow-y: auto; border: 1px solid #ced4da; border-radius: 0.375rem; padding: 10px;">
|
||||||
|
{% for supervisor in all_supervisors %}
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" name="supervisor"
|
||||||
|
value="{{ supervisor }}" id="supervisor_{{ loop.index }}"
|
||||||
|
{{ 'checked' if supervisor in selected_supervisors else '' }}>
|
||||||
|
<label class="form-check-label" for="supervisor_{{ loop.index }}">
|
||||||
|
{{ supervisor }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<div class="mt-2">
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-primary me-2" onclick="selectAllSupervisors()">全选</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="clearAllSupervisors()">清空</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">年级(多选)</label>
|
||||||
|
<div class="grade-checkboxes" style="max-height: 200px; overflow-y: auto; border: 1px solid #ced4da; border-radius: 0.375rem; padding: 10px;">
|
||||||
|
{% for grade in all_grades %}
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" name="grade"
|
||||||
|
value="{{ grade }}" id="grade_{{ grade }}"
|
||||||
|
{{ 'checked' if grade|string in selected_grades else '' }}>
|
||||||
|
<label class="form-check-label" for="grade_{{ grade }}">
|
||||||
|
{% if grade >= 20 %}
|
||||||
|
{{ grade }}级
|
||||||
|
{% else %}
|
||||||
|
{{ grade }}年级
|
||||||
|
{% endif %}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<div class="mt-2">
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-primary me-2" onclick="selectAllGrades()">全选</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="clearAllGrades()">清空</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 隐藏字段保持分页状态 -->
|
<!-- 隐藏字段保持分页状态 -->
|
||||||
<input type="hidden" name="page" value="{{ pagination.page if pagination else 1 }}">
|
<input type="hidden" name="page" value="{{ pagination.page if pagination else 1 }}">
|
||||||
</form>
|
</form>
|
||||||
@ -106,8 +160,8 @@
|
|||||||
班内工作
|
班内工作
|
||||||
<i class="fas fa-sort ms-1 text-muted sort-icon"></i>
|
<i class="fas fa-sort ms-1 text-muted sort-icon"></i>
|
||||||
</th>
|
</th>
|
||||||
<th class="sortable" data-sort="absent_days">
|
<th class="sortable" data-sort="absent_count">
|
||||||
旷工天数
|
缺勤次数
|
||||||
<i class="fas fa-sort ms-1 text-muted sort-icon"></i>
|
<i class="fas fa-sort ms-1 text-muted sort-icon"></i>
|
||||||
</th>
|
</th>
|
||||||
<th class="sortable" data-sort="late_count">
|
<th class="sortable" data-sort="late_count">
|
||||||
@ -152,10 +206,12 @@
|
|||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{% if record.absent_days > 0 %}
|
{# 🔥 修改:使用 absent_count 而不是 absent_days,单位改为"次" #}
|
||||||
<span class="badge bg-warning">{{ record.absent_days }}天</span>
|
{% set absent_count = record.absent_count if record.absent_count is defined else 0 %}
|
||||||
|
{% if absent_count > 0 %}
|
||||||
|
<span class="badge bg-warning">{{ absent_count }}次</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="badge bg-success">0天</span>
|
<span class="badge bg-success">0次</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
@ -301,9 +357,11 @@
|
|||||||
<div class="col-1">
|
<div class="col-1">
|
||||||
<div class="border-end">
|
<div class="border-end">
|
||||||
<h6 class="text-danger">
|
<h6 class="text-danger">
|
||||||
{{ attendance_records|sum(attribute='absent_days') }}
|
{# 🔥 修改:使用 absent_count 统计总缺勤次数 #}
|
||||||
|
{% set total_absent_count = attendance_records|sum(attribute='absent_count') if attendance_records[0].absent_count is defined else 0 %}
|
||||||
|
{{ total_absent_count }}
|
||||||
</h6>
|
</h6>
|
||||||
<small class="text-muted">旷工</small>
|
<small class="text-muted">缺勤</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-1">
|
<div class="col-1">
|
||||||
@ -374,6 +432,11 @@
|
|||||||
<i class="fas fa-download me-2"></i>导出数据
|
<i class="fas fa-download me-2"></i>导出数据
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-6 mb-2">
|
||||||
|
<button class="btn btn-outline-warning btn-block" onclick="clearAllFilters()">
|
||||||
|
<i class="fas fa-eraser me-2"></i>清空筛选
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -531,9 +594,8 @@ document.getElementById('confirmDelete').addEventListener('click', function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function exportData() {
|
function exportData() {
|
||||||
const params = new URLSearchParams(window.location.search);
|
// 使用 admin.js 中的 exportCurrentFilter 函数
|
||||||
params.set('export', 'excel');
|
exportCurrentFilter();
|
||||||
window.location.href = '{{ url_for("admin.attendance_management") }}?' + params.toString();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 自动设置结束日期为开始日期的一周后
|
// 自动设置结束日期为开始日期的一周后
|
||||||
@ -641,10 +703,7 @@ function setupTableSorting() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 高级搜索切换(预留功能)
|
// 高级搜索切换功能已在 admin.js 中实现
|
||||||
function toggleAdvancedSearch() {
|
|
||||||
alert('高级搜索功能开发中...');
|
|
||||||
}
|
|
||||||
|
|
||||||
// DOM加载完成后初始化
|
// DOM加载完成后初始化
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
@ -653,4 +712,5 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{% endblock %}
|
<script src="{{ url_for('static', filename='js/admin.js') }}"></script>
|
||||||
|
{% endblock %}
|
@ -66,7 +66,8 @@
|
|||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">年级 <span class="text-danger">*</span></label>
|
<label class="form-label">年级 <span class="text-danger">*</span></label>
|
||||||
<input type="number" class="form-control" name="grade" required
|
<input type="number" class="form-control" name="grade" required
|
||||||
min="2020" max="2030" value="{{ student.grade }}" placeholder="如:2023">
|
min="20" max="30" value="{{ student.grade }}" placeholder="如:23">
|
||||||
|
<div class="form-text">如:20级、21级、22级</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
|
@ -175,8 +175,8 @@
|
|||||||
<div class="text-muted small">总工作时长(小时)</div>
|
<div class="text-muted small">总工作时长(小时)</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6">
|
<div class="col-6">
|
||||||
<div class="h4 mb-0 text-warning">{{ total_absent_days }}</div>
|
<div class="h4 mb-0 text-warning">{{ total_absent_count }}</div>
|
||||||
<div class="text-muted small">旷工天数</div>
|
<div class="text-muted small">缺勤次数</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<hr class="my-3">
|
<hr class="my-3">
|
||||||
@ -286,7 +286,7 @@
|
|||||||
<th>周期</th>
|
<th>周期</th>
|
||||||
<th>实际工作时长</th>
|
<th>实际工作时长</th>
|
||||||
<th>班内工作时长</th>
|
<th>班内工作时长</th>
|
||||||
<th>旷工天数</th>
|
<th>缺勤次数</th>
|
||||||
<th>加班时长</th>
|
<th>加班时长</th>
|
||||||
<th>创建时间</th>
|
<th>创建时间</th>
|
||||||
</tr>
|
</tr>
|
||||||
@ -315,11 +315,12 @@
|
|||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{% if record.absent_days > 0 %}
|
{% set absent_count = record.absent_count if record.absent_count is defined else 0 %}
|
||||||
<span class="badge bg-warning">{{ record.absent_days }}天</span>
|
{% if absent_count > 0 %}
|
||||||
|
<span class="badge bg-warning">{{ absent_count }}次</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="text-success">
|
<span class="text-success">
|
||||||
<i class="fas fa-check"></i> 0天
|
<i class="fas fa-check"></i> 0次
|
||||||
</span>
|
</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
|
@ -160,11 +160,13 @@
|
|||||||
|
|
||||||
<!-- 分页 -->
|
<!-- 分页 -->
|
||||||
{% if pagination.pages > 1 %}
|
{% if pagination.pages > 1 %}
|
||||||
|
{% set args_without_page = request.args.copy() %}
|
||||||
|
{% if args_without_page.pop('page', None) %}{% endif %}
|
||||||
<nav aria-label="Page navigation" class="mt-4">
|
<nav aria-label="Page navigation" class="mt-4">
|
||||||
<ul class="pagination justify-content-center">
|
<ul class="pagination justify-content-center">
|
||||||
{% if pagination.has_prev %}
|
{% if pagination.has_prev %}
|
||||||
<li class="page-item">
|
<li class="page-item">
|
||||||
<a class="page-link" href="{{ url_for('admin.student_list', page=pagination.prev_num, **request.args) }}">
|
<a class="page-link" href="{{ url_for('admin.student_list', page=pagination.prev_num, **args_without_page) }}">
|
||||||
上一页
|
上一页
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@ -174,7 +176,7 @@
|
|||||||
{% if page_num %}
|
{% if page_num %}
|
||||||
{% if page_num != pagination.page %}
|
{% if page_num != pagination.page %}
|
||||||
<li class="page-item">
|
<li class="page-item">
|
||||||
<a class="page-link" href="{{ url_for('admin.student_list', page=page_num, **request.args) }}">
|
<a class="page-link" href="{{ url_for('admin.student_list', page=page_num, **args_without_page) }}">
|
||||||
{{ page_num }}
|
{{ page_num }}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@ -192,7 +194,7 @@
|
|||||||
|
|
||||||
{% if pagination.has_next %}
|
{% if pagination.has_next %}
|
||||||
<li class="page-item">
|
<li class="page-item">
|
||||||
<a class="page-link" href="{{ url_for('admin.student_list', page=pagination.next_num, **request.args) }}">
|
<a class="page-link" href="{{ url_for('admin.student_list', page=pagination.next_num, **args_without_page) }}">
|
||||||
下一页
|
下一页
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -38,3 +38,4 @@ python-dateutil==2.8.2
|
|||||||
# 其他工具
|
# 其他工具
|
||||||
click==8.1.7
|
click==8.1.7
|
||||||
itsdangerous==2.1.2
|
itsdangerous==2.1.2
|
||||||
|
openpyxl
|
51
update_foreign_keys.py
Normal file
51
update_foreign_keys.py
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
更新数据库外键约束,添加级联删除
|
||||||
|
"""
|
||||||
|
|
||||||
|
from app import create_app
|
||||||
|
from app.models import db
|
||||||
|
from sqlalchemy import text
|
||||||
|
|
||||||
|
def update_foreign_keys():
|
||||||
|
app = create_app()
|
||||||
|
|
||||||
|
with app.app_context():
|
||||||
|
try:
|
||||||
|
print("正在更新数据库外键约束...")
|
||||||
|
|
||||||
|
# 删除现有的外键约束并重新创建带级联删除的约束
|
||||||
|
queries = [
|
||||||
|
# 删除现有外键约束
|
||||||
|
"ALTER TABLE weekly_attendance DROP FOREIGN KEY weekly_attendance_ibfk_1",
|
||||||
|
"ALTER TABLE daily_attendance_details DROP FOREIGN KEY daily_attendance_details_ibfk_1",
|
||||||
|
"ALTER TABLE daily_attendance_details DROP FOREIGN KEY daily_attendance_details_ibfk_2",
|
||||||
|
"ALTER TABLE leave_records DROP FOREIGN KEY leave_records_ibfk_1",
|
||||||
|
"ALTER TABLE students DROP FOREIGN KEY students_ibfk_1",
|
||||||
|
|
||||||
|
# 重新创建带级联删除的外键约束
|
||||||
|
"ALTER TABLE weekly_attendance ADD CONSTRAINT weekly_attendance_ibfk_1 FOREIGN KEY (student_number) REFERENCES students (student_number) ON DELETE CASCADE",
|
||||||
|
"ALTER TABLE daily_attendance_details ADD CONSTRAINT daily_attendance_details_ibfk_1 FOREIGN KEY (weekly_record_id) REFERENCES weekly_attendance (record_id) ON DELETE CASCADE",
|
||||||
|
"ALTER TABLE daily_attendance_details ADD CONSTRAINT daily_attendance_details_ibfk_2 FOREIGN KEY (student_number) REFERENCES students (student_number) ON DELETE CASCADE",
|
||||||
|
"ALTER TABLE leave_records ADD CONSTRAINT leave_records_ibfk_1 FOREIGN KEY (student_number) REFERENCES students (student_number) ON DELETE CASCADE",
|
||||||
|
"ALTER TABLE students ADD CONSTRAINT students_ibfk_1 FOREIGN KEY (student_number) REFERENCES users (student_number) ON DELETE CASCADE",
|
||||||
|
]
|
||||||
|
|
||||||
|
for query in queries:
|
||||||
|
try:
|
||||||
|
db.session.execute(text(query))
|
||||||
|
print(f"执行成功: {query}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"执行失败 (可能约束不存在): {query}")
|
||||||
|
print(f"错误: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
print("数据库外键约束更新完成!")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
print(f"更新失败: {e}")
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
update_foreign_keys()
|
Loading…
x
Reference in New Issue
Block a user