Files
zlqy/templates/admin_notifications.html
2026-02-27 10:37:11 +08:00

239 lines
12 KiB
HTML

{% extends "admin_base.html" %}
{% block title %}通知管理 - 智联青云管理后台{% endblock %}
{% block admin_content %}
<div class="space-y-6">
<div class="flex items-center justify-between">
<h1 class="text-2xl font-bold text-slate-900">通知管理</h1>
<div class="flex gap-2">
<button onclick="showCreateModal()" class="px-4 py-2 bg-primary text-white rounded-md text-sm font-medium hover:bg-blue-700">发布通知</button>
<button onclick="showPrivateModal()" class="px-4 py-2 bg-green-600 text-white rounded-md text-sm font-medium hover:bg-green-700">私发通知</button>
</div>
</div>
<div id="ann-list" class="space-y-3">
<div class="text-center py-12 text-slate-400">加载中...</div>
</div>
</div>
<!-- 创建/编辑弹窗 -->
<div id="modal" class="hidden fixed inset-0 bg-black/50 z-[9990] flex items-center justify-center">
<div class="bg-white rounded-lg shadow-xl w-full max-w-lg mx-4 p-6">
<h2 id="modal-title" class="text-lg font-bold text-slate-900 mb-4">发布通知</h2>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">标题</label>
<input type="text" id="ann-title" class="w-full px-3 py-2 border border-slate-300 rounded-md text-sm focus:outline-none focus:ring-primary focus:border-primary" placeholder="通知标题">
</div>
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">内容</label>
<textarea id="ann-content" rows="6" class="w-full px-3 py-2 border border-slate-300 rounded-md text-sm focus:outline-none focus:ring-primary focus:border-primary" placeholder="通知内容"></textarea>
</div>
<div class="flex items-center gap-2">
<input type="checkbox" id="ann-pinned" class="rounded border-slate-300">
<label for="ann-pinned" class="text-sm text-slate-700">置顶</label>
</div>
</div>
<div class="flex justify-end gap-3 mt-6">
<button onclick="closeModal()" class="px-4 py-2 border border-slate-300 rounded-md text-sm text-slate-700 hover:bg-slate-50">取消</button>
<button onclick="saveAnn()" id="save-btn" class="px-4 py-2 bg-primary text-white rounded-md text-sm font-medium hover:bg-blue-700">发布</button>
</div>
</div>
</div>
<!-- 私发通知弹窗 -->
<div id="privateModal" class="hidden fixed inset-0 bg-black/50 z-[9990] flex items-center justify-center">
<div class="bg-white rounded-lg shadow-xl w-full max-w-lg mx-4 p-6">
<h2 class="text-lg font-bold text-slate-900 mb-4">私发通知</h2>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">选择用户</label>
<input type="text" id="private-user-search" class="w-full px-3 py-2 border border-slate-300 rounded-md text-sm focus:outline-none focus:ring-primary focus:border-primary" placeholder="输入用户名搜索..." oninput="searchUsers()">
<div id="user-search-results" class="mt-1 max-h-32 overflow-y-auto border border-slate-200 rounded-md hidden"></div>
<div id="selected-users" class="flex flex-wrap gap-2 mt-2"></div>
</div>
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">通知内容</label>
<textarea id="private-content" rows="4" class="w-full px-3 py-2 border border-slate-300 rounded-md text-sm focus:outline-none focus:ring-primary focus:border-primary" placeholder="输入通知内容"></textarea>
</div>
</div>
<div class="flex justify-end gap-3 mt-6">
<button onclick="closePrivateModal()" class="px-4 py-2 border border-slate-300 rounded-md text-sm text-slate-700 hover:bg-slate-50">取消</button>
<button onclick="sendPrivateNotif()" class="px-4 py-2 bg-green-600 text-white rounded-md text-sm font-medium hover:bg-green-700">发送</button>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
let editingId = null;
function esc(s) { if(!s)return''; const d=document.createElement('div'); d.textContent=s; return d.innerHTML; }
function showCreateModal() {
editingId = null;
document.getElementById('modal-title').textContent = '发布通知';
document.getElementById('save-btn').textContent = '发布';
document.getElementById('ann-title').value = '';
document.getElementById('ann-content').value = '';
document.getElementById('ann-pinned').checked = false;
document.getElementById('modal').classList.remove('hidden');
}
function showEditModal(id, title, content, pinned) {
editingId = id;
document.getElementById('modal-title').textContent = '编辑通知';
document.getElementById('save-btn').textContent = '保存';
document.getElementById('ann-title').value = title;
document.getElementById('ann-content').value = content;
document.getElementById('ann-pinned').checked = pinned;
document.getElementById('modal').classList.remove('hidden');
}
function closeModal() {
document.getElementById('modal').classList.add('hidden');
}
async function saveAnn() {
const title = document.getElementById('ann-title').value.trim();
const content = document.getElementById('ann-content').value.trim();
const pinned = document.getElementById('ann-pinned').checked;
if (!title || !content) { alert('标题和内容不能为空'); return; }
const url = editingId ? `/api/system-notifications/${editingId}` : '/api/system-notifications';
const method = editingId ? 'PUT' : 'POST';
try {
const res = await fetch(url, {
method, headers: {'Content-Type': 'application/json'},
body: JSON.stringify({title, content, pinned})
});
const d = await res.json();
if (d.success) {
closeModal();
loadAnnouncements();
} else {
alert(d.message || '操作失败');
}
} catch(e) { alert('网络错误'); }
}
async function deleteAnn(id) {
if (!confirm('确定删除该通知?')) return;
try {
const res = await fetch(`/api/system-notifications/${id}`, {method:'DELETE'});
const d = await res.json();
if (d.success) loadAnnouncements();
else alert(d.message);
} catch(e) { alert('网络错误'); }
}
async function loadAnnouncements() {
const res = await fetch('/api/system-notifications?per_page=100');
const d = await res.json();
const list = document.getElementById('ann-list');
if (!d.success || d.notifications.length === 0) {
list.innerHTML = '<div class="text-center py-12 text-slate-400">暂无通知,点击右上角发布</div>';
return;
}
list.innerHTML = d.notifications.map(a => `
<div class="bg-white border ${a.pinned ? 'border-amber-300' : 'border-slate-200'} rounded-lg p-4 shadow-sm">
<div class="flex items-start justify-between">
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2 mb-1">
${a.pinned ? '<span class="text-xs bg-amber-100 text-amber-700 px-2 py-0.5 rounded-full font-medium">置顶</span>' : ''}
<h3 class="font-semibold text-slate-900">${esc(a.title)}</h3>
</div>
<p class="text-sm text-slate-600 mt-1 whitespace-pre-wrap">${esc(a.content)}</p>
<div class="text-xs text-slate-400 mt-2">发布者:${esc(a.author_name)} · 创建:${a.created_at}${a.updated_at && a.updated_at !== a.created_at ? ' · 更新:' + a.updated_at : ''}</div>
</div>
<div class="flex gap-2 ml-4 flex-shrink-0">
<button onclick='showEditModal(${a.id}, ${JSON.stringify(a.title)}, ${JSON.stringify(a.content)}, ${a.pinned})' class="px-3 py-1 text-xs border border-slate-300 rounded hover:bg-slate-50 text-slate-600">编辑</button>
<button onclick="deleteAnn(${a.id})" class="px-3 py-1 text-xs border border-red-300 rounded hover:bg-red-50 text-red-600">删除</button>
</div>
</div>
</div>
`).join('');
}
loadAnnouncements();
// ========== 私发通知 ==========
let selectedUserIds = [];
function showPrivateModal() {
selectedUserIds = [];
document.getElementById('private-user-search').value = '';
document.getElementById('private-content').value = '';
document.getElementById('selected-users').innerHTML = '';
document.getElementById('user-search-results').classList.add('hidden');
document.getElementById('privateModal').classList.remove('hidden');
}
function closePrivateModal() {
document.getElementById('privateModal').classList.add('hidden');
}
let searchTimer = null;
function searchUsers() {
clearTimeout(searchTimer);
const q = document.getElementById('private-user-search').value.trim();
const results = document.getElementById('user-search-results');
if (q.length < 1) { results.classList.add('hidden'); return; }
searchTimer = setTimeout(async () => {
try {
const res = await fetch(`/api/admin/search-users?q=${encodeURIComponent(q)}`);
const d = await res.json();
if (!d.success || d.users.length === 0) {
results.innerHTML = '<div class="px-3 py-2 text-sm text-slate-400">未找到用户</div>';
results.classList.remove('hidden');
return;
}
results.innerHTML = d.users.filter(u => !selectedUserIds.includes(u.id)).map(u =>
`<div class="px-3 py-2 text-sm hover:bg-slate-50 cursor-pointer flex items-center justify-between" onclick="selectUser(${u.id}, '${esc(u.name)}')">
<span>${esc(u.name)}</span>
<span class="text-xs text-slate-400">${esc(u.role)}</span>
</div>`
).join('');
results.classList.remove('hidden');
} catch(e) { results.classList.add('hidden'); }
}, 300);
}
function selectUser(id, name) {
if (selectedUserIds.includes(id)) return;
selectedUserIds.push(id);
const container = document.getElementById('selected-users');
container.innerHTML += `<span class="inline-flex items-center gap-1 px-2 py-1 bg-blue-100 text-blue-700 rounded-full text-xs" data-uid="${id}">
${esc(name)}
<button onclick="removeUser(${id}, this.parentElement)" class="hover:text-red-500">&times;</button>
</span>`;
document.getElementById('user-search-results').classList.add('hidden');
document.getElementById('private-user-search').value = '';
}
function removeUser(id, el) {
selectedUserIds = selectedUserIds.filter(uid => uid !== id);
el.remove();
}
async function sendPrivateNotif() {
if (selectedUserIds.length === 0) { alert('请选择至少一个用户'); return; }
const content = document.getElementById('private-content').value.trim();
if (!content) { alert('请输入通知内容'); return; }
try {
const res = await fetch('/api/admin/send-private-notification', {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ user_ids: selectedUserIds, content })
});
const d = await res.json();
if (d.success) {
alert(`已成功发送给 ${selectedUserIds.length} 位用户`);
closePrivateModal();
} else {
alert(d.message || '发送失败');
}
} catch(e) { alert('网络错误'); }
}
</script>
{% endblock %}