Files
piano-plan/app/templates/problems.html
T

454 lines
17 KiB
HTML
Raw 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="d-flex justify-content-between align-items-center mb-4">
<h4><i class="bi bi-list-check"></i> 问题配置</h4>
<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>
<div class="mb-3">
<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 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">删除后可在 bk 文件夹中找到备份</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;
window.pageInit = function() {
loadProblems();
};
// ========== 问题配置相关 ==========
async function loadProblems() {
const response = await fetch('/api/problems');
allProblems = await response.json();
applyProblemFilters();
}
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');
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 %}