9b06d814bd
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Successful in 37s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m1s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 58s
CI/CD Pipeline / Validate - Code Quality (push) Failing after 2m33s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 1m48s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 1m17s
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 4m31s
CI/CD Pipeline / Integration Tests (push) Successful in 2m43s
CI/CD Pipeline / Unit Tests (push) Failing after 5m37s
CI/CD Pipeline / Build Staging API Image (push) Successful in 11m35s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m12s
CI/CD Pipeline / ACR Image Cleanup (push) Failing after 29s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 2m45s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 5m17s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com> Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
330 lines
9.4 KiB
TypeScript
Executable File
330 lines
9.4 KiB
TypeScript
Executable File
import React from "react"
|
|
import { UploadOutlined, SoundOutlined } from "@ant-design/icons"
|
|
import { Modal, Upload, message } from "antd"
|
|
import { type VoiceGender } from "@/pages/voices/types"
|
|
import { formatFileSize } from "@/pages/voices/utils/format"
|
|
|
|
export interface UploadVoiceModalProps {
|
|
open: boolean
|
|
uploadFile: File | null
|
|
uploadName: string
|
|
uploadGender: VoiceGender
|
|
uploadDesc: string
|
|
uploadProgress: number | null
|
|
onClose: () => void
|
|
onFileSelect: (file: File) => void
|
|
onFileRemove: () => void
|
|
onNameChange: (name: string) => void
|
|
onGenderChange: (gender: VoiceGender) => void
|
|
onDescChange: (desc: string) => void
|
|
onUpload: () => void
|
|
}
|
|
|
|
/** 上传音频弹窗 */
|
|
const UploadVoiceModal: React.FC<UploadVoiceModalProps> = ({
|
|
open,
|
|
uploadFile,
|
|
uploadName,
|
|
uploadGender,
|
|
uploadDesc,
|
|
uploadProgress,
|
|
onClose,
|
|
onFileSelect,
|
|
onFileRemove,
|
|
onNameChange,
|
|
onGenderChange,
|
|
onDescChange,
|
|
onUpload,
|
|
}) => {
|
|
return (
|
|
<Modal
|
|
title="上传音频"
|
|
open={open}
|
|
onCancel={() => {
|
|
if (uploadProgress !== null) return // 上传中不可关闭
|
|
onClose()
|
|
}}
|
|
footer={null}
|
|
width={520}
|
|
maskClosable={uploadProgress === null}
|
|
>
|
|
<div
|
|
style={{
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
gap: 16,
|
|
padding: "8px 0",
|
|
}}
|
|
>
|
|
{/* 拖拽上传区 */}
|
|
<Upload.Dragger
|
|
accept="audio/*"
|
|
maxCount={1}
|
|
beforeUpload={(file) => {
|
|
onFileSelect(file)
|
|
return false
|
|
}}
|
|
onRemove={() => {
|
|
onFileRemove()
|
|
}}
|
|
showUploadList={false}
|
|
disabled={uploadProgress !== null}
|
|
>
|
|
<p
|
|
style={{
|
|
fontSize: 32,
|
|
color: "var(--primary-color)",
|
|
marginBottom: 8,
|
|
}}
|
|
>
|
|
<UploadOutlined />
|
|
</p>
|
|
<p style={{ fontSize: 14, fontWeight: 500, margin: "0 0 4px" }}>
|
|
点击或拖拽音频文件到此处
|
|
</p>
|
|
<p
|
|
style={{
|
|
fontSize: 12,
|
|
color: "var(--text-secondary)",
|
|
margin: 0,
|
|
}}
|
|
>
|
|
支持 MP3、WAV、AAC、FLAC 等格式,最大 200MB
|
|
</p>
|
|
</Upload.Dragger>
|
|
|
|
{/* 已选文件信息 */}
|
|
{uploadFile && (
|
|
<div
|
|
style={{
|
|
padding: "10px 12px",
|
|
background: "var(--bg-secondary)",
|
|
borderRadius: 8,
|
|
display: "flex",
|
|
alignItems: "center",
|
|
gap: 10,
|
|
}}
|
|
>
|
|
<SoundOutlined style={{ fontSize: 18, color: "var(--primary-color)" }} />
|
|
<div style={{ flex: 1, minWidth: 0 }}>
|
|
<div
|
|
style={{
|
|
fontSize: 13,
|
|
fontWeight: 500,
|
|
overflow: "hidden",
|
|
textOverflow: "ellipsis",
|
|
whiteSpace: "nowrap",
|
|
}}
|
|
>
|
|
{uploadFile.name}
|
|
</div>
|
|
<div style={{ fontSize: 11, color: "var(--text-secondary)" }}>
|
|
{formatFileSize(uploadFile.size)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* 上传进度 */}
|
|
{uploadProgress !== null && (
|
|
<div style={{ textAlign: "center", padding: "8px 0" }}>
|
|
<div
|
|
style={{
|
|
fontSize: 22,
|
|
fontWeight: 700,
|
|
color: "var(--primary-color)",
|
|
}}
|
|
>
|
|
{uploadProgress}%
|
|
</div>
|
|
<div style={{ fontSize: 12, color: "var(--text-secondary)" }}>
|
|
{uploadProgress < 100 ? "上传中..." : "处理中..."}
|
|
</div>
|
|
<div
|
|
style={{
|
|
height: 4,
|
|
background: "var(--bg-tertiary)",
|
|
borderRadius: 2,
|
|
marginTop: 8,
|
|
overflow: "hidden",
|
|
}}
|
|
>
|
|
<div
|
|
style={{
|
|
height: "100%",
|
|
width: `${uploadProgress}%`,
|
|
background: "var(--primary-color)",
|
|
borderRadius: 2,
|
|
transition: "width 0.3s ease",
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* 名称 */}
|
|
<div>
|
|
<div
|
|
style={{
|
|
fontSize: 13,
|
|
color: "var(--text-secondary)",
|
|
marginBottom: 6,
|
|
}}
|
|
>
|
|
素材名称
|
|
</div>
|
|
<input
|
|
value={uploadName}
|
|
onChange={(e) => onNameChange(e.target.value)}
|
|
placeholder="输入素材名称"
|
|
maxLength={100}
|
|
disabled={uploadProgress !== null}
|
|
style={{
|
|
width: "100%",
|
|
padding: "8px 12px",
|
|
border: "1px solid var(--border-color)",
|
|
borderRadius: 8,
|
|
fontSize: 13,
|
|
background: "var(--bg-primary)",
|
|
color: "var(--text-primary)",
|
|
outline: "none",
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
{/* 性别选择 */}
|
|
<div>
|
|
<div
|
|
style={{
|
|
fontSize: 13,
|
|
color: "var(--text-secondary)",
|
|
marginBottom: 6,
|
|
}}
|
|
>
|
|
音色性别
|
|
</div>
|
|
<div style={{ display: "flex", gap: 8 }}>
|
|
{(["female", "male", "child"] as VoiceGender[]).map((g) => (
|
|
<button
|
|
key={g}
|
|
type="button"
|
|
onClick={() => onGenderChange(g)}
|
|
disabled={uploadProgress !== null}
|
|
style={{
|
|
flex: 1,
|
|
padding: "6px 0",
|
|
borderRadius: 8,
|
|
border: `1px solid ${uploadGender === g ? "var(--primary-color)" : "var(--border-color)"}`,
|
|
background: uploadGender === g ? "var(--primary-soft)" : "transparent",
|
|
color: uploadGender === g ? "var(--primary-color)" : "var(--text-secondary)",
|
|
fontSize: 13,
|
|
fontWeight: uploadGender === g ? 600 : 400,
|
|
cursor: uploadProgress !== null ? "not-allowed" : "pointer",
|
|
transition: "all 0.2s",
|
|
}}
|
|
>
|
|
{g === "female" ? "女声" : g === "male" ? "男声" : "童声"}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* 描述 */}
|
|
<div>
|
|
<div
|
|
style={{
|
|
fontSize: 13,
|
|
color: "var(--text-secondary)",
|
|
marginBottom: 6,
|
|
}}
|
|
>
|
|
音色描述(可选)
|
|
</div>
|
|
<textarea
|
|
value={uploadDesc}
|
|
onChange={(e) => onDescChange(e.target.value)}
|
|
placeholder="描述这个音色的特点..."
|
|
maxLength={500}
|
|
rows={2}
|
|
disabled={uploadProgress !== null}
|
|
style={{
|
|
width: "100%",
|
|
padding: "8px 12px",
|
|
border: "1px solid var(--border-color)",
|
|
borderRadius: 8,
|
|
fontSize: 13,
|
|
background: "var(--bg-primary)",
|
|
color: "var(--text-primary)",
|
|
outline: "none",
|
|
resize: "vertical",
|
|
fontFamily: "inherit",
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
{/* 操作按钮 */}
|
|
<div
|
|
style={{
|
|
display: "flex",
|
|
justifyContent: "flex-end",
|
|
gap: 10,
|
|
paddingTop: 4,
|
|
}}
|
|
>
|
|
<button
|
|
type="button"
|
|
onClick={onClose}
|
|
disabled={uploadProgress !== null}
|
|
style={{
|
|
padding: "8px 20px",
|
|
borderRadius: 8,
|
|
border: "1px solid var(--border-color)",
|
|
background: "transparent",
|
|
fontSize: 13,
|
|
cursor: uploadProgress !== null ? "not-allowed" : "pointer",
|
|
color: "var(--text-secondary)",
|
|
}}
|
|
>
|
|
取消
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
if (!uploadFile) {
|
|
message.warning("请先选择音频文件")
|
|
return
|
|
}
|
|
if (!uploadName.trim()) {
|
|
message.warning("请输入素材名称")
|
|
return
|
|
}
|
|
onUpload()
|
|
}}
|
|
disabled={!uploadFile || !uploadName.trim() || uploadProgress !== null}
|
|
style={{
|
|
padding: "8px 20px",
|
|
borderRadius: 8,
|
|
border: "none",
|
|
background:
|
|
!uploadFile || !uploadName.trim() || uploadProgress !== null
|
|
? "var(--text-tertiary)"
|
|
: "var(--primary-color)",
|
|
color: "#fff",
|
|
fontSize: 13,
|
|
fontWeight: 600,
|
|
cursor:
|
|
!uploadFile || !uploadName.trim() || uploadProgress !== null
|
|
? "not-allowed"
|
|
: "pointer",
|
|
}}
|
|
>
|
|
{uploadProgress !== null ? "上传中..." : "开始上传"}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</Modal>
|
|
)
|
|
}
|
|
|
|
export default UploadVoiceModal
|