feat: 目标和问题列表添加筛选和分组功能
This commit is contained in:
+104
-6
@@ -4,6 +4,49 @@
|
|||||||
<div class="container-fluid py-4">
|
<div class="container-fluid py-4">
|
||||||
<h2 class="mb-4">🎯 目标管理</h2>
|
<h2 class="mb-4">🎯 目标管理</h2>
|
||||||
|
|
||||||
|
<!-- 筛选和分组控制 -->
|
||||||
|
<div class="card mb-3">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row g-3 align-items-center">
|
||||||
|
<div class="col-auto">
|
||||||
|
<label class="form-label mb-0">筛选:</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<select class="form-select" id="filter-level" onchange="applyFilters()">
|
||||||
|
<option value="">全部级别</option>
|
||||||
|
<option value="启蒙">启蒙</option>
|
||||||
|
<option value="入门">入门</option>
|
||||||
|
<option value="进阶">进阶</option>
|
||||||
|
<option value="熟练">熟练</option>
|
||||||
|
<option value="精通">精通</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<select class="form-select" id="filter-category" onchange="applyFilters()">
|
||||||
|
<option value="">全部分类</option>
|
||||||
|
<option value="综合">综合</option>
|
||||||
|
<option value="乐理相关">乐理相关</option>
|
||||||
|
<option value="演奏能力">演奏能力</option>
|
||||||
|
<option value="其他">其他</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<label class="form-label mb-0">分组:</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<select class="form-select" id="group-by" onchange="applyFilters()">
|
||||||
|
<option value="">不分组</option>
|
||||||
|
<option value="level">按级别分组</option>
|
||||||
|
<option value="category">按分类分组</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto ms-auto">
|
||||||
|
<span id="goals-count" class="text-muted small"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 目标列表 -->
|
<!-- 目标列表 -->
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
@@ -104,28 +147,83 @@
|
|||||||
{{ super() }}
|
{{ super() }}
|
||||||
<script>
|
<script>
|
||||||
const API_BASE = '/api/goals';
|
const API_BASE = '/api/goals';
|
||||||
|
let allGoals = []; // 缓存所有目标数据
|
||||||
|
|
||||||
// 加载目标列表
|
// 加载目标列表
|
||||||
async function loadGoals() {
|
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 goalsWithChildren = await Promise.all(goals.map(async g => {
|
allGoals = await Promise.all(goals.map(async g => {
|
||||||
const childrenRes = await fetch(`${API_BASE}/${g.id}/children`);
|
const childrenRes = await fetch(`${API_BASE}/${g.id}/children`);
|
||||||
const children = await childrenRes.json();
|
const children = await childrenRes.json();
|
||||||
return {...g, children: children};
|
return {...g, children: children};
|
||||||
}));
|
}));
|
||||||
|
|
||||||
grid.innerHTML = goalsWithChildren.map(g => {
|
applyFilters();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 应用筛选和分组
|
||||||
|
function applyFilters() {
|
||||||
|
const filterLevel = document.getElementById('filter-level').value;
|
||||||
|
const filterCategory = document.getElementById('filter-category').value;
|
||||||
|
const groupBy = document.getElementById('group-by').value;
|
||||||
|
|
||||||
|
// 筛选
|
||||||
|
let filtered = allGoals.filter(g => {
|
||||||
|
if (filterLevel && g.level !== filterLevel) return false;
|
||||||
|
if (filterCategory && g.category !== filterCategory) return false;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 更新计数
|
||||||
|
document.getElementById('goals-count').textContent = `共 ${filtered.length} 个目标`;
|
||||||
|
|
||||||
|
// 渲染
|
||||||
|
const grid = document.getElementById('goals-grid');
|
||||||
|
|
||||||
|
if (groupBy) {
|
||||||
|
// 分组显示
|
||||||
|
const groups = {};
|
||||||
|
filtered.forEach(g => {
|
||||||
|
const key = groupBy === 'level' ? (g.level || '入门') : (g.category || '综合');
|
||||||
|
if (!groups[key]) groups[key] = [];
|
||||||
|
groups[key].push(g);
|
||||||
|
});
|
||||||
|
|
||||||
|
grid.innerHTML = Object.entries(groups).map(([key, goals]) => `
|
||||||
|
<div class="col-12 mb-3">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header bg-light">
|
||||||
|
<h6 class="mb-0">${groupBy === 'level' ? '级别' : '分类'}:${key}(${goals.length}个)</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-2">
|
||||||
|
<div class="row g-2">
|
||||||
|
${goals.map(g => renderGoalCard(g)).join('')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
} else {
|
||||||
|
// 不分组
|
||||||
|
grid.innerHTML = filtered.map(g => `
|
||||||
|
<div class="col-md-4 col-lg-3">
|
||||||
|
${renderGoalCard(g)}
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 渲染单个目标卡片
|
||||||
|
function renderGoalCard(g) {
|
||||||
const level = g.level || '入门';
|
const level = g.level || '入门';
|
||||||
const category = g.category || '综合';
|
const category = g.category || '综合';
|
||||||
const childNames = g.children && g.children.length > 0
|
const childNames = g.children && g.children.length > 0
|
||||||
? g.children.map(c => escapeHtml(c.name)).join(', ')
|
? g.children.map(c => escapeHtml(c.name)).join(', ')
|
||||||
: '';
|
: '';
|
||||||
return `
|
return `
|
||||||
<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>
|
||||||
@@ -142,8 +240,8 @@ async function loadGoals() {
|
|||||||
<button class="btn btn-sm btn-outline-danger" onclick="deleteGoal(${g.id})">删除</button>
|
<button class="btn btn-sm btn-outline-danger" onclick="deleteGoal(${g.id})">删除</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
`;
|
||||||
`}).join('');
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存目标
|
// 保存目标
|
||||||
|
|||||||
+103
-25
@@ -7,12 +7,10 @@
|
|||||||
.problem-card { transition: all 0.2s; }
|
.problem-card { transition: all 0.2s; }
|
||||||
.problem-card:hover { transform: translateY(-2px); box-shadow: 0 4px 15px rgba(0,0,0,0.1); }
|
.problem-card:hover { transform: translateY(-2px); box-shadow: 0 4px 15px rgba(0,0,0,0.1); }
|
||||||
.category-badge { font-size: 11px; padding: 3px 8px; }
|
.category-badge { font-size: 11px; padding: 3px 8px; }
|
||||||
.category-技术类 { background: #e3f2fd; color: #1565c0; }
|
.category-综合 { background: #f5f5f5; color: #616161; }
|
||||||
.category-认知类 { background: #f3e5f5; color: #7b1fa2; }
|
.category-乐理相关 { background: #e3f2fd; color: #1565c0; }
|
||||||
.category-节奏类 { background: #fff3e0; color: #e65100; }
|
.category-演奏能力 { background: #e8f5e9; color: #2e7d32; }
|
||||||
.category-表现类 { background: #fce4ec; color: #c2185b; }
|
.category-其他 { background: #fff3e0; color: #e65100; }
|
||||||
.category-习惯类 { background: #e8f5e9; color: #2e7d32; }
|
|
||||||
.category-综合类 { background: #f5f5f5; color: #616161; }
|
|
||||||
.editor-textarea { font-family: monospace; font-size: 13px; }
|
.editor-textarea { font-family: monospace; font-size: 13px; }
|
||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -43,7 +41,35 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<input type="text" class="form-control" placeholder="搜索问题..." id="searchInput" onkeyup="filterProblems()">
|
<input type="text" class="form-control" placeholder="搜索问题..." id="searchInput" onkeyup="applyProblemFilters()">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 筛选和分组控制 -->
|
||||||
|
<div class="row g-3 align-items-center mb-3">
|
||||||
|
<div class="col-auto">
|
||||||
|
<label class="form-label mb-0">筛选:</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<select class="form-select" id="filterCategory" onchange="applyProblemFilters()">
|
||||||
|
<option value="">全部分类</option>
|
||||||
|
<option value="综合">综合</option>
|
||||||
|
<option value="乐理相关">乐理相关</option>
|
||||||
|
<option value="演奏能力">演奏能力</option>
|
||||||
|
<option value="其他">其他</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<label class="form-label mb-0">分组:</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<select class="form-select" id="groupByCategory" onchange="applyProblemFilters()">
|
||||||
|
<option value="">不分组</option>
|
||||||
|
<option value="category">按分类分组</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto ms-auto">
|
||||||
|
<span id="problems-count" class="text-muted small"></span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row" id="problemList"></div>
|
<div class="row" id="problemList"></div>
|
||||||
@@ -71,12 +97,10 @@
|
|||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">分类</label>
|
<label class="form-label">分类</label>
|
||||||
<select class="form-select" id="newProblemCategory">
|
<select class="form-select" id="newProblemCategory">
|
||||||
<option value="技术类">技术类</option>
|
<option value="综合">综合</option>
|
||||||
<option value="认知类">认知类</option>
|
<option value="乐理相关">乐理相关</option>
|
||||||
<option value="节奏类">节奏类</option>
|
<option value="演奏能力">演奏能力</option>
|
||||||
<option value="表现类">表现类</option>
|
<option value="其他">其他</option>
|
||||||
<option value="习惯类">习惯类</option>
|
|
||||||
<option value="综合类">综合类</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -101,6 +125,15 @@
|
|||||||
<label class="form-label">问题名称 *</label>
|
<label class="form-label">问题名称 *</label>
|
||||||
<input type="text" class="form-control" id="editProblemName">
|
<input type="text" class="form-control" id="editProblemName">
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">分类</label>
|
||||||
|
<select class="form-select" id="editProblemCategory">
|
||||||
|
<option value="综合">综合</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 editor-textarea" id="editProblemContent" rows="20"></textarea>
|
<textarea class="form-control editor-textarea" id="editProblemContent" rows="20"></textarea>
|
||||||
@@ -150,20 +183,69 @@ window.pageInit = function() {
|
|||||||
async function loadProblems() {
|
async function loadProblems() {
|
||||||
const response = await fetch('/api/problems');
|
const response = await fetch('/api/problems');
|
||||||
allProblems = await response.json();
|
allProblems = await response.json();
|
||||||
renderProblems(allProblems);
|
applyProblemFilters();
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderProblems(problems) {
|
function applyProblemFilters() {
|
||||||
|
const search = document.getElementById('searchInput').value.toLowerCase();
|
||||||
|
const filterCategory = document.getElementById('filterCategory').value;
|
||||||
|
const groupBy = document.getElementById('groupByCategory').value;
|
||||||
|
|
||||||
|
// 筛选
|
||||||
|
let filtered = allProblems.filter(p => {
|
||||||
|
if (search && !p.name.toLowerCase().includes(search)) return false;
|
||||||
|
if (filterCategory && p.category !== filterCategory) return false;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 更新计数
|
||||||
|
document.getElementById('problems-count').textContent = `共 ${filtered.length} 个问题`;
|
||||||
|
|
||||||
|
renderProblems(filtered, groupBy);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderProblems(problems, groupBy) {
|
||||||
const container = document.getElementById('problemList');
|
const container = document.getElementById('problemList');
|
||||||
if (problems.length === 0) {
|
if (problems.length === 0) {
|
||||||
container.innerHTML = '<div class="col-12 text-center text-muted py-5"><p>未找到问题配置</p></div>';
|
container.innerHTML = '<div class="col-12 text-center text-muted py-5"><p>未找到问题配置</p></div>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let html = '';
|
if (groupBy) {
|
||||||
|
// 分组显示
|
||||||
|
const groups = {};
|
||||||
problems.forEach(p => {
|
problems.forEach(p => {
|
||||||
html += `
|
const key = p.category || '其他';
|
||||||
<div class="col-md-4 col-sm-6 mb-3 problem-item" data-name="${p.name.toLowerCase()}">
|
if (!groups[key]) groups[key] = [];
|
||||||
|
groups[key].push(p);
|
||||||
|
});
|
||||||
|
|
||||||
|
container.innerHTML = Object.entries(groups).map(([key, items]) => `
|
||||||
|
<div class="col-12 mb-3">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header bg-light">
|
||||||
|
<h6 class="mb-0">分类:${key}(${items.length}个)</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-2">
|
||||||
|
<div class="row g-2">
|
||||||
|
${items.map(p => renderProblemCard(p)).join('')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
} else {
|
||||||
|
// 不分组
|
||||||
|
container.innerHTML = problems.map(p => `
|
||||||
|
<div class="col-md-4 col-sm-6 mb-3">
|
||||||
|
${renderProblemCard(p)}
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderProblemCard(p) {
|
||||||
|
return `
|
||||||
<div class="card problem-card h-100">
|
<div class="card problem-card h-100">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="d-flex justify-content-between align-items-start mb-2">
|
<div class="d-flex justify-content-between align-items-start mb-2">
|
||||||
@@ -181,14 +263,8 @@ function renderProblems(problems) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
`;
|
`;
|
||||||
});
|
|
||||||
container.innerHTML = html;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function filterProblems() {
|
|
||||||
const search = document.getElementById('searchInput').value.toLowerCase();
|
|
||||||
document.querySelectorAll('.problem-item').forEach(item => {
|
document.querySelectorAll('.problem-item').forEach(item => {
|
||||||
item.style.display = item.dataset.name.includes(search) ? 'block' : 'none';
|
item.style.display = item.dataset.name.includes(search) ? 'block' : 'none';
|
||||||
});
|
});
|
||||||
@@ -242,6 +318,7 @@ async function editProblem(problemId) {
|
|||||||
|
|
||||||
const name = data.name;
|
const name = data.name;
|
||||||
document.getElementById('editProblemName').value = name;
|
document.getElementById('editProblemName').value = name;
|
||||||
|
document.getElementById('editProblemCategory').value = data.category || '综合';
|
||||||
document.getElementById('editProblemContent').value = data.content || '';
|
document.getElementById('editProblemContent').value = data.content || '';
|
||||||
|
|
||||||
// 记录原始状态
|
// 记录原始状态
|
||||||
@@ -314,6 +391,7 @@ function isEditProblemDirty() {
|
|||||||
|
|
||||||
async function saveProblem() {
|
async function saveProblem() {
|
||||||
const name = document.getElementById('editProblemName').value.trim();
|
const name = document.getElementById('editProblemName').value.trim();
|
||||||
|
const category = document.getElementById('editProblemCategory').value;
|
||||||
const content = window.problemEditor ? window.problemEditor.value() : document.getElementById('editProblemContent').value;
|
const content = window.problemEditor ? window.problemEditor.value() : document.getElementById('editProblemContent').value;
|
||||||
|
|
||||||
if (!name) {
|
if (!name) {
|
||||||
@@ -324,7 +402,7 @@ async function saveProblem() {
|
|||||||
const response = await fetch(`/api/problems/${currentEditId}`, {
|
const response = await fetch(`/api/problems/${currentEditId}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: {'Content-Type': 'application/json'},
|
headers: {'Content-Type': 'application/json'},
|
||||||
body: JSON.stringify({name, content})
|
body: JSON.stringify({name, category, content})
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
|
|||||||
Reference in New Issue
Block a user