修改弹窗的关闭图标和整体样式,增加素材云的浮动下载

This commit is contained in:
2026-06-30 16:46:18 +08:00
parent a3e70b5d49
commit 0541fc0046
5 changed files with 212 additions and 245 deletions
File diff suppressed because one or more lines are too long
+36 -36
View File
@@ -1,37 +1,37 @@
<!doctype html> <!doctype html>
<html lang="zh-CN"> <html lang="zh-CN">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="preconnect" href="https://fonts.googleapis.com" /> <link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet" /> <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet" />
<title>民众智创</title> <title>民众智创</title>
<script> <script>
(function() { (function() {
var cached = localStorage.getItem('siteInfo'); var cached = localStorage.getItem('siteInfo');
if (cached) { if (cached) {
try { try {
var info = JSON.parse(cached); var info = JSON.parse(cached);
if (info.siteName) { if (info.siteName) {
document.title = info.siteName; document.title = info.siteName;
} }
if (info.siteLogo) { if (info.siteLogo) {
var link = document.querySelector('link[rel="icon"]'); var link = document.querySelector('link[rel="icon"]');
if (link) { if (link) {
link.href = info.siteLogo; link.href = info.siteLogo;
link.type = 'image/png'; link.type = 'image/png';
} }
} }
} catch (e) {} } catch (e) {}
} }
})(); })();
</script> </script>
<script type="module" crossorigin src="/assets/index-Cs6AfEXe.js"></script> <script type="module" crossorigin src="/assets/index-BxqbNh67.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BhPFzWLH.css"> <link rel="stylesheet" crossorigin href="/assets/index-BhPFzWLH.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
</body> </body>
</html> </html>
+56 -95
View File
@@ -44,6 +44,7 @@ import {
SettingOutlined, SettingOutlined,
LayoutOutlined, LayoutOutlined,
ArrowUpOutlined, ArrowUpOutlined,
DownloadOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
@@ -2297,46 +2298,30 @@ const AIChatPage: React.FC = () => {
{/* 图片/视频预览弹窗 */} {/* 图片/视频预览弹窗 */}
<Modal <Modal
open={previewVisible} open={previewVisible}
onCancel={handleClosePreview} title={
footer={[ <div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<Button key="download" type="primary" onClick={handleDownload} style={{ background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', borderRadius: 10, border: 'none' }}> <div style={{ width: 4, height: 20, background: 'linear-gradient(180deg, #6366f1 0%, #8b5cf6 100%)', borderRadius: 2 }} />
<span style={{ fontSize: 16, fontWeight: 700, background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent', backgroundClip: 'text' }}>
</Button>,
]} </span>
width={800} </div>
centered
closeIcon={
<button
onClick={handleClosePreview}
style={{
width: 32,
height: 32,
borderRadius: '50%',
border: 'none',
background: 'rgba(99, 102, 241, 0.1)',
cursor: 'pointer',
fontSize: 16,
color: '#6366f1',
fontWeight: 'bold',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
transition: 'all 0.2s',
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = 'rgba(239, 68, 68, 0.1)';
e.currentTarget.style.color = '#ef4444';
e.currentTarget.style.transform = 'scale(1.1)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'rgba(99, 102, 241, 0.1)';
e.currentTarget.style.color = '#6366f1';
e.currentTarget.style.transform = 'scale(1)';
}}
>
×
</button>
} }
onCancel={handleClosePreview}
width={800}
footer={
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 12, padding: '16px 24px', background: 'rgba(255,255,255,0.6)', borderTop: '1px solid rgba(99, 102, 241, 0.08)' }}>
<Button
type="primary"
icon={<DownloadOutlined />}
onClick={handleDownload}
style={{ borderRadius: 8, background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', border: 'none', boxShadow: '0 4px 12px rgba(99, 102, 241, 0.3)' }}
disabled={isMediaExpired(previewUrl)}
>
{isMediaExpired(previewUrl) ? '资源已过期' : '下载'}
</Button>
</div>
}
centered
style={{ borderRadius: 16 }} style={{ borderRadius: 16 }}
styles={{ styles={{
body: { body: {
@@ -2345,21 +2330,13 @@ const AIChatPage: React.FC = () => {
justifyContent: 'center', justifyContent: 'center',
minHeight: '400px', minHeight: '400px',
}, },
header: { background: 'rgba(255,255,255,0.6)', backdropFilter: 'blur(10px)', borderBottom: '1px solid rgba(99, 102, 241, 0.08)', padding: '16px 24px' },
}} }}
> >
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', width: '100%', height: '100%' }}> <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', width: '100%', height: '100%' }}>
{isMediaExpired(previewUrl) ? ( {isMediaExpired(previewUrl) ? (
<div style={{ textAlign: 'center', padding: '40px' }}> <div style={{ textAlign: 'center', padding: '40px' }}>
{/* <div style={{ fontSize: 48, marginBottom: 16 }}>⚠️</div> */}
<p style={{ fontSize: 16, color: '#ff4d4f', marginBottom: 16 }}>/</p> <p style={{ fontSize: 16, color: '#ff4d4f', marginBottom: 16 }}>/</p>
{/* <Button
type="primary"
onClick={() => {
window.location.reload();
}}
>
刷新页面重新加载
</Button> */}
</div> </div>
) : previewType === 'image' ? ( ) : previewType === 'image' ? (
<img <img
@@ -2381,54 +2358,37 @@ const AIChatPage: React.FC = () => {
{/* 附件预览弹窗(独立弹窗) */} {/* 附件预览弹窗(独立弹窗) */}
<Modal <Modal
open={attachmentPreviewVisible} open={attachmentPreviewVisible}
onCancel={() => setAttachmentPreviewVisible(false)} title={
footer={[ <div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<Button key="download" type="primary" onClick={() => { <div style={{ width: 4, height: 20, background: 'linear-gradient(180deg, #6366f1 0%, #8b5cf6 100%)', borderRadius: 2 }} />
if (!attachmentPreviewUrl) return; <span style={{ fontSize: 16, fontWeight: 700, background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent', backgroundClip: 'text' }}>
const link = document.createElement('a');
link.href = `${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${attachmentPreviewUrl}&download=1`; </span>
link.download = attachmentPreviewName || (attachmentPreviewType === 'image' ? 'image.png' : 'video.mp4'); </div>
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}} style={{ background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', borderRadius: 10, border: 'none' }}>
</Button>,
]}
width={800}
centered
closeIcon={
<button
onClick={() => setAttachmentPreviewVisible(false)}
style={{
width: 32,
height: 32,
borderRadius: '50%',
border: 'none',
background: 'rgba(99, 102, 241, 0.1)',
cursor: 'pointer',
fontSize: 16,
color: '#6366f1',
fontWeight: 'bold',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
transition: 'all 0.2s',
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = 'rgba(239, 68, 68, 0.1)';
e.currentTarget.style.color = '#ef4444';
e.currentTarget.style.transform = 'scale(1.1)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'rgba(99, 102, 241, 0.1)';
e.currentTarget.style.color = '#6366f1';
e.currentTarget.style.transform = 'scale(1)';
}}
>
×
</button>
} }
onCancel={() => setAttachmentPreviewVisible(false)}
width={800}
footer={
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 12, padding: '16px 24px', background: 'rgba(255,255,255,0.6)', borderTop: '1px solid rgba(99, 102, 241, 0.08)' }}>
<Button
type="primary"
icon={<DownloadOutlined />}
onClick={() => {
if (!attachmentPreviewUrl) return;
const link = document.createElement('a');
link.href = `${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${attachmentPreviewUrl}&download=1`;
link.download = attachmentPreviewName || (attachmentPreviewType === 'image' ? 'image.png' : 'video.mp4');
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}}
style={{ borderRadius: 8, background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', border: 'none', boxShadow: '0 4px 12px rgba(99, 102, 241, 0.3)' }}
>
</Button>
</div>
}
centered
style={{ borderRadius: 16 }} style={{ borderRadius: 16 }}
styles={{ styles={{
body: { body: {
@@ -2437,6 +2397,7 @@ const AIChatPage: React.FC = () => {
justifyContent: 'center', justifyContent: 'center',
minHeight: '400px', minHeight: '400px',
}, },
header: { background: 'rgba(255,255,255,0.6)', backdropFilter: 'blur(10px)', borderBottom: '1px solid rgba(99, 102, 241, 0.08)', padding: '16px 24px' },
}} }}
> >
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', width: '100%', height: '100%' }}> <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', width: '100%', height: '100%' }}>
+2 -1
View File
@@ -33,6 +33,7 @@ import {
PlusOutlined, PlusOutlined,
PictureOutlined, PictureOutlined,
SwapOutlined, SwapOutlined,
XOutlined,
} from "@ant-design/icons"; } from "@ant-design/icons";
import { useNavigate, useParams } from "react-router-dom"; import { useNavigate, useParams } from "react-router-dom";
import { useAppStore } from "../store/useAppStore"; import { useAppStore } from "../store/useAppStore";
@@ -4583,7 +4584,7 @@ const GeneratePage: React.FC = () => {
e.currentTarget.style.background = "rgba(0,0,0,0.6)"; e.currentTarget.style.background = "rgba(0,0,0,0.6)";
}} }}
> >
<CloseCircleOutlined style={{ color: "#fff", fontSize: 16 }} /> <XOutlined style={{ color: "#fff", fontSize: 16 }} />
</div> </div>
</> </>
)} )}
+114 -109
View File
@@ -1,5 +1,5 @@
import React, { useEffect, useState, useLayoutEffect, useRef, useCallback } from 'react'; import React, { useEffect, useState, useLayoutEffect, useRef, useCallback } from 'react';
import { Button, Empty, Input, Select, Space, Typography, Tag, message, Modal, Table, DatePicker, Tabs, Transfer } from 'antd'; import { Button, Empty, Input, Select, Space, Typography, Tag, message, Modal, Table, DatePicker, Tabs, Transfer, Tooltip } from 'antd';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import JSZip from 'jszip'; import JSZip from 'jszip';
import { import {
@@ -216,6 +216,7 @@ const GeneratedRecord: React.FC = () => {
const [isError, setIsError] = useState(false); const [isError, setIsError] = useState(false);
const [isExpired, setIsExpired] = useState(false); const [isExpired, setIsExpired] = useState(false);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [isHovered, setIsHovered] = useState(false);
const placeholderRef = useRef<HTMLDivElement>(null); const placeholderRef = useRef<HTMLDivElement>(null);
const mediaRef = useRef<HTMLImageElement | HTMLVideoElement>(null); const mediaRef = useRef<HTMLImageElement | HTMLVideoElement>(null);
const errorTimer = useRef<ReturnType<typeof setTimeout> | null>(null); const errorTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
@@ -299,12 +300,14 @@ const GeneratedRecord: React.FC = () => {
}} }}
onClick={!isSelectionMode ? onClick : undefined} onClick={!isSelectionMode ? onClick : undefined}
onMouseEnter={(e) => { onMouseEnter={(e) => {
setIsHovered(true);
if (!isSelectionMode) { if (!isSelectionMode) {
(e.currentTarget as HTMLElement).style.transform = 'scale(1.05)'; (e.currentTarget as HTMLElement).style.transform = 'scale(1.05)';
(e.currentTarget as HTMLElement).style.boxShadow = '0 4px 16px rgba(0,0,0,0.2)'; (e.currentTarget as HTMLElement).style.boxShadow = '0 4px 16px rgba(0,0,0,0.2)';
} }
}} }}
onMouseLeave={(e) => { onMouseLeave={(e) => {
setIsHovered(false);
if (!isSelectionMode) { if (!isSelectionMode) {
(e.currentTarget as HTMLElement).style.transform = 'scale(1)'; (e.currentTarget as HTMLElement).style.transform = 'scale(1)';
(e.currentTarget as HTMLElement).style.boxShadow = '0 2px 8px rgba(0,0,0,0.1)'; (e.currentTarget as HTMLElement).style.boxShadow = '0 2px 8px rgba(0,0,0,0.1)';
@@ -484,6 +487,56 @@ const GeneratedRecord: React.FC = () => {
</div> </div>
)} )}
{/* 下载按钮 - hover时显示 */}
{!isSelectionMode && !isExpired && !isError && isLoaded && (
<div
style={{
position: 'absolute',
top: 8,
right: 8,
zIndex: 10,
opacity: isHovered ? 1 : 0,
transition: 'opacity 0.2s',
pointerEvents: isHovered ? 'auto' : 'none',
}}
onClick={(e) => {
e.stopPropagation();
const url = `${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${item.videoUrl || item.imageUrl}&download=1`;
const link = document.createElement('a');
link.href = url;
link.download = '';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}}
>
<Tooltip title="下载">
<div
style={{
width: 28,
height: 28,
borderRadius: '50%',
background: 'rgba(0,0,0,0.5)',
backdropFilter: 'blur(4px)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
cursor: 'pointer',
transition: 'background 0.15s',
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = 'rgba(0,0,0,0.7)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'rgba(0,0,0,0.5)';
}}
>
<DownloadOutlined style={{ color: '#fff', fontSize: 14 }} />
</div>
</Tooltip>
</div>
)}
{/* 选择模式下的选择框 */} {/* 选择模式下的选择框 */}
{isSelectionMode && ( {isSelectionMode && (
<div <div
@@ -2030,77 +2083,67 @@ const GeneratedRecord: React.FC = () => {
{/* 预览弹窗 */} {/* 预览弹窗 */}
{previewVisible && previewItem && ( {previewVisible && previewItem && (
<div <Modal
style={{ title={
position: 'fixed', <div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
top: 0, <div style={{ width: 4, height: 20, background: 'linear-gradient(180deg, #6366f1 0%, #8b5cf6 100%)', borderRadius: 2 }} />
left: 0, <span style={{ fontSize: 16, fontWeight: 700, background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent', backgroundClip: 'text' }}>
right: 0,
bottom: 0,
background: 'rgba(0,0,0,0.85)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1000,
padding: 16,
boxSizing: 'border-box',
overflow: 'auto',
}}
onClick={handleClosePreview}
>
<div
style={{
background: '#fff',
borderRadius: 16,
padding: 0,
width: '100%',
maxWidth: '1200px',
maxHeight: '95vh',
minHeight: '300px',
overflow: 'hidden',
position: 'relative',
display: 'flex',
flexDirection: 'column',
boxShadow: '0 20px 60px rgba(0,0,0,0.3)',
}}
onClick={(e) => e.stopPropagation()}
>
{/* 头部 */}
<div style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '12px 16px',
borderBottom: '1px solid #f0f0f0',
flexShrink: 0,
}}>
<Typography.Title level={5} style={{ margin: 0, color: '#1a1a2e', fontSize: 16 }}>
{previewItem.title || '预览'} {previewItem.title || '预览'}
</Typography.Title> </span>
<Button
icon={<XOutlined />}
onClick={handleClosePreview}
style={{
background: 'transparent',
border: 'none',
color: '#94a3b8',
fontSize: 16,
}}
/>
</div> </div>
}
{/* 内容区域 */} open={previewVisible}
<div style={{ onCancel={handleClosePreview}
flex: 1, width={1200}
display: 'flex', footer={
flexWrap: 'wrap', <div style={{ display: 'flex', justifyContent: 'flex-end', gap: 12, padding: '16px 24px', background: 'rgba(255,255,255,0.6)', borderTop: '1px solid rgba(99, 102, 241, 0.08)' }}>
height: '500px', <Button
gap: 20, type="primary"
padding: 20, icon={<DownloadOutlined />}
overflow: 'auto', onClick={() => {
justifyContent: 'center', if (videoRef.current) {
alignItems: 'center', videoRef.current.pause();
}}> }
const url = `${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${previewItem.videoUrl || previewItem.imageUrl}&download=1`;
const link = document.createElement('a');
link.href = url;
link.download = '';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}}
style={{ borderRadius: 8, background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', border: 'none', boxShadow: '0 4px 12px rgba(99, 102, 241, 0.3)' }}
disabled={isMediaExpired(previewItem.videoUrl || previewItem.imageUrl)}
>
{isMediaExpired(previewItem.videoUrl || previewItem.imageUrl) ? '资源已过期' : '下载'}
</Button>
<Button
onClick={handleSinglePushToMedia}
style={{ borderRadius: 8 }}
disabled={isMediaExpired(previewItem.videoUrl || previewItem.imageUrl)}
>
</Button>
</div>
}
style={{ borderRadius: 20 }}
styles={{
body: { height: 460, display: 'flex', flexDirection: 'column', padding: 0 },
header: { background: 'rgba(255,255,255,0.6)', backdropFilter: 'blur(10px)', borderBottom: '1px solid rgba(99, 102, 241, 0.08)', padding: '16px 24px' },
}}
>
{/* 内容区域 */}
<div style={{
flex: 1,
display: 'flex',
flexWrap: 'wrap',
height: '460px',
gap: 20,
padding: 20,
overflow: 'auto',
justifyContent: 'center',
alignItems: 'center',
}}>
{/* 媒体预览 */} {/* 媒体预览 */}
<div style={{ <div style={{
flex: 1, flex: 1,
@@ -2314,47 +2357,9 @@ const GeneratedRecord: React.FC = () => {
))} ))}
</> </>
)} )}
{/* 操作按钮 */}
<div >
<div style={{ display: 'flex', gap: 12, marginTop: 20 }}>
<Button
type="primary"
icon={<DownloadOutlined />}
onClick={() => {
// 暂停视频
if (videoRef.current) {
videoRef.current.pause();
}
const url = `${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${previewItem.videoUrl || previewItem.imageUrl}&download=1`;
window.open(url, '_blank');
}}
style={{ flex: 1, borderRadius: 8 }}
disabled={isMediaExpired(previewItem.videoUrl || previewItem.imageUrl)}
>
{isMediaExpired(previewItem.videoUrl || previewItem.imageUrl) ? '资源已过期' : '下载'}
</Button>
<Button
onClick={handleClosePreview}
style={{ flex: 1, borderRadius: 8 }}
>
</Button>
</div>
<div>
<Button
type="primary"
onClick={handleSinglePushToMedia}
style={{ width: '100%', borderRadius: 8, marginTop: 20 }}
disabled={isMediaExpired(previewItem.videoUrl || previewItem.imageUrl)}
>
</Button>
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </Modal>
)} )}
</div> </div>
); );