feat: 重构学员目标系统,支持评估日期/状态自动计算/评估目标,同时恢复方案列表的典型设置

This commit is contained in:
hmo
2026-04-24 00:06:26 +08:00
parent 68e106018b
commit 035c599c2f
4 changed files with 355 additions and 68 deletions
+19
View File
@@ -154,6 +154,25 @@ def create_app():
{"new_cat": new_cat, "old_cat": old_cat} {"new_cat": new_cat, "old_cat": old_cat}
) )
db.session.commit() db.session.commit()
# 检查student_goals表是否有新字段
result8 = db.session.execute(text("PRAGMA table_info(student_goals)"))
sg_columns = [row[1] for row in result8]
if "start_date" not in sg_columns:
db.session.execute(text("ALTER TABLE student_goals ADD COLUMN start_date TIMESTAMP"))
db.session.commit()
if "assessment_date" not in sg_columns:
db.session.execute(text("ALTER TABLE student_goals ADD COLUMN assessment_date TIMESTAMP"))
db.session.commit()
if "achievement_date" not in sg_columns:
db.session.execute(text("ALTER TABLE student_goals ADD COLUMN achievement_date TIMESTAMP"))
db.session.commit()
if "comment" not in sg_columns:
db.session.execute(text("ALTER TABLE student_goals ADD COLUMN comment TEXT"))
db.session.commit()
# 删除不再使用的字段
# deadline 和 completed_at 已被 start_date, assessment_date, achievement_date 取代
# status 字段现在由日期计算,不再存储
except Exception as e: except Exception as e:
print(f"数据库迁移: {e}") print(f"数据库迁移: {e}")
+25 -7
View File
@@ -235,25 +235,43 @@ class StudentGoal(db.Model):
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
student_id = db.Column(db.Integer, db.ForeignKey("students.id"), nullable=False) student_id = db.Column(db.Integer, db.ForeignKey("students.id"), nullable=False)
goal_id = db.Column(db.Integer, db.ForeignKey("goals.id"), nullable=False) goal_id = db.Column(db.Integer, db.ForeignKey("goals.id"), nullable=False)
status = db.Column(db.String(20), default="未开始") # 开始/进行中/已完成 start_date = db.Column(db.DateTime) # 开始日期
mastery_level = db.Column(db.Integer, default=1) # 1-5 assessment_date = db.Column(db.DateTime) # 评估日期
deadline = db.Column(db.DateTime) mastery_level = db.Column(db.Integer, nullable=True) # 掌握程度 1-5(评估时填写)
completed_at = db.Column(db.DateTime) achievement_date = db.Column(db.DateTime, nullable=True) # 达成日期
comment = db.Column(db.Text, nullable=True) # 评语
created_at = db.Column(db.DateTime, default=datetime.now) created_at = db.Column(db.DateTime, default=datetime.now)
student = db.relationship("Student", backref="goal_records") student = db.relationship("Student", backref="goal_records")
goal = db.relationship("Goal") goal = db.relationship("Goal")
def get_status(self):
"""根据日期自动计算状态"""
from datetime import datetime
now = datetime.now()
if self.start_date and now < self.start_date:
return "未开始"
elif self.assessment_date and now > self.assessment_date:
return "已结束"
else:
return "进行中"
def to_dict(self): def to_dict(self):
status = self.get_status()
return { return {
"id": self.id, "id": self.id,
"student_id": self.student_id, "student_id": self.student_id,
"goal_id": self.goal_id, "goal_id": self.goal_id,
"goal_name": self.goal.name if self.goal else None, "goal_name": self.goal.name if self.goal else None,
"status": self.status, "goal_level": self.goal.level if self.goal else None,
"goal_category": self.goal.category if self.goal else None,
"status": status,
"start_date": self.start_date.isoformat() if self.start_date else None,
"assessment_date": self.assessment_date.isoformat() if self.assessment_date else None,
"mastery_level": self.mastery_level, "mastery_level": self.mastery_level,
"deadline": self.deadline.isoformat() if self.deadline else None, "achievement_date": self.achievement_date.isoformat() if self.achievement_date else None,
"completed_at": self.completed_at.isoformat() if self.completed_at else None, "comment": self.comment,
"created_at": self.created_at.isoformat() if self.created_at else None,
} }
+57 -10
View File
@@ -1,13 +1,34 @@
from flask import Blueprint, request, jsonify from flask import Blueprint, request, jsonify
from app.models import db, StudentGoal, Goal from app.models import db, StudentGoal, Goal
from app.routes.auth import login_required_json from app.routes.auth import login_required_json
from datetime import datetime, timedelta
student_goals_bp = Blueprint("student_goals", __name__) student_goals_bp = Blueprint("student_goals", __name__)
def compute_status(start_date, assessment_date):
"""根据日期计算状态"""
now = datetime.now()
if start_date and now < start_date:
return "未开始"
elif assessment_date and now > assessment_date:
return "已结束"
else:
return "进行中"
@student_goals_bp.route("/api/students/<int:student_id>/goals", methods=["GET"]) @student_goals_bp.route("/api/students/<int:student_id>/goals", methods=["GET"])
@login_required_json @login_required_json
def get_student_goals(student_id): def get_student_goals(student_id):
records = StudentGoal.query.filter_by(student_id=student_id).all() records = StudentGoal.query.filter_by(student_id=student_id).all()
# 计算状态并排序
def sort_key(r):
status_order = {"进行中": 1, "未开始": 2, "已结束": 3}
status = compute_status(r.start_date, r.assessment_date)
order = status_order.get(status, 4)
assessment = r.assessment_date if r.assessment_date else datetime.min
return (order, -assessment.timestamp() if assessment else 0)
records.sort(key=sort_key)
return jsonify([r.to_dict() for r in records]) return jsonify([r.to_dict() for r in records])
@student_goals_bp.route("/api/students/<int:student_id>/goals", methods=["POST"]) @student_goals_bp.route("/api/students/<int:student_id>/goals", methods=["POST"])
@@ -25,12 +46,28 @@ def assign_goal(student_id):
if existing: if existing:
return jsonify({"error": "目标已分配"}), 400 return jsonify({"error": "目标已分配"}), 400
# 处理评估日期
assessment_date = None
if data.get("assessment_date"):
# 直接指定日期
assessment_date = datetime.fromisoformat(data["assessment_date"])
elif data.get("assessment_days"):
# 指定天数后
assessment_date = datetime.now() + timedelta(days=int(data["assessment_days"]))
# 处理开始日期
start_date = None
if data.get("start_date"):
start_date = datetime.fromisoformat(data["start_date"])
# 如果没有指定开始日期,默认为当前时间
elif data.get("start_now"):
start_date = datetime.now()
record = StudentGoal( record = StudentGoal(
student_id=student_id, student_id=student_id,
goal_id=data["goal_id"], goal_id=data["goal_id"],
status=data.get("status", "未开始"), start_date=start_date,
mastery_level=data.get("mastery_level", 1), assessment_date=assessment_date
deadline=data.get("deadline")
) )
db.session.add(record) db.session.add(record)
db.session.commit() db.session.commit()
@@ -42,15 +79,25 @@ def update_student_goal(student_id, goal_id):
record = StudentGoal.query.filter_by(student_id=student_id, goal_id=goal_id).first_or_404() record = StudentGoal.query.filter_by(student_id=student_id, goal_id=goal_id).first_or_404()
data = request.get_json() data = request.get_json()
if "status" in data: if "start_date" in data:
record.status = data["status"] if data["start_date"]:
if data["status"] == "已完成" and not record.completed_at: record.start_date = datetime.fromisoformat(data["start_date"])
from datetime import datetime else:
record.completed_at = datetime.now() record.start_date = None
if "assessment_date" in data:
if data["assessment_date"]:
record.assessment_date = datetime.fromisoformat(data["assessment_date"])
else:
record.assessment_date = None
if "mastery_level" in data: if "mastery_level" in data:
record.mastery_level = data["mastery_level"] record.mastery_level = data["mastery_level"]
if "deadline" in data: if "achievement_date" in data:
record.deadline = data["deadline"] if data["achievement_date"]:
record.achievement_date = datetime.fromisoformat(data["achievement_date"])
else:
record.achievement_date = None
if "comment" in data:
record.comment = data["comment"]
db.session.commit() db.session.commit()
return jsonify(record.to_dict()) return jsonify(record.to_dict())
+244 -41
View File
@@ -254,7 +254,7 @@
<!-- 分配目标 Modal --> <!-- 分配目标 Modal -->
<div class="modal fade" id="assignGoalModal" tabindex="-1"> <div class="modal fade" id="assignGoalModal" tabindex="-1">
<div class="modal-dialog"> <div class="modal-dialog modal-lg">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title">分配目标</h5> <h5 class="modal-title">分配目标</h5>
@@ -262,32 +262,141 @@
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="mb-3"> <div class="mb-3">
<label class="form-label">选择目标</label> <label class="form-label">选择目标 *</label>
<select class="form-select" id="assign-goal-select"></select> <select class="form-select" id="assign-goal-select"></select>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label class="form-label">状态</label> <label class="form-label">评估日期</label>
<select class="form-select" id="assign-status"> <div class="row g-2">
<option value="未开始">未开始</option> <div class="col-auto">
<option value="进行中">进行中</option> <select class="form-select" id="assign-assessment-days">
<option value="">指定日期</option>
<option value="15">15天后</option>
<option value="30">30天后</option>
<option value="60">60天后</option>
<option value="90">90天后</option>
<option value="180">180天后</option>
</select> </select>
</div> </div>
<div class="col-auto">
<input type="date" class="form-control" id="assign-assessment-date">
</div>
</div>
</div>
<div class="mb-3"> <div class="mb-3">
<label class="form-label">掌握程度 (1-5)</label> <a class="text-decoration-none" data-bs-toggle="collapse" href="#assignMoreSettings" role="button">
<input type="number" class="form-control" id="assign-mastery" value="1" min="1" max="5"> 更多设置 ▼
</div> </a>
<div class="collapse" id="assignMoreSettings">
<div class="card card-body mt-2">
<div class="mb-3"> <div class="mb-3">
<label class="form-label">截止日期</label> <label class="form-label">开始日期</label>
<input type="date" class="form-control" id="assign-deadline"> <input type="date" class="form-control" id="assign-start-date">
<small class="text-muted">默认立即开始</small>
</div> </div>
</div> </div>
<div class="modal-footer"> </div>
</div>
</div>
<div class="modal-footer-with-top">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" id="confirm-assign-goal">分配</button> <button type="button" class="btn btn-primary" id="confirm-assign-goal">分配</button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- 调整目标 Modal -->
<div class="modal fade" id="adjustGoalModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">调整目标</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<input type="hidden" id="adjust-goal-id">
<p class="fw-bold mb-3" id="adjust-goal-name"></p>
<div class="row g-3 mb-3">
<div class="col-md-6">
<label class="form-label">开始日期</label>
<input type="date" class="form-control" id="adjust-start-date">
</div>
<div class="col-md-6">
<label class="form-label">评估日期</label>
<input type="date" class="form-control" id="adjust-assessment-date">
</div>
</div>
</div>
<div class="modal-footer-with-top">
<button type="button" class="btn btn-danger" id="remove-assigned-goal">移除目标</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" id="confirm-adjust-goal">保存</button>
</div>
</div>
</div>
</div>
<!-- 评估目标 Modal -->
<div class="modal fade" id="assessGoalModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">评估目标</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<input type="hidden" id="assess-goal-id">
<p class="fw-bold mb-3" id="assess-goal-name"></p>
<div class="row g-3 mb-3">
<div class="col-md-4">
<label class="form-label">掌握程度</label>
<select class="form-select" id="assess-mastery">
<option value="1">⭐ 入门</option>
<option value="2">⭐⭐ 初级</option>
<option value="3">⭐⭐⭐ 进阶</option>
<option value="4">⭐⭐⭐⭐ 熟练</option>
<option value="5">⭐⭐⭐⭐⭐ 精通</option>
</select>
</div>
<div class="col-md-4">
<label class="form-label">达成日期</label>
<input type="date" class="form-control" id="assess-achievement-date">
</div>
<div class="col-md-4">
<label class="form-label">当前状态</label>
<input type="text" class="form-control" id="assess-current-status" readonly>
</div>
</div>
<div class="mb-3">
<label class="form-label">评语</label>
<textarea class="form-control" id="assess-comment" rows="3" placeholder="评估意见..."></textarea>
</div>
</div>
<div class="modal-footer-with-top">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" id="confirm-assess-goal">保存评估</button>
</div>
</div>
</div>
</div>
<style>
.modal-footer-with-top {
display: flex;
justify-content: flex-end;
gap: 0.5rem;
padding: 1rem;
border-top: 1px solid #dee2e6;
background: #f8f9fa;
position: sticky;
bottom: 0;
}
</style>
{% endblock %} {% endblock %}
{% block extra_js %} {% block extra_js %}
@@ -382,16 +491,35 @@ function renderPlanList(plans) {
return; return;
} }
container.innerHTML = plans.map(p => ` container.innerHTML = plans.map(p => `
<div class="d-flex align-items-center mb-2 p-2 border rounded"> <div class="d-flex align-items-start mb-2 p-2 border rounded ${p.is_typical ? 'border-warning bg-light' : ''}">
<a href="/plan/${p.id}" class="btn btn-sm btn-outline-primary me-2">查看</a> <div class="form-check me-2">
<span class="flex-grow-1">${p.created_at ? p.created_at.substring(0, 10) : '未知'}</span> <input class="form-check-input" type="checkbox" id="typical-${p.id}" ${p.is_typical ? 'checked' : ''} onchange="toggleTypical(${p.id})">
<label class="form-check-label" for="typical-${p.id}" title="设为典型"></label>
</div>
<div class="flex-grow-1">
<div class="d-flex justify-content-between align-items-start">
<div>
<a href="/plan/${p.id}" class="fw-bold text-decoration-none">${p.created_at ? p.created_at.substring(0, 16) : '未知'}</a>
${p.is_typical ? '<span class="badge bg-warning text-dark ms-1">典型</span>' : ''}
</div>
<button class="btn btn-sm btn-outline-danger" onclick="deletePlan(${p.id})"> <button class="btn btn-sm btn-outline-danger" onclick="deletePlan(${p.id})">
<i class="bi bi-trash"></i> <i class="bi bi-trash"></i>
</button> </button>
</div> </div>
<div class="small text-muted">
${p.problem_names && p.problem_names.length > 0 ? '问题: ' + p.problem_names.join(', ') : ''}
${p.template_name ? ' | 模板: ' + p.template_name : ''}
</div>
</div>
</div>
`).join(''); `).join('');
} }
async function toggleTypical(planId) {
await fetch(`/api/plans/${planId}/typical`, {method: 'POST'});
loadPlans();
}
function showEditStudentModal() { function showEditStudentModal() {
editStudentModal.show(); editStudentModal.show();
} }
@@ -624,6 +752,8 @@ async function deletePlan(id) {
} }
// ===== 目标相关 ===== // ===== 目标相关 =====
let adjustGoalModal, assessGoalModal;
async function loadStudentGoals() { async function loadStudentGoals() {
const res = await fetch(`/api/students/${currentStudentId}/goals`); const res = await fetch(`/api/students/${currentStudentId}/goals`);
const goals = await res.json(); const goals = await res.json();
@@ -638,42 +768,106 @@ async function loadStudentGoals() {
<div class="d-flex justify-content-between align-items-center mb-2 p-2 border rounded"> <div class="d-flex justify-content-between align-items-center mb-2 p-2 border rounded">
<div> <div>
<strong>${escapeHtml(g.goal_name)}</strong> <strong>${escapeHtml(g.goal_name)}</strong>
<div class="small text-muted"> <div class="small">
掌握程度:${'⭐'.repeat(g.mastery_level)}${'☆'.repeat(5-g.mastery_level)} <span class="badge bg-secondary me-1">${g.goal_level || '入门'}</span>
| 状态:${g.status} <span class="badge bg-info me-1">${g.goal_category || '综合'}</span>
${g.deadline ? '| 截止:'+g.deadline : ''} <span class="badge ${g.status === '进行中' ? 'bg-primary' : g.status === '未开始' ? 'bg-warning' : 'bg-secondary'}">${g.status}</span>
</div>
<div class="small text-muted mt-1">
${g.start_date ? '开始: '+formatDate(g.start_date) : '未开始'}
${g.assessment_date ? ' | 评估: '+formatDate(g.assessment_date) : ''}
${g.mastery_level ? ' | ⭐'.repeat(g.mastery_level) : ''}
</div> </div>
</div> </div>
<div> <div class="btn-group btn-group-sm">
<button class="btn btn-sm btn-outline-primary" onclick="editStudentGoal(${g.goal_id})">编辑</button> <button class="btn btn-outline-primary" onclick="openAdjustGoal(${g.goal_id})">调整目标</button>
<button class="btn btn-sm btn-outline-danger" onclick="removeStudentGoal(${g.goal_id})">移除</button> <button class="btn btn-outline-success" onclick="openAssessGoal(${g.goal_id})">评估目标</button>
</div> </div>
</div> </div>
`).join(''); `).join('');
} }
async function editStudentGoal(goalId) { function formatDate(dateStr) {
const goal = await fetch(`/api/students/${currentStudentId}/goals`).then(r => r.json()) if (!dateStr) return '';
.then(goals => goals.find(g => g.goal_id === goalId)); const d = new Date(dateStr);
return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`;
}
const status = prompt('状态 (未开始/进行中/已完成)', goal.status); async function openAdjustGoal(goalId) {
const mastery = prompt('掌握程度 (1-5)', goal.mastery_level); const goals = await fetch(`/api/students/${currentStudentId}/goals`).then(r => r.json());
const goal = goals.find(g => g.goal_id === goalId);
if (!goal) return;
document.getElementById('adjust-goal-id').value = goalId;
document.getElementById('adjust-goal-name').textContent = goal.goal_name;
document.getElementById('adjust-start-date').value = goal.start_date ? goal.start_date.split('T')[0] : '';
document.getElementById('adjust-assessment-date').value = goal.assessment_date ? goal.assessment_date.split('T')[0] : '';
new bootstrap.Modal(document.getElementById('adjustGoalModal')).show();
}
document.getElementById('remove-assigned-goal').addEventListener('click', () => {
const goalId = document.getElementById('adjust-goal-id').value;
if (!confirm('确定移除此目标?此操作不可恢复。')) return;
fetch(`/api/students/${currentStudentId}/goals/${goalId}`, {method: 'DELETE'})
.then(() => {
bootstrap.Modal.getInstance(document.getElementById('adjustGoalModal')).hide();
loadStudentGoals();
});
});
document.getElementById('confirm-adjust-goal').addEventListener('click', async () => {
const goalId = document.getElementById('adjust-goal-id').value;
const startDate = document.getElementById('adjust-start-date').value;
const assessmentDate = document.getElementById('adjust-assessment-date').value;
if (status && mastery) {
await fetch(`/api/students/${currentStudentId}/goals/${goalId}`, { await fetch(`/api/students/${currentStudentId}/goals/${goalId}`, {
method: 'PUT', method: 'PUT',
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
body: JSON.stringify({status, mastery_level: parseInt(mastery)}) body: JSON.stringify({
start_date: startDate || null,
assessment_date: assessmentDate || null
})
}); });
bootstrap.Modal.getInstance(document.getElementById('adjustGoalModal')).hide();
loadStudentGoals(); loadStudentGoals();
} });
async function openAssessGoal(goalId) {
const goals = await fetch(`/api/students/${currentStudentId}/goals`).then(r => r.json());
const goal = goals.find(g => g.goal_id === goalId);
if (!goal) return;
document.getElementById('assess-goal-id').value = goalId;
document.getElementById('assess-goal-name').textContent = goal.goal_name;
document.getElementById('assess-mastery').value = goal.mastery_level || '1';
document.getElementById('assess-achievement-date').value = goal.achievement_date ? goal.achievement_date.split('T')[0] : '';
document.getElementById('assess-current-status').value = goal.status;
document.getElementById('assess-comment').value = goal.comment || '';
new bootstrap.Modal(document.getElementById('assessGoalModal')).show();
} }
async function removeStudentGoal(goalId) { document.getElementById('confirm-assess-goal').addEventListener('click', async () => {
if (!confirm('确定移除此目标?')) return; const goalId = document.getElementById('assess-goal-id').value;
await fetch(`/api/students/${currentStudentId}/goals/${goalId}`, {method: 'DELETE'}); const masteryLevel = document.getElementById('assess-mastery').value;
const achievementDate = document.getElementById('assess-achievement-date').value;
const comment = document.getElementById('assess-comment').value;
await fetch(`/api/students/${currentStudentId}/goals/${goalId}`, {
method: 'PUT',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
mastery_level: parseInt(masteryLevel),
achievement_date: achievementDate || null,
comment: comment
})
});
bootstrap.Modal.getInstance(document.getElementById('assessGoalModal')).hide();
loadStudentGoals(); loadStudentGoals();
} });
// 加载可选目标列表到 Modal // 加载可选目标列表到 Modal
async function loadGoalOptions() { async function loadGoalOptions() {
@@ -688,32 +882,41 @@ async function loadGoalOptions() {
select.innerHTML = goals select.innerHTML = goals
.filter(g => !assignedIds.includes(g.id)) .filter(g => !assignedIds.includes(g.id))
.map(g => `<option value="${g.id}">${escapeHtml(g.name)}</option>`) .map(g => `<option value="${g.id}">${escapeHtml(g.name)} [${g.level || '入门'} - ${g.category || '综合'}]</option>`)
.join(''); .join('');
// 设置默认开始日期为今天
document.getElementById('assign-start-date').value = new Date().toISOString().split('T')[0];
} }
document.getElementById('confirm-assign-goal').addEventListener('click', async () => { document.getElementById('confirm-assign-goal').addEventListener('click', async () => {
const goalId = document.getElementById('assign-goal-select').value; const goalId = document.getElementById('assign-goal-select').value;
const status = document.getElementById('assign-status').value; const assessmentDays = document.getElementById('assign-assessment-days').value;
const mastery = document.getElementById('assign-mastery').value; const assessmentDate = document.getElementById('assign-assessment-date').value;
const deadline = document.getElementById('assign-deadline').value; const startDate = document.getElementById('assign-start-date').value;
if (!goalId) { alert('请选择目标'); return; } if (!goalId) { alert('请选择目标'); return; }
if (!assessmentDays && !assessmentDate) { alert('请选择评估方式'); return; }
const res = await fetch(`/api/students/${currentStudentId}/goals`, { const res = await fetch(`/api/students/${currentStudentId}/goals`, {
method: 'POST', method: 'POST',
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ body: JSON.stringify({
goal_id: parseInt(goalId), goal_id: parseInt(goalId),
status, assessment_days: assessmentDays || null,
mastery_level: parseInt(mastery), assessment_date: assessmentDate || null,
deadline: deadline || null start_date: startDate || null,
start_now: !startDate
}) })
}); });
if (res.ok) { if (res.ok) {
assignGoalModal.hide(); assignGoalModal.hide();
loadStudentGoals(); loadStudentGoals();
// 重置表单
document.getElementById('assign-assessment-days').value = '';
document.getElementById('assign-assessment-date').value = '';
document.getElementById('assign-start-date').value = new Date().toISOString().split('T')[0];
} else { } else {
const err = await res.json(); const err = await res.json();
alert(err.error || '分配失败'); alert(err.error || '分配失败');