364 lines
14 KiB
HTML
364 lines
14 KiB
HTML
{% 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: #e3f2fd; color: #1565c0; }
|
|
.category-认知类 { background: #f3e5f5; color: #7b1fa2; }
|
|
.category-节奏类 { background: #fff3e0; color: #e65100; }
|
|
.category-表现类 { background: #fce4ec; color: #c2185b; }
|
|
.category-习惯类 { background: #e8f5e9; color: #2e7d32; }
|
|
.category-综合类 { background: #f5f5f5; color: #616161; }
|
|
.editor-textarea { font-family: monospace; font-size: 13px; }
|
|
</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="filterProblems()">
|
|
</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>
|
|
<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="mb-3">
|
|
<label class="form-label">问题名称 *</label>
|
|
<input type="text" class="form-control" id="editProblemName">
|
|
</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">
|
|
<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();
|
|
renderProblems(allProblems);
|
|
}
|
|
|
|
function renderProblems(problems) {
|
|
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;
|
|
}
|
|
|
|
let html = '';
|
|
problems.forEach(p => {
|
|
html += `
|
|
<div class="col-md-4 col-sm-6 mb-3 problem-item" data-name="${p.name.toLowerCase()}">
|
|
<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>
|
|
</div>
|
|
`;
|
|
});
|
|
container.innerHTML = html;
|
|
}
|
|
|
|
function filterProblems() {
|
|
const search = document.getElementById('searchInput').value.toLowerCase();
|
|
document.querySelectorAll('.problem-item').forEach(item => {
|
|
item.style.display = item.dataset.name.includes(search) ? 'block' : 'none';
|
|
});
|
|
}
|
|
|
|
// 新增
|
|
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('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 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, 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 %} |