fix: 目标关联界面重做,支持多选添加子目标
This commit is contained in:
+101
-20
@@ -54,16 +54,27 @@
|
|||||||
<div class="modal-dialog modal-lg">
|
<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" id="relationModalTitle">管理子目标</h5>
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<input type="hidden" id="relation-goal-id">
|
<input type="hidden" id="relation-goal-id">
|
||||||
<h6>上级目标(父目标)</h6>
|
<p class="text-muted">为「<span id="current-goal-name" class="fw-bold"></span>」添加子目标</p>
|
||||||
<div id="parent-goals" class="mb-3"></div>
|
|
||||||
<hr>
|
<!-- 添加子目标区域 -->
|
||||||
<h6>下级目标(子目标)</h6>
|
<div class="mb-3">
|
||||||
<div id="child-goals" class="mb-3"></div>
|
<label class="form-label">添加子目标(可多选)</label>
|
||||||
|
<select class="form-select" id="available-goals-select" multiple size="5">
|
||||||
|
</select>
|
||||||
|
<div class="form-text">按住 Ctrl/Cmd 可多选</div>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary btn-sm mb-3" onclick="addSelectedChildren()">
|
||||||
|
<i class="bi bi-plus"></i> 添加选中目标
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- 已关联的子目标 -->
|
||||||
|
<h6 class="mt-3">已关联的子目标</h6>
|
||||||
|
<div id="child-goals-list" class="list-group"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -153,28 +164,98 @@ async function deleteGoal(id) {
|
|||||||
async function manageRelations(id) {
|
async function manageRelations(id) {
|
||||||
document.getElementById('relation-goal-id').value = id;
|
document.getElementById('relation-goal-id').value = id;
|
||||||
|
|
||||||
const [parents, children] = await Promise.all([
|
// 获取当前目标信息和所有目标
|
||||||
fetch(`${API_BASE}/${id}/parents`).then(r => r.json()),
|
const [currentGoal, allGoals, children] = await Promise.all([
|
||||||
|
fetch(`${API_BASE}/${id}`).then(r => r.json()),
|
||||||
|
fetch(API_BASE).then(r => r.json()),
|
||||||
fetch(`${API_BASE}/${id}/children`).then(r => r.json())
|
fetch(`${API_BASE}/${id}/children`).then(r => r.json())
|
||||||
]);
|
]);
|
||||||
|
|
||||||
document.getElementById('parent-goals').innerHTML = parents.map(g =>
|
// 显示当前目标名称
|
||||||
`<span class="badge bg-info me-1 mb-1">${escapeHtml(g.name)} <button type="button" class="btn-close btn-close-white ms-1" onclick="removeRelation(${id}, ${g.id}, 'parent')"></button></span>`
|
document.getElementById('current-goal-name').textContent = currentGoal.name;
|
||||||
).join('') || '<span class="text-muted">无</span>';
|
|
||||||
|
|
||||||
document.getElementById('child-goals').innerHTML = children.map(g =>
|
// 已关联的子目标
|
||||||
`<span class="badge bg-primary me-1 mb-1">${escapeHtml(g.name)} <button type="button" class="btn-close btn-close-white ms-1" onclick="removeRelation(${id}, ${g.id}, 'child')"></button></span>`
|
const childIds = children.map(c => c.id);
|
||||||
).join('') || '<span class="text-muted">无</span>';
|
renderChildrenList(children, id);
|
||||||
|
|
||||||
|
// 可选的子目标(下拉列表中排除自己 和 已关联的)
|
||||||
|
const availableSelect = document.getElementById('available-goals-select');
|
||||||
|
availableSelect.innerHTML = allGoals
|
||||||
|
.filter(g => g.id !== id && !childIds.includes(g.id))
|
||||||
|
.map(g => `<option value="${g.id}">${escapeHtml(g.name)}</option>`)
|
||||||
|
.join('');
|
||||||
|
|
||||||
new bootstrap.Modal(document.getElementById('relationModal')).show();
|
new bootstrap.Modal(document.getElementById('relationModal')).show();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function removeRelation(parentId, childId, type) {
|
function renderChildrenList(children, parentId) {
|
||||||
const url = type === 'parent'
|
const list = document.getElementById('child-goals-list');
|
||||||
? `${API_BASE}/${childId}/children/${parentId}`
|
if (children.length === 0) {
|
||||||
: `${API_BASE}/${parentId}/children/${childId}`;
|
list.innerHTML = '<p class="text-muted">暂无关联子目标</p>';
|
||||||
await fetch(url, {method: 'DELETE'});
|
return;
|
||||||
manageRelations(parentId);
|
}
|
||||||
|
list.innerHTML = children.map(g =>
|
||||||
|
`<div class="list-group-item d-flex justify-content-between align-items-center">
|
||||||
|
${escapeHtml(g.name)}
|
||||||
|
<button class="btn btn-sm btn-outline-danger" onclick="removeChildRelation(${parentId}, ${g.id})">
|
||||||
|
<i class="bi bi-trash"></i> 移除
|
||||||
|
</button>
|
||||||
|
</div>`
|
||||||
|
).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addSelectedChildren() {
|
||||||
|
const parentId = document.getElementById('relation-goal-id').value;
|
||||||
|
const select = document.getElementById('available-goals-select');
|
||||||
|
const selectedOptions = Array.from(select.selectedOptions);
|
||||||
|
|
||||||
|
if (selectedOptions.length === 0) {
|
||||||
|
alert('请先选择要添加的子目标');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const option of selectedOptions) {
|
||||||
|
const childId = parseInt(option.value);
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API_BASE}/${parentId}/children`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify({child_goal_id: childId})
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const err = await res.json();
|
||||||
|
alert(err.error || '添加失败');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新列表
|
||||||
|
const children = await fetch(`${API_BASE}/${parentId}/children`).then(r => r.json());
|
||||||
|
renderChildrenList(children, parentId);
|
||||||
|
|
||||||
|
// 从下拉框移除已添加的
|
||||||
|
selectedOptions.forEach(opt => opt.remove());
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeChildRelation(parentId, childId) {
|
||||||
|
if (!confirm('确定移除此关联?')) return;
|
||||||
|
await fetch(`${API_BASE}/${parentId}/children/${childId}`, {method: 'DELETE'});
|
||||||
|
|
||||||
|
// 刷新列表
|
||||||
|
const children = await fetch(`${API_BASE}/${parentId}/children`).then(r => r.json());
|
||||||
|
renderChildrenList(children, parentId);
|
||||||
|
|
||||||
|
// 刷新下拉框
|
||||||
|
const allGoals = await fetch(API_BASE).then(r => r.json());
|
||||||
|
const childIds = children.map(c => c.id);
|
||||||
|
const select = document.getElementById('available-goals-select');
|
||||||
|
const currentGoalId = parseInt(document.getElementById('relation-goal-id').value);
|
||||||
|
select.innerHTML = allGoals
|
||||||
|
.filter(g => g.id !== currentGoalId && !childIds.includes(g.id))
|
||||||
|
.map(g => `<option value="${g.id}">${escapeHtml(g.name)}</option>`)
|
||||||
|
.join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
function escapeHtml(text) {
|
function escapeHtml(text) {
|
||||||
|
|||||||
Reference in New Issue
Block a user