528 lines
25 KiB
HTML
528 lines
25 KiB
HTML
{% extends 'layout/base.html' %}
|
||
|
||
{% block title %}添加学生 - SmartDSP考勤管理系统{% endblock %}
|
||
|
||
{% block content %}
|
||
<div class="container-fluid mt-4">
|
||
<!-- 页面标题 -->
|
||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||
<h1 class="h3 mb-0">
|
||
<i class="fas fa-user-plus me-2"></i>添加学生
|
||
</h1>
|
||
<a href="{{ url_for('admin.student_list') }}" class="btn btn-outline-secondary">
|
||
<i class="fas fa-arrow-left me-1"></i>返回学生列表
|
||
</a>
|
||
</div>
|
||
|
||
<!-- 选项卡导航 -->
|
||
<ul class="nav nav-tabs mb-4" id="addStudentTabs" role="tablist">
|
||
<li class="nav-item" role="presentation">
|
||
<button class="nav-link active" id="single-tab" data-bs-toggle="tab" data-bs-target="#single"
|
||
type="button" role="tab" aria-controls="single" aria-selected="true">
|
||
<i class="fas fa-user me-2"></i>单个添加
|
||
</button>
|
||
</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>
|
||
|
||
<!-- 批量导入选项卡 -->
|
||
<div class="tab-pane fade" id="batch" role="tabpanel" aria-labelledby="batch-tab">
|
||
<div class="row justify-content-center">
|
||
<div class="col-lg-10">
|
||
<div class="card shadow">
|
||
<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 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">
|
||
<label class="form-label">选择Excel文件 <span class="text-danger">*</span></label>
|
||
<input type="file" class="form-control" name="excel_file" accept=".xlsx,.xls" required>
|
||
<div class="form-text">支持 .xlsx 和 .xls 格式</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>
|
||
</div>
|
||
</div>
|
||
{% endblock %}
|
||
|
||
{% block extra_js %}
|
||
<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) {
|
||
e.preventDefault();
|
||
|
||
const formData = new FormData(this);
|
||
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) {
|
||
alert('请填写所有必填项');
|
||
return;
|
||
}
|
||
|
||
// 验证手机号格式
|
||
if (data.phone && !/^1[3-9]\d{9}$/.test(data.phone)) {
|
||
alert('手机号格式不正确');
|
||
return;
|
||
}
|
||
|
||
// 验证密码不能为空
|
||
if (!data.password || data.password.trim() === '') {
|
||
alert('密码不能为空');
|
||
return;
|
||
}
|
||
|
||
// 禁用提交按钮
|
||
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/add', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify(data)
|
||
})
|
||
.then(response => response.json())
|
||
.then(result => {
|
||
if (result.success) {
|
||
// 显示成功消息
|
||
showAlert('success', result.message + `(密码:${data.password})`);
|
||
// 3秒后跳转到学生列表
|
||
setTimeout(() => {
|
||
window.location.href = '/admin/students';
|
||
}, 3000);
|
||
} else {
|
||
// 显示错误消息
|
||
showAlert('danger', result.message);
|
||
// 恢复提交按钮
|
||
submitBtn.disabled = false;
|
||
submitBtn.innerHTML = originalText;
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Error:', error);
|
||
showAlert('danger', '网络错误,请稍后重试');
|
||
// 恢复提交按钮
|
||
submitBtn.disabled = false;
|
||
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() {
|
||
const studentNumber = this.value.trim();
|
||
if (studentNumber) {
|
||
// 这里可以添加AJAX检查学号是否已存在的逻辑
|
||
// 为了简化,暂时不实现
|
||
}
|
||
});
|
||
</script>
|
||
{% endblock %}
|