mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-10-05 17:46:37 +08:00
feat: Announcement Popup Notification (#411)
* docs: add announcement popup design spec * docs: add announcement popup implementation plan * feat(api): include update_time in announcement response * feat(ui): add update_time to AnnouncementData interface * feat(ui): create AnnouncementModal component * feat(ui): manage announcement modal state in dashboard hook * feat(ui): add announcement modal to dashboard layout
This commit is contained in:
@@ -410,6 +410,7 @@ export const importBackup = (data: BackupImportPayload) =>
|
||||
export interface AnnouncementData {
|
||||
content: string;
|
||||
enabled: number;
|
||||
update_time?: number;
|
||||
}
|
||||
|
||||
export const getAnnouncement = () =>
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
} from "@/shadcn-bridge/heroui/modal";
|
||||
import { PageEmptyState, PageLoadingState } from "@/components/page-state";
|
||||
import { AnnouncementBanner } from "@/pages/dashboard/components/announcement-banner";
|
||||
import { AnnouncementModal } from "@/pages/dashboard/components/announcement-modal";
|
||||
import { FlowChartCard } from "@/pages/dashboard/components/flow-chart-card";
|
||||
import { MetricCard } from "@/pages/dashboard/components/metric-card";
|
||||
import {
|
||||
@@ -43,6 +44,9 @@ export default function DashboardPage() {
|
||||
nodeExpiryReminders,
|
||||
isAdmin,
|
||||
announcement,
|
||||
isAnnouncementModalOpen,
|
||||
setIsAnnouncementModalOpen,
|
||||
dismissAnnouncementModal,
|
||||
} = useDashboardData();
|
||||
|
||||
const [addressModalOpen, setAddressModalOpen] = useState(false);
|
||||
@@ -627,6 +631,14 @@ export default function DashboardPage() {
|
||||
return (
|
||||
<AnimatedPage className="px-3 lg:px-6 py-2 lg:py-4">
|
||||
{announcement && <AnnouncementBanner announcement={announcement} />}
|
||||
{announcement && (
|
||||
<AnnouncementModal
|
||||
announcement={announcement}
|
||||
isOpen={isAnnouncementModalOpen}
|
||||
onClose={() => setIsAnnouncementModalOpen(false)}
|
||||
onDontShowAgain={dismissAnnouncementModal}
|
||||
/>
|
||||
)}
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3 lg:gap-4 mb-6 lg:mb-8">
|
||||
<MetricCard
|
||||
icon={
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import type { AnnouncementData } from "@/api";
|
||||
import { Button } from "@/shadcn-bridge/heroui/button";
|
||||
import {
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
} from "@/shadcn-bridge/heroui/modal";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
|
||||
interface AnnouncementModalProps {
|
||||
announcement: AnnouncementData;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onDontShowAgain: () => void;
|
||||
}
|
||||
|
||||
export const AnnouncementModal = ({
|
||||
announcement,
|
||||
isOpen,
|
||||
onClose,
|
||||
onDontShowAgain,
|
||||
}: AnnouncementModalProps) => {
|
||||
return (
|
||||
<Modal isOpen={isOpen} onOpenChange={(open) => !open && onClose()} size="2xl">
|
||||
<ModalContent>
|
||||
<ModalHeader className="flex flex-col gap-1">平台公告</ModalHeader>
|
||||
<ModalBody>
|
||||
<div className="prose prose-sm dark:prose-invert max-w-none max-h-[60vh] overflow-y-auto">
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>
|
||||
{announcement.content}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button variant="flat" onPress={onDontShowAgain}>
|
||||
不再提示
|
||||
</Button>
|
||||
<Button color="primary" onPress={onClose}>
|
||||
关闭
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -96,6 +96,9 @@ interface DashboardDataState {
|
||||
nodeExpiryReminders: DashboardNodeExpiryItem[];
|
||||
isAdmin: boolean;
|
||||
announcement: AnnouncementData | null;
|
||||
isAnnouncementModalOpen: boolean;
|
||||
setIsAnnouncementModalOpen: (isOpen: boolean) => void;
|
||||
dismissAnnouncementModal: () => void;
|
||||
}
|
||||
|
||||
const checkExpirationNotifications = (
|
||||
@@ -267,6 +270,7 @@ export const useDashboardData = (): DashboardDataState => {
|
||||
const [announcement, setAnnouncement] = useState<AnnouncementData | null>(
|
||||
null,
|
||||
);
|
||||
const [isAnnouncementModalOpen, setIsAnnouncementModalOpen] = useState(false);
|
||||
const isMountedRef = useRef(true);
|
||||
const packageRequestInFlightRef = useRef(false);
|
||||
const nodeExpiryRequestInFlightRef = useRef(false);
|
||||
@@ -345,6 +349,19 @@ export const useDashboardData = (): DashboardDataState => {
|
||||
|
||||
if (res.code === 0 && res.data && res.data.enabled === 1) {
|
||||
setAnnouncement(res.data);
|
||||
|
||||
try {
|
||||
const storedTimeStr = localStorage.getItem("flvx_announcement_seen_time");
|
||||
const storedTime = storedTimeStr ? parseInt(storedTimeStr, 10) : 0;
|
||||
const updateTime = res.data.update_time || 0;
|
||||
|
||||
if (updateTime > storedTime) {
|
||||
setIsAnnouncementModalOpen(true);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn("Failed to read localStorage for announcement state", err);
|
||||
setIsAnnouncementModalOpen(true);
|
||||
}
|
||||
} else {
|
||||
setAnnouncement(null);
|
||||
}
|
||||
@@ -355,6 +372,17 @@ export const useDashboardData = (): DashboardDataState => {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const dismissAnnouncementModal = useCallback(() => {
|
||||
setIsAnnouncementModalOpen(false);
|
||||
if (announcement && announcement.update_time) {
|
||||
try {
|
||||
localStorage.setItem("flvx_announcement_seen_time", announcement.update_time.toString());
|
||||
} catch (err) {
|
||||
console.warn("Failed to set localStorage for announcement state", err);
|
||||
}
|
||||
}
|
||||
}, [announcement]);
|
||||
|
||||
const loadNodeExpiryData = useCallback(async () => {
|
||||
if (nodeExpiryRequestInFlightRef.current) {
|
||||
return;
|
||||
@@ -438,5 +466,8 @@ export const useDashboardData = (): DashboardDataState => {
|
||||
nodeExpiryReminders,
|
||||
isAdmin,
|
||||
announcement,
|
||||
isAnnouncementModalOpen,
|
||||
setIsAnnouncementModalOpen,
|
||||
dismissAnnouncementModal,
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user