ai_v/templates/base.html
24024 3610bedb39 ```
feat(admin): 系统通知功能增加图片支持

- 在SystemNotification模型中添加image_urls字段用于存储图片URL列表
- 实现通知图片的上传、保存和展示功能
- 前端模板增加图片预览模态框和图片上传组件
- 优化通知管理页面的UI布局,支持多图展示
- 完善图片相关的前后端交互逻辑和错误处理
```
2026-04-24 23:42:12 +08:00

232 lines
11 KiB
HTML
Raw Permalink 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 id="notifImages" class="grid grid-cols-2 gap-4 pt-2"></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>
<div id="notifImageModal"
class="fixed inset-0 bg-slate-900/80 backdrop-blur-md z-[70] hidden items-center justify-center p-6">
<button id="closeNotifImageBtn"
class="absolute top-6 right-6 w-11 h-11 rounded-full bg-white/10 text-white hover:bg-white/20 transition-all flex items-center justify-center">
<i data-lucide="x" class="w-5 h-5"></i>
</button>
<img id="notifImagePreview" src="" alt="公告图片预览"
class="max-w-[92vw] max-h-[88vh] rounded-3xl shadow-2xl object-contain bg-white">
</div>
<script>
lucide.createIcons();
document.getElementById('globalLogoutBtn').onclick = async () => {
await fetch('/api/auth/logout', { method: 'POST' });
window.location.href = '/login';
};
// 全局通知检查
let currentNotifId = null;
function openNotifImage(url) {
const modal = document.getElementById('notifImageModal');
const preview = document.getElementById('notifImagePreview');
preview.src = url;
modal.classList.remove('hidden');
modal.classList.add('flex');
}
function closeNotifImage() {
const modal = document.getElementById('notifImageModal');
const preview = document.getElementById('notifImagePreview');
modal.classList.remove('flex');
modal.classList.add('hidden');
preview.src = '';
}
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;
document.getElementById('notifImages').innerHTML = (d.image_urls || []).map(url => `
<button type="button" onclick="openNotifImage('${url}')" class="block rounded-2xl overflow-hidden border border-slate-100 bg-slate-50 hover:scale-[1.02] transition-all">
<img src="${url}" class="w-full h-48 object-cover" loading="lazy">
</button>
`).join('');
lucide.createIcons();
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 })
});
};
document.getElementById('closeNotifImageBtn').onclick = closeNotifImage;
document.getElementById('notifImageModal').onclick = (e) => {
if (e.target.id === 'notifImageModal') closeNotifImage();
};
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>