diff --git a/apps/web/src/pages/products/ProductDetail.tsx b/apps/web/src/pages/products/ProductDetail.tsx old mode 100644 new mode 100755 index a83ae9719..d85e4b1c7 --- a/apps/web/src/pages/products/ProductDetail.tsx +++ b/apps/web/src/pages/products/ProductDetail.tsx @@ -3,252 +3,28 @@ * 路由:/app/products/:id * 展示视频播放器 + 完整元数据 + 下载/分享/删除操作 */ -import React, { useRef, useState, useEffect, useCallback } from "react" -import { useParams, useNavigate } from "react-router-dom" -import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query" -import { - ArrowLeftOutlined, - DownloadOutlined, - ShareAltOutlined, - DeleteOutlined, - PlayCircleFilled, - PauseCircleFilled, - SoundOutlined, - MutedOutlined, - ExpandOutlined, - LoadingOutlined, - WarningOutlined, -} from "@ant-design/icons" -import { - getProduct, - deleteProduct, - getProductDownloadUrl, - type ProductItem, -} from "../../api/products" +import React from "react" +import { LoadingOutlined, WarningOutlined, ArrowLeftOutlined } from "@ant-design/icons" import { Button } from "../../components/ui" +import { DetailHeader } from "./components/DetailHeader" +import { DetailVideoPlayer } from "./components/DetailVideoPlayer" +import { ProductInfoPanel } from "./components/ProductInfoPanel" +import { useProductDetail } from "./hooks/useProductDetail" import "./products.css" -/* ============================================================ - * 工具函数 - * ============================================================ */ - -/** 格式化时长(秒 → "MM:SS") */ -const formatDuration = (seconds: number): string => { - if (!seconds || seconds <= 0) return "00:00" - const m = Math.floor(seconds / 60) - const s = Math.floor(seconds % 60) - return `${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}` -} - -/** 格式化文件大小(MB) */ -const formatFileSize = (mb: number): string => { - if (!mb || mb <= 0) return "-" - if (mb < 1024) return `${mb.toFixed(1)} MB` - return `${(mb / 1024).toFixed(2)} GB` -} - -/** 格式化日期 */ -const formatDate = (dateStr: string): string => { - if (!dateStr) return "-" - const d = new Date(dateStr) - return d.toLocaleDateString("zh-CN", { - year: "numeric", - month: "2-digit", - day: "2-digit", - hour: "2-digit", - minute: "2-digit", - }) -} - -/** 状态标签 */ -const STATUS_MAP: Record = { - completed: { label: "已完成", color: "#10b981" }, - processing: { label: "处理中", color: "#6366f1" }, - pending: { label: "待处理", color: "#f59e0b" }, - failed: { label: "失败", color: "#ef4444" }, -} - -/* ============================================================ - * 主组件 - * ============================================================ */ const ProductDetail: React.FC = () => { - const { id } = useParams<{ id: string }>() - const navigate = useNavigate() - const queryClient = useQueryClient() - - /* ── 获取产品详情 ── */ const { - data: product, + product, isLoading, isError, error, - } = useQuery({ - queryKey: ["product", id], - queryFn: () => getProduct(id!), - enabled: !!id, - staleTime: 10_000, - }) - - /* ── 删除 mutation ── */ - const deleteMutation = useMutation({ - mutationFn: () => deleteProduct(id!), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ["products"] }) - navigate("/app/products") - }, - }) - - /* ── 视频播放器状态 ── */ - const videoRef = useRef(null) - const progressRef = useRef(null) - const hideTimerRef = useRef>() - - const [isPlaying, setIsPlaying] = useState(false) - const [currentTime, setCurrentTime] = useState(0) - const [duration, setDuration] = useState(0) - const [buffered, setBuffered] = useState(0) - const [volume, setVolume] = useState(1) - const [isMuted, setIsMuted] = useState(false) - const [showControls, setShowControls] = useState(true) - const [isFullscreen, setIsFullscreen] = useState(false) - const containerRef = useRef(null) - - /* ── 自动隐藏控制条 ── */ - const resetHideTimer = useCallback(() => { - setShowControls(true) - if (hideTimerRef.current) clearTimeout(hideTimerRef.current) - if (isPlaying) { - hideTimerRef.current = setTimeout(() => setShowControls(false), 3000) - } - }, [isPlaying]) - - /* ── 播放控制 ── */ - const togglePlay = useCallback(() => { - const v = videoRef.current - if (!v) return - if (v.paused) { - v.play().catch(() => {}) - } else { - v.pause() - } - }, []) - - const handleSeek = useCallback( - (e: React.MouseEvent) => { - const v = videoRef.current - const bar = progressRef.current - if (!v || !bar || !duration) return - const rect = bar.getBoundingClientRect() - const ratio = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width)) - v.currentTime = ratio * duration - }, - [duration], - ) - - const handleVolumeChange = useCallback((e: React.ChangeEvent) => { - const v = videoRef.current - const val = parseFloat(e.target.value) - if (v) v.volume = val - setVolume(val) - setIsMuted(val === 0) - }, []) - - const toggleMute = useCallback(() => { - const v = videoRef.current - if (!v) return - if (isMuted) { - v.muted = false - v.volume = volume || 1 - setIsMuted(false) - } else { - v.muted = true - setIsMuted(true) - } - }, [isMuted, volume]) - - const toggleFullscreen = useCallback(() => { - const el = containerRef.current - if (!el) return - if (!document.fullscreenElement) { - el.requestFullscreen?.().catch(() => {}) - } else { - document.exitFullscreen?.().catch(() => {}) - } - }, []) - - /* ── 视频事件监听 ── */ - useEffect(() => { - const v = videoRef.current - if (!v) return - - const onPlay = () => setIsPlaying(true) - const onPause = () => setIsPlaying(false) - const onTimeUpdate = () => setCurrentTime(v.currentTime) - const onLoadedMetadata = () => setDuration(v.duration) - const onProgress = () => { - if (v.buffered.length > 0) { - setBuffered(v.buffered.end(v.buffered.length - 1)) - } - } - const onEnded = () => setIsPlaying(false) - const onFSChange = () => setIsFullscreen(!!document.fullscreenElement) - - v.addEventListener("play", onPlay) - v.addEventListener("pause", onPause) - v.addEventListener("timeupdate", onTimeUpdate) - v.addEventListener("loadedmetadata", onLoadedMetadata) - v.addEventListener("progress", onProgress) - v.addEventListener("ended", onEnded) - document.addEventListener("fullscreenchange", onFSChange) - - return () => { - v.removeEventListener("play", onPlay) - v.removeEventListener("pause", onPause) - v.removeEventListener("timeupdate", onTimeUpdate) - v.removeEventListener("loadedmetadata", onLoadedMetadata) - v.removeEventListener("progress", onProgress) - v.removeEventListener("ended", onEnded) - document.removeEventListener("fullscreenchange", onFSChange) - } - }, []) - - /* 播放时自动隐藏/显示控制条 */ - useEffect(() => { - resetHideTimer() - return () => { - if (hideTimerRef.current) clearTimeout(hideTimerRef.current) - } - }, [isPlaying, resetHideTimer]) - - /* ── 下载 ── */ - const handleDownload = async () => { - if (!product || product.status !== "completed") return - try { - const { url } = await getProductDownloadUrl(product.id) - const a = document.createElement("a") - a.href = url - a.download = "" - a.click() - } catch { - // message.error handled by caller - } - } - - /* ── 分享 ── */ - const handleShare = () => { - if (!product) return - const link = `${window.location.origin}/app/products/${product.id}` - navigator.clipboard?.writeText(link).then( - () => {}, - () => {}, - ) - } - - /* ── 删除 ── */ - const handleDelete = () => { - if (!id) return - deleteMutation.mutate() - } + goBack, + handleDownload, + handleShare, + handleDelete, + deleteLoading, + canDownload, + } = useProductDetail() /* ── 加载状态 ── */ if (isLoading) { @@ -270,7 +46,7 @@ const ProductDetail: React.FC = () => {

加载失败

{error?.message || "无法获取产品信息"}

- @@ -278,170 +54,24 @@ const ProductDetail: React.FC = () => { ) } - const statusInfo = STATUS_MAP[product.status] || { - label: product.status, - color: "#94a3b8", - } - const progress = Math.round((currentTime / (duration || 1)) * 100) - const bufferedPct = Math.round((buffered / (duration || 1)) * 100) - return (
- {/* ── 顶部导航 ── */} -
- -
- - - -
-
+ - {/* ── 主体内容 ── */}
- {/* 视频播放器 */} -
- {product.video_url ? ( -