-
{announcement.title}
-
{announcement.content}
+
+ {announcement.imageUrl ?
})
: null}
+
+
{announcement.title}
+
{announcement.content}
+
),
},
@@ -181,7 +259,7 @@ export default function AdminAnnouncementsPanel() {
footer={
{ setPage(nextPageSize !== pageSize ? 1 : nextPage); setPageSize(nextPageSize); }} />}
/>
- void publish()} onCancel={() => { setModalOpen(false); setEditingAnnouncement(null); }} destroyOnHidden>
+ void publish()} onCancel={() => void closeEditor()} destroyOnHidden>
@@ -192,6 +270,21 @@ export default function AdminAnnouncementsPanel() {
+
+
+
+ {imagePreviewUrl ? (
+
+

+
} className="!absolute right-1 top-1 !size-7 !min-w-7 !p-0" onClick={() => void clearAnnouncementImage()} aria-label="移除公告配图" title="移除公告配图" />
+
+ ) :
暂未添加配图
}
+
void uploadAnnouncementImage(event)} />
+
} loading={imageUploading} onClick={() => imageInputRef.current?.click()}>
+ {imagePreviewUrl ? "更换配图" : "上传配图"}
+
+
+
diff --git a/web/src/services/api/announcements.ts b/web/src/services/api/announcements.ts
index 8ed9905a6..360b8f91c 100644
--- a/web/src/services/api/announcements.ts
+++ b/web/src/services/api/announcements.ts
@@ -1,4 +1,5 @@
-import { apiClient, request } from "@/services/api/request";
+import { apiBaseURL, apiClient, request } from "@/services/api/request";
+import type { RemoteResource } from "@/services/api/resources";
export type AnnouncementLevel = "info" | "success" | "warning" | "critical";
export type AnnouncementStatus = "active" | "closed";
@@ -7,6 +8,8 @@ export type SystemAnnouncement = {
id: string;
title: string;
content: string;
+ imageResourceId?: string;
+ imageUrl?: string;
level: AnnouncementLevel;
pinned: boolean;
status: AnnouncementStatus;
@@ -35,6 +38,13 @@ export function getAnnouncementFeed() {
return request(api.get("/announcements"));
}
+export function announcementImageUrl(announcement: Pick) {
+ const imageUrl = announcement.imageUrl;
+ if (!imageUrl || !imageUrl.startsWith("/api/")) return imageUrl || "";
+ const base = String(apiBaseURL).replace(/\/+$/, "");
+ return base === "/api" ? imageUrl : `${base}${imageUrl.slice("/api".length)}`;
+}
+
export function markAnnouncementsRead(announcementIds: string[]) {
return request<{ unreadCount: number }>(api.post("/announcements/read", { announcementIds }));
}
@@ -43,11 +53,21 @@ export function listAdminAnnouncements(params: AdminAnnouncementListParams = {})
return request<{ announcements: SystemAnnouncement[]; total: number; page: number; limit: number }>(api.get("/admin/announcements", { params }));
}
-export function createAdminAnnouncement(input: { title: string; content: string; level: AnnouncementLevel; pinned: boolean }) {
+export function uploadAdminAnnouncementImage(file: File) {
+ const formData = new FormData();
+ formData.append("file", file, file.name);
+ return request<{ resource: RemoteResource }>(api.post("/admin/announcement-images", formData));
+}
+
+export function discardAdminAnnouncementImage(id: string) {
+ return request<{ ok: boolean }>(api.delete(`/admin/announcement-images/${encodeURIComponent(id)}`));
+}
+
+export function createAdminAnnouncement(input: { title: string; content: string; imageResourceId?: string; level: AnnouncementLevel; pinned: boolean }) {
return request<{ announcement: SystemAnnouncement }>(api.post("/admin/announcements", input));
}
-export function updateAdminAnnouncement(id: string, input: { title: string; content: string; level: AnnouncementLevel; pinned: boolean }) {
+export function updateAdminAnnouncement(id: string, input: { title: string; content: string; imageResourceId?: string; level: AnnouncementLevel; pinned: boolean }) {
return request<{ announcement: SystemAnnouncement }>(api.patch(`/admin/announcements/${encodeURIComponent(id)}`, input));
}