首页素材案例弹窗修改
This commit is contained in:
@@ -0,0 +1,199 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { PlayCircleOutlined, PauseCircleOutlined, SoundOutlined, CloseCircleOutlined } from '@ant-design/icons';
|
||||
|
||||
interface CustomAudioPlayerProps {
|
||||
src: string;
|
||||
size?: 'small' | 'normal';
|
||||
}
|
||||
|
||||
const CustomAudioPlayer: React.FC<CustomAudioPlayerProps> = ({ src, size = 'normal' }) => {
|
||||
const audioRef = useRef<HTMLAudioElement>(null);
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
const [currentTime, setCurrentTime] = useState(0);
|
||||
const [duration, setDuration] = useState(0);
|
||||
const [volume, setVolume] = useState(1);
|
||||
const [isMuted, setIsMuted] = useState(false);
|
||||
|
||||
const formatTime = (time: number) => {
|
||||
const mins = Math.floor(time / 60);
|
||||
const secs = Math.floor(time % 60);
|
||||
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
const togglePlay = useCallback(() => {
|
||||
if (!audioRef.current) return;
|
||||
if (isPlaying) {
|
||||
audioRef.current.pause();
|
||||
} else {
|
||||
audioRef.current.play();
|
||||
}
|
||||
setIsPlaying(!isPlaying);
|
||||
}, [isPlaying]);
|
||||
|
||||
const handleTimeUpdate = useCallback(() => {
|
||||
if (!audioRef.current) return;
|
||||
setCurrentTime(audioRef.current.currentTime);
|
||||
}, []);
|
||||
|
||||
const handleLoadedMetadata = useCallback(() => {
|
||||
if (!audioRef.current) return;
|
||||
setDuration(audioRef.current.duration);
|
||||
}, []);
|
||||
|
||||
const handleEnded = useCallback(() => {
|
||||
setIsPlaying(false);
|
||||
setCurrentTime(0);
|
||||
}, []);
|
||||
|
||||
const handleProgressClick = useCallback((e: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (!audioRef.current || duration === 0) return;
|
||||
const rect = e.currentTarget.getBoundingClientRect();
|
||||
const percent = (e.clientX - rect.left) / rect.width;
|
||||
const newTime = percent * duration;
|
||||
audioRef.current.currentTime = newTime;
|
||||
setCurrentTime(newTime);
|
||||
}, [duration]);
|
||||
|
||||
const handleVolumeChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (!audioRef.current) return;
|
||||
const newVolume = parseFloat(e.target.value);
|
||||
audioRef.current.volume = newVolume;
|
||||
setVolume(newVolume);
|
||||
setIsMuted(newVolume === 0);
|
||||
}, []);
|
||||
|
||||
const toggleMute = useCallback(() => {
|
||||
if (!audioRef.current) return;
|
||||
if (isMuted) {
|
||||
audioRef.current.volume = volume || 0.5;
|
||||
setIsMuted(false);
|
||||
} else {
|
||||
audioRef.current.volume = 0;
|
||||
setIsMuted(true);
|
||||
}
|
||||
}, [isMuted, volume]);
|
||||
|
||||
useEffect(() => {
|
||||
const audio = audioRef.current;
|
||||
if (!audio) return;
|
||||
audio.addEventListener('timeupdate', handleTimeUpdate);
|
||||
audio.addEventListener('loadedmetadata', handleLoadedMetadata);
|
||||
audio.addEventListener('ended', handleEnded);
|
||||
return () => {
|
||||
audio.removeEventListener('timeupdate', handleTimeUpdate);
|
||||
audio.removeEventListener('loadedmetadata', handleLoadedMetadata);
|
||||
audio.removeEventListener('ended', handleEnded);
|
||||
};
|
||||
}, [handleTimeUpdate, handleLoadedMetadata, handleEnded]);
|
||||
|
||||
const progress = duration > 0 ? (currentTime / duration) * 100 : 0;
|
||||
|
||||
return (
|
||||
<div style={{ width: '100%', height: size === 'small' ? 130 : 112, background: '#f1f5f9', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: size === 'small' ? '16px 16px' : '12px 16px' }}>
|
||||
<audio ref={audioRef} src={src} />
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: size === 'small' ? 16 : 16, width: '100%' }}>
|
||||
<button
|
||||
onClick={togglePlay}
|
||||
style={{
|
||||
width: size === 'small' ? 48 : 48,
|
||||
height: size === 'small' ? 48 : 48,
|
||||
borderRadius: '50%',
|
||||
background: 'linear-gradient(135deg, #8b5cf6, #a78bfa)',
|
||||
border: 'none',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
cursor: 'pointer',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
{isPlaying ? (
|
||||
<PauseCircleOutlined style={{ fontSize: size === 'small' ? 24 : 24, color: '#fff' }} />
|
||||
) : (
|
||||
<PlayCircleOutlined style={{ fontSize: size === 'small' ? 24 : 24, color: '#fff', marginLeft: size === 'small' ? 2 : 2 }} />
|
||||
)}
|
||||
</button>
|
||||
|
||||
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
<div
|
||||
onClick={handleProgressClick}
|
||||
style={{
|
||||
height: size === 'small' ? 5 : 5,
|
||||
background: '#e2e8f0',
|
||||
borderRadius: 10,
|
||||
cursor: 'pointer',
|
||||
overflow: 'visible',
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
height: '100%',
|
||||
width: `${progress}%`,
|
||||
background: 'linear-gradient(90deg, #8b5cf6, #a78bfa)',
|
||||
borderRadius: 10,
|
||||
transition: 'width 0.1s linear',
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: `${progress}%`,
|
||||
top: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: size === 'small' ? 12 : 14,
|
||||
height: size === 'small' ? 12 : 14,
|
||||
background: '#fff',
|
||||
borderRadius: '50%',
|
||||
boxShadow: '0 2px 6px rgba(0,0,0,0.15)',
|
||||
opacity: progress > 0 ? 1 : 0,
|
||||
transition: 'opacity 0.2s',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<span style={{ fontSize: size === 'small' ? 11 : 12, color: '#64748b' }}>
|
||||
{formatTime(currentTime)}
|
||||
</span>
|
||||
<span style={{ fontSize: size === 'small' ? 11 : 12, color: '#64748b' }}>
|
||||
{formatTime(duration)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6, flexShrink: 0 }}>
|
||||
<button
|
||||
onClick={toggleMute}
|
||||
style={{ border: 'none', background: 'transparent', cursor: 'pointer', padding: 0 }}
|
||||
>
|
||||
{isMuted ? (
|
||||
<CloseCircleOutlined style={{ fontSize: size === 'small' ? 14 : 16, color: '#94a3b8' }} />
|
||||
) : (
|
||||
<SoundOutlined style={{ fontSize: size === 'small' ? 14 : 16, color: '#94a3b8' }} />
|
||||
)}
|
||||
</button>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.1"
|
||||
value={isMuted ? 0 : volume}
|
||||
onChange={handleVolumeChange}
|
||||
style={{
|
||||
width: size === 'small' ? 40 : 50,
|
||||
height: 4,
|
||||
borderRadius: 2,
|
||||
appearance: 'none',
|
||||
background: '#e2e8f0',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomAudioPlayer;
|
||||
@@ -3,6 +3,7 @@ import { AudioOutlined, DeleteOutlined, DownloadOutlined, EyeOutlined, PictureOu
|
||||
import { Button, Checkbox, Empty, Input, message, Modal, Pagination, Select, Space, Spin, Tag, Typography } from 'antd';
|
||||
import { deleteUpload, deleteUploadResourceHistoryBatch, getUploadResourceHistory, getUploadResourceHistoryItems } from '../../api';
|
||||
import type { UploadResourceHistoryDayGroup, UploadResourceHistoryItem } from '../../types';
|
||||
import CustomAudioPlayer from './CustomAudioPlayer';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
@@ -167,7 +168,7 @@ const UploadResourceHistoryPanel: React.FC = () => {
|
||||
: { width: '100%', maxHeight: 620, objectFit: 'contain' as const };
|
||||
if (item.resourceType === 'image') return <img src={url} alt={item.fileName || item.id} style={style} />;
|
||||
if (item.resourceType === 'video') return <video src={url} controls={size === 'preview'} muted={size === 'card'} style={style} />;
|
||||
return <audio src={url} controls style={{ width: '100%' }} />;
|
||||
return <CustomAudioPlayer src={url} size={size === 'card' ? 'small' : 'normal'} />;
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { AudioOutlined, CheckOutlined, PictureOutlined, ReloadOutlined, SearchOutlined, VideoCameraOutlined } from '@ant-design/icons';
|
||||
import { Button, Empty, Input, message, Modal, Pagination, Select, Space, Spin, Tag, Typography } from 'antd';
|
||||
import { getUploadResourceHistoryItems ,getUploadResourceHistory} from '../../api';
|
||||
import { getUploadResourceHistoryItems, getUploadResourceHistory } from '../../api';
|
||||
import type { UploadResourceHistoryItem } from '../../types';
|
||||
import CustomAudioPlayer from './CustomAudioPlayer';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
@@ -326,15 +327,13 @@ const UploadResourceHistoryPicker: React.FC<UploadResourceHistoryPickerProps> =
|
||||
boxShadow: checked ? '0 10px 24px rgba(139, 92, 246, 0.18)' : '0 6px 18px rgba(15,23,42,0.06)',
|
||||
}}
|
||||
>
|
||||
<div style={{ height: 112, background: '#f1f5f9', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<div style={{ background: '#f1f5f9' }}>
|
||||
{item.resourceType === 'image' ? (
|
||||
<img src={preview} alt={item.fileName || item.id} style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
|
||||
<img src={preview} alt={item.fileName || item.id} style={{ width: '100%', height: 130, objectFit: 'cover' }} />
|
||||
) : item.resourceType === 'video' ? (
|
||||
<video src={preview} muted style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
|
||||
<video src={preview} muted style={{ width: '100%', height: 130, objectFit: 'cover' }} />
|
||||
) : (
|
||||
<div style={{ width: 58, height: 58, borderRadius: 18, background: 'linear-gradient(135deg, #8b5cf6, #a78bfa)', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#fff', fontSize: 24 }}>
|
||||
<AudioOutlined />
|
||||
</div>
|
||||
<CustomAudioPlayer src={preview} size="small" />
|
||||
)}
|
||||
</div>
|
||||
<div style={{ padding: 10 }}>
|
||||
|
||||
Reference in New Issue
Block a user