Compare commits

...

2 Commits

Author SHA1 Message Date
CI Bot 80a5203a6f style: auto-format with black + isort + prettier [skip ci-format-check] 2026-07-28 08:48:57 +00:00
xiaoxia 976c5b7215 refactor: split useProductActions into product-actions sub-hooks
CI/CD Pipeline / Staging E2E Tests (pull_request) Blocked by required conditions
CI/CD Pipeline / Staging API Integration Tests (pull_request) Blocked by required conditions
CI/CD Pipeline / Deploy Production (pull_request) Blocked by required conditions
CI/CD Pipeline / Production Browser E2E (pull_request) Blocked by required conditions
CI/CD Pipeline / ACR Image Cleanup (pull_request) Blocked by required conditions
CI/CD Pipeline / Canary Release to Production (pull_request) Blocked by required conditions
Preview Cleanup / Cleanup Preview Environment (pull_request) Waiting to run
ACR Cleanup / ACR Image Cleanup (pull_request_target) Has been cancelled
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 5s
CI/CD Pipeline / Unit Tests (pull_request) Waiting to run
CI/CD Pipeline / Frontend Unit Tests (pull_request) Waiting to run
CI/CD Pipeline / Build Production API Image (pull_request) Waiting to run
CI/CD Pipeline / Build Production Web Image (pull_request) Waiting to run
CI/CD Pipeline / Build Production Worker Image (pull_request) Waiting to run
CI/CD Pipeline / CI Gate (pull_request) Blocked by required conditions
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m37s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m35s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 35s
CI/CD Pipeline / Validate - Code Quality (pull_request) Failing after 3m2s
CI/CD Pipeline / Integration Tests (pull_request) Waiting to run
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 / Deploy Staging (Watchtower auto-deploy) (pull_request) Waiting to run
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 1m50s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 1m35s
PR Automation / Auto Approve on CI Green (pull_request) Failing after 37s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 53s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 3m42s
AI Code Review / AI Code Review (pull_request) Failing after 2m18s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 46m4s
Extract batch download, batch delete, and mutation logic into separate
hooks under product-actions/ directory. Main file reduces from 187 to 96 lines (-49%).

- useBatchDownload: bulk download with polling
- useBatchDelete: sequential batch delete with query invalidation
- useProductMutations: delete and review status mutations
2026-07-28 16:06:09 +08:00
8 changed files with 132 additions and 108 deletions
@@ -0,0 +1,30 @@
import { useQueryClient } from "@tanstack/react-query"
import { message } from "antd"
import { deleteProduct } from "@/api/products"
interface UseBatchDeleteOptions {
selectedIds: Set<string>
clearSelection: () => void
}
export function useBatchDelete({ selectedIds, clearSelection }: UseBatchDeleteOptions) {
const queryClient = useQueryClient()
const handleBatchDelete = async () => {
const ids = Array.from(selectedIds)
let successCount = 0
for (const id of ids) {
try {
await deleteProduct(id)
successCount++
} catch {
// 忽略单个失败
}
}
queryClient.invalidateQueries({ queryKey: ["products"] })
clearSelection()
message.success(`已批量删除 ${successCount}/${ids.length} 个视频`)
}
return { handleBatchDelete }
}
@@ -0,0 +1,53 @@
import { useState, useCallback } from "react"
import { message } from "antd"
import { batchDownload, getBatchDownloadStatus } from "@/api/products"
interface UseBatchDownloadOptions {
selectedIds: Set<string>
clearSelection: () => void
}
export function useBatchDownload({ selectedIds, clearSelection }: UseBatchDownloadOptions) {
const [batchDownloading, setBatchDownloading] = useState(false)
const handleBatchDownload = useCallback(async () => {
const ids = Array.from(selectedIds)
if (ids.length === 0) return
setBatchDownloading(true)
try {
const { job_id } = await batchDownload(ids)
message.info(`批量下载任务已创建,正在打包 ${ids.length} 个视频...`)
let attempts = 0
const maxAttempts = 60
const poll = async (): Promise<void> => {
if (attempts >= maxAttempts) {
message.warning("打包超时,请稍后在消息中心查看")
return
}
attempts++
await new Promise((r) => setTimeout(r, 2000))
const status = await getBatchDownloadStatus(job_id)
if (status.status === "completed" && status.download_url) {
const a = document.createElement("a")
a.href = status.download_url
a.download = ""
a.click()
message.success(`已打包下载 ${ids.length} 个视频`)
clearSelection()
} else if (status.status === "failed") {
message.error("批量下载失败,请重试")
} else {
await poll()
}
}
await poll()
} catch {
message.error("发起批量下载失败")
} finally {
setBatchDownloading(false)
}
}, [selectedIds, clearSelection])
return { batchDownloading, handleBatchDownload }
}
@@ -0,0 +1,37 @@
import { useMutation, useQueryClient } from "@tanstack/react-query"
import { message } from "antd"
import { deleteProduct, updateReviewStatus, type ReviewStatus } from "@/api/products"
export function useProductMutations() {
const queryClient = useQueryClient()
const deleteMutation = useMutation({
mutationFn: deleteProduct,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["products"] })
message.success("已删除")
},
onError: () => {
message.error("删除失败")
},
})
const reviewMutation = useMutation({
mutationFn: ({ id, status }: { id: string; status: ReviewStatus }) =>
updateReviewStatus(id, status),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["products"] })
message.success("复核状态已更新")
},
onError: () => {
message.error("更新复核状态失败")
},
})
return {
deleteMutation,
reviewMutation,
isDeleting: deleteMutation.isPending,
isUpdatingReview: reviewMutation.isPending,
}
}
+12 -104
View File
@@ -1,17 +1,11 @@
import { useState } from "react"
import { useMutation, useQueryClient } from "@tanstack/react-query"
import { useNavigate } from "react-router-dom"
import { message } from "antd"
import {
deleteProduct,
getProductDownloadUrl,
updateReviewStatus,
batchDownload,
getBatchDownloadStatus,
type ReviewStatus,
} from "@/api/products"
import { getProductDownloadUrl } from "@/api/products"
import type { ProductItem } from "../types"
import { getNextReviewStatus } from "../utils"
import { useBatchDownload } from "./product-actions/useBatchDownload"
import { useBatchDelete } from "./product-actions/useBatchDelete"
import { useProductMutations } from "./product-actions/useProductMutations"
interface UseProductActionsOptions {
selectedIds: Set<string>
@@ -26,35 +20,17 @@ export const useProductActions = ({
products,
setPlayingProduct,
}: UseProductActionsOptions) => {
const queryClient = useQueryClient()
const navigate = useNavigate()
/* ── 删除 mutation ── */
const deleteMutation = useMutation({
mutationFn: deleteProduct,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["products"] })
message.success("已删除")
},
onError: () => {
message.error("删除失败")
},
const { deleteMutation, reviewMutation, isDeleting, isUpdatingReview } = useProductMutations()
const { batchDownloading, handleBatchDownload } = useBatchDownload({
selectedIds,
clearSelection,
})
/* ── 复核状态 mutation ── */
const reviewMutation = useMutation({
mutationFn: ({ id, status }: { id: string; status: ReviewStatus }) =>
updateReviewStatus(id, status),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["products"] })
message.success("复核状态已更新")
},
onError: () => {
message.error("更新复核状态失败")
},
})
const { handleBatchDelete } = useBatchDelete({ selectedIds, clearSelection })
/* 下载 — 调用真实 API 获取下载链接 */
const handleDownload = async (product: ProductItem) => {
if (product.status !== "completed") return
try {
@@ -69,7 +45,6 @@ export const useProductActions = ({
}
}
/* 分享 */
const handleShare = (product: ProductItem) => {
if (product.status !== "completed") return
const link = `${window.location.origin}/share/${product.id}`
@@ -79,109 +54,42 @@ export const useProductActions = ({
)
}
/* 查看详情 — 跳转到产品详情页 */
const handleViewDetail = (product: ProductItem) => {
setPlayingProduct(null)
navigate(`/app/products/${product.id}`)
}
/* 删除 */
const handleDelete = (id: string) => {
deleteMutation.mutate(id)
}
/* 发布 — TODO: 后端发布 API 待实现 */
const handlePublish = (_product: ProductItem) => {
message.info("发布功能待后端 API 补齐")
}
/* 切换复核状态 */
const handleReviewStatusChange = (id: string) => {
const current = products.find((p) => p.id === id)?.reviewStatus
const nextStatus = getNextReviewStatus(current)
reviewMutation.mutate({ id, status: nextStatus })
}
/* 批量下载 */
const [batchDownloading, setBatchDownloading] = useState(false)
const handleBatchDownload = async () => {
const ids = Array.from(selectedIds)
if (ids.length === 0) return
setBatchDownloading(true)
try {
const { job_id } = await batchDownload(ids)
message.info(`批量下载任务已创建,正在打包 ${ids.length} 个视频...`)
let attempts = 0
const maxAttempts = 60
const poll = async (): Promise<void> => {
if (attempts >= maxAttempts) {
message.warning("打包超时,请稍后在消息中心查看")
return
}
attempts++
await new Promise((r) => setTimeout(r, 2000))
const status = await getBatchDownloadStatus(job_id)
if (status.status === "completed" && status.download_url) {
const a = document.createElement("a")
a.href = status.download_url
a.download = ""
a.click()
message.success(`已打包下载 ${ids.length} 个视频`)
clearSelection()
} else if (status.status === "failed") {
message.error("批量下载失败,请重试")
} else {
await poll()
}
}
await poll()
} catch {
message.error("发起批量下载失败")
} finally {
setBatchDownloading(false)
}
}
/* 批量删除 */
const handleBatchDelete = async () => {
const ids = Array.from(selectedIds)
let successCount = 0
for (const id of ids) {
try {
await deleteProduct(id)
successCount++
} catch {
// 忽略单个失败
}
}
queryClient.invalidateQueries({ queryKey: ["products"] })
clearSelection()
message.success(`已批量删除 ${successCount}/${ids.length} 个视频`)
}
/* 批量发布 */
const handleBatchPublish = () => {
message.info("批量发布功能待后端 API 补齐")
clearSelection()
}
return {
// 单个操作
handleDownload,
handleShare,
handleViewDetail,
handleDelete,
handlePublish,
handleReviewStatusChange,
// 批量操作
handleBatchDownload,
handleBatchDelete,
handleBatchPublish,
batchDownloading,
// mutation 状态
isDeleting: deleteMutation.isPending,
isUpdatingReview: reviewMutation.isPending,
isDeleting,
isUpdatingReview,
}
}
@@ -6,7 +6,6 @@
from __future__ import annotations
# ── 单轨时间计算 ──────────────────────────────────────────────────────────────
@@ -13,7 +13,6 @@
from __future__ import annotations
from packages.domain.speed_config import (
DEFAULT_SPEED,
SpeedConfig,
@@ -6,7 +6,6 @@ domain 层纯逻辑模块,0 FFmpeg 依赖,快速轻量。
from __future__ import annotations
import dataclasses
from unittest.mock import MagicMock
import pytest
-1
View File
@@ -4,7 +4,6 @@ from __future__ import annotations
import dataclasses
import pytest
from packages.domain.transition_presets import (