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
234 lines
6.5 KiB
TypeScript
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
|