add manage student password
This commit is contained in:
@@ -12,6 +12,7 @@
|
||||
- 学生端和教师端都能直接看到全部组及成员的学号、姓名,无需点开详情。
|
||||
- 教师可切换“当前开放登录班级”,未开放班级不能登录。
|
||||
- 教师可停止/恢复学生分组操作。
|
||||
- 教师可在“学生账号”页签中修改指定学生的登录密码;修改后该学生已有会话会退出。
|
||||
- 停止后学生只能查看;教师仍可手工建组、改名、设组长、加入、移出、解散。
|
||||
- 教师可导出当前班分组为 `.xlsx`。
|
||||
|
||||
|
||||
@@ -183,6 +183,17 @@ class GroupRequestHandler:
|
||||
status = HTTPStatus.OK if ok else HTTPStatus.BAD_REQUEST
|
||||
return json_response(status, {"ok": ok, "message": message})
|
||||
|
||||
if action == "teacher/student-password" and method == "POST":
|
||||
session, error = self.require_teacher()
|
||||
if error:
|
||||
return error
|
||||
data = self.read_json()
|
||||
ok, message = self.store.teacher_set_student_password(
|
||||
int(data.get("student_id", 0)), data.get("new_password", "")
|
||||
)
|
||||
status = HTTPStatus.OK if ok else HTTPStatus.BAD_REQUEST
|
||||
return json_response(status, {"ok": ok, "message": message})
|
||||
|
||||
if action == "teacher/group-limit" and method == "POST":
|
||||
_, error = self.require_teacher()
|
||||
if error:
|
||||
|
||||
@@ -136,6 +136,30 @@ class AccountStoreMixin:
|
||||
)
|
||||
return True, "密码修改成功"
|
||||
|
||||
def teacher_set_student_password(
|
||||
self, student_id: int, new_password: str
|
||||
) -> tuple[bool, str]:
|
||||
"""教师后台重置指定学生的登录密码。"""
|
||||
new_password = new_password or ""
|
||||
if len(new_password) < 8:
|
||||
return False, "新密码至少 8 位"
|
||||
with self.lock, self.connection:
|
||||
student = self.connection.execute(
|
||||
"SELECT student_no FROM students WHERE id=?", (student_id,)
|
||||
).fetchone()
|
||||
if student is None:
|
||||
return False, "学生不存在"
|
||||
if new_password == student["student_no"]:
|
||||
return False, "新密码不能设置为学号"
|
||||
self.connection.execute(
|
||||
"UPDATE students SET password_hash=?, must_change_password=0 WHERE id=?",
|
||||
(hash_password(new_password), student_id),
|
||||
)
|
||||
self.connection.execute(
|
||||
"DELETE FROM sessions WHERE student_id=?", (student_id,)
|
||||
)
|
||||
return True, f"已修改 {student['student_no']} 的密码"
|
||||
|
||||
def get_student(self, student_id: int) -> dict[str, Any] | None:
|
||||
with self.lock:
|
||||
row = self.connection.execute("SELECT * FROM students WHERE id=?", (student_id,)).fetchone()
|
||||
|
||||
@@ -124,6 +124,14 @@ h3 { margin: 0; font-size: 17px; }
|
||||
.import-label:hover { background: #e2e8f0; }
|
||||
.settings-divider { height: 1px; margin: 26px 0 22px; background: var(--border); }
|
||||
.settings-password-line { margin-top: 14px; }
|
||||
.student-search { max-width: 560px; }
|
||||
.student-search + .account-password-form { margin-top: 14px; }
|
||||
.account-password-form {
|
||||
display: grid; grid-template-columns: max-content minmax(220px, 340px);
|
||||
gap: 12px 14px; align-items: center; max-width: 560px;
|
||||
}
|
||||
.account-password-form .field-label { color: var(--muted); font-size: 14px; font-weight: 600; }
|
||||
.account-password-form button { grid-column: 2; justify-self: start; }
|
||||
|
||||
.grid {
|
||||
display: grid; grid-template-columns: repeat(auto-fill,minmax(290px,1fr)); gap: 16px;
|
||||
@@ -185,6 +193,8 @@ h3 { margin: 0; font-size: 17px; }
|
||||
.admin-tab { min-width: 96px; height: 38px; font-size: 13px; }
|
||||
.class-panel, .import-label, .create-group-line { margin-bottom: 14px; }
|
||||
.form-line { flex-direction: column; align-items: stretch; }
|
||||
.account-password-form { grid-template-columns: 1fr; }
|
||||
.account-password-form button { grid-column: auto; justify-self: stretch; }
|
||||
.form-line button, .form-line span { width: 100%; text-align: center; }
|
||||
.form-line span { height: auto; color: var(--muted); font-size: 13px; }
|
||||
}
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
<nav class="admin-tabs" role="tablist" aria-label="教师管理功能">
|
||||
<button class="admin-tab active" id="groupsTab" data-tab="groups" aria-controls="groupsPanel" aria-selected="true" type="button">小组管理</button>
|
||||
<button class="admin-tab" id="ungroupedTab" data-tab="ungrouped" aria-controls="ungroupedPanel" aria-selected="false" type="button">未分组学生</button>
|
||||
<button class="admin-tab" id="accountsTab" data-tab="accounts" aria-controls="accountsPanel" aria-selected="false" type="button">学生账号</button>
|
||||
<button class="admin-tab" id="settingsTab" data-tab="settings" aria-controls="settingsPanel" aria-selected="false" type="button">设置与安全</button>
|
||||
</nav>
|
||||
|
||||
@@ -71,6 +72,25 @@
|
||||
<div id="ungroupedList"></div>
|
||||
</section>
|
||||
|
||||
<section class="panel tab-panel hidden" id="accountsPanel" role="tabpanel" aria-labelledby="accountsTab">
|
||||
<div class="section-head"><h2>修改学生密码</h2></div>
|
||||
<div class="student-search">
|
||||
<label class="field-label" for="studentNoSearch">搜索学号</label>
|
||||
<input id="studentNoSearch" type="search" autocomplete="off" placeholder="输入部分学号筛选学生">
|
||||
</div>
|
||||
<form class="account-password-form" id="studentPasswordForm">
|
||||
<label class="field-label" for="studentSelect">学生</label>
|
||||
<select id="studentSelect" required aria-label="选择学生"></select>
|
||||
<label class="field-label" for="studentNewPassword">新密码</label>
|
||||
<input id="studentNewPassword" type="password" autocomplete="new-password" minlength="8" placeholder="至少 8 位" required>
|
||||
<label class="field-label" for="studentConfirmPassword">确认新密码</label>
|
||||
<input id="studentConfirmPassword" type="password" autocomplete="new-password" minlength="8" placeholder="再次输入新密码" required>
|
||||
<button data-working type="submit">修改密码</button>
|
||||
</form>
|
||||
<div class="notice" id="studentPasswordNotice"></div>
|
||||
<p class="hint">修改后请把新密码告知学生;该学生的所有已登录会话会立即退出。</p>
|
||||
</section>
|
||||
|
||||
<section class="panel tab-panel hidden" id="settingsPanel" role="tabpanel" aria-labelledby="settingsTab">
|
||||
<div class="section-head"><h2>设置与安全</h2></div>
|
||||
<h3>分组上限</h3>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const state = { classes: [], snapshot: null, activeClass: '', username: '' };
|
||||
const el = id => document.getElementById(id);
|
||||
const adminTabs = ['groups', 'ungrouped', 'settings'];
|
||||
const adminTabs = ['groups', 'ungrouped', 'accounts', 'settings'];
|
||||
|
||||
function renderUnauthenticated() {
|
||||
el('loginArea').classList.remove('hidden');
|
||||
@@ -28,6 +28,56 @@ function renderState() {
|
||||
renderStatistics();
|
||||
renderGroups();
|
||||
renderUngrouped();
|
||||
renderStudentAccounts();
|
||||
}
|
||||
|
||||
function allStudents() {
|
||||
return [
|
||||
...state.snapshot.groups.flatMap(group => group.members),
|
||||
...state.snapshot.ungrouped
|
||||
].sort((left, right) => String(left.student_no).localeCompare(String(right.student_no), 'zh-Hans-CN'));
|
||||
}
|
||||
|
||||
function renderStudentAccounts() {
|
||||
const keyword = el('studentNoSearch').value.trim().toLocaleLowerCase();
|
||||
const students = allStudents().filter(student =>
|
||||
String(student.student_no).toLocaleLowerCase().includes(keyword)
|
||||
);
|
||||
const select = el('studentSelect');
|
||||
const previous = select.value;
|
||||
select.innerHTML = students.length ? students.map(student => `
|
||||
<option value="${student.id}">${text(student.student_no)} · ${text(student.name)}</option>
|
||||
`).join('') : '<option value="">未找到匹配的学号</option>';
|
||||
if (students.some(student => String(student.id) === previous)) {
|
||||
select.value = previous;
|
||||
}
|
||||
}
|
||||
|
||||
async function submitStudentPassword(event) {
|
||||
event.preventDefault();
|
||||
const notice = el('studentPasswordNotice');
|
||||
const select = el('studentSelect');
|
||||
const student = allStudents().find(item => String(item.id) === select.value);
|
||||
const newPassword = el('studentNewPassword').value;
|
||||
const confirmPassword = el('studentConfirmPassword').value;
|
||||
notice.textContent = '';
|
||||
if (!student) {
|
||||
notice.textContent = '请选择要修改密码的学生';
|
||||
return;
|
||||
}
|
||||
if (newPassword !== confirmPassword) {
|
||||
notice.textContent = '两次输入的新密码不一致';
|
||||
return;
|
||||
}
|
||||
if (!requireConfirm(`确定修改 ${student.student_no} ${student.name} 的登录密码吗?修改后该学生需要重新登录。`)) return;
|
||||
setButtonsDisabled(el('studentPasswordForm'), true);
|
||||
const result = await api('teacher/student-password', {
|
||||
method: 'POST', json: { student_id: Number(select.value), new_password: newPassword }
|
||||
});
|
||||
setButtonsDisabled(el('studentPasswordForm'), false);
|
||||
notice.textContent = result.ok ? '' : result.message;
|
||||
toast(result.message, !result.ok);
|
||||
if (result.ok) el('studentPasswordForm').reset();
|
||||
}
|
||||
|
||||
document.querySelectorAll('.admin-tab').forEach(button => {
|
||||
@@ -263,6 +313,8 @@ async function logout() {
|
||||
|
||||
el('loginForm').addEventListener('submit', submitLogin);
|
||||
el('teacherPasswordForm').addEventListener('submit', submitTeacherPassword);
|
||||
el('studentPasswordForm').addEventListener('submit', submitStudentPassword);
|
||||
el('studentNoSearch').addEventListener('input', renderStudentAccounts);
|
||||
el('groupLimitForm').addEventListener('submit', submitGroupLimit);
|
||||
el('logoutButton').addEventListener('click', logout);
|
||||
el('lockButton').addEventListener('click', toggleLock);
|
||||
|
||||
Reference in New Issue
Block a user