Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3b0a1aa743 | |||
| 76e6c6aef1 | |||
| 12fd78191d |
Regular → Executable
+5
-81
@@ -1,11 +1,10 @@
|
|||||||
import React, { useState, useRef } from "react"
|
import React, { useState } from "react"
|
||||||
import { UploadOutlined, SoundOutlined, CloseOutlined } from "@ant-design/icons"
|
|
||||||
import { Button, Input } from "@/components/ui"
|
import { Button, Input } from "@/components/ui"
|
||||||
import { type TagItem } from "@/api/tags"
|
import { type TagItem } from "@/api/tags"
|
||||||
import { type VoiceGender, type VoiceMaterial } from "../types"
|
import { type VoiceGender, type VoiceMaterial } from "../types"
|
||||||
import { GENDER_OPTIONS } from "../constants"
|
|
||||||
import { genderClass, formatFileSize } from "../utils/format"
|
|
||||||
import TagSelector from "./TagSelector"
|
import TagSelector from "./TagSelector"
|
||||||
|
import FileUploadField from "./material-form/FileUploadField"
|
||||||
|
import GenderSelector from "./material-form/GenderSelector"
|
||||||
|
|
||||||
export interface MaterialFormProps {
|
export interface MaterialFormProps {
|
||||||
initial?: VoiceMaterial
|
initial?: VoiceMaterial
|
||||||
@@ -33,7 +32,6 @@ const MaterialForm: React.FC<MaterialFormProps> = ({
|
|||||||
const [gender, setGender] = useState<VoiceGender>(initial?.gender ?? "female")
|
const [gender, setGender] = useState<VoiceGender>(initial?.gender ?? "female")
|
||||||
const [selectedTagIds, setSelectedTagIds] = useState<string[]>(initial?.tagIds ?? [])
|
const [selectedTagIds, setSelectedTagIds] = useState<string[]>(initial?.tagIds ?? [])
|
||||||
const [file, setFile] = useState<File | undefined>(undefined)
|
const [file, setFile] = useState<File | undefined>(undefined)
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
|
||||||
|
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
if (!name.trim()) return
|
if (!name.trim()) return
|
||||||
@@ -53,65 +51,10 @@ const MaterialForm: React.FC<MaterialFormProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="vmat-form">
|
<div className="vmat-form">
|
||||||
{/* 音频文件上传(编辑模式不显示) */}
|
|
||||||
{!initial && (
|
{!initial && (
|
||||||
<div className="vmat-form-field">
|
<FileUploadField file={file} onChange={setFile} uploadProgress={uploadProgress} />
|
||||||
<label className="vmat-form-label">音频文件 *</label>
|
|
||||||
<div
|
|
||||||
className="vmat-upload-zone"
|
|
||||||
onClick={() => fileInputRef.current?.click()}
|
|
||||||
onDragOver={(e) => e.preventDefault()}
|
|
||||||
onDrop={(e) => {
|
|
||||||
e.preventDefault()
|
|
||||||
const f = e.dataTransfer.files[0]
|
|
||||||
if (f?.type.startsWith("audio/")) setFile(f)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
ref={fileInputRef}
|
|
||||||
type="file"
|
|
||||||
accept="audio/*"
|
|
||||||
style={{ display: "none" }}
|
|
||||||
onChange={(e) => {
|
|
||||||
const f = e.target.files?.[0]
|
|
||||||
if (f) setFile(f)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{file ? (
|
|
||||||
<div className="vmat-upload-selected">
|
|
||||||
<SoundOutlined className="vmat-upload-icon" />
|
|
||||||
<span className="vmat-upload-filename">{file.name}</span>
|
|
||||||
<span className="vmat-upload-filesize">{formatFileSize(file.size)}</span>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="vmat-upload-clear"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation()
|
|
||||||
setFile(undefined)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<CloseOutlined />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="vmat-upload-placeholder">
|
|
||||||
<UploadOutlined className="vmat-upload-icon" />
|
|
||||||
<p>点击或拖拽音频文件到此处</p>
|
|
||||||
<span>支持 MP3、WAV、AAC、FLAC 等格式</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{/* 上传进度条 */}
|
|
||||||
{uploadProgress !== null && uploadProgress !== undefined && (
|
|
||||||
<div className="vmat-upload-progress">
|
|
||||||
<div className="vmat-upload-progress-bar" style={{ width: `${uploadProgress}%` }} />
|
|
||||||
<span className="vmat-upload-progress-text">{uploadProgress}%</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 名称 */}
|
|
||||||
<div className="vmat-form-field">
|
<div className="vmat-form-field">
|
||||||
<label className="vmat-form-label">名称 *</label>
|
<label className="vmat-form-label">名称 *</label>
|
||||||
<Input
|
<Input
|
||||||
@@ -122,7 +65,6 @@ const MaterialForm: React.FC<MaterialFormProps> = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 音色描述 */}
|
|
||||||
<div className="vmat-form-field">
|
<div className="vmat-form-field">
|
||||||
<label className="vmat-form-label">音色描述</label>
|
<label className="vmat-form-label">音色描述</label>
|
||||||
<Input.TextArea
|
<Input.TextArea
|
||||||
@@ -134,25 +76,8 @@ const MaterialForm: React.FC<MaterialFormProps> = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 性别 */}
|
<GenderSelector value={gender} onChange={setGender} />
|
||||||
<div className="vmat-form-field">
|
|
||||||
<label className="vmat-form-label">性别</label>
|
|
||||||
<div className="vmat-gender-group">
|
|
||||||
{GENDER_OPTIONS.map((opt) => (
|
|
||||||
<button
|
|
||||||
key={opt.value}
|
|
||||||
type="button"
|
|
||||||
className={`vmat-gender-btn${gender === opt.value ? " active" : ""} ${genderClass(opt.value)}`}
|
|
||||||
onClick={() => setGender(opt.value)}
|
|
||||||
>
|
|
||||||
{opt.icon}
|
|
||||||
{opt.label}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 风格标签 */}
|
|
||||||
<div className="vmat-form-field">
|
<div className="vmat-form-field">
|
||||||
<label className="vmat-form-label">风格标签</label>
|
<label className="vmat-form-label">风格标签</label>
|
||||||
<TagSelector
|
<TagSelector
|
||||||
@@ -164,7 +89,6 @@ const MaterialForm: React.FC<MaterialFormProps> = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 操作按钮 */}
|
|
||||||
<div className="vmat-form-actions">
|
<div className="vmat-form-actions">
|
||||||
<Button buttonType="ghost" buttonSize="md" onClick={onCancel}>
|
<Button buttonType="ghost" buttonSize="md" onClick={onCancel}>
|
||||||
取消
|
取消
|
||||||
|
|||||||
+73
@@ -0,0 +1,73 @@
|
|||||||
|
import React, { useRef } from "react"
|
||||||
|
import { UploadOutlined, SoundOutlined, CloseOutlined } from "@ant-design/icons"
|
||||||
|
import { formatFileSize } from "../../utils/format"
|
||||||
|
|
||||||
|
interface FileUploadFieldProps {
|
||||||
|
file: File | undefined
|
||||||
|
onChange: (file: File | undefined) => void
|
||||||
|
uploadProgress?: number | null
|
||||||
|
}
|
||||||
|
|
||||||
|
const FileUploadField: React.FC<FileUploadFieldProps> = ({ file, onChange, uploadProgress }) => {
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
|
const handleDrop = (e: React.DragEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
const f = e.dataTransfer.files[0]
|
||||||
|
if (f?.type.startsWith("audio/")) onChange(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="vmat-form-field">
|
||||||
|
<label className="vmat-form-label">音频文件 *</label>
|
||||||
|
<div
|
||||||
|
className="vmat-upload-zone"
|
||||||
|
onClick={() => fileInputRef.current?.click()}
|
||||||
|
onDragOver={(e) => e.preventDefault()}
|
||||||
|
onDrop={handleDrop}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
ref={fileInputRef}
|
||||||
|
type="file"
|
||||||
|
accept="audio/*"
|
||||||
|
style={{ display: "none" }}
|
||||||
|
onChange={(e) => {
|
||||||
|
const f = e.target.files?.[0]
|
||||||
|
if (f) onChange(f)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{file ? (
|
||||||
|
<div className="vmat-upload-selected">
|
||||||
|
<SoundOutlined className="vmat-upload-icon" />
|
||||||
|
<span className="vmat-upload-filename">{file.name}</span>
|
||||||
|
<span className="vmat-upload-filesize">{formatFileSize(file.size)}</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="vmat-upload-clear"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
onChange(undefined)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CloseOutlined />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="vmat-upload-placeholder">
|
||||||
|
<UploadOutlined className="vmat-upload-icon" />
|
||||||
|
<p>点击或拖拽音频文件到此处</p>
|
||||||
|
<span>支持 MP3、WAV、AAC、FLAC 等格式</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{uploadProgress !== null && uploadProgress !== undefined && (
|
||||||
|
<div className="vmat-upload-progress">
|
||||||
|
<div className="vmat-upload-progress-bar" style={{ width: `${uploadProgress}%` }} />
|
||||||
|
<span className="vmat-upload-progress-text">{uploadProgress}%</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FileUploadField
|
||||||
+32
@@ -0,0 +1,32 @@
|
|||||||
|
import React from "react"
|
||||||
|
import type { VoiceGender } from "../../types"
|
||||||
|
import { GENDER_OPTIONS } from "../../constants"
|
||||||
|
import { genderClass } from "../../utils/format"
|
||||||
|
|
||||||
|
interface GenderSelectorProps {
|
||||||
|
value: VoiceGender
|
||||||
|
onChange: (value: VoiceGender) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const GenderSelector: React.FC<GenderSelectorProps> = ({ value, onChange }) => {
|
||||||
|
return (
|
||||||
|
<div className="vmat-form-field">
|
||||||
|
<label className="vmat-form-label">性别</label>
|
||||||
|
<div className="vmat-gender-group">
|
||||||
|
{GENDER_OPTIONS.map((opt) => (
|
||||||
|
<button
|
||||||
|
key={opt.value}
|
||||||
|
type="button"
|
||||||
|
className={`vmat-gender-btn${value === opt.value ? " active" : ""} ${genderClass(opt.value)}`}
|
||||||
|
onClick={() => onChange(opt.value)}
|
||||||
|
>
|
||||||
|
{opt.icon}
|
||||||
|
{opt.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default GenderSelector
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
import { type VoiceMaterial } from "../../../types"
|
import { type VoiceMaterial } from "../../types"
|
||||||
import { type AssetLibraryItem } from "@/api/assets"
|
import { type AssetLibraryItem } from "@/api/assets"
|
||||||
import { useVoiceUpload } from "./actions/useVoiceUpload"
|
import { useVoiceUpload } from "./actions/useVoiceUpload"
|
||||||
import { useVoiceEdit } from "./actions/useVoiceEdit"
|
import { useVoiceEdit } from "./actions/useVoiceEdit"
|
||||||
|
|||||||
Reference in New Issue
Block a user