```
feat(admin): 系统通知功能增加图片支持 - 在SystemNotification模型中添加image_urls字段用于存储图片URL列表 - 实现通知图片的上传、保存和展示功能 - 前端模板增加图片预览模态框和图片上传组件 - 优化通知管理页面的UI布局,支持多图展示 - 完善图片相关的前后端交互逻辑和错误处理 ```
This commit is contained in:
parent
dfcd4fcc56
commit
3610bedb39
@ -1,5 +1,6 @@
|
|||||||
from flask import Blueprint, request, jsonify, g
|
from flask import Blueprint, request, jsonify, g
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
import json
|
||||||
from extensions import db
|
from extensions import db
|
||||||
from models import User, Role, Permission, SystemDict, SystemNotification, Order, PointsGrant, InviteReward, to_bj_time, get_bj_now
|
from models import User, Role, Permission, SystemDict, SystemNotification, Order, PointsGrant, InviteReward, to_bj_time, get_bj_now
|
||||||
from middlewares.auth import permission_required, login_required
|
from middlewares.auth import permission_required, login_required
|
||||||
@ -234,6 +235,7 @@ def get_notifications():
|
|||||||
"id": n.id,
|
"id": n.id,
|
||||||
"title": n.title,
|
"title": n.title,
|
||||||
"content": n.content,
|
"content": n.content,
|
||||||
|
"image_urls": json.loads(n.image_urls) if n.image_urls else [],
|
||||||
"is_active": n.is_active,
|
"is_active": n.is_active,
|
||||||
"created_at": n.created_at_bj.strftime('%Y-%m-%d %H:%M')
|
"created_at": n.created_at_bj.strftime('%Y-%m-%d %H:%M')
|
||||||
} for n in notifs]
|
} for n in notifs]
|
||||||
@ -244,6 +246,9 @@ def get_notifications():
|
|||||||
def save_notification():
|
def save_notification():
|
||||||
data = request.json
|
data = request.json
|
||||||
notif_id = data.get('id')
|
notif_id = data.get('id')
|
||||||
|
image_urls = data.get('image_urls') or []
|
||||||
|
if not isinstance(image_urls, list):
|
||||||
|
image_urls = []
|
||||||
|
|
||||||
if notif_id:
|
if notif_id:
|
||||||
n = SystemNotification.query.get(notif_id)
|
n = SystemNotification.query.get(notif_id)
|
||||||
@ -256,6 +261,7 @@ def save_notification():
|
|||||||
|
|
||||||
n.title = data['title']
|
n.title = data['title']
|
||||||
n.content = data['content']
|
n.content = data['content']
|
||||||
|
n.image_urls = json.dumps(image_urls, ensure_ascii=False)
|
||||||
n.is_active = data.get('is_active', True)
|
n.is_active = data.get('is_active', True)
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|||||||
24
migrations/versions/b3f4c2d9aa10_add_notification_images.py
Normal file
24
migrations/versions/b3f4c2d9aa10_add_notification_images.py
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
"""add notification images
|
||||||
|
|
||||||
|
Revision ID: b3f4c2d9aa10
|
||||||
|
Revises: 0cc7ce54ecc0
|
||||||
|
Create Date: 2026-04-24 23:20:00.000000
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'b3f4c2d9aa10'
|
||||||
|
down_revision = '0cc7ce54ecc0'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
op.add_column('system_notifications', sa.Column('image_urls', sa.Text(), nullable=True))
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
op.drop_column('system_notifications', 'image_urls')
|
||||||
@ -133,6 +133,7 @@ class SystemNotification(db.Model):
|
|||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
title = db.Column(db.String(200), nullable=False)
|
title = db.Column(db.String(200), nullable=False)
|
||||||
content = db.Column(db.Text, nullable=False)
|
content = db.Column(db.Text, nullable=False)
|
||||||
|
image_urls = db.Column(db.Text) # JSON 字符串,存储公告图片 URL 列表
|
||||||
is_active = db.Column(db.Boolean, default=True)
|
is_active = db.Column(db.Boolean, default=True)
|
||||||
created_at = db.Column(db.DateTime, default=get_bj_now)
|
created_at = db.Column(db.DateTime, default=get_bj_now)
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
from models import SystemDict, SystemNotification, db
|
from models import SystemDict, SystemNotification, db
|
||||||
from flask import jsonify
|
from flask import jsonify
|
||||||
|
import json
|
||||||
|
|
||||||
def get_system_config_data():
|
def get_system_config_data():
|
||||||
"""获取系统配置数据的业务逻辑"""
|
"""获取系统配置数据的业务逻辑"""
|
||||||
@ -40,10 +41,15 @@ def get_user_latest_notification(user_id):
|
|||||||
.order_by(SystemNotification.created_at.desc()).first()
|
.order_by(SystemNotification.created_at.desc()).first()
|
||||||
|
|
||||||
if latest:
|
if latest:
|
||||||
|
try:
|
||||||
|
image_urls = json.loads(latest.image_urls) if latest.image_urls else []
|
||||||
|
except Exception:
|
||||||
|
image_urls = []
|
||||||
return {
|
return {
|
||||||
"id": latest.id,
|
"id": latest.id,
|
||||||
"title": latest.title,
|
"title": latest.title,
|
||||||
"content": latest.content,
|
"content": latest.content,
|
||||||
|
"image_urls": image_urls,
|
||||||
"time": latest.created_at_bj.strftime('%Y-%m-%d %H:%M')
|
"time": latest.created_at_bj.strftime('%Y-%m-%d %H:%M')
|
||||||
}
|
}
|
||||||
return {"id": None}
|
return {"id": None}
|
||||||
|
|||||||
@ -114,6 +114,7 @@
|
|||||||
<div id="notifContent"
|
<div id="notifContent"
|
||||||
class="text-slate-500 text-sm font-bold leading-relaxed whitespace-pre-wrap pt-4">
|
class="text-slate-500 text-sm font-bold leading-relaxed whitespace-pre-wrap pt-4">
|
||||||
</div>
|
</div>
|
||||||
|
<div id="notifImages" class="grid grid-cols-2 gap-4 pt-2"></div>
|
||||||
</div>
|
</div>
|
||||||
<button id="closeNotifBtn"
|
<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">
|
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">
|
||||||
@ -122,6 +123,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</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>
|
<script>
|
||||||
lucide.createIcons();
|
lucide.createIcons();
|
||||||
|
|
||||||
@ -133,6 +144,21 @@
|
|||||||
|
|
||||||
// 全局通知检查
|
// 全局通知检查
|
||||||
let currentNotifId = null;
|
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() {
|
async function checkNotifications() {
|
||||||
try {
|
try {
|
||||||
const r = await fetch('/api/notifications/latest');
|
const r = await fetch('/api/notifications/latest');
|
||||||
@ -142,6 +168,12 @@
|
|||||||
document.getElementById('notifTitle').innerText = d.title;
|
document.getElementById('notifTitle').innerText = d.title;
|
||||||
document.getElementById('notifTime').innerText = d.time || '';
|
document.getElementById('notifTime').innerText = d.time || '';
|
||||||
document.getElementById('notifContent').innerText = d.content;
|
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');
|
const modal = document.getElementById('notifModal');
|
||||||
modal.classList.remove('hidden');
|
modal.classList.remove('hidden');
|
||||||
@ -174,6 +206,11 @@
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
document.getElementById('closeNotifImageBtn').onclick = closeNotifImage;
|
||||||
|
document.getElementById('notifImageModal').onclick = (e) => {
|
||||||
|
if (e.target.id === 'notifImageModal') closeNotifImage();
|
||||||
|
};
|
||||||
|
|
||||||
checkNotifications();
|
checkNotifications();
|
||||||
|
|
||||||
function showToast(message, type = 'info') {
|
function showToast(message, type = 'info') {
|
||||||
@ -191,4 +228,4 @@
|
|||||||
{% block scripts %}{% endblock %}
|
{% block scripts %}{% endblock %}
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -5,7 +5,6 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="w-full h-full overflow-y-auto p-8 lg:p-12 custom-scrollbar">
|
<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="max-w-6xl mx-auto space-y-8">
|
||||||
<!-- 头部 -->
|
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div class="flex items-center gap-4">
|
<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">
|
<div class="w-12 h-12 bg-indigo-600 text-white rounded-2xl flex items-center justify-center shadow-lg">
|
||||||
@ -13,7 +12,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-3xl font-black text-slate-900 tracking-tight">系统通知管理</h1>
|
<h1 class="text-3xl font-black text-slate-900 tracking-tight">系统通知管理</h1>
|
||||||
<p class="text-slate-400 text-sm font-bold">发布全局公告,通知每一位创作者</p>
|
<p class="text-slate-400 text-sm font-bold">发布全局公告,支持正文与图片展示</p>
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<button onclick="openModal()" class="btn-primary px-6 py-3 rounded-xl text-sm font-bold shadow-lg flex items-center gap-2">
|
||||||
@ -22,7 +21,6 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 列表 -->
|
|
||||||
<div class="bg-white rounded-[2.5rem] shadow-xl border border-slate-100 overflow-hidden">
|
<div class="bg-white rounded-[2.5rem] shadow-xl border border-slate-100 overflow-hidden">
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto">
|
||||||
<table class="w-full text-left border-collapse">
|
<table class="w-full text-left border-collapse">
|
||||||
@ -30,22 +28,20 @@
|
|||||||
<tr class="bg-slate-50 border-b border-slate-100 sticky top-0 z-10">
|
<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">图片</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>
|
<th class="px-8 py-5 text-[10px] font-black text-slate-400 uppercase tracking-widest bg-slate-50 text-right">操作</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="notifTableBody" class="text-sm font-medium">
|
<tbody id="notifTableBody" class="text-sm font-medium"></tbody>
|
||||||
<!-- 动态加载 -->
|
|
||||||
</tbody>
|
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</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 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-lg rounded-[2.5rem] shadow-2xl p-10 space-y-8 transform scale-95 transition-transform 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>
|
<h2 id="modalTitle" class="text-2xl font-black text-slate-900">发布通知</h2>
|
||||||
<form id="notifForm" class="space-y-5">
|
<form id="notifForm" class="space-y-5">
|
||||||
<input type="hidden" id="notifId">
|
<input type="hidden" id="notifId">
|
||||||
@ -57,6 +53,17 @@
|
|||||||
<label class="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">详细内容</label>
|
<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>
|
<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>
|
||||||
|
<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">
|
<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">
|
<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>
|
<label for="notifActive" class="text-sm font-bold text-slate-600">立即上线 (激活状态)</label>
|
||||||
@ -72,12 +79,62 @@
|
|||||||
|
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
<script>
|
<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() {
|
async function loadNotifications() {
|
||||||
try {
|
try {
|
||||||
const r = await fetch('/api/admin/notifications');
|
const r = await fetch('/api/admin/notifications');
|
||||||
const d = await r.json();
|
const d = await r.json();
|
||||||
const body = document.getElementById('notifTableBody');
|
const body = document.getElementById('notifTableBody');
|
||||||
|
|
||||||
body.innerHTML = d.notifications.map(n => `
|
body.innerHTML = d.notifications.map(n => `
|
||||||
<tr class="border-b border-slate-50 hover:bg-slate-50/50 transition-colors">
|
<tr class="border-b border-slate-50 hover:bg-slate-50/50 transition-colors">
|
||||||
<td class="px-8 py-5">
|
<td class="px-8 py-5">
|
||||||
@ -86,9 +143,10 @@
|
|||||||
}">${n.is_active ? '激活中' : '已下线'}</span>
|
}">${n.is_active ? '激活中' : '已下线'}</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-8 py-5 text-slate-700 font-bold">${n.title}</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-slate-400 font-mono text-xs">${n.created_at}</td>
|
||||||
<td class="px-8 py-5 text-right space-x-2">
|
<td class="px-8 py-5 text-right space-x-2">
|
||||||
<button onclick='editNotif(${JSON.stringify(n)})' class="p-2 text-indigo-600 hover:bg-indigo-50 rounded-lg transition-colors">
|
<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>
|
<i data-lucide="edit-3" class="w-4 h-4"></i>
|
||||||
</button>
|
</button>
|
||||||
<button onclick="deleteNotif(${n.id})" class="p-2 text-rose-500 hover:bg-rose-50 rounded-lg transition-colors">
|
<button onclick="deleteNotif(${n.id})" class="p-2 text-rose-500 hover:bg-rose-50 rounded-lg transition-colors">
|
||||||
@ -98,23 +156,30 @@
|
|||||||
</tr>
|
</tr>
|
||||||
`).join('');
|
`).join('');
|
||||||
lucide.createIcons();
|
lucide.createIcons();
|
||||||
} catch (e) { console.error(e); }
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function openModal() {
|
function openModal() {
|
||||||
document.getElementById('modalTitle').innerText = '发布通知';
|
document.getElementById('modalTitle').innerText = '发布通知';
|
||||||
document.getElementById('notifForm').reset();
|
document.getElementById('notifForm').reset();
|
||||||
document.getElementById('notifId').value = '';
|
document.getElementById('notifId').value = '';
|
||||||
const m = document.getElementById('notifModal');
|
notifImageUrls = [];
|
||||||
m.classList.remove('hidden');
|
renderNotifImages();
|
||||||
setTimeout(() => { m.classList.add('opacity-100'); m.querySelector('div').classList.remove('scale-95'); }, 10);
|
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() {
|
function closeModal() {
|
||||||
const m = document.getElementById('notifModal');
|
const modal = document.getElementById('notifModal');
|
||||||
m.classList.remove('opacity-100');
|
modal.classList.remove('opacity-100');
|
||||||
m.querySelector('div').classList.add('scale-95');
|
modal.querySelector('div').classList.add('scale-95');
|
||||||
setTimeout(() => m.classList.add('hidden'), 300);
|
setTimeout(() => modal.classList.add('hidden'), 300);
|
||||||
}
|
}
|
||||||
|
|
||||||
function editNotif(n) {
|
function editNotif(n) {
|
||||||
@ -124,16 +189,24 @@
|
|||||||
document.getElementById('notifTitle').value = n.title;
|
document.getElementById('notifTitle').value = n.title;
|
||||||
document.getElementById('notifContent').value = n.content;
|
document.getElementById('notifContent').value = n.content;
|
||||||
document.getElementById('notifActive').checked = n.is_active;
|
document.getElementById('notifActive').checked = n.is_active;
|
||||||
|
notifImageUrls = n.image_urls || [];
|
||||||
|
renderNotifImages();
|
||||||
|
}
|
||||||
|
|
||||||
|
function editNotifFromPayload(payload) {
|
||||||
|
editNotif(JSON.parse(decodeURIComponent(payload)));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteNotif(id) {
|
async function deleteNotif(id) {
|
||||||
if(!confirm('确定要删除这条通知吗?用户将无法再看到它。')) return;
|
if (!confirm('确定要删除这条通知吗?用户将无法再看到它。')) return;
|
||||||
const r = await fetch('/api/admin/notifications/delete', {
|
const r = await fetch('/api/admin/notifications/delete', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {'Content-Type': 'application/json'},
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({id})
|
body: JSON.stringify({ id })
|
||||||
});
|
});
|
||||||
if(r.ok) loadNotifications();
|
if (r.ok) {
|
||||||
|
loadNotifications();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('notifForm').onsubmit = async (e) => {
|
document.getElementById('notifForm').onsubmit = async (e) => {
|
||||||
@ -142,21 +215,32 @@
|
|||||||
id: document.getElementById('notifId').value,
|
id: document.getElementById('notifId').value,
|
||||||
title: document.getElementById('notifTitle').value,
|
title: document.getElementById('notifTitle').value,
|
||||||
content: document.getElementById('notifContent').value,
|
content: document.getElementById('notifContent').value,
|
||||||
|
image_urls: notifImageUrls,
|
||||||
is_active: document.getElementById('notifActive').checked
|
is_active: document.getElementById('notifActive').checked
|
||||||
};
|
};
|
||||||
|
|
||||||
const r = await fetch('/api/admin/notifications', {
|
const r = await fetch('/api/admin/notifications', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {'Content-Type': 'application/json'},
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(data)
|
body: JSON.stringify(data)
|
||||||
});
|
});
|
||||||
if(r.ok) {
|
if (r.ok) {
|
||||||
closeModal();
|
closeModal();
|
||||||
loadNotifications();
|
loadNotifications();
|
||||||
showToast('通知发布成功', 'success');
|
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();
|
loadNotifications();
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user