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

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 %}