fix(*): 模型与数据同步 - 完善鉴权、供应商模型和本地持久化能力
This commit is contained in:
@@ -6,14 +6,11 @@ import { WorkspaceCommandPalette } from "@/components/layout/workspace-command-p
|
||||
import { WorkspaceSidebarNav } from "@/components/layout/workspace-sidebar-nav";
|
||||
import { WorkspaceTopBar } from "@/components/layout/workspace-top-bar";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { refreshFeatureAvailability } from "@/lib/user-session";
|
||||
import { isSpatialWorkbenchPath } from "@/lib/workspace-routes";
|
||||
import { useUserStore } from "@/stores/use-user-store";
|
||||
|
||||
export function AppWorkspaceShell({ children }: { children: ReactNode }) {
|
||||
const { pathname } = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const user = useUserStore((state) => state.user);
|
||||
const [mobileSidebarExpanded, setMobileSidebarExpanded] = useState(false);
|
||||
const [desktopSidebarCollapsed, setDesktopSidebarCollapsed] = useState(false);
|
||||
const [paletteOpen, setPaletteOpen] = useState(false);
|
||||
@@ -60,17 +57,6 @@ export function AppWorkspaceShell({ children }: { children: ReactNode }) {
|
||||
return () => window.removeEventListener("workspace:navigate", handleWorkspaceNavigation);
|
||||
}, [navigate]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!user) return;
|
||||
const refresh = () => void refreshFeatureAvailability().catch((error) => console.warn("功能开放状态刷新失败", error));
|
||||
const timer = window.setInterval(refresh, 30_000);
|
||||
window.addEventListener("focus", refresh);
|
||||
return () => {
|
||||
window.clearInterval(timer);
|
||||
window.removeEventListener("focus", refresh);
|
||||
};
|
||||
}, [user]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={cn("app-workspace-shell flex h-dvh min-h-0 w-full flex-col overflow-hidden", spatialWorkbench && "is-spatial", creationWorkspace && "is-creation-workspace")}>
|
||||
|
||||
@@ -8,12 +8,13 @@ import { useLocalDreaminaModelBootstrap } from "@/stores/use-local-dreamina-mode
|
||||
import { useLocalRuntimeBootstrap } from "@/stores/use-local-runtime-store";
|
||||
|
||||
export function ClientRootInit({ children }: { children: ReactNode }) {
|
||||
useLocalRuntimeBootstrap();
|
||||
const config = useConfigStore((state) => state.config);
|
||||
const localRuntimeConfigured = config.channels.some((channel) => channel.transport === "local-runtime" && channel.enabled !== false);
|
||||
useLocalRuntimeBootstrap(localRuntimeConfigured);
|
||||
useLocalDreaminaModelBootstrap();
|
||||
const { message } = App.useApp();
|
||||
const handledConfigParams = useRef(false);
|
||||
const updateConfig = useConfigStore((state) => state.updateConfig);
|
||||
const config = useConfigStore((state) => state.config);
|
||||
|
||||
useEffect(() => {
|
||||
const interactiveSelector = 'button, [role="button"], a, [class*="card"], [class*="Card"]';
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
|
||||
import { Tag } from "antd";
|
||||
import { Bell } from "lucide-react";
|
||||
import { useCallback, useEffect, useRef, useState, type CSSProperties } from "react";
|
||||
import { useState, type CSSProperties } from "react";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { AnnouncementTimelineModal } from "@/components/ui/aceternity/announcement-timeline-modal";
|
||||
import { aceternityMotion } from "@/lib/aceternity-motion";
|
||||
import { getAnnouncementFeed, markAnnouncementsRead, type SystemAnnouncement } from "@/services/api/announcements";
|
||||
import { getAnnouncementFeed, markAnnouncementsRead } from "@/services/api/announcements";
|
||||
|
||||
const ANNOUNCEMENT_REFRESH_INTERVAL_MS = 60_000;
|
||||
const ANNOUNCEMENT_REFRESH_INTERVAL_MS = 5 * 60_000;
|
||||
const ANNOUNCEMENT_CACHE_TTL_MS = 60_000;
|
||||
|
||||
type AnnouncementFeed = Awaited<ReturnType<typeof getAnnouncementFeed>>;
|
||||
|
||||
type SystemAnnouncementCenterProps = {
|
||||
userId: string;
|
||||
@@ -20,63 +24,31 @@ type SystemAnnouncementCenterProps = {
|
||||
|
||||
export function SystemAnnouncementCenter({ userId, className, style, showLabel = false, labelClassName, staticMotion = false }: SystemAnnouncementCenterProps) {
|
||||
const reducedMotion = useReducedMotion();
|
||||
const activeUserIdRef = useRef(userId);
|
||||
activeUserIdRef.current = userId;
|
||||
const queryClient = useQueryClient();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [announcements, setAnnouncements] = useState<SystemAnnouncement[]>([]);
|
||||
const [unreadCount, setUnreadCount] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
const refresh = useCallback(async (showLoading = false) => {
|
||||
if (!userId) return null;
|
||||
const requestUserId = userId;
|
||||
if (showLoading) setLoading(true);
|
||||
try {
|
||||
const feed = await getAnnouncementFeed();
|
||||
if (activeUserIdRef.current !== requestUserId) return null;
|
||||
setAnnouncements(feed.announcements || []);
|
||||
setUnreadCount(Math.max(0, feed.unreadCount || 0));
|
||||
setError("");
|
||||
return feed;
|
||||
} catch (requestError) {
|
||||
if (activeUserIdRef.current !== requestUserId) return null;
|
||||
setError(requestError instanceof Error ? requestError.message : "读取公告失败");
|
||||
return null;
|
||||
} finally {
|
||||
if (showLoading && activeUserIdRef.current === requestUserId) setLoading(false);
|
||||
}
|
||||
}, [userId]);
|
||||
|
||||
useEffect(() => {
|
||||
setAnnouncements([]);
|
||||
setUnreadCount(0);
|
||||
setError("");
|
||||
if (!userId) return;
|
||||
void refresh();
|
||||
const timer = window.setInterval(() => void refresh(), ANNOUNCEMENT_REFRESH_INTERVAL_MS);
|
||||
const onFocus = () => void refresh();
|
||||
const onVisibilityChange = () => {
|
||||
if (document.visibilityState === "visible") void refresh();
|
||||
};
|
||||
window.addEventListener("focus", onFocus);
|
||||
document.addEventListener("visibilitychange", onVisibilityChange);
|
||||
return () => {
|
||||
window.clearInterval(timer);
|
||||
window.removeEventListener("focus", onFocus);
|
||||
document.removeEventListener("visibilitychange", onVisibilityChange);
|
||||
};
|
||||
}, [refresh, userId]);
|
||||
const queryKey = ["system-announcements", userId] as const;
|
||||
const feedQuery = useQuery({
|
||||
queryKey,
|
||||
queryFn: getAnnouncementFeed,
|
||||
enabled: Boolean(userId),
|
||||
staleTime: ANNOUNCEMENT_CACHE_TTL_MS,
|
||||
refetchInterval: ANNOUNCEMENT_REFRESH_INTERVAL_MS,
|
||||
// 公告在打开面板时会显式 refetch;不把浏览器 focus 变成每个工作区实例的请求触发器。
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
const announcements = feedQuery.data?.announcements || [];
|
||||
const unreadCount = Math.max(0, feedQuery.data?.unreadCount || 0);
|
||||
const error = feedQuery.error instanceof Error ? feedQuery.error.message : feedQuery.error ? "读取公告失败" : "";
|
||||
|
||||
const openAnnouncements = async () => {
|
||||
setOpen(true);
|
||||
const feed = await refresh(announcements.length === 0);
|
||||
const feed = (await feedQuery.refetch()).data;
|
||||
if (!feed?.unreadCount) return;
|
||||
try {
|
||||
const result = await markAnnouncementsRead(feed.announcements.map((announcement) => announcement.id));
|
||||
const nextUnreadCount = Math.max(0, result.unreadCount || 0);
|
||||
setUnreadCount(nextUnreadCount);
|
||||
if (nextUnreadCount > 0) void refresh();
|
||||
queryClient.setQueryData<AnnouncementFeed>(queryKey, (current) => current ? { ...current, unreadCount: nextUnreadCount } : current);
|
||||
if (nextUnreadCount > 0) void queryClient.invalidateQueries({ queryKey });
|
||||
} catch {
|
||||
// 已读状态是辅助读路径,失败时保留角标,下一次打开或轮询会继续尝试同步。
|
||||
}
|
||||
@@ -118,7 +90,7 @@ export function SystemAnnouncementCenter({ userId, className, style, showLabel =
|
||||
</span>
|
||||
) : null}
|
||||
</motion.button>
|
||||
<AnnouncementTimelineModal open={open} announcements={announcements} loading={loading} error={announcements.length ? "" : error} onClose={() => setOpen(false)} onRetry={() => void refresh(true)} />
|
||||
<AnnouncementTimelineModal open={open} announcements={announcements} loading={feedQuery.isFetching} error={announcements.length ? "" : error} onClose={() => setOpen(false)} onRetry={() => void feedQuery.refetch()} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user