ai_v/templates/base.html
2026-03-31 08:58:28 +08:00

194 lines
9.2 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.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}AI 视界{% endblock %}</title>
<link rel="icon" href="/static/A_2IGfT6uTwlgAAAAAQmAAAAgAerF1AQ.png" type="image/png">
<script src="https://127007.xyz/https://cdn.tailwindcss.com?plugins=typography"></script>
<link
href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700&family=Noto+Sans+SC:wght@300;400;500;700&display=swap"
rel="stylesheet">
<script src="{{ url_for('static', filename='js/lucide.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/Sortable.min.js') }}"></script>
<script src="https://127007.xyz/https://cdn.jsdelivr.net/npm/chart.js"></script>
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
<style>
/* 关键路径 CSS防止侧边栏及图标闪烁 */
#globalNav {
width: 5rem;
flex-shrink: 0;
display: flex;
}
#globalNav.hidden {
display: none !important;
}
[data-lucide] {
width: 1.25rem;
height: 1.25rem;
display: inline-block;
}
</style>
{% block head %}{% endblock %}
</head>
<body class="text-slate-700 antialiased bg-slate-50 overflow-hidden">
<div class="bg-mesh"></div>
<div id="toastContainer" class="toast-container"></div>
<div class="flex h-screen w-screen overflow-hidden">
<!-- 全局系统菜单栏 (服务端渲染,防止闪烁) -->
<nav id="globalNav"
class="{% if not nav_menu %}hidden{% endif %} w-20 flex-shrink-0 bg-slate-900 flex flex-col items-center py-8 z-40 shadow-2xl">
<div class="w-12 h-12 mb-12 flex items-center justify-center p-1">
<img src="/static/A_2IGfT6uTwlgAAAAAQmAAAAgAerF1AQ.png" alt="Logo"
class="w-full h-full object-contain rounded-xl shadow-lg">
</div>
<div id="dynamicMenuList" class="flex-1 w-full px-2 space-y-4">
{% for item in nav_menu %}
{% set isActive = request.path == item.url %}
<div class="relative group flex justify-center">
<a href="{{ item.url }}"
class="w-12 h-12 flex items-center justify-center rounded-2xl transition-all duration-300 {{ 'bg-indigo-600 text-white shadow-lg shadow-indigo-500/40' if isActive else 'text-slate-500 hover:bg-slate-800 hover:text-white' }}">
<i data-lucide="{{ item.icon }}" class="w-5 h-5"></i>
</a>
<div
class="absolute left-full ml-4 px-3 py-2 bg-slate-800 text-white text-[10px] font-black rounded-lg opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all whitespace-nowrap z-50 shadow-xl">
{{ item.name }}
<div
class="absolute right-full top-1/2 -translate-y-1/2 border-8 border-transparent border-r-slate-800">
</div>
</div>
</div>
{% endfor %}
</div>
<div id="globalUserProfile" class="flex flex-col items-center gap-4 mb-4">
<!-- 联系方式 -->
<div class="relative group flex justify-center mb-2">
<div
class="w-10 h-10 bg-slate-800 rounded-xl flex items-center justify-center text-slate-400 border border-slate-700 hover:text-indigo-400 transition-colors cursor-help">
<i data-lucide="message-circle" class="w-5 h-5"></i>
</div>
<div
class="absolute left-full ml-4 px-3 py-2 bg-slate-800 text-white text-[10px] font-black rounded-lg opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all whitespace-nowrap z-50 shadow-xl border border-slate-700">
联系客服 QQ: 240241002
<div
class="absolute right-full top-1/2 -translate-y-1/2 border-8 border-transparent border-r-slate-800">
</div>
</div>
</div>
<div
class="w-10 h-10 bg-slate-800 rounded-xl flex items-center justify-center text-slate-400 border border-slate-700">
<i data-lucide="user" class="w-5 h-5"></i>
</div>
<button id="globalLogoutBtn" class="text-slate-500 hover:text-rose-400 transition-colors">
<i data-lucide="log-out" class="w-5 h-5"></i>
</button>
</div>
</nav>
<!-- 主内容区域 (全屏) -->
<main class="flex-1 flex overflow-hidden relative w-full h-full">
{% block content %}{% endblock %}
</main>
</div>
<!-- 全局通知弹窗 -->
<div id="notifModal"
class="fixed inset-0 bg-slate-900/60 backdrop-blur-md z-[60] flex items-center justify-center hidden opacity-0 transition-opacity duration-500">
<div
class="bg-white w-full max-w-2xl rounded-[2.5rem] shadow-2xl p-10 space-y-6 transform scale-90 transition-transform duration-500">
<div
class="w-16 h-16 bg-indigo-50 text-indigo-600 rounded-2xl flex items-center justify-center mx-auto mb-2">
<i data-lucide="bell-ring" class="w-8 h-8 animate-tada"></i>
</div>
<div class="text-center space-y-2">
<h2 id="notifTitle" class="text-2xl font-black text-slate-900">系统通知</h2>
<p id="notifTime" class="text-[10px] font-black text-slate-300 uppercase tracking-widest"></p>
<div id="notifContent"
class="text-slate-500 text-sm font-bold leading-relaxed whitespace-pre-wrap pt-4">
</div>
</div>
<button id="closeNotifBtn"
class="w-full py-4 rounded-2xl bg-slate-900 text-white font-black text-sm hover:bg-slate-800 transition-all shadow-xl shadow-slate-200">
我已收到
</button>
</div>
</div>
<script>
lucide.createIcons();
document.getElementById('globalLogoutBtn').onclick = async () => {
await fetch('/api/auth/logout', { method: 'POST' });
window.location.href = '/login';
};
// 全局通知检查
let currentNotifId = null;
async function checkNotifications() {
try {
const r = await fetch('/api/notifications/latest');
const d = await r.json();
if (d.id) {
currentNotifId = d.id;
document.getElementById('notifTitle').innerText = d.title;
document.getElementById('notifTime').innerText = d.time || '';
document.getElementById('notifContent').innerText = d.content;
const modal = document.getElementById('notifModal');
modal.classList.remove('hidden');
setTimeout(() => {
modal.classList.add('opacity-100');
modal.querySelector('div').classList.remove('scale-90');
}, 10);
}
} catch (e) { console.error('通知检查失败', e); }
}
document.getElementById('closeNotifBtn').onclick = async () => {
if (!currentNotifId) return;
// 立即隐藏
const modal = document.getElementById('notifModal');
modal.classList.remove('opacity-100');
modal.querySelector('div').classList.add('scale-90');
setTimeout(() => {
modal.classList.add('hidden');
// 递归检查是否有下一条通知
checkNotifications();
}, 500);
// 后端同步已读
await fetch('/api/notifications/read', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: currentNotifId })
});
};
checkNotifications();
function showToast(message, type = 'info') {
const container = document.getElementById('toastContainer');
const toast = document.createElement('div');
toast.className = `toast ${type}`;
const colors = { success: 'text-emerald-500', error: 'text-rose-500', warning: 'text-amber-500', info: 'text-indigo-500' };
const icons = { success: 'check-circle', error: 'x-circle', warning: 'alert-triangle', info: 'info' };
toast.innerHTML = `<i data-lucide="${icons[type]}" class="w-5 h-5 ${colors[type]} flex-shrink-0"></i><span class="text-sm font-medium text-slate-700">${message}</span>`;
container.appendChild(toast);
lucide.createIcons();
setTimeout(() => { toast.style.animation = 'slideOutRight 0.3s ease-in'; setTimeout(() => toast.remove(), 300); }, 3000);
}
</script>
{% block scripts %}{% endblock %}
</body>
</html>