Compare commits

..

8 Commits

Author SHA1 Message Date
xiaoxia 07562a71c2 fix: 删除同名文件避免循环导入(目录index.ts已作为入口)
CI/CD Pipeline / CI Gate (pull_request) CI运行中 [frontend-only]
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 16s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 44s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m35s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m32s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Code Quality (pull_request) Failing after 2m25s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 49s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 1m57s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 53s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m1s
AI Code Review / AI Code Review (pull_request) Successful in 1m43s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 5m46s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Has been cancelled
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 11m5s
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 16s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 9s
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
2026-07-27 15:24:38 +08:00
xiaoxia 8036b134de fix: 修正 useClipTrim.ts 中 types 导入路径(../types → ../../types)
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 5s
CI/CD Pipeline / Validate - Code Quality (pull_request) Failing after 1m34s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m13s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 26s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m15s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 22s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 1m52s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 38s
AI Code Review / AI Code Review (pull_request) Successful in 1m36s
Preview Deploy / Deploy Preview Environment (pull_request) Failing after 32s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 41s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 3m5s
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
2026-07-27 12:41:34 +08:00
xiaoxia 663f936a8c fix: 修正 useClipEffects.ts 中 types 导入路径(../types → ../../types)
CI/CD Pipeline / Check if frontend-only change (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Code Quality (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Has been cancelled
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Frontend Lint (pull_request) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / PR Build API Image (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Web Image (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Staging API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
AI Code Review / AI Code Review (pull_request) Has been cancelled
Preview Deploy / Deploy Preview Environment (pull_request) Has been cancelled
PR Automation / Auto Approve on CI Green (pull_request) Successful in 35s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 36s
2026-07-27 12:41:33 +08:00
xiaoxia eadb2eef57 fix: 修正 useClipBasicOps.ts 中 types 导入路径(../types → ../../types)
CI/CD Pipeline / Check if frontend-only change (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Code Quality (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Has been cancelled
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Frontend Lint (pull_request) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / PR Build API Image (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Web Image (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Staging API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
AI Code Review / AI Code Review (pull_request) Has been cancelled
Preview Deploy / Deploy Preview Environment (pull_request) Has been cancelled
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 45m51s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m38s
2026-07-27 12:41:32 +08:00
xiaoxia 9a854679f8 fix: 修正 index.ts 中 types 导入路径(../types → ../../types) 2026-07-27 12:41:31 +08:00
xiaoxia 2a16c755bf fix: 恢复 useClipOperations.ts 主入口,re-export 子目录保持路径兼容
CI/CD Pipeline / Check if frontend-only change (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Code Quality (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Has been cancelled
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Frontend Lint (pull_request) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / PR Build API Image (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Web Image (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Staging API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
AI Code Review / AI Code Review (pull_request) Has been cancelled
Preview Deploy / Deploy Preview Environment (pull_request) Has been cancelled
PR Automation / Auto Approve on CI Green (pull_request) Successful in 35s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 36s
2026-07-27 12:41:29 +08:00
xiaoxia b052199e56 Merge branch 'develop' into refactor/use-clip-operations-split
CI/CD Pipeline / Check if frontend-only change (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Code Quality (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Has been cancelled
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Frontend Lint (pull_request) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / PR Build API Image (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Web Image (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Staging API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
AI Code Review / AI Code Review (pull_request) Has been cancelled
Preview Deploy / Deploy Preview Environment (pull_request) Has been cancelled
PR Automation / Auto Approve on CI Green (pull_request) Successful in 55s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 56s
2026-07-27 12:34:26 +08:00
xiaoxia ee60770853 refactor(editing-planner): 拆分 useClipOperations Hook,按职责拆为 3 个子模块
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 20s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 46s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m8s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m15s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Code Quality (pull_request) Failing after 1m50s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 28s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m18s
Preview Deploy / Deploy Preview Environment (pull_request) Failing after 1m10s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 1m30s
AI Code Review / AI Code Review (pull_request) Successful in 2m1s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 2m44s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 7m9s
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 53s
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
主文件 238→60 行(-75%),拆分为:
- useClipBasicOps: 选中状态 + 增删改查重排
- useClipTrim: 裁剪、分割、重置裁剪
- useClipEffects: 转场、调速、TTS、配音选择

导入路径保持向后兼容
2026-07-27 07:27:24 +08:00
11 changed files with 545 additions and 663 deletions
@@ -1,238 +0,0 @@
import { useState, useCallback, useMemo } from "react"
import { Modal, message } from "antd"
import type {
ClipData,
ClipType,
TransitionConfig,
SpeedConfig,
TtsConfig,
TrimConfig,
} from "../types"
import type { AssetItem } from "@/api/assets"
interface UseClipOperationsParams {
clips: ClipData[]
setClips: (updater: (prev: ClipData[]) => ClipData[]) => void
}
/**
* 片段操作 Hook
* 片段增删改查、排序、裁剪、分割、转场、调速、TTS、配音选择
*/
export const useClipOperations = ({ clips, setClips }: UseClipOperationsParams) => {
const [selectedClipId, setSelectedClipId] = useState<string | null>(null)
const selectedClip = useMemo(
() => clips.find((c) => c.id === selectedClipId) || null,
[clips, selectedClipId],
)
/* ── 选中 / 重排 / 删除 ── */
const handleClipSelect = useCallback((clipId: string) => {
setSelectedClipId(clipId)
}, [])
const handleClipReorder = useCallback(
(fromIdx: number, toIdx: number) => {
setClips((prev) => {
const updated = [...prev]
const [moved] = updated.splice(fromIdx, 1)
updated.splice(toIdx, 0, moved)
return updated.map((c, i) => ({ ...c, order: i }))
})
},
[setClips],
)
const handleClipRemove = useCallback(
(clipId: string) => {
Modal.confirm({
title: "删除片段",
content: "确定要删除这个片段吗?此操作可通过撤销恢复。",
okText: "删除",
okType: "danger",
cancelText: "取消",
onOk: () => {
setClips((prev) => prev.filter((c) => c.id !== clipId))
if (selectedClipId === clipId) setSelectedClipId(null)
},
})
},
[setClips, selectedClipId],
)
const handleClipUpdate = useCallback(
(clipId: string, data: Partial<ClipData>) => {
setClips((prev) => prev.map((c) => (c.id === clipId ? { ...c, ...data } : c)))
},
[setClips],
)
/**
* 添加片段(不绑定任何素材)
* 片段 = 时间规划 + 类型标记
*/
const handleAddClip = useCallback(
(type: ClipType, duration: number) => {
const newClip: ClipData = {
id: `clip-${Date.now()}`,
type,
duration,
startOffset: 0,
order: clips.length,
}
setClips((prev) => [...prev, newClip])
},
[clips.length, setClips],
)
/* ── 裁剪 / 分割 / 重置 ── */
const handleClipTrim = useCallback(
(clipId: string, trimConfig: TrimConfig, newDuration: number) => {
setClips((prev) =>
prev.map((c) =>
c.id === clipId ? { ...c, trim_config: trimConfig, duration: newDuration } : c,
),
)
},
[setClips],
)
const handleClipSplit = useCallback(
(clipId: string, splitRatio: number) => {
setClips((prev) => {
const idx = prev.findIndex((c) => c.id === clipId)
if (idx === -1) return prev
const clip = prev[idx]
const splitPoint = Math.round(clip.duration * splitRatio * 10) / 10
if (splitPoint < 0.5 || splitPoint >= clip.duration - 0.5) return prev
// 前半段
const firstHalf: ClipData = {
...clip,
duration: splitPoint,
trim_config: clip.trim_config
? {
...clip.trim_config,
end_time: clip.trim_config.start_time + splitPoint,
}
: undefined,
}
// 后半段
const secondHalf: ClipData = {
...clip,
id: `clip-${Date.now()}`,
duration: clip.duration - splitPoint,
startOffset: clip.startOffset + splitPoint,
trim_config: clip.trim_config
? {
...clip.trim_config,
start_time: clip.trim_config.start_time + splitPoint,
}
: undefined,
order: (clip.order ?? idx) + 1,
}
const updated = [...prev]
updated[idx] = firstHalf
updated.splice(idx + 1, 0, secondHalf)
return updated.map((c, i) => ({ ...c, order: i }))
})
},
[setClips],
)
const handleClipResetTrim = useCallback(
(clipId: string) => {
setClips((prev) =>
prev.map((c) => {
if (c.id !== clipId || !c.trim_config) return c
const originalDuration = c.trim_config.original_duration ?? c.duration
return {
...c,
duration: originalDuration,
trim_config: undefined,
}
}),
)
},
[setClips],
)
/* ── 转场 / 调速 / TTS ── */
const handleTransitionChange = useCallback(
(targetClipId: string | null, config: TransitionConfig) => {
if (targetClipId) {
handleClipUpdate(targetClipId, { transition: config })
}
// 同时更新全局默认转场(供新片段使用)—— 暂未实现全局默认
},
[handleClipUpdate],
)
const handleSpeedChange = useCallback(
(targetClipId: string | null, config: SpeedConfig) => {
if (targetClipId) {
handleClipUpdate(targetClipId, { speed: config })
}
},
[handleClipUpdate],
)
const handleApplySpeedAll = useCallback(
(config: SpeedConfig) => {
setClips((prev) => prev.map((c) => ({ ...c, speed: { ...config } })))
message.success("已应用到所有片段")
},
[setClips],
)
const handleTtsChange = useCallback(
(targetClipId: string | null, ttsConfig: TtsConfig) => {
if (!targetClipId) return
handleClipUpdate(targetClipId, { tts_config: ttsConfig })
},
[handleClipUpdate],
)
/** 为片段选择配音素材 */
const handleClipVoiceSelect = useCallback(
(clipId: string, asset: AssetItem | null) => {
setClips((prev) =>
prev.map((c) =>
c.id === clipId
? {
...c,
voice_asset_id: asset?.id ?? undefined,
voice_file_url: asset?.file_url ?? undefined,
}
: c,
),
)
},
[setClips],
)
return {
selectedClipId,
setSelectedClipId,
selectedClip,
handleClipSelect,
handleClipReorder,
handleClipRemove,
handleClipUpdate,
handleAddClip,
handleClipTrim,
handleClipSplit,
handleClipResetTrim,
handleTransitionChange,
handleSpeedChange,
handleApplySpeedAll,
handleTtsChange,
handleClipVoiceSelect,
}
}
@@ -0,0 +1,38 @@
import type { ClipData } from "../../types"
import { useClipBasicOps } from "./useClipBasicOps"
import { useClipTrim } from "./useClipTrim"
import { useClipEffects } from "./useClipEffects"
interface UseClipOperationsParams {
clips: ClipData[]
setClips: (updater: (prev: ClipData[]) => ClipData[]) => void
}
/**
* 片段操作 Hook
* 片段增删改查、排序、裁剪、分割、转场、调速、TTS、配音选择
*/
export const useClipOperations = ({ clips, setClips }: UseClipOperationsParams) => {
const basicOps = useClipBasicOps({ clips, setClips })
const trimOps = useClipTrim({ setClips })
const effectOps = useClipEffects({ setClips, handleClipUpdate: basicOps.handleClipUpdate })
return {
selectedClipId: basicOps.selectedClipId,
setSelectedClipId: basicOps.setSelectedClipId,
selectedClip: basicOps.selectedClip,
handleClipSelect: basicOps.handleClipSelect,
handleClipReorder: basicOps.handleClipReorder,
handleClipRemove: basicOps.handleClipRemove,
handleClipUpdate: basicOps.handleClipUpdate,
handleAddClip: basicOps.handleAddClip,
handleClipTrim: trimOps.handleClipTrim,
handleClipSplit: trimOps.handleClipSplit,
handleClipResetTrim: trimOps.handleClipResetTrim,
handleTransitionChange: effectOps.handleTransitionChange,
handleSpeedChange: effectOps.handleSpeedChange,
handleApplySpeedAll: effectOps.handleApplySpeedAll,
handleTtsChange: effectOps.handleTtsChange,
handleClipVoiceSelect: effectOps.handleClipVoiceSelect,
}
}
@@ -0,0 +1,86 @@
import { useState, useMemo, useCallback } from "react"
import { Modal } from "antd"
import type { ClipData, ClipType } from "../../types"
interface UseClipBasicOpsParams {
clips: ClipData[]
setClips: (updater: (prev: ClipData[]) => ClipData[]) => void
}
/**
* 片段基础操作 Hook
* 选中状态、增删改查、重排
*/
export function useClipBasicOps({ clips, setClips }: UseClipBasicOpsParams) {
const [selectedClipId, setSelectedClipId] = useState<string | null>(null)
const selectedClip = useMemo(
() => clips.find((c) => c.id === selectedClipId) || null,
[clips, selectedClipId],
)
const handleClipSelect = useCallback((clipId: string) => {
setSelectedClipId(clipId)
}, [])
const handleClipReorder = useCallback(
(fromIdx: number, toIdx: number) => {
setClips((prev) => {
const updated = [...prev]
const [moved] = updated.splice(fromIdx, 1)
updated.splice(toIdx, 0, moved)
return updated.map((c, i) => ({ ...c, order: i }))
})
},
[setClips],
)
const handleClipRemove = useCallback(
(clipId: string) => {
Modal.confirm({
title: "删除片段",
content: "确定要删除这个片段吗?此操作可通过撤销恢复。",
okText: "删除",
okType: "danger",
cancelText: "取消",
onOk: () => {
setClips((prev) => prev.filter((c) => c.id !== clipId))
if (selectedClipId === clipId) setSelectedClipId(null)
},
})
},
[setClips, selectedClipId],
)
const handleClipUpdate = useCallback(
(clipId: string, data: Partial<ClipData>) => {
setClips((prev) => prev.map((c) => (c.id === clipId ? { ...c, ...data } : c)))
},
[setClips],
)
const handleAddClip = useCallback(
(type: ClipType, duration: number) => {
const newClip: ClipData = {
id: `clip-${Date.now()}`,
type,
duration,
startOffset: 0,
order: clips.length,
}
setClips((prev) => [...prev, newClip])
},
[clips.length, setClips],
)
return {
selectedClipId,
setSelectedClipId,
selectedClip,
handleClipSelect,
handleClipReorder,
handleClipRemove,
handleClipUpdate,
handleAddClip,
}
}
@@ -0,0 +1,74 @@
import { useCallback } from "react"
import { message } from "antd"
import type { ClipData, TransitionConfig, SpeedConfig, TtsConfig } from "../../types"
import type { AssetItem } from "@/api/assets"
interface UseClipEffectsParams {
setClips: (updater: (prev: ClipData[]) => ClipData[]) => void
handleClipUpdate: (clipId: string, data: Partial<ClipData>) => void
}
/**
* 片段效果操作 Hook
* 转场、调速、TTS、配音选择
*/
export function useClipEffects({ setClips, handleClipUpdate }: UseClipEffectsParams) {
const handleTransitionChange = useCallback(
(targetClipId: string | null, config: TransitionConfig) => {
if (targetClipId) {
handleClipUpdate(targetClipId, { transition: config })
}
},
[handleClipUpdate],
)
const handleSpeedChange = useCallback(
(targetClipId: string | null, config: SpeedConfig) => {
if (targetClipId) {
handleClipUpdate(targetClipId, { speed: config })
}
},
[handleClipUpdate],
)
const handleApplySpeedAll = useCallback(
(config: SpeedConfig) => {
setClips((prev) => prev.map((c) => ({ ...c, speed: { ...config } })))
message.success("已应用到所有片段")
},
[setClips],
)
const handleTtsChange = useCallback(
(targetClipId: string | null, ttsConfig: TtsConfig) => {
if (!targetClipId) return
handleClipUpdate(targetClipId, { tts_config: ttsConfig })
},
[handleClipUpdate],
)
const handleClipVoiceSelect = useCallback(
(clipId: string, asset: AssetItem | null) => {
setClips((prev) =>
prev.map((c) =>
c.id === clipId
? {
...c,
voice_asset_id: asset?.id ?? undefined,
voice_file_url: asset?.file_url ?? undefined,
}
: c,
),
)
},
[setClips],
)
return {
handleTransitionChange,
handleSpeedChange,
handleApplySpeedAll,
handleTtsChange,
handleClipVoiceSelect,
}
}
@@ -0,0 +1,89 @@
import { useCallback } from "react"
import type { ClipData, TrimConfig } from "../../types"
interface UseClipTrimParams {
setClips: (updater: (prev: ClipData[]) => ClipData[]) => void
}
/**
* 片段裁剪分割 Hook
* 裁剪、分割、重置裁剪
*/
export function useClipTrim({ setClips }: UseClipTrimParams) {
const handleClipTrim = useCallback(
(clipId: string, trimConfig: TrimConfig, newDuration: number) => {
setClips((prev) =>
prev.map((c) =>
c.id === clipId ? { ...c, trim_config: trimConfig, duration: newDuration } : c,
),
)
},
[setClips],
)
const handleClipSplit = useCallback(
(clipId: string, splitRatio: number) => {
setClips((prev) => {
const idx = prev.findIndex((c) => c.id === clipId)
if (idx === -1) return prev
const clip = prev[idx]
const splitPoint = Math.round(clip.duration * splitRatio * 10) / 10
if (splitPoint < 0.5 || splitPoint >= clip.duration - 0.5) return prev
const firstHalf: ClipData = {
...clip,
duration: splitPoint,
trim_config: clip.trim_config
? {
...clip.trim_config,
end_time: clip.trim_config.start_time + splitPoint,
}
: undefined,
}
const secondHalf: ClipData = {
...clip,
id: `clip-${Date.now()}`,
duration: clip.duration - splitPoint,
startOffset: clip.startOffset + splitPoint,
trim_config: clip.trim_config
? {
...clip.trim_config,
start_time: clip.trim_config.start_time + splitPoint,
}
: undefined,
order: (clip.order ?? idx) + 1,
}
const updated = [...prev]
updated[idx] = firstHalf
updated.splice(idx + 1, 0, secondHalf)
return updated.map((c, i) => ({ ...c, order: i }))
})
},
[setClips],
)
const handleClipResetTrim = useCallback(
(clipId: string) => {
setClips((prev) =>
prev.map((c) => {
if (c.id !== clipId || !c.trim_config) return c
const originalDuration = c.trim_config.original_duration ?? c.duration
return {
...c,
duration: originalDuration,
trim_config: undefined,
}
}),
)
},
[setClips],
)
return {
handleClipTrim,
handleClipSplit,
handleClipResetTrim,
}
}
+258 -46
View File
@@ -1,88 +1,300 @@
/**
* 任务历史页面 — V21 设计系统
* 页面头部 + 圆角胶囊 Tab 筛选(含计数)+ 卡片式任务列表 + 分页 + 空状态
* 使用 useQuery 对接后端真实 APIapi/tasks.ts
*/
import React from "react"
import { PageHeader, LoadingState, ErrorState, EmptyState } from "./components/States"
import { HistoryTabs, TaskItem, Pagination } from "./components/TaskList"
import { useTaskHistory } from "./hooks/useTaskHistory"
import React, { useState } from "react"
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
import { Button } from "@/components/ui"
import { getUserTasks, retryTask, type TaskItem } from "@/api/tasks"
import "./history.css"
/* ============================================================
* 类型 & 常量
* ============================================================ */
type TaskStatus = "completed" | "processing" | "pending" | "failed"
const statusLabel: Record<TaskStatus, string> = {
completed: "已完成",
processing: "进行中",
pending: "排队中",
failed: "失败",
}
/** 将后端 status 字符串映射为前端 TaskStatus */
const normalizeStatus = (s: string): TaskStatus => {
const map: Record<string, TaskStatus> = {
completed: "completed",
succeeded: "completed",
success: "completed",
processing: "processing",
running: "processing",
pending: "pending",
queued: "pending",
failed: "failed",
error: "failed",
}
return map[s] ?? "pending"
}
/** 格式化日期 */
const formatDate = (iso?: string | null): string => {
if (!iso) return "—"
const d = new Date(iso)
if (isNaN(d.getTime())) return "—"
const pad = (n: number) => String(n).padStart(2, "0")
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`
}
/* ============================================================
* Tab 配置
* ============================================================ */
interface TabConfig {
key: string
label: string
statusFilter?: TaskStatus
}
const tabs: TabConfig[] = [
{ key: "all", label: "全部" },
{ key: "processing", label: "进行中", statusFilter: "processing" },
{ key: "completed", label: "已完成", statusFilter: "completed" },
{ key: "failed", label: "失败", statusFilter: "failed" },
]
/* ============================================================
* 分页配置
* ============================================================ */
const PAGE_SIZE = 10
/* ============================================================
* 组件
* ============================================================ */
const TaskHistory: React.FC = () => {
const [activeTab, setActiveTab] = useState("all")
const [currentPage, setCurrentPage] = useState(1)
const queryClient = useQueryClient()
// ── 获取任务列表 ──
const {
activeTab,
currentPage,
totalPages,
tabs,
tabCounts,
paginatedTasks,
data: tasks = [],
isLoading,
isError,
error,
retryLoading,
setCurrentPage,
handleTabChange,
handleRetry,
handleView,
refetch,
} = useTaskHistory()
} = useQuery<TaskItem[], Error>({
queryKey: ["tasks"],
queryFn: getUserTasks,
staleTime: 30_000,
})
// Loading 状态
// ── 重试任务 mutation ──
const retryMutation = useMutation({
mutationFn: retryTask,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["tasks"] })
},
})
// 将后端数据映射为页面展示用的结构
const mappedTasks = tasks.map((t) => ({
id: t.id,
name: t.user_message || t.task_type,
type: t.task_type,
template: t.template_id,
status: normalizeStatus(t.status),
date: formatDate(t.created_at),
duration: undefined as string | undefined,
progress: t.progress,
retryable: t.retryable,
errorMessage: t.error_message,
}))
// 获取当前 Tab 的筛选状态
const currentTab = tabs.find((t) => t.key === activeTab)
const statusFilter = currentTab?.statusFilter
// 过滤任务
const filteredTasks = statusFilter
? mappedTasks.filter((t) => t.status === statusFilter)
: mappedTasks
// 计算各 Tab 的数量
const tabCounts: Record<string, number> = {
all: mappedTasks.length,
processing: mappedTasks.filter((t) => t.status === "processing").length,
completed: mappedTasks.filter((t) => t.status === "completed").length,
failed: mappedTasks.filter((t) => t.status === "failed").length,
}
// 分页
const totalPages = Math.ceil(filteredTasks.length / PAGE_SIZE)
const paginatedTasks = filteredTasks.slice((currentPage - 1) * PAGE_SIZE, currentPage * PAGE_SIZE)
// 切换 Tab 时重置页码
const handleTabChange = (key: string) => {
setActiveTab(key)
setCurrentPage(1)
}
// 重试任务
const handleRetry = (taskId: string) => {
retryMutation.mutate(taskId)
}
// 查看任务详情
const handleView = (taskId: string) => {
// TODO: 跳转到任务详情页(待路由实现)
console.log("查看任务:", taskId)
}
// ── Loading 状态 ──
if (isLoading) {
return (
<div className="xx-history-page">
<PageHeader />
<LoadingState />
<div className="xx-history-header">
<h2></h2>
<p></p>
</div>
<div className="xx-history-empty">
<div className="xx-history-empty-icon"></div>
<h3>...</h3>
</div>
</div>
)
}
// Error 状态
// ── Error 状态 ──
if (isError) {
return (
<div className="xx-history-page">
<PageHeader />
<ErrorState message={error?.message} onRetry={() => refetch()} />
<div className="xx-history-header">
<h2></h2>
<p></p>
</div>
<div className="xx-history-empty">
<div className="xx-history-empty-icon"></div>
<h3></h3>
<p>{error?.message || "网络异常,请稍后重试"}</p>
<Button buttonType="primary" buttonSize="md" onClick={() => refetch()}>
</Button>
</div>
</div>
)
}
const totalCount = tabCounts[activeTab] ?? paginatedTasks.length
return (
<div className="xx-history-page">
<PageHeader />
{/* ── 页面头部 ──────────────────────────────────────────── */}
<div className="xx-history-header">
<h2></h2>
<p></p>
</div>
<HistoryTabs
tabs={tabs}
activeTab={activeTab}
tabCounts={tabCounts}
onChange={handleTabChange}
/>
{/* ── Tab 切换 ──────────────────────────────────────────── */}
<div className="xx-history-tabs">
{tabs.map((tab) => (
<button
key={tab.key}
className={`xx-history-tab${activeTab === tab.key ? " active" : ""}`}
onClick={() => handleTabChange(tab.key)}
>
{tab.label}
<span className="xx-history-tab-count">{tabCounts[tab.key]}</span>
</button>
))}
</div>
{/* 任务列表 */}
{/* ── 任务列表 ──────────────────────────────────────────── */}
{paginatedTasks.length === 0 ? (
<EmptyState activeTab={activeTab} />
<div className="xx-history-empty">
<div className="xx-history-empty-icon">📭</div>
<h3></h3>
<p>{activeTab === "all" ? "点击上方按钮开始创建任务" : "当前分类下没有任务"}</p>
</div>
) : (
<div className="xx-history-task-list">
{paginatedTasks.map((task) => (
<TaskItem
key={task.id}
task={task}
onRetry={handleRetry}
onView={handleView}
retryLoading={retryLoading}
/>
<div key={task.id} className="xx-history-task-item">
{/* 任务信息 */}
<div className="xx-history-task-info">
<h4>{task.name}</h4>
<span>
{task.type} · {task.template}
</span>
</div>
{/* 状态标签 */}
<span className={`xx-history-status xx-history-status--${task.status}`}>
{statusLabel[task.status]}
</span>
{/* 时间区 */}
<div className="xx-history-task-time">
<span>{task.date}</span>
{task.status === "completed" ? (
<span></span>
) : task.status === "processing" ? (
<span> {task.progress}%</span>
) : task.status === "failed" ? (
<span>{task.errorMessage || "请重试"}</span>
) : (
<span></span>
)}
</div>
{/* 操作按钮 */}
<div className="xx-history-task-action">
{task.status === "failed" && task.retryable ? (
<Button
buttonType="ghost"
buttonSize="sm"
onClick={() => handleRetry(task.id)}
disabled={retryMutation.isPending}
>
{retryMutation.isPending ? "重试中..." : "重试"}
</Button>
) : (
<Button buttonType="ghost" buttonSize="sm" onClick={() => handleView(task.id)}>
</Button>
)}
</div>
</div>
))}
</div>
)}
<Pagination
currentPage={currentPage}
totalPages={totalPages}
total={totalCount}
onChange={setCurrentPage}
/>
{/* ── 分页 ──────────────────────────────────────────────── */}
{totalPages > 1 && (
<div className="xx-history-pagination">
<button
className="xx-history-page-btn"
disabled={currentPage === 1}
onClick={() => setCurrentPage(currentPage - 1)}
>
</button>
{Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => (
<button
key={page}
className={`xx-history-page-btn${currentPage === page ? " active" : ""}`}
onClick={() => setCurrentPage(page)}
>
{page}
</button>
))}
<button
className="xx-history-page-btn"
disabled={currentPage === totalPages}
onClick={() => setCurrentPage(currentPage + 1)}
>
</button>
<span className="xx-history-page-info"> {filteredTasks.length} </span>
</div>
)}
</div>
)
}
@@ -1,52 +0,0 @@
import React from "react"
import { Button } from "@/components/ui"
interface LoadingStateProps {
title?: string
}
/** 加载状态 */
export const LoadingState: React.FC<LoadingStateProps> = ({ title = "加载中..." }) => (
<div className="xx-history-empty">
<div className="xx-history-empty-icon"></div>
<h3>{title}</h3>
</div>
)
interface ErrorStateProps {
message?: string
onRetry: () => void
}
/** 错误状态 */
export const ErrorState: React.FC<ErrorStateProps> = ({ message, onRetry }) => (
<div className="xx-history-empty">
<div className="xx-history-empty-icon"></div>
<h3></h3>
<p>{message || "网络异常,请稍后重试"}</p>
<Button buttonType="primary" buttonSize="md" onClick={onRetry}>
</Button>
</div>
)
interface EmptyStateProps {
activeTab?: string
}
/** 空状态 */
export const EmptyState: React.FC<EmptyStateProps> = ({ activeTab = "all" }) => (
<div className="xx-history-empty">
<div className="xx-history-empty-icon">📭</div>
<h3></h3>
<p>{activeTab === "all" ? "点击上方按钮开始创建任务" : "当前分类下没有任务"}</p>
</div>
)
/** 页面头部 */
export const PageHeader: React.FC = () => (
<div className="xx-history-header">
<h2></h2>
<p></p>
</div>
)
@@ -1,142 +0,0 @@
import React from "react"
import { Button } from "@/components/ui"
import type { TabConfig } from "../constants"
import type { MappedTask } from "../hooks/useTaskHistory"
import { statusLabel } from "../constants"
interface HistoryTabsProps {
tabs: TabConfig[]
activeTab: string
tabCounts: Record<string, number>
onChange: (key: string) => void
}
/** Tab 切换栏 */
export const HistoryTabs: React.FC<HistoryTabsProps> = ({
tabs,
activeTab,
tabCounts,
onChange,
}) => (
<div className="xx-history-tabs">
{tabs.map((tab) => (
<button
key={tab.key}
className={`xx-history-tab${activeTab === tab.key ? " active" : ""}`}
onClick={() => onChange(tab.key)}
>
{tab.label}
<span className="xx-history-tab-count">{tabCounts[tab.key]}</span>
</button>
))}
</div>
)
interface TaskItemProps {
task: MappedTask
onRetry: (id: string) => void
onView: (id: string) => void
retryLoading?: boolean
}
/** 单个任务卡片 */
export const TaskItem: React.FC<TaskItemProps> = ({ task, onRetry, onView, retryLoading }) => {
const getSubText = () => {
switch (task.status) {
case "completed":
return "完成"
case "processing":
return `进度 ${task.progress}%`
case "failed":
return task.errorMessage || "请重试"
default:
return "等待中"
}
}
return (
<div className="xx-history-task-item">
{/* 任务信息 */}
<div className="xx-history-task-info">
<h4>{task.name}</h4>
<span>
{task.type} · {task.template}
</span>
</div>
{/* 状态标签 */}
<span className={`xx-history-status xx-history-status--${task.status}`}>
{statusLabel[task.status]}
</span>
{/* 时间区 */}
<div className="xx-history-task-time">
<span>{task.date}</span>
<span>{getSubText()}</span>
</div>
{/* 操作按钮 */}
<div className="xx-history-task-action">
{task.status === "failed" && task.retryable ? (
<Button
buttonType="ghost"
buttonSize="sm"
onClick={() => onRetry(task.id)}
disabled={retryLoading}
>
{retryLoading ? "重试中..." : "重试"}
</Button>
) : (
<Button buttonType="ghost" buttonSize="sm" onClick={() => onView(task.id)}>
</Button>
)}
</div>
</div>
)
}
interface PaginationProps {
currentPage: number
totalPages: number
total: number
onChange: (page: number) => void
}
/** 分页组件 */
export const Pagination: React.FC<PaginationProps> = ({
currentPage,
totalPages,
total,
onChange,
}) => {
if (totalPages <= 1) return null
return (
<div className="xx-history-pagination">
<button
className="xx-history-page-btn"
disabled={currentPage === 1}
onClick={() => onChange(currentPage - 1)}
>
</button>
{Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => (
<button
key={page}
className={`xx-history-page-btn${currentPage === page ? " active" : ""}`}
onClick={() => onChange(page)}
>
{page}
</button>
))}
<button
className="xx-history-page-btn"
disabled={currentPage === totalPages}
onClick={() => onChange(currentPage + 1)}
>
</button>
<span className="xx-history-page-info"> {total} </span>
</div>
)
}
-28
View File
@@ -1,28 +0,0 @@
/** 任务状态 */
export type TaskStatus = "completed" | "processing" | "pending" | "failed"
/** 状态标签 */
export const statusLabel: Record<TaskStatus, string> = {
completed: "已完成",
processing: "进行中",
pending: "排队中",
failed: "失败",
}
/** Tab 配置 */
export interface TabConfig {
key: string
label: string
statusFilter?: TaskStatus
}
/** 默认 Tab 列表 */
export const TABS: TabConfig[] = [
{ key: "all", label: "全部" },
{ key: "processing", label: "进行中", statusFilter: "processing" },
{ key: "completed", label: "已完成", statusFilter: "completed" },
{ key: "failed", label: "失败", statusFilter: "failed" },
]
/** 每页数量 */
export const PAGE_SIZE = 10
@@ -1,131 +0,0 @@
import { useState, useMemo, useCallback } from "react"
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
import { getUserTasks, retryTask, type TaskItem } from "@/api/tasks"
import { TABS, PAGE_SIZE, type TabConfig } from "../constants"
import { normalizeStatus, formatDate } from "../utils"
/** 映射后的任务列表项 */
export interface MappedTask {
id: string
name: string
type: string
template?: string
status: "completed" | "processing" | "pending" | "failed"
date: string
progress?: number
retryable?: boolean
errorMessage?: string
}
/**
* 任务历史业务 Hook
*/
export const useTaskHistory = () => {
const [activeTab, setActiveTab] = useState("all")
const [currentPage, setCurrentPage] = useState(1)
const queryClient = useQueryClient()
// 获取任务列表
const {
data: tasks = [],
isLoading,
isError,
error,
refetch,
} = useQuery<TaskItem[], Error>({
queryKey: ["tasks"],
queryFn: getUserTasks,
staleTime: 30_000,
})
// 重试 mutation
const retryMutation = useMutation({
mutationFn: retryTask,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["tasks"] })
},
})
// 映射后端数据
const mappedTasks: MappedTask[] = useMemo(
() =>
tasks.map((t) => ({
id: t.id,
name: t.user_message || t.task_type,
type: t.task_type,
template: t.template_id,
status: normalizeStatus(t.status),
date: formatDate(t.created_at),
progress: t.progress,
retryable: t.retryable,
errorMessage: t.error_message,
})),
[tasks],
)
// 当前 Tab 筛选
const currentTabConfig: TabConfig | undefined = TABS.find((t) => t.key === activeTab)
const statusFilter = currentTabConfig?.statusFilter
// 过滤任务
const filteredTasks = useMemo(
() => (statusFilter ? mappedTasks.filter((t) => t.status === statusFilter) : mappedTasks),
[mappedTasks, statusFilter],
)
// 各 Tab 计数
const tabCounts: Record<string, number> = useMemo(
() => ({
all: mappedTasks.length,
processing: mappedTasks.filter((t) => t.status === "processing").length,
completed: mappedTasks.filter((t) => t.status === "completed").length,
failed: mappedTasks.filter((t) => t.status === "failed").length,
}),
[mappedTasks],
)
// 分页
const totalPages = Math.ceil(filteredTasks.length / PAGE_SIZE)
const paginatedTasks = filteredTasks.slice((currentPage - 1) * PAGE_SIZE, currentPage * PAGE_SIZE)
// 切换 Tab
const handleTabChange = useCallback((key: string) => {
setActiveTab(key)
setCurrentPage(1)
}, [])
// 重试
const handleRetry = useCallback(
(taskId: string) => {
retryMutation.mutate(taskId)
},
[retryMutation],
)
// 查看详情
const handleView = useCallback((taskId: string) => {
// TODO: 跳转到任务详情页(待路由实现)
console.log("查看任务:", taskId)
}, [])
return {
// 状态
activeTab,
currentPage,
totalPages,
// 数据
tabs: TABS,
tabCounts,
paginatedTasks,
isLoading,
isError,
error,
retryLoading: retryMutation.isPending,
// 操作
setCurrentPage,
handleTabChange,
handleRetry,
handleView,
refetch,
}
}
-26
View File
@@ -1,26 +0,0 @@
import type { TaskStatus } from "./constants"
/** 将后端 status 字符串映射为前端 TaskStatus */
export const normalizeStatus = (s: string): TaskStatus => {
const map: Record<string, TaskStatus> = {
completed: "completed",
succeeded: "completed",
success: "completed",
processing: "processing",
running: "processing",
pending: "pending",
queued: "pending",
failed: "failed",
error: "failed",
}
return map[s] ?? "pending"
}
/** 格式化日期 */
export const formatDate = (iso?: string | null): string => {
if (!iso) return "—"
const d = new Date(iso)
if (isNaN(d.getTime())) return "—"
const pad = (n: number) => String(n).padStart(2, "0")
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`
}