🐛 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",
"import": "Import",
"latestAnnouncement": "Latest Announcement",
"announcements": "Announcements",
"all": "All",
"pinned": "Pinned",
"pinnedOnly": "Pinned Only",
"popup": "Popup",
"popupOnly": "Popup Only",
"manualImportMessage": "Please import manually",
"mySubscriptions": "My Subscriptions",
"nextResetDays": "Next Reset Days",

View File

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

View File

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

View File

@ -1,34 +1,133 @@
"use client";
import { useQuery } from "@tanstack/react-query";
import { Timeline } from "@workspace/ui/components/timeline";
import Empty from "@workspace/ui/composed/empty";
import {
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 { 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() {
const { data } = useQuery({
queryKey: ["queryAnnouncement"],
queryFn: async () => {
const { data } = await queryAnnouncement({
page: 1,
size: 99,
pinned: false,
popup: false,
});
return data.data?.announcements || [];
},
});
return data && data.length > 0 ? (
<Timeline
data={
data.map((item) => ({
title: item.title,
content: <Markdown>{item.content}</Markdown>,
})) || []
}
/>
) : (
<Empty border />
const { t } = useTranslation("dashboard");
const [activeTab, setActiveTab] = useState("all");
const pinnedRef = useRef<ProListActions>(null);
const normalRef = useRef<ProListActions>(null);
const requestAnnouncements = async (
pagination: { page: number; size: number },
filter: { pinned?: boolean; popup?: boolean }
) => {
const response = await queryAnnouncement({
...pagination,
...filter,
});
return {
list: response.data.data?.announcements || [],
total: response.data.data?.total || 0,
};
};
const renderAnnouncement = (item: any) => (
<Card className="overflow-hidden">
<CardHeader>
<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>
);
}