diff --git a/apps/web/src/pages/products/ProductLibrary.tsx b/apps/web/src/pages/products/ProductLibrary.tsx index 61fccd9c5..3dd96ee89 100755 --- a/apps/web/src/pages/products/ProductLibrary.tsx +++ b/apps/web/src/pages/products/ProductLibrary.tsx @@ -2,11 +2,18 @@ * 成片库页面 — V21 设计系统 * 卡片网格布局,支持视频播放/下载/分享、批量操作、筛选 * 使用 useQuery 对接后端真实 API(api/products.ts) + * + * 代码结构(三阶段重构后): + * - types.ts: 类型定义 + * - constants.ts: 常量配置 + * - utils/index.ts: 工具函数 + * - components/ProductCard.tsx: 产品卡片组件 + * - components/VideoPlayer.tsx: 视频播放器组件 + * - hooks/useProductList.ts: 列表查询与筛选 + * - hooks/useProductActions.ts: 单个/批量操作 */ -import React, { useMemo, useState } from "react" -import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query" -import { useNavigate } from "react-router-dom" -import { message, Popconfirm } from "antd" +import React, { useState } from "react" +import { Popconfirm, message } from "antd" import { SearchOutlined, VideoCameraOutlined, @@ -16,319 +23,66 @@ import { CloudUploadOutlined, } from "@ant-design/icons" import { Button, Input, Select } from "@/components/ui" -import { - getProducts, - deleteProduct, - getProductDownloadUrl, - updateReviewStatus, - batchDownload, - getBatchDownloadStatus, - type ProductItem as ApiProductItem, - type ReviewStatus, -} from "@/api/products" -import "./products.css" import type { ProductItem } from "./types" -import { mapApiProduct, getNextReviewStatus } from "./utils" import { ProductCard } from "./components/ProductCard" import { VideoPlayer } from "./components/VideoPlayer" +import { useProductList } from "./hooks/useProductList" +import { useProductActions } from "./hooks/useProductActions" +import "./products.css" /* ============================================================ * 主组件 * ============================================================ */ const ProductLibrary: React.FC = () => { - const queryClient = useQueryClient() - const navigate = useNavigate() - - /* ── 获取成品列表 ── */ const { - data: apiProducts = [], + products, + filteredProducts, isLoading, isError, error, refetch, - } = useQuery({ - queryKey: ["products"], - queryFn: () => getProducts(), - staleTime: 30_000, - }) - - // 映射为前端类型,按创建时间倒序排列,防御非数组返回 - const products = useMemo(() => { - const list = (Array.isArray(apiProducts) ? apiProducts : []).map(mapApiProduct) - // 按创建时间倒序(最新的在最前面),无时间的排最后 - return list.sort((a, b) => { - if (!a.date || a.date === "—") return 1 - if (!b.date || b.date === "—") return -1 - return new Date(b.date).getTime() - new Date(a.date).getTime() - }) - }, [apiProducts]) - - /* ── 删除 mutation ── */ - const deleteMutation = useMutation({ - mutationFn: deleteProduct, - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ["products"] }) - message.success("已删除") - }, - onError: () => { - message.error("删除失败") - }, - }) - - /* ── 复核状态 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 [searchText, setSearchText] = useState("") - const [filterStatus, setFilterStatus] = useState("all") - const [filterTime, setFilterTime] = useState("all") - const [filterDuration, setFilterDuration] = useState("all") - const [filterProject, setFilterProject] = useState("all") - const [filterReviewStatus, setFilterReviewStatus] = useState("all") - - /* 批量操作 */ - const [selectedIds, setSelectedIds] = useState>(new Set()) + searchText, + setSearchText, + filterStatus, + setFilterStatus, + filterTime, + setFilterTime, + filterDuration, + setFilterDuration, + filterProject, + setFilterProject, + filterReviewStatus, + setFilterReviewStatus, + projectOptions, + selectedIds, + batchMode, + allSelected, + handleSelectAll, + handleToggleSelect, + clearSelection, + } = useProductList() /* 播放器 */ const [playingProduct, setPlayingProduct] = useState(null) - /* 派生数据 */ - const batchMode = selectedIds.size > 0 - - const filteredProducts = useMemo(() => { - let list = products - - /* 状态筛选 */ - if (filterStatus !== "all") { - list = list.filter((p) => p.status === filterStatus) - } - - /* 时间筛选 */ - if (filterTime !== "all") { - const now = new Date() - list = list.filter((p) => { - const d = new Date(p.date) - const diff = (now.getTime() - d.getTime()) / (1000 * 60 * 60 * 24) - switch (filterTime) { - case "today": - return diff < 1 - case "week": - return diff <= 7 - case "month": - return diff <= 30 - default: - return true - } - }) - } - - /* 时长筛选 */ - if (filterDuration !== "all") { - list = list.filter((p) => { - switch (filterDuration) { - case "short": - return p.duration > 0 && p.duration <= 60 - case "medium": - return p.duration > 60 && p.duration <= 180 - case "long": - return p.duration > 180 - default: - return true - } - }) - } - - /* 项目筛选 */ - if (filterProject !== "all") { - list = list.filter((p) => p.projectId === filterProject) - } - - /* 复核状态筛选 */ - if (filterReviewStatus !== "all") { - if (filterReviewStatus === "none") { - list = list.filter((p) => !p.reviewStatus) - } else { - list = list.filter((p) => p.reviewStatus === filterReviewStatus) - } - } - - /* 搜索 */ - if (searchText.trim()) { - const q = searchText.trim().toLowerCase() - list = list.filter((p) => p.name.toLowerCase().includes(q)) - } - - return list - }, [ + const { + handleDownload, + handleShare, + handleViewDetail, + handleDelete, + handlePublish, + handleReviewStatusChange, + handleBatchDownload, + handleBatchDelete, + handleBatchPublish, + batchDownloading, + } = useProductActions({ + selectedIds, + clearSelection, products, - filterStatus, - filterTime, - filterDuration, - filterProject, - filterReviewStatus, - searchText, - ]) + setPlayingProduct, + }) - /* 全选 */ - const allSelected = - filteredProducts.length > 0 && filteredProducts.every((p) => selectedIds.has(p.id)) - - const handleSelectAll = () => { - if (allSelected) { - setSelectedIds(new Set()) - } else { - setSelectedIds(new Set(filteredProducts.map((p) => p.id))) - } - } - - /* 切换单个选择 */ - const handleToggleSelect = (id: string) => { - setSelectedIds((prev) => { - const next = new Set(prev) - if (next.has(id)) { - next.delete(id) - } else { - next.add(id) - } - return next - }) - } - - /* 下载 — 调用真实 API 获取下载链接 */ - const handleDownload = async (product: ProductItem) => { - if (product.status !== "completed") return - try { - const { url } = await getProductDownloadUrl(product.id) - // 打开下载链接 - const a = document.createElement("a") - a.href = url - a.download = "" - a.click() - message.success(`正在下载"${product.name}"`) - } catch { - message.error("获取下载链接失败") - } - } - - /* 分享 */ - const handleShare = (product: ProductItem) => { - if (product.status !== "completed") return - const link = `${window.location.origin}/share/${product.id}` - navigator.clipboard?.writeText(link).then( - () => message.success(`分享链接已复制:${link}`), - () => message.success(`分享链接:${link}(请手动复制)`), - ) - } - - /* 查看详情 — 跳转到产品详情页 */ - const handleViewDetail = (product: ProductItem) => { - setPlayingProduct(null) // 关闭播放器 - navigate(`/app/products/${product.id}`) - } - - /* 删除 */ - const handleDelete = (id: string) => { - deleteMutation.mutate(id) - setSelectedIds((prev) => { - const next = new Set(prev) - next.delete(id) - return next - }) - } - - /* 发布 — TODO: 后端发布 API 待实现 */ - const handlePublish = (_product: ProductItem) => { - // TODO: 对接后端发布 API(当前后端未提供发布接口) - 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 }) - } - - /* 批量下载 — 使用 batch-download API + 轮询 */ - 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} 个视频...`) - - // 轮询下载状态(最多 60 次,每次 2 秒) - let attempts = 0 - const maxAttempts = 60 - const poll = async (): Promise => { - 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} 个视频`) - setSelectedIds(new Set()) - } 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"] }) - setSelectedIds(new Set()) - message.success(`已批量删除 ${successCount}/${ids.length} 个视频`) - } - - /* 批量发布 */ - const handleBatchPublish = () => { - // TODO: 对接后端批量发布 API - message.info("批量发布功能待后端 API 补齐") - setSelectedIds(new Set()) - } - - // ── Loading 状态 ── if (isLoading) { return (
@@ -440,7 +194,7 @@ const ProductLibrary: React.FC = () => { 批量删除 -
@@ -498,16 +252,7 @@ const ProductLibrary: React.FC = () => { style={{ width: 140 }} options={[ { value: "all", label: "全部项目" }, - ...Array.from( - new Map( - products - .filter((p) => p.projectId && p.projectName) - .map((p) => [p.projectId!, p.projectName!] as const), - ), - ).map(([id, name]) => ({ - value: id as string, - label: name as string, - })), + ...projectOptions, ]} />