|
|
@@ -1,14 +1,16 @@
|
|
|
import './index.css'
|
|
|
import { useSearchParams } from 'react-router-dom'
|
|
|
-import { PlayCircleOutlined, UploadOutlined } from '@ant-design/icons'
|
|
|
-import { message, Upload, Radio, Input, Select, Button, Form } from 'antd'
|
|
|
+import { PlayCircleOutlined, PlusOutlined, UploadOutlined } from '@ant-design/icons'
|
|
|
+import { message, Upload, Radio, Input, Select, Button, Form, Modal } from 'antd'
|
|
|
import { uploadFile } from '@/apis/upload'
|
|
|
import type { UploadChangeParam } from 'antd/es/upload'
|
|
|
import { useEffect, useState, useRef } from 'react'
|
|
|
import type { RcFile } from 'antd/es/upload'
|
|
|
import { getArtinfo } from '@/utils/artlist'
|
|
|
import calculateMD5 from '@/utils/calculateMD5'
|
|
|
-
|
|
|
+import { albumAddapi, albumQueryapi } from '@/apis/album'
|
|
|
+import { songAddapi } from '@/apis/song'
|
|
|
+import { useNavigate } from 'react-router-dom';
|
|
|
const Songup = () => {
|
|
|
const [searchParams] = useSearchParams()
|
|
|
const type = searchParams.get('type')
|
|
|
@@ -20,6 +22,7 @@ const Songup = () => {
|
|
|
const [duration, setDuration] = useState(0)
|
|
|
const [audioUrl, setAudioUrl] = useState<string | null>(null)
|
|
|
const [form] = Form.useForm()
|
|
|
+ const [createAlbumForm] = Form.useForm()
|
|
|
const artinfo = getArtinfo()
|
|
|
const audioRef = useRef<HTMLAudioElement | null>(null)
|
|
|
const [lyrics, setLyrics] = useState<string>('') // 存储带时间戳的歌词(输入框和预览区共用)
|
|
|
@@ -29,6 +32,25 @@ const Songup = () => {
|
|
|
const [isLyricsFocused, setIsLyricsFocused] = useState(false)
|
|
|
const lyricsTextareaRef = useRef<HTMLTextAreaElement>(null)
|
|
|
|
|
|
+ // 专辑相关状态
|
|
|
+ const [workType, setWorkType] = useState<'single' | 'album'>('single')
|
|
|
+ const [albums, setAlbums] = useState<{ id: number, name: string }[]>([])
|
|
|
+ const [selectedAlbum, setSelectedAlbum] = useState<number | null>(null)
|
|
|
+ const [isCreateAlbumModalVisible, setIsCreateAlbumModalVisible] = useState(false)
|
|
|
+
|
|
|
+ const [coverUrl, setCoverUrl] = useState<string>(''); // 添加此状态
|
|
|
+ const [coverUploadStatus, setCoverUploadStatus] = useState<'idle' | 'uploading' | 'done' | 'error'>('idle');
|
|
|
+
|
|
|
+ const [songCoverUrl, setSongCoverUrl] = useState<string>('');
|
|
|
+ const [songCoverUploadStatus, setSongCoverUploadStatus] = useState<'idle' | 'uploading' | 'done' | 'error'>('idle');
|
|
|
+
|
|
|
+ // 添加一个状态来跟踪是否付费
|
|
|
+ const [isPaid, setIsPaid] = useState<number>(0);
|
|
|
+ const navigate = useNavigate();
|
|
|
+
|
|
|
+
|
|
|
+ const [submitLoading, setSubmitLoading] = useState(false);
|
|
|
+
|
|
|
// 文件上传处理
|
|
|
const customUploadAvatar = async (options: any) => {
|
|
|
const { file, onSuccess, onError, onProgress, data } = options
|
|
|
@@ -41,7 +63,16 @@ const Songup = () => {
|
|
|
onError(error)
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
+ const customUploadWithoutMD5 = async (options: any) => {
|
|
|
+ const { file, onSuccess, onError, onProgress, data } = options
|
|
|
+ try {
|
|
|
+ // 直接上传文件,不计算 MD5
|
|
|
+ const res = await uploadFile(data, file, '', onProgress)
|
|
|
+ onSuccess(res, file)
|
|
|
+ } catch (error) {
|
|
|
+ onError(error)
|
|
|
+ }
|
|
|
+ }
|
|
|
// 上传状态变化处理
|
|
|
const handleAvatarChange = (info: UploadChangeParam) => {
|
|
|
setFileList(info.fileList)
|
|
|
@@ -50,32 +81,77 @@ const Songup = () => {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // 表单提交处理
|
|
|
const handleSubmit = () => {
|
|
|
- form.validateFields().then((values) => {
|
|
|
- const submitData = {
|
|
|
- ...values,
|
|
|
- lyrics: lyrics // 提交带时间戳的歌词
|
|
|
+ if (submitLoading) return;
|
|
|
+ form.validateFields().then(async (values) => {
|
|
|
+ try {
|
|
|
+ setSubmitLoading(true); // 在开始提交时设置加载状态
|
|
|
+ // 获取音频文件时长
|
|
|
+ let duration = 0;
|
|
|
+ if (audioRef.current) {
|
|
|
+ duration = Math.floor(audioRef.current.duration);
|
|
|
+ }
|
|
|
+ let actualAudioUrl = '';
|
|
|
+ if (fileList[0]?.response?.data) {
|
|
|
+ actualAudioUrl = fileList[0].response.data; // 使用服务器返回的 URL
|
|
|
+ } else {
|
|
|
+ messageApi.error('未能获取到有效的音频文件地址');
|
|
|
+ setSubmitLoading(false); // 恢复状态
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const submitData = {
|
|
|
+ ...values,
|
|
|
+ lyrics: lyrics,
|
|
|
+ workType: workType,
|
|
|
+ artistId: artinfo.id,
|
|
|
+ fileUrl: actualAudioUrl,
|
|
|
+ coverUrl: songCoverUrl || coverUrl || '',
|
|
|
+ duration: duration,
|
|
|
+ releaseTime: new Date().toISOString().split('T')[0],
|
|
|
+ ...(workType === 'album' && selectedAlbum && { albumId: selectedAlbum }),
|
|
|
+ isPaid: values.isPaid !== undefined ? values.isPaid : 0,
|
|
|
+ price: values.isPaid === 1 && values.price ? Number(values.price) : 0
|
|
|
+ };
|
|
|
+
|
|
|
+ console.log('提交数据:', submitData);
|
|
|
+ // 调用歌曲上传API
|
|
|
+ const res = await songAddapi(submitData);
|
|
|
+ if (res.code === 200) {
|
|
|
+ messageApi.success({
|
|
|
+ content: '歌曲上传成功',
|
|
|
+ duration: 2,
|
|
|
+ });
|
|
|
+
|
|
|
+ // 延迟跳转,让用户看到成功消息
|
|
|
+ setTimeout(() => {
|
|
|
+ setSubmitLoading(false); // 在跳转前恢复按钮状态
|
|
|
+ navigate('/musician/works/wsongs');
|
|
|
+ }, 2000);
|
|
|
+ } else {
|
|
|
+ messageApi.error(res.message || '提交失败');
|
|
|
+ setSubmitLoading(false); // 恢复状态
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('提交失败:', error);
|
|
|
+ messageApi.error('提交失败');
|
|
|
+ setSubmitLoading(false); // 恢复状态
|
|
|
}
|
|
|
- console.log('提交数据:', submitData)
|
|
|
}).catch((errorInfo) => {
|
|
|
- console.log('验证失败:', errorInfo)
|
|
|
- })
|
|
|
- }
|
|
|
+ console.log('验证失败:', errorInfo);
|
|
|
+ setSubmitLoading(false); // 验证失败也恢复状态
|
|
|
+ });
|
|
|
+ };
|
|
|
|
|
|
// 音频播放控制
|
|
|
const handlePlayPause = () => {
|
|
|
if (!audioRef.current) return;
|
|
|
-
|
|
|
try {
|
|
|
const newIsPlaying = !isPlaying;
|
|
|
-
|
|
|
if (newIsPlaying) {
|
|
|
audioRef.current.play();
|
|
|
} else {
|
|
|
audioRef.current.pause();
|
|
|
}
|
|
|
-
|
|
|
setTimeout(() => {
|
|
|
setIsPlaying(newIsPlaying);
|
|
|
}, 0);
|
|
|
@@ -112,7 +188,6 @@ const Songup = () => {
|
|
|
const handleLyricsKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
|
|
if (e.key === 'Enter' && !e.shiftKey && isLyricsFocused) {
|
|
|
e.preventDefault();
|
|
|
-
|
|
|
const textarea = e.target as HTMLTextAreaElement;
|
|
|
const currentValue = textarea.value;
|
|
|
const lines = currentValue.split('\n');
|
|
|
@@ -156,7 +231,8 @@ const Songup = () => {
|
|
|
setIsRecordingLyrics(true);
|
|
|
setHasStartedRecording(true);
|
|
|
}
|
|
|
- };
|
|
|
+ }
|
|
|
+
|
|
|
// 处理歌词输入变化
|
|
|
const handleLyricsChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
|
setLyrics(e.target.value);
|
|
|
@@ -185,6 +261,45 @@ const Songup = () => {
|
|
|
}
|
|
|
};
|
|
|
|
|
|
+ const handleCreateAlbum = async (values: any) => {
|
|
|
+ try {
|
|
|
+ const submitValues = {
|
|
|
+ ...values,
|
|
|
+ coverUrl: coverUrl || values.cover_url,
|
|
|
+ artistId: artinfo.id
|
|
|
+ };
|
|
|
+ const res = await albumAddapi(submitValues)
|
|
|
+ console.log(res);
|
|
|
+
|
|
|
+ // 重新获取专辑列表
|
|
|
+ try {
|
|
|
+ const albumlist = await albumQueryapi(artinfo.id);
|
|
|
+ setAlbums(albumlist.data);
|
|
|
+ } catch (error) {
|
|
|
+ console.error('获取专辑列表失败:', error);
|
|
|
+ messageApi.error('获取专辑列表失败');
|
|
|
+ }
|
|
|
+ setIsCreateAlbumModalVisible(false);
|
|
|
+ createAlbumForm.resetFields();
|
|
|
+ messageApi.success('专辑创建成功');
|
|
|
+ setCoverUrl(''); // 重置封面URL
|
|
|
+ } catch (error) {
|
|
|
+ console.error('创建专辑失败:', error);
|
|
|
+ messageApi.error('专辑创建失败');
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ // 处理是否付费的变更
|
|
|
+ const handleIsPaidChange = (e: any) => {
|
|
|
+ const value = e.target.value;
|
|
|
+ setIsPaid(value);
|
|
|
+
|
|
|
+ // 如果选择免费,清空价格字段
|
|
|
+ if (value === 0) {
|
|
|
+ form.setFieldsValue({ price: undefined });
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
// 全局键盘事件处理
|
|
|
useEffect(() => {
|
|
|
const handleGlobalKeyDown = (e: KeyboardEvent) => {
|
|
|
@@ -241,12 +356,58 @@ const Songup = () => {
|
|
|
}
|
|
|
};
|
|
|
}, [uploadedFile]);
|
|
|
+ // 修复 useEffect 中的专辑查询
|
|
|
+ useEffect(() => {
|
|
|
+ if (workType === 'album') {
|
|
|
+ const fetchAlbums = async () => {
|
|
|
+ try {
|
|
|
+ const albumlist = await albumQueryapi(artinfo.id);
|
|
|
+ setAlbums(albumlist.data);
|
|
|
+ } catch (error) {
|
|
|
+ console.error('获取专辑列表失败:', error);
|
|
|
+ messageApi.error('获取专辑列表失败');
|
|
|
+ }
|
|
|
+ };
|
|
|
+ fetchAlbums();
|
|
|
+ }
|
|
|
+ }, [workType]);
|
|
|
|
|
|
return (
|
|
|
<div className='upload_page'>
|
|
|
{contextHolder}
|
|
|
- <h2>上传单曲</h2>
|
|
|
+ <h2>{workType === 'single' ? '上传单曲' : '上传专辑歌曲'}</h2>
|
|
|
<p>当前选择的类型: {type}</p>
|
|
|
+
|
|
|
+ {/* 作品类型选择 */}
|
|
|
+ <div style={{ marginBottom: '20px' }}>
|
|
|
+ <Radio.Group
|
|
|
+ value={workType}
|
|
|
+ onChange={(e) => setWorkType(e.target.value)}
|
|
|
+ >
|
|
|
+ <Radio.Button value="single">单曲</Radio.Button>
|
|
|
+ <Radio.Button value="album">专辑</Radio.Button>
|
|
|
+ </Radio.Group>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* 专辑选择 */}
|
|
|
+ {workType === 'album' && (
|
|
|
+ <div style={{ marginBottom: '20px', display: 'flex', alignItems: 'center', gap: '10px' }}>
|
|
|
+ <Select
|
|
|
+ style={{ width: '300px' }}
|
|
|
+ placeholder="请选择专辑"
|
|
|
+ value={selectedAlbum}
|
|
|
+ onChange={(value) => setSelectedAlbum(value)}
|
|
|
+ options={albums.map(album => ({
|
|
|
+ label: album.name,
|
|
|
+ value: album.id
|
|
|
+ }))}
|
|
|
+ />
|
|
|
+ <Button type="primary" onClick={() => setIsCreateAlbumModalVisible(true)}>
|
|
|
+ 新建专辑
|
|
|
+ </Button>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+
|
|
|
<div className="upload-container">
|
|
|
<Upload
|
|
|
customRequest={customUploadAvatar}
|
|
|
@@ -276,20 +437,61 @@ const Songup = () => {
|
|
|
</div>
|
|
|
|
|
|
{fileList.length > 0 && uploadedFile && (
|
|
|
+
|
|
|
<Form
|
|
|
form={form}
|
|
|
layout="vertical"
|
|
|
style={{ maxWidth: '720px', margin: '0 auto' }}
|
|
|
initialValues={{
|
|
|
- songName: uploadedFile?.name,
|
|
|
+ songName: uploadedFile?.name.replace(/\.[^/.]+$/, ""),
|
|
|
singerName: artinfo.artistName,
|
|
|
version: 'studio',
|
|
|
songType: 'original',
|
|
|
lyricist: artinfo.artistName,
|
|
|
composer: artinfo.artistName,
|
|
|
- arranger: artinfo.artistName
|
|
|
+ arranger: artinfo.artistName,
|
|
|
+ language: 'chinese',
|
|
|
+ isPaid: 0,
|
|
|
+ price: undefined
|
|
|
}}
|
|
|
>
|
|
|
+ {/* 歌曲封面上传 */}
|
|
|
+ <Form.Item label="歌曲封面">
|
|
|
+ <Upload
|
|
|
+ customRequest={customUploadWithoutMD5}
|
|
|
+ data={'MUSIC_COVERS'}
|
|
|
+ listType="picture-card"
|
|
|
+ maxCount={1}
|
|
|
+ accept=".jpg,.jpeg,.png"
|
|
|
+ noStyle
|
|
|
+ onChange={(info) => {
|
|
|
+ if (info.file.status === 'uploading') {
|
|
|
+ setSongCoverUploadStatus('uploading');
|
|
|
+ setSongCoverUrl('');
|
|
|
+ }
|
|
|
+ if (info.file.status === 'done') {
|
|
|
+ if (info.file.response?.data) {
|
|
|
+ const url = info.file.response.data;
|
|
|
+ setSongCoverUrl(url);
|
|
|
+ setSongCoverUploadStatus('done');
|
|
|
+ messageApi.success('封面上传成功');
|
|
|
+ } else {
|
|
|
+ messageApi.error('封面上传失败');
|
|
|
+ setSongCoverUploadStatus('error');
|
|
|
+ }
|
|
|
+ } else if (info.file.status === 'error') {
|
|
|
+ setSongCoverUploadStatus('error');
|
|
|
+ setSongCoverUrl('');
|
|
|
+ messageApi.error('封面上传失败');
|
|
|
+ }
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <div>
|
|
|
+ <PlusOutlined />
|
|
|
+ <div style={{ marginTop: 8 }}>上传歌曲封面</div>
|
|
|
+ </div>
|
|
|
+ </Upload>
|
|
|
+ </Form.Item>
|
|
|
{/* 歌曲类型 */}
|
|
|
<Form.Item
|
|
|
label="歌曲类型"
|
|
|
@@ -387,6 +589,45 @@ const Songup = () => {
|
|
|
</Select>
|
|
|
</Form.Item>
|
|
|
|
|
|
+ {/* 是否付费 - 修改后的部分 */}
|
|
|
+ <Form.Item
|
|
|
+ label="是否付费"
|
|
|
+ name="isPaid"
|
|
|
+ initialValue={0}
|
|
|
+ >
|
|
|
+ <Radio.Group onChange={handleIsPaidChange}>
|
|
|
+ <Radio value={0}>免费</Radio>
|
|
|
+ <Radio value={1}>付费</Radio>
|
|
|
+ </Radio.Group>
|
|
|
+ </Form.Item>
|
|
|
+
|
|
|
+ <Form.Item
|
|
|
+ label="单曲价格"
|
|
|
+ name="price"
|
|
|
+ rules={[
|
|
|
+ {
|
|
|
+ required: isPaid === 1,
|
|
|
+ message: '请输入价格'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ validator: (_, value) => {
|
|
|
+ if (isPaid === 1 && (!value || value <= 0)) {
|
|
|
+ return Promise.reject('请输入大于0的价格');
|
|
|
+ }
|
|
|
+ return Promise.resolve();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ ]}
|
|
|
+ >
|
|
|
+ <Input
|
|
|
+ type="number"
|
|
|
+ min={0.01}
|
|
|
+ step={0.01}
|
|
|
+ placeholder={isPaid === 1 ? "请输入价格" : "免费歌曲无需输入价格"}
|
|
|
+ disabled={isPaid !== 1}
|
|
|
+ />
|
|
|
+ </Form.Item>
|
|
|
+
|
|
|
{/* 作词 */}
|
|
|
<Form.Item
|
|
|
label="作词"
|
|
|
@@ -505,12 +746,116 @@ const Songup = () => {
|
|
|
|
|
|
{/* 提交按钮 */}
|
|
|
<Form.Item>
|
|
|
- <Button type="primary" htmlType="submit" onClick={handleSubmit}>
|
|
|
+ <Button type="primary" htmlType="submit"
|
|
|
+ onClick={handleSubmit}
|
|
|
+ loading={submitLoading} // 显示加载状态
|
|
|
+ disabled={submitLoading} // 禁用按钮
|
|
|
+ >
|
|
|
提交
|
|
|
</Button>
|
|
|
</Form.Item>
|
|
|
</Form>
|
|
|
)}
|
|
|
+
|
|
|
+ {/* 创建专辑模态框 */}
|
|
|
+ <Modal
|
|
|
+ title="新建专辑"
|
|
|
+ open={isCreateAlbumModalVisible}
|
|
|
+ onCancel={() => {
|
|
|
+ setIsCreateAlbumModalVisible(false);
|
|
|
+ createAlbumForm.resetFields();
|
|
|
+ }}
|
|
|
+ onOk={() => createAlbumForm.submit()}
|
|
|
+ okButtonProps={{
|
|
|
+ disabled: coverUploadStatus === 'uploading' || coverUploadStatus === 'error',
|
|
|
+ loading: coverUploadStatus === 'uploading'
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <Form
|
|
|
+ form={createAlbumForm}
|
|
|
+ layout="vertical"
|
|
|
+ onFinish={handleCreateAlbum}
|
|
|
+ >
|
|
|
+ <Form.Item
|
|
|
+ label="专辑名称"
|
|
|
+ name="albumName"
|
|
|
+ rules={[{ required: true, message: '请输入专辑名称' }]}
|
|
|
+ >
|
|
|
+ <Input placeholder="请输入专辑名称" />
|
|
|
+ </Form.Item>
|
|
|
+ <Form.Item
|
|
|
+ label="封面"
|
|
|
+ name="coverUrl">
|
|
|
+ <Upload
|
|
|
+ customRequest={customUploadWithoutMD5}
|
|
|
+ data={'MUSIC_COVERS'}
|
|
|
+ listType="picture-card"
|
|
|
+ maxCount={1}
|
|
|
+ accept=".jpg,.jpeg,.png"
|
|
|
+ noStyle
|
|
|
+ name="cover_url"
|
|
|
+ onChange={(info) => {
|
|
|
+ console.log(info);
|
|
|
+ if (info.file.status === 'uploading') {
|
|
|
+ setCoverUploadStatus('uploading');
|
|
|
+ setCoverUrl('');
|
|
|
+ }
|
|
|
+ if (info.file.status === 'done') {
|
|
|
+ // 假设返回的URL在response.data中
|
|
|
+ console.log(info.file.response.data);
|
|
|
+
|
|
|
+ if (info.file.response.data) {
|
|
|
+ const url = info.file.response.data;
|
|
|
+ setCoverUrl(url);
|
|
|
+ // 更新表单值
|
|
|
+ createAlbumForm.setFieldsValue({ cover_url: url });
|
|
|
+ setCoverUploadStatus('done');
|
|
|
+ } else {
|
|
|
+ messageApi.error('上传失败请重试');
|
|
|
+ }
|
|
|
+ } else if (info.file.status === 'error') {
|
|
|
+ setCoverUploadStatus('error');
|
|
|
+ setCoverUrl('');
|
|
|
+ messageApi.error('封面上传失败');
|
|
|
+ }
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <div>
|
|
|
+ <PlusOutlined />
|
|
|
+ <div style={{ marginTop: 8 }}>上传专辑封面</div>
|
|
|
+ </div>
|
|
|
+ </Upload>
|
|
|
+ </Form.Item>
|
|
|
+
|
|
|
+ <Form.Item
|
|
|
+ label="专辑类型"
|
|
|
+ name="albumType"
|
|
|
+ initialValue={1}
|
|
|
+ >
|
|
|
+ <Radio.Group>
|
|
|
+ <Radio value={1}>数字专辑</Radio>
|
|
|
+ <Radio value={2}>实体专辑</Radio>
|
|
|
+ <Radio value={3}>EP</Radio>
|
|
|
+ </Radio.Group>
|
|
|
+ </Form.Item>
|
|
|
+ <Form.Item
|
|
|
+ label="专辑价格"
|
|
|
+ name="price"
|
|
|
+ rules={[{ required: true, message: '请输入价格' }]}
|
|
|
+ >
|
|
|
+ <Input
|
|
|
+ type="number"
|
|
|
+ min={0}
|
|
|
+ step={0.01} placeholder="请输入价格" />
|
|
|
+ </Form.Item>
|
|
|
+ <Form.Item
|
|
|
+ label="专辑描述"
|
|
|
+ name="description"
|
|
|
+ >
|
|
|
+ <Input.TextArea placeholder="请输入专辑描述" rows={4} />
|
|
|
+ </Form.Item>
|
|
|
+ </Form>
|
|
|
+ </Modal>
|
|
|
</div>
|
|
|
)
|
|
|
}
|