6ff8d48c15
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 2s
CI/CD Pipeline / Check push changed paths (push) Successful in 4s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 1m23s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 2m39s
CI/CD Pipeline / Build Staging API Image (push) Successful in 2m45s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m9s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 4m10s
CI/CD Pipeline / Validate - Style (push) Successful in 4m33s
CI/CD Pipeline / Integration Tests (push) Successful in 5m1s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m42s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 2m32s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m47s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m10s
CI/CD Pipeline / Validate - Security (push) Successful in 10m36s
CI/CD Pipeline / Unit Tests (push) Successful in 10m43s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Failing after 60h28m16s
CI/CD Pipeline / Frontend Lint (push) Failing after 60h39m0s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 60h35m47s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 60h38m38s
CI/CD Pipeline / PR Build API Image (push) Failing after 60h38m38s
CI/CD Pipeline / Build Production Web Image (push) Failing after 60h27m50s
CI/CD Pipeline / CI Gate (push) Failing after 60h27m50s
CI/CD Pipeline / Canary Release to Production (push) Failing after 60h27m46s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 60h35m46s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 60h35m46s
CI/CD Pipeline / PR Build Web Image (push) Failing after 60h38m38s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 60h38m40s
CI/CD Pipeline / Deploy Production (push) Failing after 60h27m46s
CI/CD Pipeline / Build Production API Image (push) Failing after 60h27m50s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com> Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
109 lines
2.9 KiB
TypeScript
109 lines
2.9 KiB
TypeScript
import React, { useCallback, useEffect, useRef, useState } from "react"
|
|
import { formatDuration } from "../utils/formatDuration"
|
|
|
|
interface PreviewProgressBarProps {
|
|
currentTime: number
|
|
totalDuration: number
|
|
onSeek: (time: number) => void
|
|
compact?: boolean
|
|
}
|
|
|
|
/**
|
|
* 进度条组件:点击/拖拽 seek
|
|
*/
|
|
export const PreviewProgressBar: React.FC<PreviewProgressBarProps> = ({
|
|
currentTime,
|
|
totalDuration,
|
|
onSeek,
|
|
compact = false,
|
|
}) => {
|
|
const progressRef = useRef<HTMLDivElement>(null)
|
|
const [isDragging, setIsDragging] = useState(false)
|
|
|
|
const seekByClientX = useCallback(
|
|
(clientX: number) => {
|
|
if (!progressRef.current || totalDuration <= 0) return
|
|
const rect = progressRef.current.getBoundingClientRect()
|
|
const ratio = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width))
|
|
onSeek(ratio * totalDuration)
|
|
},
|
|
[totalDuration, onSeek],
|
|
)
|
|
|
|
const handleMouseDown = useCallback(
|
|
(e: React.MouseEvent<HTMLDivElement>) => {
|
|
setIsDragging(true)
|
|
seekByClientX(e.clientX)
|
|
},
|
|
[seekByClientX],
|
|
)
|
|
|
|
useEffect(() => {
|
|
if (!isDragging) return
|
|
const handleMouseMove = (e: MouseEvent) => seekByClientX(e.clientX)
|
|
const handleMouseUp = () => setIsDragging(false)
|
|
window.addEventListener("mousemove", handleMouseMove)
|
|
window.addEventListener("mouseup", handleMouseUp)
|
|
return () => {
|
|
window.removeEventListener("mousemove", handleMouseMove)
|
|
window.removeEventListener("mouseup", handleMouseUp)
|
|
}
|
|
}, [isDragging, seekByClientX])
|
|
|
|
const progressPercent = totalDuration > 0 ? (currentTime / totalDuration) * 100 : 0
|
|
|
|
return (
|
|
<>
|
|
<span
|
|
style={{
|
|
fontSize: compact ? 10 : 11,
|
|
color: "rgba(255,255,255,0.85)",
|
|
minWidth: compact ? 58 : 72,
|
|
fontVariantNumeric: "tabular-nums",
|
|
letterSpacing: 0.2,
|
|
}}
|
|
>
|
|
{formatDuration(currentTime)} / {formatDuration(totalDuration)}
|
|
</span>
|
|
|
|
<div
|
|
ref={progressRef}
|
|
onMouseDown={handleMouseDown}
|
|
style={{
|
|
flex: 1,
|
|
height: 3,
|
|
background: "rgba(255,255,255,0.2)",
|
|
borderRadius: 2,
|
|
cursor: "pointer",
|
|
position: "relative",
|
|
}}
|
|
>
|
|
<div
|
|
style={{
|
|
height: "100%",
|
|
width: `${progressPercent}%`,
|
|
background: "#fff",
|
|
borderRadius: 2,
|
|
transition: isDragging ? "none" : "width 0.1s linear",
|
|
}}
|
|
/>
|
|
<div
|
|
style={{
|
|
position: "absolute",
|
|
top: "50%",
|
|
left: `${progressPercent}%`,
|
|
transform: "translate(-50%, -50%)",
|
|
width: 10,
|
|
height: 10,
|
|
borderRadius: "50%",
|
|
background: "#fff",
|
|
boxShadow: "0 0 6px rgba(255,255,255,0.5)",
|
|
opacity: isDragging ? 1 : 0,
|
|
transition: "opacity 0.15s",
|
|
}}
|
|
/>
|
|
</div>
|
|
</>
|
|
)
|
|
}
|