ai_v/templates/notifications.html

247 lines
12 KiB
HTML
Raw Normal View History

{% extends "base.html" %}
{% block title %}通知管理 - AI 视界{% endblock %}
{% block content %}
<div class="w-full h-full overflow-y-auto p-8 lg:p-12 custom-scrollbar">
<div class="max-w-6xl mx-auto space-y-8">
<div class="flex items-center justify-between">
<div class="flex items-center gap-4">
<div class="w-12 h-12 bg-indigo-600 text-white rounded-2xl flex items-center justify-center shadow-lg">
<i data-lucide="megaphone" class="w-7 h-7"></i>
</div>
<div>
<h1 class="text-3xl font-black text-slate-900 tracking-tight">系统通知管理</h1>
<p class="text-slate-400 text-sm font-bold">发布全局公告,支持正文与图片展示</p>
</div>
</div>
<button onclick="openModal()" class="btn-primary px-6 py-3 rounded-xl text-sm font-bold shadow-lg flex items-center gap-2">
<i data-lucide="plus" class="w-4 h-4"></i>
发布新通知
</button>
</div>
<div class="bg-white rounded-[2.5rem] shadow-xl border border-slate-100 overflow-hidden">
<div class="overflow-x-auto">
<table class="w-full text-left border-collapse">
<thead>
<tr class="bg-slate-50 border-b border-slate-100 sticky top-0 z-10">
<th class="px-8 py-5 text-[10px] font-black text-slate-400 uppercase tracking-widest bg-slate-50">状态</th>
<th class="px-8 py-5 text-[10px] font-black text-slate-400 uppercase tracking-widest bg-slate-50">标题</th>
<th class="px-8 py-5 text-[10px] font-black text-slate-400 uppercase tracking-widest bg-slate-50">图片</th>
<th class="px-8 py-5 text-[10px] font-black text-slate-400 uppercase tracking-widest bg-slate-50">发布时间</th>
<th class="px-8 py-5 text-[10px] font-black text-slate-400 uppercase tracking-widest bg-slate-50 text-right">操作</th>
</tr>
</thead>
<tbody id="notifTableBody" class="text-sm font-medium"></tbody>
</table>
</div>
</div>
</div>
</div>
<div id="notifModal" class="fixed inset-0 bg-slate-900/40 backdrop-blur-sm z-50 flex items-center justify-center hidden opacity-0 transition-opacity duration-300">
<div class="bg-white w-full max-w-2xl rounded-[2.5rem] shadow-2xl p-10 space-y-8 transform scale-95 transition-transform duration-300 max-h-[90vh] overflow-y-auto">
<h2 id="modalTitle" class="text-2xl font-black text-slate-900">发布通知</h2>
<form id="notifForm" class="space-y-5">
<input type="hidden" id="notifId">
<div class="space-y-2">
<label class="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">通知标题</label>
<input type="text" id="notifTitle" required placeholder="例如:系统维护公告" class="w-full bg-slate-50 border border-slate-100 rounded-2xl p-4 outline-none focus:border-indigo-500 transition-all text-sm font-bold">
</div>
<div class="space-y-2">
<label class="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">详细内容</label>
<textarea id="notifContent" required rows="6" placeholder="支持换行展示..." class="w-full bg-slate-50 border border-slate-100 rounded-2xl p-4 outline-none focus:border-indigo-500 transition-all text-sm font-bold resize-none"></textarea>
</div>
<div class="space-y-3">
<div class="flex items-center justify-between">
<label class="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">公告图片</label>
<label class="px-3 py-2 rounded-xl bg-slate-900 text-white text-xs font-bold cursor-pointer hover:bg-slate-800 transition-all">
上传图片
<input id="notifImagesInput" type="file" accept="image/*" multiple class="hidden">
</label>
</div>
<div id="notifImagesPreview" class="grid grid-cols-4 gap-3"></div>
<p class="text-[11px] text-slate-400 font-bold">支持多图展示,图片将上传到 MinIO 后随公告保存。</p>
</div>
<div class="flex items-center gap-2 pt-2">
<input type="checkbox" id="notifActive" checked class="w-4 h-4 rounded border-slate-200 text-indigo-600 focus:ring-indigo-500">
<label for="notifActive" class="text-sm font-bold text-slate-600">立即上线 (激活状态)</label>
</div>
<div class="flex gap-4 pt-4">
<button type="button" onclick="closeModal()" class="flex-1 px-8 py-4 rounded-2xl border border-slate-100 font-bold text-slate-400 hover:bg-slate-50 transition-all">取消</button>
<button type="submit" class="flex-1 btn-primary px-8 py-4 rounded-2xl font-bold shadow-lg shadow-indigo-100">确认发布</button>
</div>
</form>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
let notifImageUrls = [];
function renderNotifImages() {
const preview = document.getElementById('notifImagesPreview');
if (!preview) return;
if (notifImageUrls.length === 0) {
preview.innerHTML = `
<div class="col-span-4 text-xs text-slate-300 font-bold py-4 text-center border border-dashed border-slate-100 rounded-2xl">
暂无公告图片
</div>
`;
return;
}
preview.innerHTML = notifImageUrls.map((url, index) => `
<div class="relative group aspect-square rounded-2xl overflow-hidden border border-slate-100 bg-slate-50">
<img src="${url}" class="w-full h-full object-cover" loading="lazy">
<button type="button" onclick="removeNotifImage(${index})" class="absolute top-2 right-2 w-7 h-7 rounded-full bg-rose-500 text-white flex items-center justify-center opacity-0 group-hover:opacity-100 transition-all">
<i data-lucide="x" class="w-4 h-4"></i>
</button>
</div>
`).join('');
lucide.createIcons();
}
function removeNotifImage(index) {
notifImageUrls.splice(index, 1);
renderNotifImages();
}
async function uploadNotifImages(files) {
if (!files || files.length === 0) return;
const formData = new FormData();
Array.from(files).forEach(file => formData.append('images', file));
const r = await fetch('/api/upload', {
method: 'POST',
body: formData
});
const d = await r.json();
if (!r.ok || d.error) {
throw new Error(d.error || '图片上传失败');
}
notifImageUrls = notifImageUrls.concat(d.urls || []);
renderNotifImages();
}
async function loadNotifications() {
try {
const r = await fetch('/api/admin/notifications');
const d = await r.json();
const body = document.getElementById('notifTableBody');
body.innerHTML = d.notifications.map(n => `
<tr class="border-b border-slate-50 hover:bg-slate-50/50 transition-colors">
<td class="px-8 py-5">
<span class="px-2 py-0.5 rounded text-[10px] font-black uppercase ${
n.is_active ? 'bg-emerald-50 text-emerald-600' : 'bg-slate-100 text-slate-400'
}">${n.is_active ? '激活中' : '已下线'}</span>
</td>
<td class="px-8 py-5 text-slate-700 font-bold">${n.title}</td>
<td class="px-8 py-5 text-slate-400 font-mono text-xs">${(n.image_urls || []).length} 张</td>
<td class="px-8 py-5 text-slate-400 font-mono text-xs">${n.created_at}</td>
<td class="px-8 py-5 text-right space-x-2">
<button onclick="editNotifFromPayload('${encodeURIComponent(JSON.stringify(n))}')" class="p-2 text-indigo-600 hover:bg-indigo-50 rounded-lg transition-colors">
<i data-lucide="edit-3" class="w-4 h-4"></i>
</button>
<button onclick="deleteNotif(${n.id})" class="p-2 text-rose-500 hover:bg-rose-50 rounded-lg transition-colors">
<i data-lucide="trash-2" class="w-4 h-4"></i>
</button>
</td>
</tr>
`).join('');
lucide.createIcons();
} catch (e) {
console.error(e);
}
}
function openModal() {
document.getElementById('modalTitle').innerText = '发布通知';
document.getElementById('notifForm').reset();
document.getElementById('notifId').value = '';
notifImageUrls = [];
renderNotifImages();
const modal = document.getElementById('notifModal');
modal.classList.remove('hidden');
setTimeout(() => {
modal.classList.add('opacity-100');
modal.querySelector('div').classList.remove('scale-95');
}, 10);
}
function closeModal() {
const modal = document.getElementById('notifModal');
modal.classList.remove('opacity-100');
modal.querySelector('div').classList.add('scale-95');
setTimeout(() => modal.classList.add('hidden'), 300);
}
function editNotif(n) {
openModal();
document.getElementById('modalTitle').innerText = '编辑通知';
document.getElementById('notifId').value = n.id;
document.getElementById('notifTitle').value = n.title;
document.getElementById('notifContent').value = n.content;
document.getElementById('notifActive').checked = n.is_active;
notifImageUrls = n.image_urls || [];
renderNotifImages();
}
function editNotifFromPayload(payload) {
editNotif(JSON.parse(decodeURIComponent(payload)));
}
async function deleteNotif(id) {
if (!confirm('确定要删除这条通知吗?用户将无法再看到它。')) return;
const r = await fetch('/api/admin/notifications/delete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id })
});
if (r.ok) {
loadNotifications();
}
}
document.getElementById('notifForm').onsubmit = async (e) => {
e.preventDefault();
const data = {
id: document.getElementById('notifId').value,
title: document.getElementById('notifTitle').value,
content: document.getElementById('notifContent').value,
image_urls: notifImageUrls,
is_active: document.getElementById('notifActive').checked
};
const r = await fetch('/api/admin/notifications', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (r.ok) {
closeModal();
loadNotifications();
showToast('通知发布成功', 'success');
}
};
document.getElementById('notifImagesInput').addEventListener('change', async (e) => {
try {
await uploadNotifImages(e.target.files);
e.target.value = '';
} catch (err) {
showToast(err.message || '图片上传失败', 'error');
}
});
renderNotifImages();
loadNotifications();
</script>
{% endblock %}