fix: 上传音频弹窗修复 — 去掉性别选择 + 优化错误提示 + 更新文案 #1258
@@ -117,11 +117,9 @@ const VoiceLibrary: React.FC = () => {
|
||||
uploadOpen,
|
||||
uploadFile,
|
||||
uploadName,
|
||||
uploadGender,
|
||||
uploadDesc,
|
||||
uploadProgress,
|
||||
setUploadName,
|
||||
setUploadGender,
|
||||
setUploadDesc,
|
||||
setUploadOpen,
|
||||
handleFileSelect,
|
||||
@@ -252,14 +250,12 @@ const VoiceLibrary: React.FC = () => {
|
||||
uploadOpen={uploadOpen}
|
||||
uploadFile={uploadFile}
|
||||
uploadName={uploadName}
|
||||
uploadGender={uploadGender}
|
||||
uploadDesc={uploadDesc}
|
||||
uploadProgress={uploadProgress}
|
||||
onUploadClose={handleUploadClose}
|
||||
onFileSelect={handleFileSelect}
|
||||
onFileRemove={handleFileRemove}
|
||||
onNameChange={setUploadName}
|
||||
onGenderChange={setUploadGender}
|
||||
onDescChange={setUploadDesc}
|
||||
onUpload={handleUpload}
|
||||
ttsOpen={ttsOpen}
|
||||
|
||||
@@ -14,14 +14,12 @@ const UploadVoiceModal: React.FC<UploadVoiceModalProps> = ({
|
||||
open,
|
||||
uploadFile,
|
||||
uploadName,
|
||||
uploadGender,
|
||||
uploadDesc,
|
||||
uploadProgress,
|
||||
onClose,
|
||||
onFileSelect,
|
||||
onFileRemove,
|
||||
onNameChange,
|
||||
onGenderChange,
|
||||
onDescChange,
|
||||
onUpload,
|
||||
}) => {
|
||||
@@ -30,7 +28,7 @@ const UploadVoiceModal: React.FC<UploadVoiceModalProps> = ({
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="上传音频"
|
||||
title="上传配音素材"
|
||||
open={open}
|
||||
onCancel={() => {
|
||||
if (uploading) return // 上传中不可关闭
|
||||
@@ -64,11 +62,9 @@ const UploadVoiceModal: React.FC<UploadVoiceModalProps> = ({
|
||||
{/* 表单字段 */}
|
||||
<FormFields
|
||||
name={uploadName}
|
||||
gender={uploadGender}
|
||||
desc={uploadDesc}
|
||||
disabled={uploading}
|
||||
onNameChange={onNameChange}
|
||||
onGenderChange={onGenderChange}
|
||||
onDescChange={onDescChange}
|
||||
/>
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* VoiceLibrary 弹窗集合
|
||||
*/
|
||||
import React from "react"
|
||||
import type { ClonedVoiceDisplay, PresetVoiceDisplay, VoiceGender } from "../types"
|
||||
import type { ClonedVoiceDisplay, PresetVoiceDisplay } from "../types"
|
||||
import type { VoiceClone } from "@/api/voice-clone"
|
||||
import type { TtsStatus } from "./TtsModal"
|
||||
import CloneModal from "@/components/voice/CloneModal"
|
||||
@@ -27,14 +27,12 @@ export interface VoiceModalsProps {
|
||||
uploadOpen: boolean
|
||||
uploadFile: File | null
|
||||
uploadName: string
|
||||
uploadGender: VoiceGender
|
||||
uploadDesc: string
|
||||
uploadProgress: number | null
|
||||
onUploadClose: () => void
|
||||
onFileSelect: (file: File) => void
|
||||
onFileRemove: () => void
|
||||
onNameChange: (name: string) => void
|
||||
onGenderChange: (gender: VoiceGender) => void
|
||||
onDescChange: (desc: string) => void
|
||||
onUpload: () => void
|
||||
|
||||
@@ -67,14 +65,12 @@ export const VoiceModals: React.FC<VoiceModalsProps> = ({
|
||||
uploadOpen,
|
||||
uploadFile,
|
||||
uploadName,
|
||||
uploadGender,
|
||||
uploadDesc,
|
||||
uploadProgress,
|
||||
onUploadClose,
|
||||
onFileSelect,
|
||||
onFileRemove,
|
||||
onNameChange,
|
||||
onGenderChange,
|
||||
onDescChange,
|
||||
onUpload,
|
||||
ttsOpen,
|
||||
@@ -113,14 +109,12 @@ export const VoiceModals: React.FC<VoiceModalsProps> = ({
|
||||
open={uploadOpen}
|
||||
uploadFile={uploadFile}
|
||||
uploadName={uploadName}
|
||||
uploadGender={uploadGender}
|
||||
uploadDesc={uploadDesc}
|
||||
uploadProgress={uploadProgress}
|
||||
onClose={onUploadClose}
|
||||
onFileSelect={onFileSelect}
|
||||
onFileRemove={onFileRemove}
|
||||
onNameChange={onNameChange}
|
||||
onGenderChange={onGenderChange}
|
||||
onDescChange={onDescChange}
|
||||
onUpload={onUpload}
|
||||
/>
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
import React from "react"
|
||||
import type { VoiceGender } from "@/pages/voices/types"
|
||||
import { GENDER_OPTIONS, UPLOAD_CONFIG } from "./types"
|
||||
import { UPLOAD_CONFIG } from "./types"
|
||||
|
||||
interface FormFieldsProps {
|
||||
name: string
|
||||
gender: VoiceGender
|
||||
desc: string
|
||||
disabled: boolean
|
||||
onNameChange: (name: string) => void
|
||||
onGenderChange: (gender: VoiceGender) => void
|
||||
onDescChange: (desc: string) => void
|
||||
}
|
||||
|
||||
@@ -31,64 +28,33 @@ const inputStyle: React.CSSProperties = {
|
||||
|
||||
const FormFields: React.FC<FormFieldsProps> = ({
|
||||
name,
|
||||
gender,
|
||||
desc,
|
||||
disabled,
|
||||
onNameChange,
|
||||
onGenderChange,
|
||||
onDescChange,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
{/* 名称 */}
|
||||
<div>
|
||||
<div style={labelStyle}>素材名称</div>
|
||||
<div style={labelStyle}>配音名称</div>
|
||||
<input
|
||||
value={name}
|
||||
onChange={(e) => onNameChange(e.target.value)}
|
||||
placeholder="输入素材名称"
|
||||
placeholder="输入配音名称"
|
||||
maxLength={UPLOAD_CONFIG.maxNameLength}
|
||||
disabled={disabled}
|
||||
style={inputStyle}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 性别选择 */}
|
||||
<div>
|
||||
<div style={labelStyle}>音色性别</div>
|
||||
<div style={{ display: "flex", gap: 8 }}>
|
||||
{GENDER_OPTIONS.map((opt) => (
|
||||
<button
|
||||
key={opt.value}
|
||||
type="button"
|
||||
onClick={() => onGenderChange(opt.value)}
|
||||
disabled={disabled}
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: "6px 0",
|
||||
borderRadius: 8,
|
||||
border: `1px solid ${gender === opt.value ? "var(--primary-color)" : "var(--border-color)"}`,
|
||||
background: gender === opt.value ? "var(--primary-soft)" : "transparent",
|
||||
color: gender === opt.value ? "var(--primary-color)" : "var(--text-secondary)",
|
||||
fontSize: 13,
|
||||
fontWeight: gender === opt.value ? 600 : 400,
|
||||
cursor: disabled ? "not-allowed" : "pointer",
|
||||
transition: "all 0.2s",
|
||||
}}
|
||||
>
|
||||
{opt.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 描述 */}
|
||||
<div>
|
||||
<div style={labelStyle}>音色描述(可选)</div>
|
||||
<div style={labelStyle}>配音描述(可选)</div>
|
||||
<textarea
|
||||
value={desc}
|
||||
onChange={(e) => onDescChange(e.target.value)}
|
||||
placeholder="描述这个音色的特点..."
|
||||
placeholder="描述这段配音的内容..."
|
||||
maxLength={UPLOAD_CONFIG.maxDescLength}
|
||||
rows={2}
|
||||
disabled={disabled}
|
||||
|
||||
@@ -1,27 +1,17 @@
|
||||
import type { VoiceGender } from "@/pages/voices/types"
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
export const GENDER_OPTIONS: { value: VoiceGender; label: string }[] = [
|
||||
{ value: "female", label: "女声" },
|
||||
{ value: "male", label: "男声" },
|
||||
{ value: "child", label: "童声" },
|
||||
]
|
||||
|
||||
export const UPLOAD_CONFIG = {
|
||||
maxSizeMB: 200,
|
||||
maxNameLength: 100,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useState, useCallback } from "react"
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query"
|
||||
import { type VoiceGender } from "../types"
|
||||
import { uploadAssetDirect, getAssetLibraries, createAsset } from "@/api/assets"
|
||||
import { getAudioDuration } from "../utils/audio"
|
||||
import { buildVoiceMetadata } from "../types"
|
||||
@@ -19,17 +18,11 @@ export function useVoiceUpload({ showToast }: UseVoiceUploadProps) {
|
||||
const [uploadOpen, setUploadOpen] = useState(false)
|
||||
const [uploadFile, setUploadFile] = useState<File | null>(null)
|
||||
const [uploadName, setUploadName] = useState("")
|
||||
const [uploadGender, setUploadGender] = useState<VoiceGender>("female")
|
||||
const [uploadDesc, setUploadDesc] = useState("")
|
||||
const [uploadProgress, setUploadProgress] = useState<number | null>(null)
|
||||
|
||||
const uploadMutation = useMutation({
|
||||
mutationFn: async (data: {
|
||||
file: File
|
||||
name: string
|
||||
gender: VoiceGender
|
||||
description: string
|
||||
}) => {
|
||||
mutationFn: async (data: { file: File; name: string; description: string }) => {
|
||||
setUploadProgress(0)
|
||||
try {
|
||||
/* 获取或创建默认配音库 */
|
||||
@@ -57,7 +50,6 @@ export function useVoiceUpload({ showToast }: UseVoiceUploadProps) {
|
||||
storage_key,
|
||||
mime_type: data.file.type || "audio/mpeg",
|
||||
metadata: buildVoiceMetadata({
|
||||
gender: data.gender,
|
||||
description: data.description,
|
||||
duration,
|
||||
}),
|
||||
@@ -71,8 +63,15 @@ export function useVoiceUpload({ showToast }: UseVoiceUploadProps) {
|
||||
showToast("上传成功", "success")
|
||||
handleUploadClose()
|
||||
},
|
||||
onError: (err: Error) => {
|
||||
showToast(err.message || "上传失败,请重试", "error")
|
||||
onError: (err: Error & { response?: { status?: number } }) => {
|
||||
const status = err?.response?.status
|
||||
let msg = "上传失败,请重试"
|
||||
if (status && status >= 500) {
|
||||
msg = "服务器暂时不可用,请稍后重试"
|
||||
} else if (err?.message) {
|
||||
msg = err.message
|
||||
}
|
||||
showToast(msg, "error")
|
||||
},
|
||||
})
|
||||
|
||||
@@ -102,10 +101,9 @@ export function useVoiceUpload({ showToast }: UseVoiceUploadProps) {
|
||||
uploadMutation.mutate({
|
||||
file: uploadFile,
|
||||
name: uploadName.trim(),
|
||||
gender: uploadGender,
|
||||
description: uploadDesc.trim(),
|
||||
})
|
||||
}, [uploadFile, uploadName, uploadGender, uploadDesc, uploadMutation])
|
||||
}, [uploadFile, uploadName, uploadDesc, uploadMutation])
|
||||
|
||||
return {
|
||||
// 弹窗状态
|
||||
@@ -114,13 +112,11 @@ export function useVoiceUpload({ showToast }: UseVoiceUploadProps) {
|
||||
// 表单状态
|
||||
uploadFile,
|
||||
uploadName,
|
||||
uploadGender,
|
||||
uploadDesc,
|
||||
uploadProgress,
|
||||
isUploading: uploadMutation.isPending,
|
||||
// Setters
|
||||
setUploadName,
|
||||
setUploadGender,
|
||||
setUploadDesc,
|
||||
// Handlers
|
||||
handleFileSelect,
|
||||
|
||||
Reference in New Issue
Block a user