feat: 重构学员目标系统,支持评估日期/状态自动计算/评估目标,同时恢复方案列表的典型设置
This commit is contained in:
@@ -154,6 +154,25 @@ def create_app():
|
||||
{"new_cat": new_cat, "old_cat": old_cat}
|
||||
)
|
||||
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:
|
||||
print(f"数据库迁移: {e}")
|
||||
|
||||
|
||||
+25
-7
@@ -235,25 +235,43 @@ class StudentGoal(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
student_id = db.Column(db.Integer, db.ForeignKey("students.id"), nullable=False)
|
||||
goal_id = db.Column(db.Integer, db.ForeignKey("goals.id"), nullable=False)
|
||||
status = db.Column(db.String(20), default="未开始") # 未开始/进行中/已完成
|
||||
mastery_level = db.Column(db.Integer, default=1) # 1-5
|
||||
deadline = db.Column(db.DateTime)
|
||||
completed_at = db.Column(db.DateTime)
|
||||
start_date = db.Column(db.DateTime) # 开始日期
|
||||
assessment_date = db.Column(db.DateTime) # 评估日期
|
||||
mastery_level = db.Column(db.Integer, nullable=True) # 掌握程度 1-5(评估时填写)
|
||||
achievement_date = db.Column(db.DateTime, nullable=True) # 达成日期
|
||||
comment = db.Column(db.Text, nullable=True) # 评语
|
||||
created_at = db.Column(db.DateTime, default=datetime.now)
|
||||
|
||||
student = db.relationship("Student", backref="goal_records")
|
||||
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):
|
||||
status = self.get_status()
|
||||
return {
|
||||
"id": self.id,
|
||||
"student_id": self.student_id,
|
||||
"goal_id": self.goal_id,
|
||||
"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,
|
||||
"deadline": self.deadline.isoformat() if self.deadline else None,
|
||||
"completed_at": self.completed_at.isoformat() if self.completed_at else None,
|
||||
"achievement_date": self.achievement_date.isoformat() if self.achievement_date else None,
|
||||
"comment": self.comment,
|
||||
"created_at": self.created_at.isoformat() if self.created_at else None,
|
||||
}
|
||||
|
||||
|
||||
|
||||
+58
-11
@@ -1,13 +1,34 @@
|
||||
from flask import Blueprint, request, jsonify
|
||||
from app.models import db, StudentGoal, Goal
|
||||
from app.routes.auth import login_required_json
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
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"])
|
||||
@login_required_json
|
||||
def get_student_goals(student_id):
|
||||
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])
|
||||
|
||||
@student_goals_bp.route("/api/students/<int:student_id>/goals", methods=["POST"])
|
||||
@@ -25,12 +46,28 @@ def assign_goal(student_id):
|
||||
if existing:
|
||||
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(
|
||||
student_id=student_id,
|
||||
goal_id=data["goal_id"],
|
||||
status=data.get("status", "未开始"),
|
||||
mastery_level=data.get("mastery_level", 1),
|
||||
deadline=data.get("deadline")
|
||||
start_date=start_date,
|
||||
assessment_date=assessment_date
|
||||
)
|
||||
db.session.add(record)
|
||||
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()
|
||||
data = request.get_json()
|
||||
|
||||
if "status" in data:
|
||||
record.status = data["status"]
|
||||
if data["status"] == "已完成" and not record.completed_at:
|
||||
from datetime import datetime
|
||||
record.completed_at = datetime.now()
|
||||
if "start_date" in data:
|
||||
if data["start_date"]:
|
||||
record.start_date = datetime.fromisoformat(data["start_date"])
|
||||
else:
|
||||
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:
|
||||
record.mastery_level = data["mastery_level"]
|
||||
if "deadline" in data:
|
||||
record.deadline = data["deadline"]
|
||||
if "achievement_date" in data:
|
||||
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()
|
||||
return jsonify(record.to_dict())
|
||||
@@ -61,4 +108,4 @@ def remove_student_goal(student_id, goal_id):
|
||||
record = StudentGoal.query.filter_by(student_id=student_id, goal_id=goal_id).first_or_404()
|
||||
db.session.delete(record)
|
||||
db.session.commit()
|
||||
return jsonify({"message": "移除成功"})
|
||||
return jsonify({"message": "移除成功"})
|
||||
|
||||
+253
-50
@@ -254,7 +254,7 @@
|
||||
|
||||
<!-- 分配目标 Modal -->
|
||||
<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-header">
|
||||
<h5 class="modal-title">分配目标</h5>
|
||||
@@ -262,32 +262,141 @@
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">选择目标</label>
|
||||
<label class="form-label">选择目标 *</label>
|
||||
<select class="form-select" id="assign-goal-select"></select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">状态</label>
|
||||
<select class="form-select" id="assign-status">
|
||||
<option value="未开始">未开始</option>
|
||||
<option value="进行中">进行中</option>
|
||||
</select>
|
||||
<label class="form-label">评估日期</label>
|
||||
<div class="row g-2">
|
||||
<div class="col-auto">
|
||||
<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>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<input type="date" class="form-control" id="assign-assessment-date">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">掌握程度 (1-5)</label>
|
||||
<input type="number" class="form-control" id="assign-mastery" value="1" min="1" max="5">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">截止日期</label>
|
||||
<input type="date" class="form-control" id="assign-deadline">
|
||||
<a class="text-decoration-none" data-bs-toggle="collapse" href="#assignMoreSettings" role="button">
|
||||
更多设置 ▼
|
||||
</a>
|
||||
<div class="collapse" id="assignMoreSettings">
|
||||
<div class="card card-body mt-2">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">开始日期</label>
|
||||
<input type="date" class="form-control" id="assign-start-date">
|
||||
<small class="text-muted">默认立即开始</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<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-assign-goal">分配</button>
|
||||
</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 %}
|
||||
|
||||
{% block extra_js %}
|
||||
@@ -382,16 +491,35 @@ function renderPlanList(plans) {
|
||||
return;
|
||||
}
|
||||
container.innerHTML = plans.map(p => `
|
||||
<div class="d-flex align-items-center mb-2 p-2 border rounded">
|
||||
<a href="/plan/${p.id}" class="btn btn-sm btn-outline-primary me-2">查看</a>
|
||||
<span class="flex-grow-1">${p.created_at ? p.created_at.substring(0, 10) : '未知'}</span>
|
||||
<button class="btn btn-sm btn-outline-danger" onclick="deletePlan(${p.id})">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
<div class="d-flex align-items-start mb-2 p-2 border rounded ${p.is_typical ? 'border-warning bg-light' : ''}">
|
||||
<div class="form-check me-2">
|
||||
<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})">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</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('');
|
||||
}
|
||||
|
||||
async function toggleTypical(planId) {
|
||||
await fetch(`/api/plans/${planId}/typical`, {method: 'POST'});
|
||||
loadPlans();
|
||||
}
|
||||
|
||||
function showEditStudentModal() {
|
||||
editStudentModal.show();
|
||||
}
|
||||
@@ -624,6 +752,8 @@ async function deletePlan(id) {
|
||||
}
|
||||
|
||||
// ===== 目标相关 =====
|
||||
let adjustGoalModal, assessGoalModal;
|
||||
|
||||
async function loadStudentGoals() {
|
||||
const res = await fetch(`/api/students/${currentStudentId}/goals`);
|
||||
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>
|
||||
<strong>${escapeHtml(g.goal_name)}</strong>
|
||||
<div class="small text-muted">
|
||||
掌握程度:${'⭐'.repeat(g.mastery_level)}${'☆'.repeat(5-g.mastery_level)}
|
||||
| 状态:${g.status}
|
||||
${g.deadline ? '| 截止:'+g.deadline : ''}
|
||||
<div class="small">
|
||||
<span class="badge bg-secondary me-1">${g.goal_level || '入门'}</span>
|
||||
<span class="badge bg-info me-1">${g.goal_category || '综合'}</span>
|
||||
<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>
|
||||
<button class="btn btn-sm btn-outline-primary" onclick="editStudentGoal(${g.goal_id})">编辑</button>
|
||||
<button class="btn btn-sm btn-outline-danger" onclick="removeStudentGoal(${g.goal_id})">移除</button>
|
||||
<div class="btn-group btn-group-sm">
|
||||
<button class="btn btn-outline-primary" onclick="openAdjustGoal(${g.goal_id})">调整目标</button>
|
||||
<button class="btn btn-outline-success" onclick="openAssessGoal(${g.goal_id})">评估目标</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
async function editStudentGoal(goalId) {
|
||||
const goal = await fetch(`/api/students/${currentStudentId}/goals`).then(r => r.json())
|
||||
.then(goals => goals.find(g => g.goal_id === goalId));
|
||||
function formatDate(dateStr) {
|
||||
if (!dateStr) return '';
|
||||
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);
|
||||
const mastery = prompt('掌握程度 (1-5)', goal.mastery_level);
|
||||
async function openAdjustGoal(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;
|
||||
|
||||
if (status && mastery) {
|
||||
await fetch(`/api/students/${currentStudentId}/goals/${goalId}`, {
|
||||
method: 'PUT',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({status, mastery_level: parseInt(mastery)})
|
||||
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();
|
||||
});
|
||||
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;
|
||||
|
||||
await fetch(`/api/students/${currentStudentId}/goals/${goalId}`, {
|
||||
method: 'PUT',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({
|
||||
start_date: startDate || null,
|
||||
assessment_date: assessmentDate || null
|
||||
})
|
||||
});
|
||||
|
||||
bootstrap.Modal.getInstance(document.getElementById('adjustGoalModal')).hide();
|
||||
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) {
|
||||
if (!confirm('确定移除此目标?')) return;
|
||||
await fetch(`/api/students/${currentStudentId}/goals/${goalId}`, {method: 'DELETE'});
|
||||
document.getElementById('confirm-assess-goal').addEventListener('click', async () => {
|
||||
const goalId = document.getElementById('assess-goal-id').value;
|
||||
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();
|
||||
}
|
||||
});
|
||||
|
||||
// 加载可选目标列表到 Modal
|
||||
async function loadGoalOptions() {
|
||||
@@ -688,32 +882,41 @@ async function loadGoalOptions() {
|
||||
|
||||
select.innerHTML = goals
|
||||
.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('');
|
||||
|
||||
// 设置默认开始日期为今天
|
||||
document.getElementById('assign-start-date').value = new Date().toISOString().split('T')[0];
|
||||
}
|
||||
|
||||
document.getElementById('confirm-assign-goal').addEventListener('click', async () => {
|
||||
const goalId = document.getElementById('assign-goal-select').value;
|
||||
const status = document.getElementById('assign-status').value;
|
||||
const mastery = document.getElementById('assign-mastery').value;
|
||||
const deadline = document.getElementById('assign-deadline').value;
|
||||
const assessmentDays = document.getElementById('assign-assessment-days').value;
|
||||
const assessmentDate = document.getElementById('assign-assessment-date').value;
|
||||
const startDate = document.getElementById('assign-start-date').value;
|
||||
|
||||
if (!goalId) { alert('请选择目标'); return; }
|
||||
if (!assessmentDays && !assessmentDate) { alert('请选择评估方式'); return; }
|
||||
|
||||
const res = await fetch(`/api/students/${currentStudentId}/goals`, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({
|
||||
goal_id: parseInt(goalId),
|
||||
status,
|
||||
mastery_level: parseInt(mastery),
|
||||
deadline: deadline || null
|
||||
assessment_days: assessmentDays || null,
|
||||
assessment_date: assessmentDate || null,
|
||||
start_date: startDate || null,
|
||||
start_now: !startDate
|
||||
})
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
assignGoalModal.hide();
|
||||
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 {
|
||||
const err = await res.json();
|
||||
alert(err.error || '分配失败');
|
||||
|
||||
Reference in New Issue
Block a user