This commit is contained in:
2026-07-02 20:30:57 +08:00
parent b0a56eccde
commit 5a98d7c1f2
3 changed files with 154 additions and 201 deletions
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -28,7 +28,7 @@
}
})();
</script>
<script type="module" crossorigin src="/assets/index-Cxnc8Q5N.js"></script>
<script type="module" crossorigin src="/assets/index-tJxTOT0B.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Bi8mcSs8.css">
</head>
<body>
+152 -199
View File
@@ -2078,205 +2078,12 @@ const AIChatPage: React.FC = () => {
{/* 输入区域 - 始终显示 */}
<div style={{ position: 'relative' }}>
{/* 层叠附件展示 - 位于输入区域左侧,不影响高度 */}
{currentMedia.length > 0 && !(mediaType === 'video' && referenceMode === 'first_last_frame') && (
<div
className="ai-reference-stack-float"
style={{
position: 'absolute',
right: '100%',
bottom: 0,
marginRight: 12,
zIndex: 50,
}}
onMouseEnter={openMediaStackTray}
onMouseLeave={closeMediaStackTray}
>
<div style={{ position: 'relative', width: 104, height: 66 }}>
{currentMedia.slice(0, 4).map((media, idx) => {
const reversedIdx = currentMedia.slice(0, 4).length - 1 - idx;
const offsetX = mediaStackHovered ? idx * 7 : idx * 3;
const offsetY = mediaStackHovered ? idx * 5 : idx * 2;
return (
<div
key={`media-float-${idx}`}
style={{
position: 'absolute',
left: offsetX,
top: offsetY,
zIndex: idx + 1,
transition: 'all 0.28s cubic-bezier(0.4, 0, 0.2, 1)',
transform: `scale(${1 - reversedIdx * 0.035})`,
opacity: mediaStackHovered ? 1 : (1 - reversedIdx * 0.15),
}}
>
<div style={{ position: 'relative', width: 50, height: 54 }}>
{media.type === 'image' ? (
<img
src={`${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${media.url}`}
alt={media.name}
onClick={() => {
setAttachmentPreviewUrl(media.url);
setAttachmentPreviewType('image');
setAttachmentPreviewName(media.name);
setAttachmentPreviewVisible(true);
}}
style={{ width: 50, height: 54, objectFit: 'cover', borderRadius: 11, cursor: 'pointer', border: '2px solid rgba(255,255,255,0.98)', boxShadow: '0 8px 18px rgba(31, 41, 55, 0.14)' }}
/>
) : (
<video
src={`${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${media.url}`}
muted
onClick={() => {
setAttachmentPreviewUrl(media.url);
setAttachmentPreviewType('video');
setAttachmentPreviewName(media.name);
setAttachmentPreviewVisible(true);
}}
style={{ width: 50, height: 54, objectFit: 'cover', borderRadius: 11, cursor: 'pointer', border: '2px solid rgba(255,255,255,0.98)', boxShadow: '0 8px 18px rgba(31, 41, 55, 0.14)' }}
/>
)}
{mediaStackHovered && (
<button
onClick={() => handleRemoveMedia(idx)}
style={{
position: 'absolute',
top: -5,
right: -5,
width: 18,
height: 18,
border: '1px solid rgba(255,255,255,0.9)',
background: '#A45B5B',
borderRadius: 50,
cursor: 'pointer',
color: '#fff',
fontSize: 9,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
boxShadow: '0 4px 10px rgba(164, 91, 91, 0.28)',
zIndex: 10,
}}
>
<DeleteOutlined style={{ fontSize: 9 }} />
</button>
)}
</div>
</div>
);
})}
{currentMedia.length > 4 && (
<div style={{ position: 'absolute', right: -8, bottom: -4, zIndex: 10, background: '#8b5cf6', color: '#fff', fontSize: 10, fontWeight: 700, padding: '2px 7px', borderRadius: 999, boxShadow: '0 6px 14px rgba(31, 41, 55, 0.18)' }}>
+{currentMedia.length - 4}
</div>
)}
</div>
{mediaStackHovered && currentMedia.length > 0 && (
<div
className="ai-reference-hover-bridge"
style={{
position: 'absolute',
left: -14,
bottom: 46,
width: 392,
height: 44,
background: 'transparent',
zIndex: 1,
}}
/>
)}
{mediaStackHovered && currentMedia.length > 0 && (
<div
className="ai-reference-expanded-tray"
onMouseEnter={openMediaStackTray}
onMouseLeave={closeMediaStackTray}
style={{
position: 'absolute',
left: 0,
bottom: 54,
width: 360,
maxWidth: 'min(360px, calc(100vw - 360px))',
padding: '10px 10px 8px',
borderRadius: 18,
background: 'rgba(255,255,255,0.98)',
border: '1px solid #E7EAF0',
boxShadow: '0 24px 60px rgba(31, 41, 55, 0.12)',
backdropFilter: 'blur(18px)',
zIndex: 20,
pointerEvents: 'auto',
}}
>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 8 }}>
<span style={{ fontSize: 12, color: '#344054', fontWeight: 800 }}></span>
<span style={{ fontSize: 11, color: '#98A2B3', fontWeight: 700 }}>{currentMedia.length}/{maxImageCount + maxVideoCount}</span>
</div>
<div className="ai-reference-tray-scroll" style={{ display: 'flex', gap: 8, overflowX: 'auto', paddingBottom: 2 }}>
{currentMedia.map((media, idx) => (
<div key={`tray-float-${idx}`} style={{ position: 'relative', flex: '0 0 auto', width: 58 }}>
{media.type === 'image' ? (
<img
src={`${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${media.url}`}
alt={media.name}
onClick={() => {
setAttachmentPreviewUrl(media.url);
setAttachmentPreviewType('image');
setAttachmentPreviewName(media.name);
setAttachmentPreviewVisible(true);
}}
style={{ width: 58, height: 62, objectFit: 'cover', borderRadius: 12, cursor: 'pointer', border: '1px solid #E7EAF0', boxShadow: '0 6px 14px rgba(31, 41, 55, 0.08)' }}
/>
) : (
<video
src={`${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${media.url}`}
muted
onClick={() => {
setAttachmentPreviewUrl(media.url);
setAttachmentPreviewType('video');
setAttachmentPreviewName(media.name);
setAttachmentPreviewVisible(true);
}}
style={{ width: 58, height: 62, objectFit: 'cover', borderRadius: 12, cursor: 'pointer', border: '1px solid #E7EAF0', boxShadow: '0 6px 14px rgba(31, 41, 55, 0.08)' }}
/>
)}
<span style={{ display: 'block', marginTop: 4, textAlign: 'center', fontSize: 10, color: '#667085', fontWeight: 700, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{media.label || `${media.type === 'image' ? '图片' : '视频'}${idx + 1}`}
</span>
<button
onClick={() => handleRemoveMedia(idx)}
style={{
position: 'absolute',
top: -6,
right: -6,
width: 20,
height: 20,
border: '1.5px solid #fff',
background: '#A45B5B',
borderRadius: 50,
cursor: 'pointer',
color: '#fff',
fontSize: 10,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
boxShadow: '0 4px 10px rgba(164, 91, 91, 0.3)',
zIndex: 10,
}}
>
<DeleteOutlined style={{ fontSize: 10 }} />
</button>
</div>
))}
</div>
</div>
)}
</div>
)}
<div
style={{
position: 'relative',
background: '#ffffff',
borderRadius: 24,
padding: '18px 20px 16px',
padding: '18px 72px 16px 20px',
boxShadow: '0 22px 64px rgba(31, 41, 55, 0.08), 0 1px 0 rgba(255,255,255,0.98) inset',
transition: 'all 0.25s ease',
border: '1px solid rgba(231, 234, 240, 0.78)',
@@ -2286,7 +2093,154 @@ const AIChatPage: React.FC = () => {
{/* 上方输入布局 */}
<div style={{ display: 'flex', gap: isFirstLastFrameComposer ? 18 : 18, alignItems: 'flex-end', marginBottom: 14 }}>
{/* 左侧附件区域 */}
<div style={{ width: isFirstLastFrameComposer ? 220 : 128, minWidth: isFirstLastFrameComposer ? 220 : 128, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'flex-start', paddingTop: 2 }}>
<div style={{ width: isFirstLastFrameComposer ? 220 : 70, minWidth: isFirstLastFrameComposer ? 220 : 70, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'flex-start', paddingTop: 2, gap: 8 }}>
{/* 层叠附件展示 - 在上传按钮上边 */}
{currentMedia.length > 0 && !(mediaType === 'video' && referenceMode === 'first_last_frame') && (
<div
style={{
width: 58,
flexShrink: 0,
position: 'relative',
}}
onMouseEnter={openMediaStackTray}
onMouseLeave={closeMediaStackTray}
>
<div style={{ position: 'relative', width: 58, height: 66 }}>
{currentMedia.slice(0, 3).map((media, idx) => {
const reversedIdx = Math.min(currentMedia.slice(0, 3).length, 3) - 1 - idx;
const offsetX = mediaStackHovered ? idx * 8 : idx * 4;
const offsetY = mediaStackHovered ? idx * 6 : idx * 3;
return (
<div
key={`stack-${idx}`}
style={{
position: 'absolute',
left: offsetX,
top: offsetY,
zIndex: idx + 1,
transition: 'all 0.28s cubic-bezier(0.4, 0, 0.2, 1)',
transform: `scale(${1 - reversedIdx * 0.06})`,
opacity: mediaStackHovered ? 1 : (1 - reversedIdx * 0.15),
}}
>
<div style={{ position: 'relative', width: 50, height: 58 }}>
{media.type === 'image' ? (
<img
src={`${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${media.url}`}
alt={media.name}
onClick={() => {
setAttachmentPreviewUrl(media.url);
setAttachmentPreviewType('image');
setAttachmentPreviewName(media.name);
setAttachmentPreviewVisible(true);
}}
style={{ width: 50, height: 58, objectFit: 'cover', borderRadius: 12, cursor: 'pointer', border: '2px solid rgba(255,255,255,0.98)', boxShadow: '0 6px 16px rgba(31,41,55,0.2)' }}
/>
) : (
<video
src={`${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${media.url}`}
muted
onClick={() => {
setAttachmentPreviewUrl(media.url);
setAttachmentPreviewType('video');
setAttachmentPreviewName(media.name);
setAttachmentPreviewVisible(true);
}}
style={{ width: 50, height: 58, objectFit: 'cover', borderRadius: 12, cursor: 'pointer', border: '2px solid rgba(255,255,255,0.98)', boxShadow: '0 6px 16px rgba(31,41,55,0.2)' }}
/>
)}
</div>
</div>
);
})}
{currentMedia.length > 3 && (
<div style={{ position: 'absolute', right: 0, bottom: 2, zIndex: 10, background: '#8b5cf6', color: '#fff', fontSize: 9, fontWeight: 700, padding: '2px 6px', borderRadius: 999, boxShadow: '0 3px 8px rgba(31,41,55,0.25)' }}>
+{currentMedia.length - 3}
</div>
)}
</div>
{mediaStackHovered && currentMedia.length > 0 && (
<div
onMouseEnter={openMediaStackTray}
onMouseLeave={closeMediaStackTray}
style={{
position: 'absolute',
left: -10,
bottom: 70,
width: 280,
padding: '12px 12px 12px',
borderRadius: 16,
background: 'rgba(255,255,255,0.98)',
border: '1px solid #E7EAF0',
boxShadow: '0 20px 48px rgba(31,41,55,0.18)',
backdropFilter: 'blur(16px)',
zIndex: 60,
pointerEvents: 'auto',
}}
>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 10 }}>
<span style={{ fontSize: 12, color: '#344054', fontWeight: 700 }}></span>
<span style={{ fontSize: 11, color: '#98A2B3', fontWeight: 600 }}>{currentMedia.length}/{maxImageCount + maxVideoCount}</span>
</div>
<div style={{ display: 'flex', gap: 8, overflowX: 'auto', paddingBottom: 4 }}>
{currentMedia.map((media, idx) => (
<div key={`tray-${idx}`} style={{ position: 'relative', flex: '0 0 auto', width: 54 }}>
{media.type === 'image' ? (
<img
src={`${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${media.url}`}
alt={media.name}
onClick={() => {
setAttachmentPreviewUrl(media.url);
setAttachmentPreviewType('image');
setAttachmentPreviewName(media.name);
setAttachmentPreviewVisible(true);
}}
style={{ width: 54, height: 58, objectFit: 'cover', borderRadius: 10, cursor: 'pointer', border: '1px solid #E7EAF0' }}
/>
) : (
<video
src={`${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${media.url}`}
muted
onClick={() => {
setAttachmentPreviewUrl(media.url);
setAttachmentPreviewType('video');
setAttachmentPreviewName(media.name);
setAttachmentPreviewVisible(true);
}}
style={{ width: 54, height: 58, objectFit: 'cover', borderRadius: 10, cursor: 'pointer', border: '1px solid #E7EAF0' }}
/>
)}
<button
onClick={() => handleRemoveMedia(idx)}
style={{
position: 'absolute',
top: 4,
right: 4,
width: 20,
height: 20,
border: 'none',
background: 'rgba(0,0,0,0.5)',
borderRadius: 50,
cursor: 'pointer',
color: '#fff',
fontSize: 10,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backdropFilter: 'blur(4px)',
zIndex: 10,
}}
>
<DeleteOutlined style={{ fontSize: 10 }} />
</button>
</div>
))}
</div>
</div>
)}
</div>
)}
{/* 首尾帧模式 */}
{mediaType === 'video' && referenceMode === 'first_last_frame' ? (
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'stretch', gap: 12, width: '100%', height: 100 }}>
@@ -2391,9 +2345,9 @@ const AIChatPage: React.FC = () => {
</div>) : (
<>
{/* 上传按钮 - 非首尾帧模式且还有上传空间时显示 */}
{/* 上传按钮 - 非首尾帧模式且还有上传空间时始终显示 */}
{!(mediaType === 'video' && referenceMode === 'first_last_frame') && (
<div style={{ marginTop: currentMedia.length > 0 ? 8 : 0, width: '100%', display: 'flex', justifyContent: 'center', minHeight: currentMedia.length > 0 ? 82 : 84 }}>
<div style={{ width: '100%', display: 'flex', justifyContent: 'center', minHeight: 84 }}>
<Upload
accept={mediaType === 'image' ? 'image/*' : 'image/*,video/*,audio/*'}
showUploadList={false}
@@ -3489,7 +3443,6 @@ const AIChatPage: React.FC = () => {
</div>
</div>
</div>
</div>{/* 结束 position: relative 包装器 */}
</Content>
</Layout>