You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 

13 KiB

状态管理

文档版本:v1.1
最后更新:2025-01-18


目录

  1. 状态分层策略
  2. Zustand Store 设计
  3. TanStack Query 配置
  4. 自定义 Query Hook

1. 状态分层策略

┌─────────────────────────────────────────────────┐
│                   服务端状态                      │
│          (TanStack Query 管理)                   │
│      - 项目列表、分镜数据、资源数据等              │
└─────────────────────────────────────────────────┘
                        │
                        ▼
┌─────────────────────────────────────────────────┐
│                   客户端状态                      │
│            (Zustand 管理)                        │
│     - 当前选中项、UI 状态、编辑器状态              │
└─────────────────────────────────────────────────┘
                        │
                        ▼
┌─────────────────────────────────────────────────┐
│                   组件本地状态                    │
│            (useState 管理)                       │
│      - 表单输入、临时 UI 状态                     │
└─────────────────────────────────────────────────┘

1.1 状态分类原则

状态类型 管理方式 示例
服务端状态 TanStack Query 项目列表、分镜数据、用户信息
全局客户端状态 Zustand 当前选中项、UI 展开/折叠状态
组件本地状态 useState 表单输入、临时 UI 状态

2. Zustand Store 设计

2.1 应用全局状态

// src/stores/appStore.ts
import { create } from "zustand";
import { devtools, persist } from "zustand/middleware";

interface AppState {
  // 状态
  isInitialized: boolean;
  currentProjectId: string | null;
  user: User | null;

  // 操作
  setInitialized: (value: boolean) => void;
  setCurrentProject: (projectId: string | null) => void;
  setUser: (user: User | null) => void;
  reset: () => void;
}

export const useAppStore = create<AppState>()(
  devtools(
    persist(
      (set) => ({
        // 初始状态
        isInitialized: false,
        currentProjectId: null,
        user: null,

        // 操作
        setInitialized: (value) => set({ isInitialized: value }),
        setCurrentProject: (projectId) => set({ currentProjectId: projectId }),
        setUser: (user) => set({ user }),
        reset: () =>
          set({
            isInitialized: false,
            currentProjectId: null,
            user: null,
          }),
      }),
      {
        name: "app-store",
        partialize: (state) => ({
          currentProjectId: state.currentProjectId,
        }),
      },
    ),
    { name: "AppStore" },
  ),
);

2.2 编辑器状态

// src/stores/editorStore.ts
import { create } from "zustand";
import { devtools } from "zustand/middleware";

interface EditorState {
  // 选中状态
  selectedStoryboardId: string | null;
  selectedStoryboard BoardItems: string[];

  // 播放状态
  isPlaying: boolean;
  currentTime: number;
  duration: number;

  // 分镜看板状态
  timelineZoom: number;
  timelineScrollPosition: number;

  // 操作
  selectStoryboard: (id: string | null) => void;
  selectStoryboard BoardItems: (ids: string[]) => void;
  toggleStoryboard BoardItem: (id: string) => void;

  setPlaying: (playing: boolean) => void;
  setCurrentTime: (time: number) => void;
  setDuration: (duration: number) => void;

  setStoryboard BoardZoom: (zoom: number) => void;
  setStoryboard BoardScrollPosition: (position: number) => void;

  reset: () => void;
}

const initialState = {
  selectedStoryboardId: null,
  selectedStoryboard BoardItems: [],
  isPlaying: false,
  currentTime: 0,
  duration: 0,
  timelineZoom: 1,
  timelineScrollPosition: 0,
};

export const useEditorStore = create<EditorState>()(
  devtools(
    (set, get) => ({
      ...initialState,

      selectStoryboard: (id) => set({ selectedStoryboardId: id }),

      selectStoryboard BoardItems: (ids) => set({ selectedStoryboard BoardItems: ids }),

      toggleStoryboard BoardItem: (id) => {
        const current = get().selectedStoryboard BoardItems;
        const isSelected = current.includes(id);
        set({
          selectedStoryboard BoardItems: isSelected
            ? current.filter((i) => i !== id)
            : [...current, id],
        });
      },

      setPlaying: (playing) => set({ isPlaying: playing }),
      setCurrentTime: (time) => set({ currentTime: time }),
      setDuration: (duration) => set({ duration }),

      setStoryboard BoardZoom: (zoom) =>
        set({ timelineZoom: Math.max(0.1, Math.min(5, zoom)) }),
      setStoryboard BoardScrollPosition: (position) =>
        set({ timelineScrollPosition: position }),

      reset: () => set(initialState),
    }),
    { name: "EditorStore" },
  ),
);

2.3 UI 状态

// src/stores/uiStore.ts
import { create } from "zustand";
import { devtools, persist } from "zustand/middleware";

type LeftPanelTab = "project" | "library";
type RightPanelTab = "imagePrompt" | "angleTransform" | "cameraAdjust";

interface UIState {
  // 侧边栏状态
  leftSidebarCollapsed: boolean;
  rightSidebarCollapsed: boolean;
  leftPanelActiveTab: LeftPanelTab;
  rightPanelActiveTab: RightPanelTab;

  // 分镜看板状态
  timelineExpanded: boolean;

  // AI 面板
  aiPanelVisible: boolean;

  // 弹窗状态
  createProjectModalOpen: boolean;
  exportModalOpen: boolean;
  settingsModalOpen: boolean;

  // 引导状态
  showOnboarding: boolean;

  // 操作
  toggleLeftSidebar: () => void;
  toggleRightSidebar: () => void;
  setLeftPanelTab: (tab: LeftPanelTab) => void;
  setRightPanelTab: (tab: RightPanelTab) => void;
  toggleStoryboard Board: () => void;
  toggleAIPanel: () => void;

  setCreateProjectModalOpen: (open: boolean) => void;
  setExportModalOpen: (open: boolean) => void;
  setSettingsModalOpen: (open: boolean) => void;
  setShowOnboarding: (show: boolean) => void;
}

export const useUIStore = create<UIState>()(
  devtools(
    persist(
      (set, get) => ({
        // 初始状态
        leftSidebarCollapsed: false,
        rightSidebarCollapsed: false,
        leftPanelActiveTab: "project",
        rightPanelActiveTab: "imagePrompt",
        timelineExpanded: true,
        aiPanelVisible: false,
        createProjectModalOpen: false,
        exportModalOpen: false,
        settingsModalOpen: false,
        showOnboarding: true,

        // 操作
        toggleLeftSidebar: () =>
          set({ leftSidebarCollapsed: !get().leftSidebarCollapsed }),
        toggleRightSidebar: () =>
          set({ rightSidebarCollapsed: !get().rightSidebarCollapsed }),
        setLeftPanelTab: (tab) => set({ leftPanelActiveTab: tab }),
        setRightPanelTab: (tab) => set({ rightPanelActiveTab: tab }),
        toggleStoryboard Board: () =>
          set({ timelineExpanded: !get().timelineExpanded }),
        toggleAIPanel: () => set({ aiPanelVisible: !get().aiPanelVisible }),

        setCreateProjectModalOpen: (open) =>
          set({ createProjectModalOpen: open }),
        setExportModalOpen: (open) => set({ exportModalOpen: open }),
        setSettingsModalOpen: (open) => set({ settingsModalOpen: open }),
        setShowOnboarding: (show) => set({ showOnboarding: show }),
      }),
      {
        name: "ui-store",
        partialize: (state) => ({
          leftSidebarCollapsed: state.leftSidebarCollapsed,
          rightSidebarCollapsed: state.rightSidebarCollapsed,
          leftPanelActiveTab: state.leftPanelActiveTab,
          timelineExpanded: state.timelineExpanded,
          showOnboarding: state.showOnboarding,
        }),
      },
    ),
    { name: "UIStore" },
  ),
);

3. TanStack Query 配置

3.1 Query Provider

// src/app/providers/QueryProvider.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 1000 * 60 * 5, // 5 分钟
      gcTime: 1000 * 60 * 30, // 30 分钟 (原 cacheTime)
      retry: 3,
      refetchOnWindowFocus: false,
    },
    mutations: {
      retry: 1,
    },
  },
});

export function QueryProvider({ children }: { children: React.ReactNode }) {
  return (
    <QueryClientProvider client={queryClient}>
      {children}
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  );
}

3.2 配置说明

配置项 说明
staleTime 5 分钟 数据被视为过期的时间
gcTime 30 分钟 缓存数据的保留时间
retry 3 次 请求失败重试次数
refetchOnWindowFocus false 窗口聚焦时不自动重新请求

4. 自定义 Query Hook

4.1 查询键设计

// src/hooks/api/useStoryboards.ts
export const storyboardKeys = {
  all: ["storyboards"] as const,
  lists: () => [...storyboardKeys.all, "list"] as const,
  list: (projectId: string) => [...storyboardKeys.lists(), projectId] as const,
  details: () => [...storyboardKeys.all, "detail"] as const,
  detail: (id: string) => [...storyboardKeys.details(), id] as const,
};

4.2 查询 Hook

// 获取分镜列表
export function useStoryboards(projectId: string) {
  return useQuery({
    queryKey: storyboardKeys.list(projectId),
    queryFn: () => storyboardApi.getList(projectId),
    enabled: !!projectId,
  });
}

// 获取单个分镜
export function useStoryboard(id: string) {
  return useQuery({
    queryKey: storyboardKeys.detail(id),
    queryFn: () => storyboardApi.getById(id),
    enabled: !!id,
  });
}

4.3 变更 Hook

// 创建分镜
export function useCreateStoryboard() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (data: CreateStoryboardDto) => storyboardApi.create(data),
    onSuccess: (_, variables) => {
      queryClient.invalidateQueries({
        queryKey: storyboardKeys.list(variables.projectId),
      });
    },
  });
}

// 更新分镜
export function useUpdateStoryboard() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: ({ id, data }: { id: string; data: UpdateStoryboardDto }) =>
      storyboardApi.update(id, data),
    onSuccess: (result) => {
      queryClient.setQueryData(storyboardKeys.detail(result.id), result);
      queryClient.invalidateQueries({
        queryKey: storyboardKeys.lists(),
      });
    },
  });
}

// 删除分镜
export function useDeleteStoryboard() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (id: string) => storyboardApi.delete(id),
    onSuccess: () => {
      queryClient.invalidateQueries({
        queryKey: storyboardKeys.lists(),
      });
    },
  });
}

4.4 使用示例

function StoryboardPanel({ projectId }: { projectId: string }) {
  // 查询数据
  const { data: storyboards, isLoading, error } = useStoryboards(projectId);

  // 变更操作
  const createMutation = useCreateStoryboard();
  const updateMutation = useUpdateStoryboard();
  const deleteMutation = useDeleteStoryboard();

  const handleCreate = async (data: CreateStoryboardDto) => {
    try {
      await createMutation.mutateAsync(data);
      toast.success('创建成功');
    } catch (error) {
      toast.error('创建失败');
    }
  };

  if (isLoading) return <LoadingSpinner />;
  if (error) return <ErrorMessage error={error} />;

  return (
    <div>
      {storyboards?.map((storyboard) => (
        <StoryboardItem key={storyboard.id} storyboard={storyboard} />
      ))}
    </div>
  );
}

相关文档


最后更新:2025-01-18 | 版本:v1.1