首页素材案例弹窗修改
This commit is contained in:
+97
-97
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -28,7 +28,7 @@
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<script type="module" crossorigin src="/assets/index-I0gVfD2c.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-CHhXoygD.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-JhRVnnL-.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -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 }}>
|
||||
|
||||
@@ -498,6 +498,9 @@ const AIChatPage: React.FC = () => {
|
||||
});
|
||||
setCurrentMedia(mediaItems);
|
||||
}
|
||||
if (state.aspectRatio) {
|
||||
setVideoAspectRatio(state.aspectRatio);
|
||||
}
|
||||
window.history.replaceState({}, '', window.location.pathname);
|
||||
}
|
||||
}, [location.state]);
|
||||
|
||||
@@ -189,7 +189,9 @@ const HomePage: React.FC = () => {
|
||||
const [activeCaseTab, setActiveCaseTab] = useState<string>('');
|
||||
const [caseAssets, setCaseAssets] = useState<any[]>([]);
|
||||
const [previewAsset, setPreviewAsset] = useState<any>(null);
|
||||
const [aspectRatio, setAspectRatio] = useState<string>('');
|
||||
const previewVideoRef = useRef<HTMLVideoElement>(null);
|
||||
const currentAssetIdRef = useRef<string>('');
|
||||
const [activeContentTab, setActiveContentTab] = useState<'works' | 'cases'>('cases');
|
||||
const [playingVideoIndex, setPlayingVideoIndex] = useState<number | null>(null);
|
||||
const [playingAudioUrl, setPlayingAudioUrl] = useState<string | null>(null);
|
||||
@@ -236,10 +238,73 @@ const HomePage: React.FC = () => {
|
||||
}
|
||||
setPlayingAudioUrl(null);
|
||||
setPlayingVideoIndex(null);
|
||||
setAspectRatio('');
|
||||
currentAssetIdRef.current = '';
|
||||
const allVideos = document.querySelectorAll('.preview-video-item');
|
||||
allVideos.forEach((v) => {
|
||||
(v as HTMLVideoElement).pause();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const assetId = previewAsset.id || previewAsset.url;
|
||||
currentAssetIdRef.current = assetId;
|
||||
|
||||
const gcd = (a: number, b: number): number => b === 0 ? a : gcd(b, a % b);
|
||||
|
||||
const width = previewAsset.width || previewAsset.videoWidth || previewAsset.imageWidth || 0;
|
||||
const height = previewAsset.height || previewAsset.videoHeight || previewAsset.imageHeight || 0;
|
||||
|
||||
if (width && height) {
|
||||
const divisor = gcd(width, height);
|
||||
setAspectRatio(`${width / divisor}:${height / divisor}`);
|
||||
return;
|
||||
}
|
||||
|
||||
setAspectRatio('');
|
||||
|
||||
if (previewAsset.mediaType === 'video') {
|
||||
const video = previewVideoRef.current;
|
||||
const handleLoadedMetadata = () => {
|
||||
if (currentAssetIdRef.current !== assetId) return;
|
||||
const w = video!.videoWidth;
|
||||
const h = video!.videoHeight;
|
||||
if (!w || !h) return;
|
||||
const divisor = gcd(w, h);
|
||||
setAspectRatio(`${w / divisor}:${h / divisor}`);
|
||||
};
|
||||
|
||||
video?.removeEventListener('loadedmetadata', handleLoadedMetadata);
|
||||
|
||||
if (video && video.videoWidth && video.videoHeight) {
|
||||
const divisor = gcd(video.videoWidth, video.videoHeight);
|
||||
setAspectRatio(`${video.videoWidth / divisor}:${video.videoHeight / divisor}`);
|
||||
} else {
|
||||
video?.addEventListener('loadedmetadata', handleLoadedMetadata);
|
||||
}
|
||||
|
||||
return () => {
|
||||
video?.removeEventListener('loadedmetadata', handleLoadedMetadata);
|
||||
};
|
||||
} else {
|
||||
const img = new Image();
|
||||
img.crossOrigin = 'anonymous';
|
||||
img.onload = () => {
|
||||
if (currentAssetIdRef.current !== assetId) return;
|
||||
const w = img.width;
|
||||
const h = img.height;
|
||||
if (!w || !h) {
|
||||
setAspectRatio('未知');
|
||||
return;
|
||||
}
|
||||
const divisor = gcd(w, h);
|
||||
setAspectRatio(`${w / divisor}:${h / divisor}`);
|
||||
};
|
||||
img.onerror = () => {
|
||||
if (currentAssetIdRef.current !== assetId) return;
|
||||
setAspectRatio('未知');
|
||||
};
|
||||
img.src = `${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${previewAsset.url}`;
|
||||
}
|
||||
}, [previewAsset]);
|
||||
|
||||
@@ -1003,6 +1068,8 @@ const HomePage: React.FC = () => {
|
||||
onCancel={() => {
|
||||
previewVideoRef.current?.pause();
|
||||
setPreviewAsset(null);
|
||||
setAspectRatio('');
|
||||
currentAssetIdRef.current = '';
|
||||
}}
|
||||
footer={null}
|
||||
width={900}
|
||||
@@ -1065,6 +1132,10 @@ const HomePage: React.FC = () => {
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
<div style={{ fontSize: 12, color: '#94a3b8', marginBottom: 16 }}>
|
||||
<span style={{ color: '#6366f1' }}>比例:</span>
|
||||
{aspectRatio || '计算中...'}
|
||||
</div>
|
||||
{/* 视频提示词 */}
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<div style={{ fontSize: 12, color: '#000000ff', marginBottom: 8 }}>视频提示词</div>
|
||||
@@ -1303,6 +1374,7 @@ const HomePage: React.FC = () => {
|
||||
state: {
|
||||
generationPrompt: previewAsset?.generationPrompt,
|
||||
mediaReferences: previewAsset?.mediaReferences,
|
||||
aspectRatio: aspectRatio,
|
||||
},
|
||||
});
|
||||
}}
|
||||
|
||||
@@ -558,7 +558,6 @@ export default function VideoFrameExtractor() {
|
||||
<Table
|
||||
columns={[
|
||||
{
|
||||
title: '产品名称',
|
||||
dataIndex: 'title',
|
||||
key: 'title',
|
||||
render: (text: string) => (
|
||||
@@ -664,7 +663,7 @@ export default function VideoFrameExtractor() {
|
||||
},
|
||||
}}
|
||||
style={{ fontSize: 13 }}
|
||||
scroll={{ y: 350 }}
|
||||
scroll={{ y: 300 }}
|
||||
components={{
|
||||
body: {
|
||||
row: ({ className, style, ...rest }) => (
|
||||
|
||||
@@ -76,7 +76,7 @@ export const useAppStore = create<AppState>((set, get) => ({
|
||||
width: 2048,
|
||||
height: 2048,
|
||||
videoDuration: 5,
|
||||
videoAspectRatio: '16:9',
|
||||
videoAspectRatio: '9:16',
|
||||
videoResolution: '720p',
|
||||
engineOptions: {
|
||||
ratios: ['16:9', '4:3', '1:1', '3:4', '9:16', '21:9'],
|
||||
@@ -184,7 +184,7 @@ export const useAppStore = create<AppState>((set, get) => ({
|
||||
width: 2048,
|
||||
height: 2048,
|
||||
videoDuration: 5,
|
||||
videoAspectRatio: '16:9',
|
||||
videoAspectRatio: '9:16',
|
||||
videoResolution: '720p',
|
||||
engineOptions: {
|
||||
ratios: ['16:9', '4:3', '1:1', '3:4', '9:16', '21:9'],
|
||||
|
||||
Reference in New Issue
Block a user