🐛 fix: announcement manager

This commit is contained in:
EUForest 2026-01-06 23:09:48 +08:00
parent 22b24041a7
commit 93926d9c99
4 changed files with 151 additions and 29 deletions

View File

@ -11,6 +11,12 @@
"finished": "Finished", "finished": "Finished",
"import": "Import", "import": "Import",
"latestAnnouncement": "Latest Announcement", "latestAnnouncement": "Latest Announcement",
"announcements": "Announcements",
"all": "All",
"pinned": "Pinned",
"pinnedOnly": "Pinned Only",
"popup": "Popup",
"popupOnly": "Popup Only",
"manualImportMessage": "Please import manually", "manualImportMessage": "Please import manually",
"mySubscriptions": "My Subscriptions", "mySubscriptions": "My Subscriptions",
"nextResetDays": "Next Reset Days", "nextResetDays": "Next Reset Days",

View File

@ -11,6 +11,12 @@
"finished": "已完成", "finished": "已完成",
"import": "一键导入", "import": "一键导入",
"latestAnnouncement": "最新公告", "latestAnnouncement": "最新公告",
"announcements": "公告管理",
"all": "全部",
"pinned": "置顶",
"pinnedOnly": "仅置顶",
"popup": "弹窗",
"popupOnly": "仅弹窗",
"manualImportMessage": "请手动导入", "manualImportMessage": "请手动导入",
"mySubscriptions": "我的订阅", "mySubscriptions": "我的订阅",
"nextResetDays": "距离下次重置天数", "nextResetDays": "距离下次重置天数",

View File

@ -32,7 +32,7 @@ export default function Announcement({ type }: { type: "popup" | "pinned" }) {
skipErrorHandler: true, skipErrorHandler: true,
} }
); );
return result.data.data?.announcements.find((item) => item[type]) || null; return result.data.data?.announcements?.[0] || null;
}, },
enabled: !!user, enabled: !!user,
}); });
@ -44,13 +44,16 @@ export default function Announcement({ type }: { type: "popup" | "pinned" }) {
<Dialog defaultOpen={!!data}> <Dialog defaultOpen={!!data}>
<DialogContent className="sm:max-w-[425px]"> <DialogContent className="sm:max-w-[425px]">
<DialogHeader> <DialogHeader>
<DialogTitle>{data?.title}</DialogTitle> <DialogTitle>{data.title}</DialogTitle>
</DialogHeader> </DialogHeader>
<Markdown>{data?.content}</Markdown> <div className="prose prose-sm max-w-none dark:prose-invert">
<Markdown>{data.content}</Markdown>
</div>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
); );
} }
if (type === "pinned") { if (type === "pinned") {
return ( return (
<> <>
@ -59,9 +62,17 @@ export default function Announcement({ type }: { type: "popup" | "pinned" }) {
{t("latestAnnouncement", "Latest Announcement")} {t("latestAnnouncement", "Latest Announcement")}
</h2> </h2>
<Card className="p-6"> <Card className="p-6">
{data?.content ? <Markdown>{data?.content}</Markdown> : <Empty />} {data.content ? (
<div className="prose prose-sm max-w-none dark:prose-invert">
<Markdown>{data.content}</Markdown>
</div>
) : (
<Empty />
)}
</Card> </Card>
</> </>
); );
} }
return null;
} }

View File

@ -1,34 +1,133 @@
"use client"; "use client";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { Timeline } from "@workspace/ui/components/timeline"; import {
import Empty from "@workspace/ui/composed/empty"; Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@workspace/ui/components/card";
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "@workspace/ui/components/tabs";
import {
ProList,
type ProListActions,
} from "@workspace/ui/composed/pro-list/pro-list";
import { Icon } from "@workspace/ui/composed/icon";
import { Markdown } from "@workspace/ui/composed/markdown"; import { Markdown } from "@workspace/ui/composed/markdown";
import { queryAnnouncement } from "@workspace/ui/services/user/announcement"; import { queryAnnouncement } from "@workspace/ui/services/user/announcement";
import { formatDate } from "@workspace/ui/utils/formatting";
import { useRef, useState } from "react";
import { useTranslation } from "react-i18next";
export default function Announcement() { export default function Announcement() {
const { data } = useQuery({ const { t } = useTranslation("dashboard");
queryKey: ["queryAnnouncement"], const [activeTab, setActiveTab] = useState("all");
queryFn: async () => { const pinnedRef = useRef<ProListActions>(null);
const { data } = await queryAnnouncement({ const normalRef = useRef<ProListActions>(null);
page: 1,
size: 99, const requestAnnouncements = async (
pinned: false, pagination: { page: number; size: number },
popup: false, filter: { pinned?: boolean; popup?: boolean }
}); ) => {
return data.data?.announcements || []; const response = await queryAnnouncement({
}, ...pagination,
}); ...filter,
return data && data.length > 0 ? ( });
<Timeline return {
data={ list: response.data.data?.announcements || [],
data.map((item) => ({ total: response.data.data?.total || 0,
title: item.title, };
content: <Markdown>{item.content}</Markdown>, };
})) || []
} const renderAnnouncement = (item: any) => (
/> <Card className="overflow-hidden">
) : ( <CardHeader>
<Empty border /> <div className="flex items-start justify-between gap-4">
<div className="flex items-center gap-2">
<CardTitle className="text-lg">{item.title}</CardTitle>
{item.pinned && (
<span className="flex items-center gap-1 rounded-full bg-primary/10 px-2 py-0.5 text-xs font-medium text-primary">
<Icon className="size-3" icon="uil:pin" />
{t("pinned", "Pinned")}
</span>
)}
{item.popup && (
<span className="flex items-center gap-1 rounded-full bg-orange-500/10 px-2 py-0.5 text-xs font-medium text-orange-500">
<Icon className="size-3" icon="uil:popcorn" />
{t("popup", "Popup")}
</span>
)}
</div>
<CardDescription className="text-sm">
<time dateTime={item.created_at}>
{formatDate(item.created_at)}
</time>
</CardDescription>
</div>
</CardHeader>
<CardContent>
<div className="prose prose-sm max-w-none dark:prose-invert">
<Markdown>{item.content}</Markdown>
</div>
</CardContent>
</Card>
);
return (
<div className="space-y-4">
<h2 className="flex items-center gap-1.5 font-semibold">
<Icon className="size-5" icon="uil:bell" />
{t("announcements", "Announcements")}
</h2>
<Tabs defaultValue="all" onValueChange={setActiveTab}>
<TabsList>
<TabsTrigger value="all">
{t("all", "All")}
</TabsTrigger>
<TabsTrigger value="pinned">
{t("pinnedOnly", "Pinned Only")}
</TabsTrigger>
<TabsTrigger value="popup">
{t("popupOnly", "Popup Only")}
</TabsTrigger>
</TabsList>
<TabsContent value="all">
<ProList
action={normalRef}
renderItem={renderAnnouncement}
request={async (pagination) => {
return requestAnnouncements(pagination, {});
}}
/>
</TabsContent>
<TabsContent value="pinned">
<ProList
action={pinnedRef}
renderItem={renderAnnouncement}
request={async (pagination) => {
return requestAnnouncements(pagination, { pinned: true });
}}
/>
</TabsContent>
<TabsContent value="popup">
<ProList
action={normalRef}
renderItem={renderAnnouncement}
request={async (pagination) => {
return requestAnnouncements(pagination, { popup: true });
}}
/>
</TabsContent>
</Tabs>
</div>
); );
} }