feat: 目标添加级别字段,卡片显示级别和子目标数量,修复Modal重置

This commit is contained in:
hmo
2026-04-23 21:02:24 +08:00
parent 7d439b4b17
commit 7da9e9a43c
3 changed files with 39 additions and 9 deletions
+2
View File
@@ -197,6 +197,7 @@ class Goal(db.Model):
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False) name = db.Column(db.String(100), nullable=False)
content = db.Column(db.Text) content = db.Column(db.Text)
level = db.Column(db.String(20), default="入门") # 启蒙/入门/进阶/熟练/精通
created_at = db.Column(db.DateTime, default=datetime.now) created_at = db.Column(db.DateTime, default=datetime.now)
updated_at = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now) updated_at = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now)
@@ -205,6 +206,7 @@ class Goal(db.Model):
"id": self.id, "id": self.id,
"name": self.name, "name": self.name,
"content": self.content, "content": self.content,
"level": self.level,
"created_at": self.created_at.isoformat() if self.created_at else None, "created_at": self.created_at.isoformat() if self.created_at else None,
"updated_at": self.updated_at.isoformat() if self.updated_at else None, "updated_at": self.updated_at.isoformat() if self.updated_at else None,
} }
+7 -1
View File
@@ -22,7 +22,11 @@ def get_goals():
@login_required_json @login_required_json
def create_goal(): def create_goal():
data = request.get_json() data = request.get_json()
goal = Goal(name=data["name"], content=data.get("content", "")) goal = Goal(
name=data["name"],
content=data.get("content", ""),
level=data.get("level", "入门")
)
db.session.add(goal) db.session.add(goal)
db.session.commit() db.session.commit()
return jsonify(goal.to_dict()), 201 return jsonify(goal.to_dict()), 201
@@ -42,6 +46,8 @@ def update_goal(goal_id):
goal.name = data["name"] goal.name = data["name"]
if "content" in data: if "content" in data:
goal.content = data["content"] goal.content = data["content"]
if "level" in data:
goal.level = data["level"]
db.session.commit() db.session.commit()
return jsonify(goal.to_dict()) return jsonify(goal.to_dict())
+30 -8
View File
@@ -36,6 +36,16 @@
<label class="form-label">目标名称</label> <label class="form-label">目标名称</label>
<input type="text" class="form-control" id="goal-name" required> <input type="text" class="form-control" id="goal-name" required>
</div> </div>
<div class="mb-3">
<label class="form-label">级别</label>
<select class="form-select" id="goal-level">
<option value="启蒙">启蒙</option>
<option value="入门" selected>入门</option>
<option value="进阶">进阶</option>
<option value="熟练">熟练</option>
<option value="精通">精通</option>
</select>
</div>
<div class="mb-3"> <div class="mb-3">
<label class="form-label">目标内容 (Markdown)</label> <label class="form-label">目标内容 (Markdown)</label>
<textarea class="form-control" id="goal-content" rows="8"></textarea> <textarea class="form-control" id="goal-content" rows="8"></textarea>
@@ -91,12 +101,24 @@ async function loadGoals() {
const res = await fetch(API_BASE); const res = await fetch(API_BASE);
const goals = await res.json(); const goals = await res.json();
const grid = document.getElementById('goals-grid'); const grid = document.getElementById('goals-grid');
grid.innerHTML = goals.map(g => `
// 获取每个目标的子目标数量
const goalsWithChildren = await Promise.all(goals.map(async g => {
const childrenRes = await fetch(`${API_BASE}/${g.id}/children`);
const children = await childrenRes.json();
return {...g, childCount: children.length};
}));
grid.innerHTML = goalsWithChildren.map(g => `
<div class="col-md-4 col-lg-3"> <div class="col-md-4 col-lg-3">
<div class="card h-100"> <div class="card h-100">
<div class="card-body"> <div class="card-body">
<h6 class="card-title">${escapeHtml(g.name)}</h6> <h6 class="card-title">${escapeHtml(g.name)}</h6>
<p class="card-text small text-muted">${escapeHtml(g.content || '').substring(0, 100)}...</p> <div class="mb-1">
<span class="badge bg-secondary">${g.level}</span>
${g.childCount > 0 ? `<span class="badge bg-info">${g.childCount}个子目标</span>` : ''}
</div>
<p class="card-text small text-muted">${escapeHtml(g.content || '').substring(0, 80)}${g.content && g.content.length > 80 ? '...' : ''}</p>
</div> </div>
<div class="card-footer bg-transparent"> <div class="card-footer bg-transparent">
<button class="btn btn-sm btn-outline-primary" onclick="editGoal(${g.id})">编辑</button> <button class="btn btn-sm btn-outline-primary" onclick="editGoal(${g.id})">编辑</button>
@@ -110,25 +132,22 @@ async function loadGoals() {
// 保存目标 // 保存目标
async function saveGoal() { async function saveGoal() {
console.log('saveGoal called');
const id = document.getElementById('goal-id').value; const id = document.getElementById('goal-id').value;
const name = document.getElementById('goal-name').value; const name = document.getElementById('goal-name').value;
const level = document.getElementById('goal-level').value;
const content = document.getElementById('goal-content').value; const content = document.getElementById('goal-content').value;
console.log('Form data:', {id, name, content});
if (!name) { alert('请输入目标名称'); return; } if (!name) { alert('请输入目标名称'); return; }
const method = id ? 'PUT' : 'POST'; const method = id ? 'PUT' : 'POST';
const url = id ? `${API_BASE}/${id}` : API_BASE; const url = id ? `${API_BASE}/${id}` : API_BASE;
console.log('Sending to:', url, 'method:', method);
try { try {
const res = await fetch(url, { const res = await fetch(url, {
method, method,
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
body: JSON.stringify({name, content}) body: JSON.stringify({name, level, content})
}); });
console.log('Response status:', res.status);
if (res.ok) { if (res.ok) {
bootstrap.Modal.getInstance(document.getElementById('goalModal')).hide(); bootstrap.Modal.getInstance(document.getElementById('goalModal')).hide();
@@ -138,7 +157,6 @@ async function saveGoal() {
alert(err.error || '保存失败'); alert(err.error || '保存失败');
} }
} catch (e) { } catch (e) {
console.error('Fetch error:', e);
alert('网络错误: ' + e.message); alert('网络错误: ' + e.message);
} }
} }
@@ -149,6 +167,7 @@ function editGoal(id) {
.then(g => { .then(g => {
document.getElementById('goal-id').value = g.id; document.getElementById('goal-id').value = g.id;
document.getElementById('goal-name').value = g.name; document.getElementById('goal-name').value = g.name;
document.getElementById('goal-level').value = g.level || '入门';
document.getElementById('goal-content').value = g.content || ''; document.getElementById('goal-content').value = g.content || '';
document.getElementById('goalModalTitle').textContent = '编辑目标'; document.getElementById('goalModalTitle').textContent = '编辑目标';
new bootstrap.Modal(document.getElementById('goalModal')).show(); new bootstrap.Modal(document.getElementById('goalModal')).show();
@@ -267,6 +286,9 @@ function escapeHtml(text) {
document.getElementById('save-goal').addEventListener('click', saveGoal); document.getElementById('save-goal').addEventListener('click', saveGoal);
document.getElementById('goalModal').addEventListener('hidden.bs.modal', () => { document.getElementById('goalModal').addEventListener('hidden.bs.modal', () => {
document.getElementById('goal-id').value = ''; document.getElementById('goal-id').value = '';
document.getElementById('goal-name').value = '';
document.getElementById('goal-level').value = '入门';
document.getElementById('goal-content').value = '';
document.getElementById('goalModalTitle').textContent = '新建目标'; document.getElementById('goalModalTitle').textContent = '新建目标';
}); });