Files
xiaoxia-saas/apps/web/src/pages/editing-planner/hooks/useEditPlanClips.ts
T
xiaoxia 3862996045
CI/CD Pipeline / Check if frontend-only change (push) Waiting to run
CI/CD Pipeline / Validate - Code Quality (push) Waiting to run
CI/CD Pipeline / Validate - Type Check (mypy) (push) Waiting to run
CI/CD Pipeline / Validate - Migration (alembic) (push) Waiting to run
CI/CD Pipeline / Unit Tests (push) Blocked by required conditions
CI/CD Pipeline / Integration Tests (push) Blocked by required conditions
CI/CD Pipeline / Frontend Lint (push) Waiting to run
CI/CD Pipeline / Frontend Unit Tests (push) Blocked by required conditions
CI/CD Pipeline / PR Build API Image (push) Waiting to run
CI/CD Pipeline / PR Build Web Image (push) Waiting to run
CI/CD Pipeline / PR Build Worker Image (push) Waiting to run
CI/CD Pipeline / Build Staging API Image (push) Waiting to run
CI/CD Pipeline / Build Staging Web Image (push) Waiting to run
CI/CD Pipeline / Build Staging Worker Image (push) Waiting to run
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Blocked by required conditions
CI/CD Pipeline / Staging E2E Tests (push) Blocked by required conditions
CI/CD Pipeline / Staging API Integration Tests (push) Blocked by required conditions
CI/CD Pipeline / Build Production API Image (push) Waiting to run
CI/CD Pipeline / Build Production Web Image (push) Waiting to run
CI/CD Pipeline / Build Production Worker Image (push) Waiting to run
CI/CD Pipeline / Deploy Production (push) Blocked by required conditions
CI/CD Pipeline / Production Browser E2E (push) Blocked by required conditions
CI/CD Pipeline / ACR Image Cleanup (push) Blocked by required conditions
refactor(#783): 统一API文件命名风格为kebab-case (#788)
2026-07-23 22:34:56 +08:00

234 lines
6.5 KiB
TypeScript

/**
* 模板片段管理 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<string | null>(null)
const selectedClip = clips.find((c) => c.id === selectedClipId) ?? null
/* ── 本地撤销/重做(用于拖拽等即时操作的回退) ── */
const {
state: localClips,
set: setLocalClips,
undo,
redo,
canUndo,
canRedo,
reset: resetLocalClips,
} = useUndoRedo<EditPlanClip[]>([])
// 当服务端数据变化时同步本地
// 注意:实际使用时以服务端为准,本地仅用于拖拽等临时操作
/* ── 创建片段 ── */
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<CreateEditPlanClipRequest, "order"> & { 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