first commit
This commit is contained in:
239
templates/admin_notifications.html
Normal file
239
templates/admin_notifications.html
Normal file
@@ -0,0 +1,239 @@
|
||||
{% 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">×</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 %}
|
||||
Reference in New Issue
Block a user