首页素材案例弹窗修改

This commit is contained in:
sjy
2026-07-10 14:20:49 +08:00
parent d99062ca98
commit f2f8625425
9 changed files with 383 additions and 110 deletions
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -28,7 +28,7 @@
} }
})(); })();
</script> </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"> <link rel="stylesheet" crossorigin href="/assets/index-JhRVnnL-.css">
</head> </head>
<body> <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 { Button, Checkbox, Empty, Input, message, Modal, Pagination, Select, Space, Spin, Tag, Typography } from 'antd';
import { deleteUpload, deleteUploadResourceHistoryBatch, getUploadResourceHistory, getUploadResourceHistoryItems } from '../../api'; import { deleteUpload, deleteUploadResourceHistoryBatch, getUploadResourceHistory, getUploadResourceHistoryItems } from '../../api';
import type { UploadResourceHistoryDayGroup, UploadResourceHistoryItem } from '../../types'; import type { UploadResourceHistoryDayGroup, UploadResourceHistoryItem } from '../../types';
import CustomAudioPlayer from './CustomAudioPlayer';
const { Text } = Typography; const { Text } = Typography;
@@ -167,7 +168,7 @@ const UploadResourceHistoryPanel: React.FC = () => {
: { width: '100%', maxHeight: 620, objectFit: 'contain' as const }; : { 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 === '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} />; 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 ( return (
@@ -3,6 +3,7 @@ import { AudioOutlined, CheckOutlined, PictureOutlined, ReloadOutlined, SearchOu
import { Button, Empty, Input, message, Modal, Pagination, Select, Space, Spin, Tag, Typography } from 'antd'; 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 type { UploadResourceHistoryItem } from '../../types';
import CustomAudioPlayer from './CustomAudioPlayer';
const { Text } = Typography; 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)', 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' ? ( {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' ? ( ) : 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 }}> <CustomAudioPlayer src={preview} size="small" />
<AudioOutlined />
</div>
)} )}
</div> </div>
<div style={{ padding: 10 }}> <div style={{ padding: 10 }}>
@@ -498,6 +498,9 @@ const AIChatPage: React.FC = () => {
}); });
setCurrentMedia(mediaItems); setCurrentMedia(mediaItems);
} }
if (state.aspectRatio) {
setVideoAspectRatio(state.aspectRatio);
}
window.history.replaceState({}, '', window.location.pathname); window.history.replaceState({}, '', window.location.pathname);
} }
}, [location.state]); }, [location.state]);
+72
View File
@@ -189,7 +189,9 @@ const HomePage: React.FC = () => {
const [activeCaseTab, setActiveCaseTab] = useState<string>(''); const [activeCaseTab, setActiveCaseTab] = useState<string>('');
const [caseAssets, setCaseAssets] = useState<any[]>([]); const [caseAssets, setCaseAssets] = useState<any[]>([]);
const [previewAsset, setPreviewAsset] = useState<any>(null); const [previewAsset, setPreviewAsset] = useState<any>(null);
const [aspectRatio, setAspectRatio] = useState<string>('');
const previewVideoRef = useRef<HTMLVideoElement>(null); const previewVideoRef = useRef<HTMLVideoElement>(null);
const currentAssetIdRef = useRef<string>('');
const [activeContentTab, setActiveContentTab] = useState<'works' | 'cases'>('cases'); const [activeContentTab, setActiveContentTab] = useState<'works' | 'cases'>('cases');
const [playingVideoIndex, setPlayingVideoIndex] = useState<number | null>(null); const [playingVideoIndex, setPlayingVideoIndex] = useState<number | null>(null);
const [playingAudioUrl, setPlayingAudioUrl] = useState<string | null>(null); const [playingAudioUrl, setPlayingAudioUrl] = useState<string | null>(null);
@@ -236,10 +238,73 @@ const HomePage: React.FC = () => {
} }
setPlayingAudioUrl(null); setPlayingAudioUrl(null);
setPlayingVideoIndex(null); setPlayingVideoIndex(null);
setAspectRatio('');
currentAssetIdRef.current = '';
const allVideos = document.querySelectorAll('.preview-video-item'); const allVideos = document.querySelectorAll('.preview-video-item');
allVideos.forEach((v) => { allVideos.forEach((v) => {
(v as HTMLVideoElement).pause(); (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]); }, [previewAsset]);
@@ -1003,6 +1068,8 @@ const HomePage: React.FC = () => {
onCancel={() => { onCancel={() => {
previewVideoRef.current?.pause(); previewVideoRef.current?.pause();
setPreviewAsset(null); setPreviewAsset(null);
setAspectRatio('');
currentAssetIdRef.current = '';
}} }}
footer={null} footer={null}
width={900} width={900}
@@ -1065,6 +1132,10 @@ const HomePage: React.FC = () => {
</div> </div>
</div> */} </div> */}
<div style={{ fontSize: 12, color: '#94a3b8', marginBottom: 16 }}>
<span style={{ color: '#6366f1' }}></span>
{aspectRatio || '计算中...'}
</div>
{/* 视频提示词 */} {/* 视频提示词 */}
<div style={{ marginBottom: 16 }}> <div style={{ marginBottom: 16 }}>
<div style={{ fontSize: 12, color: '#000000ff', marginBottom: 8 }}></div> <div style={{ fontSize: 12, color: '#000000ff', marginBottom: 8 }}></div>
@@ -1303,6 +1374,7 @@ const HomePage: React.FC = () => {
state: { state: {
generationPrompt: previewAsset?.generationPrompt, generationPrompt: previewAsset?.generationPrompt,
mediaReferences: previewAsset?.mediaReferences, mediaReferences: previewAsset?.mediaReferences,
aspectRatio: aspectRatio,
}, },
}); });
}} }}
+1 -2
View File
@@ -558,7 +558,6 @@ export default function VideoFrameExtractor() {
<Table <Table
columns={[ columns={[
{ {
title: '产品名称',
dataIndex: 'title', dataIndex: 'title',
key: 'title', key: 'title',
render: (text: string) => ( render: (text: string) => (
@@ -664,7 +663,7 @@ export default function VideoFrameExtractor() {
}, },
}} }}
style={{ fontSize: 13 }} style={{ fontSize: 13 }}
scroll={{ y: 350 }} scroll={{ y: 300 }}
components={{ components={{
body: { body: {
row: ({ className, style, ...rest }) => ( row: ({ className, style, ...rest }) => (
+2 -2
View File
@@ -76,7 +76,7 @@ export const useAppStore = create<AppState>((set, get) => ({
width: 2048, width: 2048,
height: 2048, height: 2048,
videoDuration: 5, videoDuration: 5,
videoAspectRatio: '16:9', videoAspectRatio: '9:16',
videoResolution: '720p', videoResolution: '720p',
engineOptions: { engineOptions: {
ratios: ['16:9', '4:3', '1:1', '3:4', '9:16', '21:9'], 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, width: 2048,
height: 2048, height: 2048,
videoDuration: 5, videoDuration: 5,
videoAspectRatio: '16:9', videoAspectRatio: '9:16',
videoResolution: '720p', videoResolution: '720p',
engineOptions: { engineOptions: {
ratios: ['16:9', '4:3', '1:1', '3:4', '9:16', '21:9'], ratios: ['16:9', '4:3', '1:1', '3:4', '9:16', '21:9'],