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

308 lines
14 KiB
HTML

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>{% block title %}有音个性化教学系统{% endblock %}</title>
<!-- 公共CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/easymde/dist/easymde.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/tabulator-tables@5/dist/css/tabulator.min.css" rel="stylesheet">
<!-- Chart.js -->
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
{% block extra_css %}{% endblock %}
<style>
body { background-color: #f5f7fa; }
.sidebar { min-height: 100vh; background: #2c3e50; color: white; }
.sidebar .nav-link { color: rgba(255,255,255,0.8); padding: 12px 20px; }
.sidebar .nav-link:hover, .sidebar .nav-link.active { background: rgba(255,255,255,0.1); color: white; }
.main-content { padding: 20px; }
.card { border: none; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.05); }
/* 移动端响应式 */
@media (max-width: 767.98px) {
.sidebar {
position: fixed;
top: 60px;
left: 0;
width: 100%;
min-height: auto;
max-height: calc(100vh - 60px);
z-index: 1040;
overflow-y: auto;
transform: translateY(-100%);
transition: transform 0.3s ease;
}
.sidebar.collapsed {
transform: translateY(-100%);
}
.sidebar:not(.collapsed) {
transform: translateY(0);
}
.main-content {
margin-top: 60px;
padding: 10px;
}
.mobile-nav-toggle {
display: flex !important;
}
}
@media (min-width: 768px) {
.mobile-nav-toggle {
display: none !important;
}
.sidebar {
position: static;
transform: none !important;
}
.sidebar.collapsed {
transform: none !important;
}
}
</style>
{% block page_css %}{% endblock %}
</head>
<body>
<!-- 移动端顶部导航 -->
<nav class="mobile-nav-toggle navbar navbar-dark bg-dark d-flex d-md-none" style="position:fixed;top:0;left:0;right:0;z-index:1050;padding:10px;">
<div class="container-fluid d-flex justify-content-between align-items-center">
<div>
<a href="/" class="navbar-brand mb-0 text-white text-decoration-none"><i class="bi bi-music-note-beamed"></i> 有音个性化教学系统</a>
<small id="mobileUserDisplay" class="d-block text-white-50" style="font-size:10px;"></small>
</div>
<button class="btn btn-outline-light btn-sm" onclick="toggleMobileNav()">
<i class="bi bi-list"></i>
</button>
</div>
</nav>
<div class="container-fluid">
<div class="row">
<!-- 侧边栏 -->
<div class="col-md-2 sidebar p-0 collapsed" id="sidebar">
<div class="p-3 text-center border-bottom border-secondary d-none d-md-block">
<h5><a href="/" class="text-white text-decoration-none"><i class="bi bi-music-note-beamed"></i> 有音个性化教学系统</a></h5>
<small id="currentUserDisplay" class="text-light"></small>
</div>
<nav class="nav flex-column">
<a class="nav-link {% if active_nav == 'students' %}active{% endif %}" href="/students">
<i class="bi bi-people"></i> 学员管理
</a>
<a class="nav-link {% if active_nav == 'classes' %}active{% endif %}" href="/classes" id="classesNav">
<i class="bi bi-collection"></i> 班级管理
</a>
<a class="nav-link {% if active_nav == 'plans' %}active{% endif %}" href="/plans">
<i class="bi bi-clipboard-check"></i> 方案管理
</a>
<a class="nav-link {% if active_nav == 'goals' %}active{% endif %}" href="/goals">
<i class="bi bi-crosshair"></i> 目标管理
</a>
<a class="nav-link {% if active_nav == 'problems' %}active{% endif %}" href="/problems">
<i class="bi bi-gear"></i> 问题配置
</a>
<a class="nav-link {% if active_nav == 'templates' %}active{% endif %}" href="/templates" id="templatesNav" style="display:none;">
<i class="bi bi-file-earmark-text"></i> 模板管理
</a>
<a class="nav-link {% if active_nav == 'users' %}active{% endif %}" href="/users" id="usersNav" style="display:none;">
<i class="bi bi-person"></i> 用户管理
</a>
<a class="nav-link {% if active_nav == 'api-settings' %}active{% endif %}" href="/api-settings" id="apiSettingsNav" style="display:none;">
<i class="bi bi-key"></i> API设置
</a>
<a class="nav-link {% if active_nav == 'statistics' %}active{% endif %}" href="/statistics">
<i class="bi bi-bar-chart"></i> 数据统计
</a>
{% block sidebar_extra %}
<hr>
<a class="nav-link" href="#" onclick="showChangePasswordModal(); return false;">
<i class="bi bi-key"></i> 修改密码
</a>
<a class="nav-link" href="#" onclick="logout(); return false;">
<i class="bi bi-box-arrow-right"></i> 退出登录
</a>
{% endblock %}
</nav>
</div>
<!-- 主内容区 -->
<div class="col-md-10 main-content">
{% block content %}{% endblock %}
</div>
</div>
</div>
<!-- 公共JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/easymde/dist/easymde.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/tabulator-tables@5/dist/js/tabulator.min.js"></script>
<script src="/static/js/plan_common.js"></script>
<script>
// 统一登录检查和权限处理
window.addEventListener('DOMContentLoaded', function() {
fetch('/api/check-login').then(r => r.json()).then(data => {
if (!data.logged_in) {
window.location.href = '/login';
return;
}
// 显示用户名
const ROLE_LABELS = { 'admin': '管理员', 'user': '普通用户' };
const userDisplay = data.username + ' (' + (ROLE_LABELS[data.role] || data.role) + ')';
const currentUserEl = document.getElementById('currentUserDisplay');
if (currentUserEl) currentUserEl.textContent = userDisplay;
const mobileUserEl = document.getElementById('mobileUserDisplay');
if (mobileUserEl) mobileUserEl.textContent = userDisplay;
// 侧边栏权限控制
const setDisplay = (id, val) => {
const el = document.getElementById(id);
if (el) el.style.display = val;
};
if (data.role === 'admin') {
setDisplay('apiSettingsNav', '');
setDisplay('templatesNav', '');
setDisplay('usersNav', '');
setDisplay('classesNav', '');
setDisplay('settingsNav', '');
} else {
setDisplay('settingsNav', '');
setDisplay('classesNav', '');
}
// 调用页面初始化函数(如果定义了)
if (typeof window.pageInit === 'function') {
window.pageInit(data);
}
}).catch(() => {
window.location.href = '/login';
});
});
// 移动端导航切换
function toggleMobileNav() {
const sidebar = document.getElementById('sidebar');
if (sidebar) {
sidebar.classList.toggle('collapsed');
}
}
// 点击导航链接后关闭移动端导航
document.querySelectorAll('.sidebar .nav-link').forEach(link => {
link.addEventListener('click', () => {
if (window.innerWidth < 768) {
toggleMobileNav();
}
});
});
// 退出登录
function logout() {
if (!confirm('确定要退出登录吗?')) return;
fetch('/api/logout', { method: 'POST' }).then(() => window.location.href = '/login');
}
// 修改密码
function showChangePasswordModal() {
document.getElementById('oldPassword').value = '';
document.getElementById('newPassword').value = '';
document.getElementById('confirmPassword').value = '';
new bootstrap.Modal(document.getElementById('changePwdModal')).show();
}
</script>
{% block extra_js %}{% endblock %}
<!-- 方案详情弹窗 -->
<div class="modal fade" id="planDetailModal" tabindex="-1">
<div class="modal-dialog modal-fullscreen modal-dialog-scrollable">
<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" id="planDetailContent">
<!-- 方案内容将通过JS动态生成 -->
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
<button type="button" class="btn btn-warning" onclick="editPlanContent()">
<i class="bi bi-edit"></i> 编辑内容
</button>
<button type="button" class="btn btn-primary" onclick="downloadPDF()">
<i class="bi bi-download"></i> 下载PDF
</button>
</div>
</div>
</div>
</div>
<!-- 修改密码弹窗 -->
<div class="modal fade" id="changePwdModal" 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">
<form id="changePwdForm">
<div class="mb-3">
<label class="form-label">原密码</label>
<input type="password" class="form-control" id="oldPassword" required>
</div>
<div class="mb-3">
<label class="form-label">新密码</label>
<input type="password" class="form-control" id="newPassword" required>
<div class="form-text">8位以上,包含大小写字母、数字和特殊字符</div>
</div>
<div class="mb-3">
<label class="form-label">确认新密码</label>
<input type="password" class="form-control" id="confirmPassword" required>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" id="changePwdBtn">确认修改</button>
</div>
</div>
</div>
</div>
<script>
document.getElementById('changePwdBtn').addEventListener('click', function() {
const oldPassword = document.getElementById('oldPassword').value;
const newPassword = document.getElementById('newPassword').value;
const confirmPassword = document.getElementById('confirmPassword').value;
if (!oldPassword || !newPassword || !confirmPassword) {
alert('请填写完整');
return;
}
if (newPassword !== confirmPassword) {
alert('两次输入的新密码不一致');
return;
}
fetch('/api/users/change-password', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ old_password: oldPassword, new_password: newPassword })
}).then(r => r.json()).then(data => {
if (data.error) {
alert(data.error);
} else {
alert('密码修改成功,请重新登录');
logout();
}
}).catch(err => {
console.error(err);
alert('请求失败');
});
});
</script>
</body>
</html>