/** * 模板片段管理 Hook * 对接后端 PR#389 片段 CRUD API,替代原来的 config.segments 模式 * * 功能: * - 加载/刷新片段列表 * - 单个增删改查 * - 批量删除 * - 拖拽重排序 * - 从素材批量导入 * - 乐观更新 + 撤销重做 */ import { useCallback, useState } from "react" import { message } from "antd" import { useQuery, useQueryClient, useMutation } from "@tanstack/react-query" import type { EditPlanClip, CreateEditPlanClipRequest, UpdateEditPlanClipRequest, ClipReorderItem, } from "@/api/template-editor" import { getEditPlanClips, createEditPlanClip, updateEditPlanClip, deleteEditPlanClip, reorderEditPlanClips, batchDeleteEditPlanClips, createClipsFromAssets, } from "@/api/template-editor" import { useUndoRedo } from "./useUndoRedo" const QUERY_KEY = "editPlanClips" export function useEditPlanClips(planId: string | undefined) { const queryClient = useQueryClient() /* ── 片段列表查询 ── */ const { data: clipListData, isLoading: clipsLoading, refetch: refetchClips, } = useQuery({ queryKey: [QUERY_KEY, planId], queryFn: () => getEditPlanClips(planId!, { limit: 500 }), enabled: !!planId, staleTime: 30_000, }) const clips: EditPlanClip[] = clipListData?.items ?? [] const clipsTotal = clipListData?.total ?? 0 /* ── 选中片段 ── */ const [selectedClipId, setSelectedClipId] = useState(null) const selectedClip = clips.find((c) => c.id === selectedClipId) ?? null /* ── 本地撤销/重做(用于拖拽等即时操作的回退) ── */ const { state: localClips, set: setLocalClips, undo, redo, canUndo, canRedo, reset: resetLocalClips, } = useUndoRedo([]) // 当服务端数据变化时同步本地 // 注意:实际使用时以服务端为准,本地仅用于拖拽等临时操作 /* ── 创建片段 ── */ const createMutation = useMutation({ mutationFn: (data: CreateEditPlanClipRequest) => createEditPlanClip(planId!, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: [QUERY_KEY, planId] }) message.success("片段已添加") }, onError: () => { message.error("添加片段失败") }, }) const addClip = useCallback( (data: Omit & { order?: number }) => { if (!planId) return const order = data.order ?? clips.length createMutation.mutate({ ...data, order }) }, [planId, clips.length, createMutation], ) /* ── 更新片段 ── */ const updateMutation = useMutation({ mutationFn: ({ clipId, data }: { clipId: string; data: UpdateEditPlanClipRequest }) => updateEditPlanClip(planId!, clipId, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: [QUERY_KEY, planId] }) }, onError: () => { message.error("更新片段失败") }, }) const updateClip = useCallback( (clipId: string, data: UpdateEditPlanClipRequest) => { if (!planId) return updateMutation.mutate({ clipId, data }) }, [planId, updateMutation], ) /* ── 删除片段 ── */ const deleteMutation = useMutation({ mutationFn: (clipId: string) => deleteEditPlanClip(planId!, clipId), onSuccess: () => { queryClient.invalidateQueries({ queryKey: [QUERY_KEY, planId] }) message.success("片段已删除") }, onError: () => { message.error("删除片段失败") }, }) const removeClip = useCallback( (clipId: string) => { if (!planId) return if (selectedClipId === clipId) { setSelectedClipId(null) } deleteMutation.mutate(clipId) }, [planId, selectedClipId, deleteMutation], ) /* ── 批量删除 ── */ const batchDeleteMutation = useMutation({ mutationFn: (clipIds: string[]) => batchDeleteEditPlanClips(planId!, clipIds), onSuccess: (res) => { queryClient.invalidateQueries({ queryKey: [QUERY_KEY, planId] }) message.success(`已删除 ${res.deleted_count} 个片段`) }, onError: () => { message.error("批量删除失败") }, }) const batchRemoveClips = useCallback( (clipIds: string[]) => { if (!planId || clipIds.length === 0) return if (selectedClipId && clipIds.includes(selectedClipId)) { setSelectedClipId(null) } batchDeleteMutation.mutate(clipIds) }, [planId, selectedClipId, batchDeleteMutation], ) /* ── 重排序(拖拽结束后一次性提交) ── */ const reorderMutation = useMutation({ mutationFn: (items: ClipReorderItem[]) => reorderEditPlanClips(planId!, items), onSuccess: () => { queryClient.invalidateQueries({ queryKey: [QUERY_KEY, planId] }) }, onError: () => { message.error("排序失败") // 失败后刷新回服务端状态 queryClient.invalidateQueries({ queryKey: [QUERY_KEY, planId] }) }, }) const reorderClips = useCallback( (items: ClipReorderItem[]) => { if (!planId || items.length === 0) return reorderMutation.mutate(items) }, [planId, reorderMutation], ) /* ── 从素材批量导入 ── */ const importFromAssetsMutation = useMutation({ mutationFn: (assetIds: string[]) => createClipsFromAssets(planId!, assetIds), onSuccess: (res) => { queryClient.invalidateQueries({ queryKey: [QUERY_KEY, planId] }) message.success(`已导入 ${res.created_count} 个素材片段`) }, onError: () => { message.error("导入素材失败") }, }) const importFromAssets = useCallback( (assetIds: string[]) => { if (!planId || assetIds.length === 0) return importFromAssetsMutation.mutate(assetIds) }, [planId, importFromAssetsMutation], ) return { // 数据 clips, clipsTotal, clipsLoading, selectedClipId, selectedClip, // 选中 setSelectedClipId, // 操作 addClip, updateClip, removeClip, batchRemoveClips, reorderClips, importFromAssets, refetchClips, // 状态 isCreating: createMutation.isPending, isUpdating: updateMutation.isPending, isDeleting: deleteMutation.isPending, isReordering: reorderMutation.isPending, isImporting: importFromAssetsMutation.isPending, // 本地撤销重做(供拖拽等场景使用) localClips, setLocalClips, undo, redo, canUndo, canRedo, resetLocalClips, } } export default useEditPlanClips