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
13 KiB
状态管理
文档版本:v1.1
最后更新:2025-01-18
目录
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