diff --git a/apps/web/src/pages/products/ProductLibrary.tsx b/apps/web/src/pages/products/ProductLibrary.tsx index d2c4ca906..d4d2ea40d 100644 --- a/apps/web/src/pages/products/ProductLibrary.tsx +++ b/apps/web/src/pages/products/ProductLibrary.tsx @@ -1,6 +1,6 @@ /** * 成片库页面 — V21 设计系统 - * 卡片网格布局,支持视频播放/下载/分享、批量操作、筛选 + * 卡片网格布局,支持视频内联播放/下载/分享、批量操作、筛选 * * 主组件仅保留 Hook 组装与整体布局 * 列表查询 → hooks/useProductList @@ -8,15 +8,12 @@ * 筛选栏 → components/ProductFilterBar * 批量操作栏 → components/ProductBatchBar * 空状态 → components/ProductEmptyState - * 产品卡片 → components/ProductCard - * 视频播放 → components/VideoPlayer + * 产品卡片 → components/ProductCard(内联视频播放) */ -import React, { useState } from "react" +import React from "react" import { VideoCameraOutlined, DownloadOutlined } from "@ant-design/icons" import { Button } from "@/components/ui" -import type { ProductItem } from "./types" import { ProductCard } from "./components/ProductCard" -import { VideoPlayer } from "./components/VideoPlayer" import { ProductFilterBar } from "./components/ProductFilterBar" import { ProductBatchBar } from "./components/ProductBatchBar" import { ProductEmptyState } from "./components/ProductEmptyState" @@ -53,13 +50,9 @@ const ProductLibrary: React.FC = () => { clearSelection, } = useProductList() - /* 播放器 */ - const [playingProduct, setPlayingProduct] = useState(null) - const { handleDownload, handleShare, - handleViewDetail, handleDelete, handlePublish, handleReviewStatusChange, @@ -71,7 +64,7 @@ const ProductLibrary: React.FC = () => { selectedIds, clearSelection, products, - setPlayingProduct, + setPlayingProduct: () => {}, // 不再使用弹窗播放 }) // ── Loading 状态 ── @@ -146,7 +139,6 @@ const ProductLibrary: React.FC = () => { isSelected={selectedIds.has(product.id)} batchMode={batchMode} onToggleSelect={handleToggleSelect} - onPlay={setPlayingProduct} onDownload={handleDownload} onShare={handleShare} onDelete={handleDelete} @@ -158,17 +150,6 @@ const ProductLibrary: React.FC = () => { ) : ( )} - - {/* 视频播放弹窗 */} - {playingProduct && ( - setPlayingProduct(null)} - onDownload={handleDownload} - onShare={handleShare} - onViewDetail={handleViewDetail} - /> - )} ) } diff --git a/apps/web/src/pages/products/components/ProductCard.tsx b/apps/web/src/pages/products/components/ProductCard.tsx index d17413d05..366e7762e 100644 --- a/apps/web/src/pages/products/components/ProductCard.tsx +++ b/apps/web/src/pages/products/components/ProductCard.tsx @@ -1,4 +1,4 @@ -import React from "react" +import React, { useState, useRef, useCallback, useEffect } from "react" import { Popconfirm } from "antd" import { CheckOutlined, @@ -7,6 +7,7 @@ import { ShareAltOutlined, DeleteOutlined, CloudUploadOutlined, + VideoCameraOutlined, } from "@ant-design/icons" import type { ProductItem } from "../types" import { statusConfig, reviewStatusConfig } from "../constants" @@ -17,7 +18,6 @@ interface ProductCardProps { isSelected: boolean batchMode: boolean onToggleSelect: (id: string) => void - onPlay: (product: ProductItem) => void onDownload: (product: ProductItem) => void onShare: (product: ProductItem) => void onDelete: (id: string) => void @@ -30,7 +30,6 @@ export const ProductCard: React.FC = ({ isSelected, batchMode, onToggleSelect, - onPlay, onDownload, onShare, onDelete, @@ -38,17 +37,39 @@ export const ProductCard: React.FC = ({ onReviewStatusChange, }) => { const st = statusConfig[product.status] + const videoRef = useRef(null) + const [isPlaying, setIsPlaying] = useState(false) + const [aspectRatio, setAspectRatio] = useState(null) + const [hasVideo, setHasVideo] = useState(!!product.videoUrl) /** 查重率样式 */ const dupClass = product.duplicateRate <= 5 ? "good" : product.duplicateRate <= 15 ? "warn" : "bad" - /** 点击卡片 */ - const handleCardClick = () => { - if (batchMode) { - onToggleSelect(product.id) + /** 视频元数据加载后获取实际比例 */ + const handleVideoLoaded = useCallback(() => { + const v = videoRef.current + if (v && v.videoWidth > 0 && v.videoHeight > 0) { + setAspectRatio(`${v.videoWidth} / ${v.videoHeight}`) + } + }, []) + + /** 视频播放/暂停结束事件 */ + const handlePlay = useCallback(() => setIsPlaying(true), []) + const handlePause = useCallback(() => setIsPlaying(false), []) + + /** 视频出错时降级为缩略图 */ + const handleVideoError = useCallback(() => setHasVideo(false), []) + + /** 点击缩略图区域:有视频则内联播放,否则不响应 */ + const handleThumbClick = () => { + if (batchMode) return + if (!hasVideo || !videoRef.current) return + const v = videoRef.current + if (v.paused) { + v.play() } else { - onPlay(product) + v.pause() } } @@ -58,6 +79,24 @@ export const ProductCard: React.FC = ({ onToggleSelect(product.id) } + /** 卡片容器点击(非批量模式下不再触发弹窗播放) */ + const handleCardClick = () => { + if (batchMode) { + onToggleSelect(product.id) + } + } + + /** 组件卸载时暂停视频 */ + useEffect(() => { + const v = videoRef.current + return () => { + v?.pause() + } + }, []) + + /** 缩略图区域 style:有实际比例则用实际比例,否则默认 16:9 */ + const thumbStyle: React.CSSProperties = aspectRatio ? { aspectRatio } : { aspectRatio: "16 / 9" } + return (
= ({
)} - {/* 缩略图 */} -
- {product.thumbnailUrl ? ( - {product.name} + {/* 视频/缩略图区域 */} +
+ {hasVideo ? ( + <> +