feat: 目标添加级别字段,卡片显示级别和子目标数量,修复Modal重置
This commit is contained in:
@@ -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
@@ -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())
|
||||||
|
|
||||||
|
|||||||
@@ -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 = '新建目标';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user