feat: support announcement image upload

# Conflicts:
#	database.py
#	db/migrations.py
#	routes/admin_api/core.py
#	static/admin/.vite/manifest.json
#	static/admin/assets/AnnouncementsPage-Btl9JP7M.js
#	static/admin/assets/EmailPage-CwqlBGU2.js
#	static/admin/assets/FeedbacksPage-B_qDNL3q.js
#	static/admin/assets/LogsPage-DzdymdrQ.js
#	static/admin/assets/ReportPage-Bp26gOA-.js
#	static/admin/assets/SettingsPage-__r25pN8.js
#	static/admin/assets/SystemPage-C1OfxrU-.js
#	static/admin/assets/UsersPage-DhnABKcY.js
#	static/admin/assets/email-By53DCWv.js
#	static/admin/assets/email-ByiJ74rd.js
#	static/admin/assets/email-DkWacopQ.js
#	static/admin/assets/index-D5wU2pVd.js
#	static/admin/assets/tasks-1acmkoIX.js
#	static/admin/assets/update-DdQLVpC3.js
#	static/admin/assets/users-B1w166uc.js
#	static/admin/assets/users-CPJP5r-B.js
#	static/admin/assets/users-CnIyvFWm.js
#	static/admin/index.html
#	static/app/.vite/manifest.json
#	static/app/assets/AccountsPage-C48gJL8c.js
#	static/app/assets/AccountsPage-D387XNsv.js
#	static/app/assets/AccountsPage-DBJCAsJz.js
#	static/app/assets/LoginPage-BgK_Vl6X.js
#	static/app/assets/RegisterPage-CwADxWfe.js
#	static/app/assets/ResetPasswordPage-CVfZX_5z.js
#	static/app/assets/SchedulesPage-CWuZpJ5h.js
#	static/app/assets/SchedulesPage-Dw-mXbG5.js
#	static/app/assets/SchedulesPage-DwzGOBuc.js
#	static/app/assets/ScreenshotsPage-C6vX2U3V.js
#	static/app/assets/ScreenshotsPage-CreOSjVc.js
#	static/app/assets/ScreenshotsPage-DuTeRzLR.js
#	static/app/assets/VerifyResultPage-BzGlCgtE.js
#	static/app/assets/VerifyResultPage-CN_nr4V6.js
#	static/app/assets/VerifyResultPage-CNbQc83z.js
#	static/app/assets/accounts-BFaVMUve.js
#	static/app/assets/accounts-BYq3lLev.js
#	static/app/assets/accounts-Bc9j2moH.js
#	static/app/assets/auth-Dk_ApO4B.js
#	static/app/assets/index-BIng7uZJ.css
#	static/app/assets/index-CDxVo_1Z.js
#	static/app/index.html
This commit is contained in:
2026-01-06 12:15:16 +08:00
parent 82acc3470f
commit 4c492122dd
48 changed files with 450 additions and 121 deletions

View File

@@ -1233,6 +1233,18 @@
<label>公告内容</label>
<textarea id="announcementContent" rows="5" placeholder="请输入公告内容(将以弹窗形式展示)"></textarea>
</div>
<div class="form-group">
<label>公告图片(可选)</label>
<div style="display: flex; align-items: center; gap: 10px; flex-wrap: wrap;">
<button class="btn btn-secondary" onclick="triggerAnnouncementImageUpload()">+ 上传图片</button>
<button class="btn" onclick="clearAnnouncementImage()" style="background: #eee;">移除</button>
<input type="file" id="announcementImageFile" accept="image/*" style="display: none;" onchange="uploadAnnouncementImageFile()">
<input type="text" id="announcementImageUrl" placeholder="上传后自动填充" readonly style="flex: 1; min-width: 220px;">
</div>
<div id="announcementImagePreview" style="display: none; margin-top: 8px;">
<img id="announcementImagePreviewImg" src="" alt="公告图片预览" style="max-width: 260px; max-height: 160px; border-radius: 8px; border: 1px solid #e5e7eb; object-fit: contain;">
</div>
</div>
<div style="display: flex; gap: 10px; flex-wrap: wrap;">
<button class="btn btn-primary" onclick="createAnnouncement(true)">发布并启用</button>
<button class="btn btn-secondary" onclick="createAnnouncement(false)">保存但不启用</button>
@@ -1632,6 +1644,7 @@
<th style="width: 70px;">ID</th>
<th>标题</th>
<th style="width: 90px;">状态</th>
<th style="width: 70px;">图片</th>
<th style="width: 170px;">创建时间</th>
<th style="width: 220px;">操作</th>
</tr>
@@ -1646,6 +1659,7 @@
${a.is_active ? '启用' : '停用'}
</span>
</td>
<td>${a.image_url ? '有图' : '-'}</td>
<td>${a.created_at || '-'}</td>
<td>
<div class="action-buttons">
@@ -1670,17 +1684,82 @@
const content = document.getElementById('announcementContent');
if (title) title.value = '';
if (content) content.value = '';
clearAnnouncementImage();
}
function triggerAnnouncementImageUpload() {
const input = document.getElementById('announcementImageFile');
if (input) input.click();
}
async function uploadAnnouncementImageFile() {
const input = document.getElementById('announcementImageFile');
const urlInput = document.getElementById('announcementImageUrl');
const file = input?.files?.[0];
if (!file || !urlInput) return;
if (file.type && !String(file.type).startsWith('image/')) {
showNotification('请选择图片文件', 'error');
input.value = '';
return;
}
const formData = new FormData();
formData.append('file', file);
try {
const response = await fetch('/yuyx/api/announcements/upload_image', {
method: 'POST',
body: formData
});
const data = await response.json();
if (!response.ok || !data?.success) {
showNotification(data?.error || '上传失败', 'error');
return;
}
urlInput.value = data.url || '';
updateAnnouncementImagePreview();
showNotification('上传成功', 'success');
} catch (e) {
showNotification('上传失败', 'error');
} finally {
input.value = '';
}
}
function clearAnnouncementImage() {
const imageUrl = document.getElementById('announcementImageUrl');
const imageFile = document.getElementById('announcementImageFile');
if (imageUrl) imageUrl.value = '';
if (imageFile) imageFile.value = '';
updateAnnouncementImagePreview();
}
function updateAnnouncementImagePreview() {
const imageUrl = document.getElementById('announcementImageUrl');
const previewWrap = document.getElementById('announcementImagePreview');
const previewImg = document.getElementById('announcementImagePreviewImg');
if (!imageUrl || !previewWrap || !previewImg) return;
const url = String(imageUrl.value || '').trim();
if (url) {
previewImg.src = url;
previewWrap.style.display = 'block';
} else {
previewImg.removeAttribute('src');
previewWrap.style.display = 'none';
}
}
function viewAnnouncement(id) {
const announcement = announcements.find(a => a.id === id);
if (!announcement) return;
alert(`标题:${announcement.title || ''}\n\n内容:\n${announcement.content || ''}`);
const imageLine = announcement.image_url ? `\n图片:${announcement.image_url}` : '';
alert(`标题:${announcement.title || ''}${imageLine}\n\n内容:\n${announcement.content || ''}`);
}
async function createAnnouncement(isActive) {
const title = (document.getElementById('announcementTitle')?.value || '').trim();
const content = (document.getElementById('announcementContent')?.value || '').trim();
const image_url = (document.getElementById('announcementImageUrl')?.value || '').trim();
if (!title || !content) {
showNotification('标题和内容不能为空', 'error');
return;
@@ -1690,7 +1769,7 @@
const response = await fetch('/yuyx/api/announcements', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title, content, is_active: !!isActive })
body: JSON.stringify({ title, content, image_url, is_active: !!isActive })
});
const data = await response.json();
if (!response.ok) {