Files
xiaoxia-saas/apps/web/src/pages/voices/components/VideoExtractModal.tsx
T
xiaoxia aaf89fc245
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 4s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 7s
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 / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 1m24s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 2m0s
AI Code Review / AI Code Review (pull_request) Successful in 2m8s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 1m26s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 1m53s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m17s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m25s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 2m38s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 6m47s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 7s
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 5m27s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 13s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 36s
fix: unify extract video voice progress text & cleanup file input
1. Unify progress modal text to '正在提取音频,请稍后...' throughout the
   entire upload+extraction flow, instead of showing different text for
   upload vs extraction phases.

2. Clear file input value on modal close to help browser release the
   file handle sooner, reducing residual cache in browser temp storage.
2026-09-04 14:51:11 +08:00

209 lines
5.8 KiB
TypeScript

import React, { useRef } from "react"
import { Modal } from "antd"
import { InboxOutlined, CloseOutlined } from "@ant-design/icons"
interface VideoExtractModalProps {
open: boolean
file: File | null
progress: number | null
isExtracting: boolean
onClose: () => void
onFileSelect: (file: File | null) => void
onExtract: () => void
}
const ACCEPT_TYPES = ".mp4,.mov,.webm"
const VideoExtractModal: React.FC<VideoExtractModalProps> = ({
open,
file,
progress,
isExtracting,
onClose,
onFileSelect,
onExtract,
}) => {
const inputRef = useRef<HTMLInputElement>(null)
return (
<Modal
title={<span style={{ fontSize: 16, fontWeight: 600 }}>提取视频配音</span>}
open={open}
onCancel={() => {
if (inputRef.current) inputRef.current.value = ""
if (isExtracting) return
onClose()
}}
footer={null}
width={480}
maskClosable={!isExtracting}
>
{!file ? (
<div
className="vmat-upload-dropzone"
onClick={() => inputRef.current?.click()}
style={{
border: "2px dashed #d9d9d9",
borderRadius: 8,
padding: "40px 20px",
textAlign: "center",
cursor: "pointer",
transition: "border-color 0.3s",
}}
onMouseEnter={(e) => (e.currentTarget.style.borderColor = "#7c3aed")}
onMouseLeave={(e) => (e.currentTarget.style.borderColor = "#d9d9d9")}
>
<InboxOutlined style={{ fontSize: 32, color: "#7c3aed", marginBottom: 12 }} />
<p style={{ margin: "0 0 8px", fontSize: 14, color: "#333" }}>点击选择视频文件</p>
<span style={{ fontSize: 12, color: "#999" }}>支持 MP4MOVWebM 格式</span>
<input
ref={inputRef}
type="file"
accept={ACCEPT_TYPES}
style={{ display: "none" }}
onChange={(e) => {
const f = e.target.files?.[0]
if (f) onFileSelect(f)
}}
/>
</div>
) : (
<div>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
padding: "12px 16px",
background: "#fafafa",
borderRadius: 8,
marginBottom: 16,
}}
>
<span
style={{
flex: 1,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
fontSize: 14,
fontWeight: 500,
}}
title={file.name}
>
{file.name}
</span>
<span style={{ fontSize: 12, color: "#999", marginLeft: 8, flexShrink: 0 }}>
{(file.size / (1024 * 1024)).toFixed(1)} MB
</span>
{!isExtracting && (
<button
type="button"
onClick={() => {
if (inputRef.current) inputRef.current.value = ""
onFileSelect(null)
}}
style={{
border: "none",
background: "none",
cursor: "pointer",
color: "#999",
marginLeft: 8,
fontSize: 14,
}}
aria-label="移除文件"
>
<CloseOutlined />
</button>
)}
</div>
{progress !== null && (
<div style={{ marginBottom: 12 }}>
<div
style={{
height: 6,
background: "#f0f0f0",
borderRadius: 3,
overflow: "hidden",
}}
>
<div
style={{
height: "100%",
width: `${progress}%`,
background: "linear-gradient(90deg, #7c3aed, #a78bfa)",
borderRadius: 3,
transition: "width 0.3s",
}}
/>
</div>
<div
style={{
textAlign: "right",
fontSize: 12,
color: "#999",
marginTop: 4,
}}
>
{progress}%
</div>
</div>
)}
{isExtracting && (
<p style={{ textAlign: "center", fontSize: 13, color: "#7c3aed", margin: "12px 0 0" }}>
{"正在提取音频,请稍后..."}
</p>
)}
</div>
)}
<div
style={{
display: "flex",
justifyContent: "flex-end",
gap: 8,
marginTop: 24,
}}
>
<button
type="button"
onClick={onClose}
disabled={isExtracting}
style={{
padding: "6px 16px",
borderRadius: 6,
border: "1px solid #d9d9d9",
background: "#fff",
cursor: isExtracting ? "not-allowed" : "pointer",
fontSize: 14,
opacity: isExtracting ? 0.5 : 1,
}}
>
取消
</button>
<button
type="button"
onClick={onExtract}
disabled={!file || isExtracting}
style={{
padding: "6px 16px",
borderRadius: 6,
border: "none",
background: !file || isExtracting ? "#d9d9d9" : "#7c3aed",
color: "#fff",
cursor: !file || isExtracting ? "not-allowed" : "pointer",
fontSize: 14,
fontWeight: 500,
}}
>
{isExtracting ? "提取中..." : "开始提取"}
</button>
</div>
</Modal>
)
}
export default VideoExtractModal