135 lines
3.5 KiB
TypeScript
135 lines
3.5 KiB
TypeScript
import { useState, useRef, useCallback, useEffect } from "react"
|
|
import type { ModalPhase } from "../types/cloneModal"
|
|
import { MIN_VOICE_NAME_LENGTH, MAX_VOICE_NAME_LENGTH } from "../constants/cloneModal"
|
|
import useAudioRecorder from "./useAudioRecorder"
|
|
|
|
/**
|
|
* 克隆弹窗表单状态 Hook
|
|
* 管理表单字段、录音、文件选择、验证逻辑
|
|
*/
|
|
export function useCloneFormState({ open, onClose }: { open: boolean; onClose: () => void }) {
|
|
const [phase, setPhase] = useState<ModalPhase>("input")
|
|
const [voiceName, setVoiceName] = useState("")
|
|
const [voiceDescription, setVoiceDescription] = useState("")
|
|
const [selectedFile, setSelectedFile] = useState<File | null>(null)
|
|
const [dragActive, setDragActive] = useState(false)
|
|
const [errorMessage, setErrorMessage] = useState("")
|
|
|
|
const { isRecording, recordTime, recordedBlob, toggleRecording, resetRecording } =
|
|
useAudioRecorder()
|
|
|
|
/** 默认音色名称计数器 */
|
|
const cloneCounterRef = useRef(1)
|
|
|
|
const getNextDefaultName = useCallback((): string => {
|
|
const name = `我的声音 ${cloneCounterRef.current}`
|
|
cloneCounterRef.current += 1
|
|
return name
|
|
}, [])
|
|
|
|
const hasAudio = selectedFile !== null || recordedBlob !== null
|
|
|
|
const canSubmit =
|
|
voiceName.trim().length >= MIN_VOICE_NAME_LENGTH &&
|
|
voiceName.trim().length <= MAX_VOICE_NAME_LENGTH &&
|
|
hasAudio
|
|
|
|
const isProcessing = phase === "uploading" || phase === "cloning"
|
|
|
|
/** 重置弹窗状态 */
|
|
const resetState = useCallback(() => {
|
|
setPhase("input")
|
|
setVoiceName(getNextDefaultName())
|
|
setVoiceDescription("")
|
|
setSelectedFile(null)
|
|
setDragActive(false)
|
|
setErrorMessage("")
|
|
resetRecording()
|
|
}, [getNextDefaultName, resetRecording])
|
|
|
|
/** 关闭弹窗 */
|
|
const handleClose = useCallback(() => {
|
|
resetState()
|
|
onClose()
|
|
}, [resetState, onClose])
|
|
|
|
/** 弹窗打开时重置状态 */
|
|
useEffect(() => {
|
|
if (open) {
|
|
resetState()
|
|
}
|
|
}, [open, resetState])
|
|
|
|
/** 选择文件(来自上传或拖拽) */
|
|
const handleFileSelect = useCallback(
|
|
(file: File | null, error: string) => {
|
|
if (error) {
|
|
setErrorMessage(error)
|
|
setSelectedFile(null)
|
|
} else {
|
|
setErrorMessage("")
|
|
setSelectedFile(file)
|
|
// 清除录音
|
|
resetRecording()
|
|
}
|
|
},
|
|
[resetRecording],
|
|
)
|
|
|
|
/** 录音切换 */
|
|
const handleRecordToggle = useCallback(() => {
|
|
setErrorMessage("")
|
|
if (isRecording) {
|
|
toggleRecording()
|
|
} else {
|
|
// 开始录制前清除已选文件
|
|
setSelectedFile(null)
|
|
toggleRecording()
|
|
}
|
|
}, [isRecording, toggleRecording])
|
|
|
|
/** 表单验证 */
|
|
const validateForm = useCallback((): string | null => {
|
|
const name = voiceName.trim()
|
|
if (!name) {
|
|
return "请输入音色名称"
|
|
}
|
|
if (name.length < MIN_VOICE_NAME_LENGTH || name.length > MAX_VOICE_NAME_LENGTH) {
|
|
return `音色名称需在 ${MIN_VOICE_NAME_LENGTH}-${MAX_VOICE_NAME_LENGTH} 个字符之间`
|
|
}
|
|
if (!hasAudio) {
|
|
return "请上传音频文件或录制一段声音"
|
|
}
|
|
return null
|
|
}, [voiceName, hasAudio])
|
|
|
|
return {
|
|
// 状态
|
|
phase,
|
|
setPhase,
|
|
voiceName,
|
|
setVoiceName,
|
|
voiceDescription,
|
|
setVoiceDescription,
|
|
selectedFile,
|
|
dragActive,
|
|
setDragActive,
|
|
errorMessage,
|
|
setErrorMessage,
|
|
// 录音
|
|
isRecording,
|
|
recordTime,
|
|
recordedBlob,
|
|
// 计算属性
|
|
hasAudio,
|
|
canSubmit,
|
|
isProcessing,
|
|
// handlers
|
|
handleFileSelect,
|
|
handleRecordToggle,
|
|
handleClose,
|
|
validateForm,
|
|
resetState,
|
|
}
|
|
}
|