Files

481 lines
18 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.html" %}
{% block title %}问题配置 - 有音个性化教学系统{% endblock %}
{% block page_css %}
<style>
.problem-card { transition: all 0.2s; }
.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-综合 { background: #f5f5f5; color: #616161; }
.category-乐理相关 { background: #e3f2fd; color: #1565c0; }
.category-演奏能力 { background: #e8f5e9; color: #2e7d32; }
.category-其他 { background: #fff3e0; color: #e65100; }
.editor-textarea { font-family: monospace; font-size: 13px; }
.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 content %}
<!-- Tab导航 -->
<ul class="nav nav-tabs mb-4" id="settingsTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="problems-tab" data-bs-toggle="tab" data-bs-target="#problemsPane" type="button">
<i class="bi bi-list-check"></i> 问题配置
</button>
</li>
</ul>
<div class="tab-content" id="settingsTabsContent">
<!-- 问题配置面板 -->
<div class="tab-pane fade show active" id="problemsPane" role="tabpanel">
<!-- 工具栏 -->
<div class="card mb-3">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<h5 class="mb-0"><i class="bi bi-list-check"></i> 问题配置</h5>
<div>
<button type="button" class="btn btn-outline-secondary btn-sm me-2" onclick="loadProblems()">
<i class="bi bi-arrow-clockwise"></i> 刷新
</button>
<button type="button" class="btn btn-primary btn-sm" onclick="showAddModal()">
<i class="bi bi-plus-lg"></i> 新增问题
</button>
</div>
</div>
<input type="text" class="form-control mb-3" placeholder="搜索问题..." id="searchInput" onkeyup="applyProblemFilters()">
<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="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>
<div class="row" id="problemList"></div>
</div>
</div>
<!-- 新增问题模态框 -->
<div class="modal fade" id="addModal" tabindex="-1">
<div class="modal-dialog">
<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">
<div class="mb-3">
<label class="form-label">问题编号</label>
<input type="text" class="form-control" id="newProblemId" placeholder="如: 16">
<small class="text-muted">数字编号,将自动补齐为2位</small>
</div>
<div class="mb-3">
<label class="form-label">问题名称 *</label>
<input type="text" class="form-control" id="newProblemName" placeholder="如: 手小">
</div>
<div class="mb-3">
<label class="form-label">分类</label>
<select class="form-select" id="newProblemCategory">
<option value="综合">综合</option>
<option value="乐理相关">乐理相关</option>
<option value="演奏能力">演奏能力</option>
<option value="其他">其他</option>
</select>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" onclick="createProblem()">创建</button>
</div>
</div>
</div>
</div>
<!-- 编辑问题模态框 -->
<div class="modal fade" id="editModal" 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">
<div class="row g-3">
<div class="col-md-8">
<div class="mb-3">
<label class="form-label">问题名称 *</label>
<input type="text" class="form-control" id="editProblemName">
</div>
</div>
<div class="col-md-4">
<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>
</div>
<div class="mb-3">
<label class="form-label">问题描述 (Markdown)</label>
<textarea class="form-control editor-textarea" id="editProblemContent" rows="20"></textarea>
</div>
</div>
<div class="modal-footer-with-top">
<button type="button" class="btn btn-secondary" id="cancelEditProblemBtn">取消</button>
<button type="button" class="btn btn-primary" onclick="saveProblem()">保存</button>
</div>
</div>
</div>
</div>
<!-- 删除确认模态框 -->
<div class="modal fade" id="deleteModal" tabindex="-1">
<div class="modal-dialog">
<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">
<p>确定要删除问题 <strong id="deleteProblemName"></strong> 吗?</p>
<p class="text-muted small">已被学员使用的问题无法删除</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-danger" onclick="confirmDelete()">删除</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
let allProblems = [];
let currentEditId = null;
let currentDeleteId = null;
const PROBLEM_FILTER_KEY = 'problem_filters';
function saveProblemFilterState() {
const state = {
search: document.getElementById('searchInput').value,
filterCategory: document.getElementById('filterCategory').value,
groupBy: document.getElementById('groupByCategory').value
};
sessionStorage.setItem(PROBLEM_FILTER_KEY, JSON.stringify(state));
}
function restoreProblemFilterState() {
const saved = sessionStorage.getItem(PROBLEM_FILTER_KEY);
if (!saved) return;
try {
const state = JSON.parse(saved);
if (state.search) document.getElementById('searchInput').value = state.search;
if (state.filterCategory) document.getElementById('filterCategory').value = state.filterCategory;
if (state.groupBy) document.getElementById('groupByCategory').value = state.groupBy;
saveProblemFilterState();
} catch (e) {
console.error('恢复问题筛选状态失败', e);
}
}
window.pageInit = function() {
loadProblems();
};
// ========== 问题配置相关 ==========
async function loadProblems() {
const response = await fetch('/api/problems');
allProblems = await response.json();
restoreProblemFilterState();
applyProblemFilters();
}
function applyProblemFilters() {
saveProblemFilterState();
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');
if (problems.length === 0) {
container.innerHTML = '<div class="col-12 text-center text-muted py-5"><p>未找到问题配置</p></div>';
return;
}
if (groupBy) {
// 分组显示
const groups = {};
problems.forEach(p => {
const key = p.category || '其他';
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 => `<div class="col-md-4 col-sm-6">${renderProblemCard(p)}</div>`).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-body">
<div class="d-flex justify-content-between align-items-start mb-2">
<h6 class="card-title mb-0">${p.name}</h6>
<span class="badge category-badge category-${p.category}">${p.category}</span>
</div>
<p class="text-muted small mb-2">编号: ${p.id}</p>
<div class="btn-group btn-group-sm">
<button type="button" class="btn btn-outline-primary" onclick="editProblem('${p.id}')">
<i class="bi bi-pencil"></i> 编辑
</button>
<button type="button" class="btn btn-outline-danger" onclick="showDeleteConfirm('${p.id}', '${p.name}')">
<i class="bi bi-trash"></i> 删除
</button>
</div>
</div>
</div>
`;
}
// 新增
function showAddModal() {
document.getElementById('newProblemId').value = '';
document.getElementById('newProblemName').value = '';
document.getElementById('newProblemCategory').value = '综合';
new bootstrap.Modal(document.getElementById('addModal')).show();
}
async function createProblem() {
const id = document.getElementById('newProblemId').value.trim();
const name = document.getElementById('newProblemName').value.trim();
const category = document.getElementById('newProblemCategory').value;
if (!id || !name) {
alert('请填写编号和名称');
return;
}
const response = await fetch('/api/problems', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({id, name, category})
});
if (response.ok) {
bootstrap.Modal.getInstance(document.getElementById('addModal')).hide();
loadProblems();
} else {
const err = await response.json();
alert(err.error || '创建失败');
}
}
// 编辑
let editProblemOriginalState = { name: '', content: '' }; // 记录原始状态
async function editProblem(problemId) {
currentEditId = problemId;
const response = await fetch(`/api/problems/${problemId}`);
const data = await response.json();
if (data.error) {
alert(data.error);
return;
}
const name = data.name;
document.getElementById('editProblemName').value = name;
document.getElementById('editProblemCategory').value = data.category || '综合';
document.getElementById('editProblemContent').value = data.content || '';
// 记录原始状态
editProblemOriginalState.name = name;
editProblemOriginalState.content = data.content;
if (window.problemEditor) {
window.problemEditor.toTextArea();
window.problemEditor = null;
}
if (window.EasyMDE) {
window.problemEditor = new EasyMDE({
element: document.getElementById('editProblemContent'),
spellChecker: false,
status: false,
toolbar: ['bold', 'italic', 'heading', '|', 'code', 'quote', 'unordered-list', '|', 'preview', 'side-by-side', 'fullscreen'],
initialValue: data.content
});
}
const editModal = new bootstrap.Modal(document.getElementById('editModal'), {
keyboard: false // 禁用 ESC 关闭,由我们手动处理
});
const modalEl = document.getElementById('editModal');
// 取消按钮点击处理
document.getElementById('cancelEditProblemBtn').onclick = () => {
if (isEditProblemDirty()) {
if (confirm('内容已修改,确定要关闭吗?')) {
editModal.hide();
}
} else {
editModal.hide();
}
};
// ESC 键处理
const handleEscape = (e) => {
if (e.key === 'Escape') {
e.preventDefault();
e.stopPropagation();
if (isEditProblemDirty()) {
if (confirm('内容已修改,确定要关闭吗?')) {
modalEl.removeEventListener('keydown', handleEscape);
editModal.hide();
}
} else {
modalEl.removeEventListener('keydown', handleEscape);
editModal.hide();
}
}
};
modalEl.addEventListener('keydown', handleEscape);
editModal.show();
editModal._element.addEventListener('shown.bs.modal', function() {
if (window.problemEditor && window.problemEditor.codemirror) {
window.problemEditor.codemirror.refresh();
}
});
}
// 检测编辑问题是否有修改
function isEditProblemDirty() {
const currentName = document.getElementById('editProblemName').value;
const currentContent = window.problemEditor ? window.problemEditor.value() : document.getElementById('editProblemContent').value;
return currentName !== editProblemOriginalState.name || currentContent !== editProblemOriginalState.content;
}
async function saveProblem() {
const name = document.getElementById('editProblemName').value.trim();
const category = document.getElementById('editProblemCategory').value;
const content = window.problemEditor ? window.problemEditor.value() : document.getElementById('editProblemContent').value;
if (!name) {
alert('请填写名称');
return;
}
const response = await fetch(`/api/problems/${currentEditId}`, {
method: 'PUT',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({name, category, content})
});
if (response.ok) {
bootstrap.Modal.getInstance(document.getElementById('editModal')).hide();
if (window.problemEditor) {
window.problemEditor.toTextArea();
window.problemEditor = null;
}
loadProblems();
} else {
const err = await response.json();
alert(err.error || '保存失败');
}
}
// 删除
function showDeleteConfirm(problemId, problemName) {
currentDeleteId = problemId;
document.getElementById('deleteProblemName').textContent = problemName;
new bootstrap.Modal(document.getElementById('deleteModal')).show();
}
async function confirmDelete() {
const response = await fetch(`/api/problems/${currentDeleteId}`, {
method: 'DELETE'
});
if (response.ok) {
bootstrap.Modal.getInstance(document.getElementById('deleteModal')).hide();
loadProblems();
} else {
const err = await response.json();
alert(err.error || '删除失败');
}
}
</script>
{% endblock %}