This commit is contained in:
2026-07-01 13:44:51 +08:00
5 changed files with 210 additions and 146 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-C_zmnmex.js"></script> <script type="module" crossorigin src="/assets/index-BsNc32TJ.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BhPFzWLH.css"> <link rel="stylesheet" crossorigin href="/assets/index-BhPFzWLH.css">
</head> </head>
<body> <body>
+6 -2
View File
@@ -338,11 +338,15 @@ export async function getEngine(): Promise<any> {
} }
}, },
default_size: "2K", default_size: "2K",
priority: 10 priority: 10,
maxImageCount: 4,
maxVideoCount: 0
} }
], ],
video: [ video: [
{ id: 'mock', name: 'Seedance', provider: 'seedance', supportedRatios: ['16:9', '9:16', '1:1', '4:3', '3:4', '21:9'], supportedResolutions: ['480p', '720p', '1080p'], supportedDurations: [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] } { id: '0019e1697667d0eff39', name: 'Seedance 2.0', provider: 'ark', model_name: 'seedance-2-0', supportedRatios: ['16:9', '9:16', '1:1', '4:3', '3:4', '21:9'], supportedResolutions: ['480p', '720p', '1080p'], supportedDurations: [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], maxImageCount: 4, maxVideoCount: 1 },
{ id: '0019e1697667d0eff38', name: 'Seedance 2.0 fast', provider: 'ark', model_name: 'seedance-2-0-fast', supportedRatios: ['16:9', '9:16', '1:1'], supportedResolutions: ['720p', '1080p'], supportedDurations: [5, 8, 10], maxImageCount: 2, maxVideoCount: 1 },
{ id: '0019f1b89b545d8e4b1', name: 'Seedance 2.0 mini', provider: 'ark', model_name: 'seedance-2-0-mini', supportedRatios: ['16:9', '9:16', '1:1'], supportedResolutions: ['480p', '720p'], supportedDurations: [4, 5, 6], maxImageCount: 1, maxVideoCount: 1 }
] ]
} }
}; };
@@ -143,16 +143,17 @@ const StorageCard: React.FC<{ data: ResourceCapacityData | null }> = ({ data })
color: '#475569', color: '#475569',
}} }}
> >
<span style={{ display: 'flex', alignItems: 'center', gap: 4 }}> {/* <span style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
<DatabaseOutlined style={{ fontSize: 12, color: isOver ? '#ef4444' : '#6366f1' }} /> <DatabaseOutlined style={{ fontSize: 12, color: isOver ? '#ef4444' : '#6366f1' }} />
{rawPercent.toFixed(1)}% {rawPercent.toFixed(1)}%
</span> </span> */}
{data.enabled ? ( {data.enabled ? (
<span style={{ fontWeight: 500, color: isOver ? '#ef4444' : '#1e293b' }}> <></>
{used.toFixed(2)} / {total.toFixed(2)} {unit}
</span>
) : ( ) : (
<span style={{ fontWeight: 500, color: '#1e293b' }}> <span style={{ fontWeight: 500, color: '#1e293b' }}>
<DatabaseOutlined style={{ fontSize: 12, color: isOver ? '#ef4444' : '#6366f1', marginRight: 4 }} />
使 {usedAuto.val} {usedAuto.unit} 使 {usedAuto.val} {usedAuto.unit}
</span> </span>
)} )}
@@ -162,12 +163,46 @@ const StorageCard: React.FC<{ data: ResourceCapacityData | null }> = ({ data })
<div <div
style={{ style={{
width: '100%', width: '100%',
height: 6, height: 20,
background: '#e2e8f0', background: '#e2e8f0',
borderRadius: 3, borderRadius: 3,
overflow: 'hidden', overflow: 'hidden',
position: 'relative',
}} }}
> >
<div style={{
position: 'absolute',
top: 0,
left: 0,
height: '100%',
background: 'transparent',
width: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
marginBottom: 6,
fontSize: 12,
color: '#000000ff',
padding: '0 8px',
}}>
<span style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
<DatabaseOutlined style={{ fontSize: 12, color: '#000000ff' }} />
{rawPercent.toFixed(1)}%
</span>
{data.enabled ? (
<span style={{ fontWeight: 500, color: isOver ? '#ef4444' : '#000000ff' }}>
{used.toFixed(2)} / {total.toFixed(2)} {unit}
</span>
) : (
<span style={{ fontWeight: 500, color: '#1e293b' }}>
使 {usedAuto.val} {usedAuto.unit}
</span>
)}
</div>
<div <div
style={{ style={{
width: `${barPercent}%`, width: `${barPercent}%`,
@@ -179,6 +214,7 @@ const StorageCard: React.FC<{ data: ResourceCapacityData | null }> = ({ data })
transition: 'width 0.4s ease', transition: 'width 0.4s ease',
}} }}
/> />
</div> </div>
<div <div
style={{ style={{
@@ -283,9 +319,9 @@ const AppLayout: React.FC = () => {
const [contactHovered, setContactHovered] = useState(false); const [contactHovered, setContactHovered] = useState(false);
const [contactForm] = Form.useForm(); const [contactForm] = Form.useForm();
const [submittingContact, setSubmittingContact] = useState(false); const [submittingContact, setSubmittingContact] = useState(false);
const [contactPosition, setContactPosition] = useState<{ x: number; y: number }>(() => ({ const [contactPosition, setContactPosition] = useState<{ x: number; y: number }>(() => ({
x: 24, x: 24,
y: window.innerHeight * 0.75 y: window.innerHeight * 0.75
})); }));
const [isDragging, setIsDragging] = useState(false); const [isDragging, setIsDragging] = useState(false);
const hasMovedRef = useRef(false); const hasMovedRef = useRef(false);
@@ -408,16 +444,16 @@ const AppLayout: React.FC = () => {
const handleContactMouseMove = (e: MouseEvent) => { const handleContactMouseMove = (e: MouseEvent) => {
if (!isDragging) return; if (!isDragging) return;
const deltaX = Math.abs(e.clientX - dragStartRef.current.x); const deltaX = Math.abs(e.clientX - dragStartRef.current.x);
const deltaY = Math.abs(e.clientY - dragStartRef.current.y); const deltaY = Math.abs(e.clientY - dragStartRef.current.y);
if (deltaX > 5 || deltaY > 5) { if (deltaX > 5 || deltaY > 5) {
hasMovedRef.current = true; hasMovedRef.current = true;
} }
const newY = Math.max(60, Math.min(window.innerHeight - 60, e.clientY - (dragStartRef.current.y - contactPosition.y))); const newY = Math.max(60, Math.min(window.innerHeight - 60, e.clientY - (dragStartRef.current.y - contactPosition.y)));
setContactPosition(prev => ({ x: prev.x, y: newY })); setContactPosition(prev => ({ x: prev.x, y: newY }));
}; };
@@ -425,7 +461,7 @@ const AppLayout: React.FC = () => {
const moved = hasMovedRef.current; const moved = hasMovedRef.current;
setIsDragging(false); setIsDragging(false);
hasMovedRef.current = false; hasMovedRef.current = false;
if (!moved) { if (!moved) {
setContactModalOpen(true); setContactModalOpen(true);
} }
@@ -680,12 +716,12 @@ const AppLayout: React.FC = () => {
return ( return (
<div key={item.id}> <div key={item.id}>
<div <div
onClick={() => { onClick={() => {
if (!(isGroup || hasChildren) && item.path) { if (!(isGroup || hasChildren) && item.path) {
navigate(item.path); navigate(item.path);
} }
}} }}
style={{ style={{
display: 'flex', alignItems: 'center', display: 'flex', alignItems: 'center',
justifyContent: 'flex-start', justifyContent: 'flex-start',
@@ -720,7 +756,7 @@ const AppLayout: React.FC = () => {
{item.label} {item.label}
</span> </span>
</div> </div>
{(isGroup || hasChildren) && ( {(isGroup || hasChildren) && (
<div style={{ overflow: 'hidden' }}> <div style={{ overflow: 'hidden' }}>
{(childMap[item.id] || []).map(c => renderMenuItem(c, depth + 1))} {(childMap[item.id] || []).map(c => renderMenuItem(c, depth + 1))}
@@ -760,7 +796,7 @@ const AppLayout: React.FC = () => {
boxShadow: '0 4px 16px rgba(99, 102, 241, 0.35)', boxShadow: '0 4px 16px rgba(99, 102, 241, 0.35)',
overflow: 'hidden', overflow: 'hidden',
}} }}
> >
{siteLogo ? ( {siteLogo ? (
<img src={siteLogo} alt="logo" style={{ width: 28, height: 28, objectFit: 'contain' }} /> <img src={siteLogo} alt="logo" style={{ width: 28, height: 28, objectFit: 'contain' }} />
@@ -811,9 +847,9 @@ const AppLayout: React.FC = () => {
<span></span> <span></span>
</div> </div>
{/* 资源存储容量展示(来自 getUser.resourceCapacity */} {/* 资源存储容量展示(来自 getUser.resourceCapacity */}
<StorageCard data={resourceCapacity} /> <StorageCard data={resourceCapacity} />
<div style={{ padding: '16px 16px', flexShrink: 0 ,paddingTop: 0}}> <div style={{ padding: '16px 16px', flexShrink: 0, paddingTop: 0 }}>
<Dropdown menu={{ items: userMenuItems, onClick: handleUserMenuClick }} placement="topRight" arrow> <Dropdown menu={{ items: userMenuItems, onClick: handleUserMenuClick }} placement="topRight" arrow>
<div style={{ <div style={{
display: 'flex', alignItems: 'center', display: 'flex', alignItems: 'center',
@@ -840,7 +876,7 @@ const AppLayout: React.FC = () => {
flexShrink: 0, flexShrink: 0,
boxShadow: '0 4px 12px rgba(99, 102, 241, 0.3)', boxShadow: '0 4px 12px rgba(99, 102, 241, 0.3)',
}} /> }} />
<div style={{ flex: 1, minWidth: 0 }}> <div style={{ flex: 1, minWidth: 0 }}>
<div style={{ color: '#1e293b', fontSize: 16, fontWeight: 600, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', letterSpacing: -0.01 }}> <div style={{ color: '#1e293b', fontSize: 16, fontWeight: 600, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', letterSpacing: -0.01 }}>
{user?.username} {user?.username}
@@ -859,7 +895,7 @@ const AppLayout: React.FC = () => {
</div> </div>
</Dropdown> </Dropdown>
</div> </div>
</div> </div>
@@ -975,7 +1011,7 @@ const AppLayout: React.FC = () => {
{menuIcon} {menuIcon}
</span> </span>
)} )}
<span className="mobile-menu-label" style={{ <span className="mobile-menu-label" style={{
paddingLeft: menuType === 'group' ? 0 : 0, paddingLeft: menuType === 'group' ? 0 : 0,
color: menuType === 'group' ? '#94a3b8' : (isActive ? '#4f46e5' : '#475569'), color: menuType === 'group' ? '#94a3b8' : (isActive ? '#4f46e5' : '#475569'),
fontWeight: menuType === 'group' ? 600 : (isActive ? 600 : 400), fontWeight: menuType === 'group' ? 600 : (isActive ? 600 : 400),
@@ -1010,7 +1046,7 @@ const AppLayout: React.FC = () => {
<span className="mobile-menu-icon" style={{ color: childActive ? '#6366f1' : '#94a3b8' }}> <span className="mobile-menu-icon" style={{ color: childActive ? '#6366f1' : '#94a3b8' }}>
{childIcon} {childIcon}
</span> </span>
<span className="mobile-menu-label" style={{ <span className="mobile-menu-label" style={{
color: childActive ? '#4f46e5' : '#64748b', color: childActive ? '#4f46e5' : '#64748b',
fontWeight: childActive ? 600 : 400, fontWeight: childActive ? 600 : 400,
fontSize: 14, fontSize: 14,
@@ -1099,7 +1135,7 @@ const AppLayout: React.FC = () => {
</span> </span>
<span className="mobile-menu-label" style={{ color: '#fff', fontWeight: 600 }}></span> <span className="mobile-menu-label" style={{ color: '#fff', fontWeight: 600 }}></span>
</div> </div>
<div <div
className="mobile-menu-item" className="mobile-menu-item"
onClick={() => { onClick={() => {
@@ -1162,9 +1198,9 @@ const AppLayout: React.FC = () => {
<InfoOutlined style={{ color: '#6366f1', fontSize: 14 }} /> <InfoOutlined style={{ color: '#6366f1', fontSize: 14 }} />
<Typography.Text style={{ color: '#64748b', fontSize: 13 }}> <Typography.Text style={{ color: '#64748b', fontSize: 13 }}>
/ /
<Typography.Text <Typography.Text
style={{ style={{
color: '#ff0000ff', color: '#ff0000ff',
cursor: 'pointer', cursor: 'pointer',
textDecoration: 'underline', textDecoration: 'underline',
}} }}
@@ -1444,7 +1480,7 @@ const AppLayout: React.FC = () => {
<NotificationPopup /> <NotificationPopup />
<div <div
className="contact-button-wrapper" className="contact-button-wrapper"
style={{ style={{
right: `${contactPosition.x}px`, right: `${contactPosition.x}px`,
+44 -20
View File
@@ -143,6 +143,12 @@ const AIChatPage: React.FC = () => {
setCurrentMedia, setCurrentMedia,
} = useAppStore(); } = useAppStore();
// 获取当前选中引擎的媒体上传限制
const currentEngineList = mediaType === 'image' ? enginesele?.image : enginesele?.video;
const currentEngine = currentEngineList?.find((e: any) => e.id === countType);
const maxImageCount = currentEngine?.maxImageCount ?? 4;
const maxVideoCount = currentEngine?.maxVideoCount ?? 1;
const [uploading, setUploading] = useState<boolean>(false); const [uploading, setUploading] = useState<boolean>(false);
const [loading, setLoading] = useState<boolean>(false); const [loading, setLoading] = useState<boolean>(false);
@@ -427,19 +433,19 @@ const AIChatPage: React.FC = () => {
} }
} }
let supportedSizes = (data as any).engine.image?.[0]?.supportedSizes || {}; let supportedSizes = (data as any).engine?.image?.[0]?.supported_sizes || {};
let supportedResolutions = []; let supportedResolutions: string[] = [];
let twokwidth = []; let twokwidth: string[] = [];
let fourkwidth = []; let fourkwidth: string[] = [];
for (let key in supportedSizes["2K"]) { for (let key in supportedSizes["2K"]) {
supportedResolutions.push(key); supportedResolutions.push(key);
twokwidth.push(supportedSizes["2K"][key]); twokwidth.push(supportedSizes["2K"][key]);
} }
const newRatioOptions = supportedResolutions.map((res: any, index: number) => ({ const newRatioOptions = supportedResolutions.map((res: any) => ({
value: res, value: res,
label: String(index), label: res,
})); }));
setRatioOptions(newRatioOptions); setRatioOptions(newRatioOptions);
@@ -838,6 +844,18 @@ const AIChatPage: React.FC = () => {
return false; return false;
} }
// 获取当前选中引擎的限制
const currentEngineList = mediaType === 'image' ? enginesele.image : enginesele.video;
const currentEngine = currentEngineList?.find((e: any) => e.id === countType);
const maxImage = currentEngine?.maxImageCount ?? 4;
const maxVideo = currentEngine?.maxVideoCount ?? 1;
// 根据 mediaType 判断是否允许该文件类型
if (mediaType === 'image' && isVideo) {
message.error('图片模式仅支持上传图片');
return false;
}
// 验证文件大小 // 验证文件大小
const maxMB = isVideo ? 100 : 10; const maxMB = isVideo ? 100 : 10;
if (file.size / 1024 / 1024 > maxMB) { if (file.size / 1024 / 1024 > maxMB) {
@@ -845,17 +863,17 @@ const AIChatPage: React.FC = () => {
return false; return false;
} }
// 验证图片数量(最多4张) // 验证图片数量
const imageCount = currentMedia.filter((m) => m.type === 'image').length; const imageCount = currentMedia.filter((m) => m.type === 'image').length;
if (isImage && imageCount >= 4) { if (isImage && imageCount >= maxImage) {
message.error('最多上传4张图片'); message.error(`该引擎最多上传${maxImage}张图片`);
return false; return false;
} }
// 验证视频数量(最多1个) // 验证视频数量
const videoCount = currentMedia.filter((m) => m.type === 'video').length; const videoCount = currentMedia.filter((m) => m.type === 'video').length;
if (isVideo && videoCount >= 1) { if (isVideo && videoCount >= maxVideo) {
message.error('最多上传1个视频'); message.error(`该引擎最多上传${maxVideo}个视频`);
return false; return false;
} }
@@ -1092,9 +1110,9 @@ const AIChatPage: React.FC = () => {
border: '1px solid rgba(99, 102, 241, 0.08)', border: '1px solid rgba(99, 102, 241, 0.08)',
position: 'relative', overflow: 'hidden', flexWrap: 'wrap', gap: 12, position: 'relative', overflow: 'hidden', flexWrap: 'wrap', gap: 12,
}}> }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 16 }}> <div style={{ width: '100%', display: 'flex', alignItems: 'center', gap: 16 }}>
<div style={{ width: 32, height: 2, background: 'linear-gradient(90deg, transparent, #6366f1, #8b5cf6, transparent)', borderRadius: 1 }} /> <div style={{ width: '15%', height: 2, background: 'linear-gradient(90deg, transparent, #6366f1, #8b5cf6, transparent)', borderRadius: 1 }} />
<div> <div style={{ flex: 1, minWidth: 0, textAlign: 'center' }}>
<h2 style={{ <h2 style={{
margin: 0, margin: 0,
fontSize: 18, fontSize: 18,
@@ -1102,7 +1120,7 @@ const AIChatPage: React.FC = () => {
background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)',
WebkitBackgroundClip: 'text', WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent', WebkitTextFillColor: 'transparent',
backgroundClip: 'text' backgroundClip: 'text',
}}> }}>
AI创作 AI创作
</h2> </h2>
@@ -1110,7 +1128,7 @@ const AIChatPage: React.FC = () => {
/ /
</p> </p>
</div> </div>
<div style={{ width: 32, height: 2, background: 'linear-gradient(90deg, transparent, #8b5cf6, #6366f1, transparent)', borderRadius: 1 }} /> <div style={{ width: '15%', height: 2, background: 'linear-gradient(90deg, transparent, #8b5cf6, #6366f1, transparent)', borderRadius: 1 }} />
</div> </div>
</div> </div>
@@ -1633,11 +1651,14 @@ const AIChatPage: React.FC = () => {
}}> }}>
{/* 上传按钮 */} {/* 上传按钮 */}
<Upload <Upload
accept="image/*,video/*" accept={mediaType === 'image' ? 'image/*' : 'image/*,video/*'}
showUploadList={false} showUploadList={false}
beforeUpload={handleUpload} beforeUpload={handleUpload}
> >
<Tooltip title={`图片${currentMedia.filter(m => m.type === 'image').length}/4,视频${currentMedia.filter(m => m.type === 'video').length}/1`}> <Tooltip title={mediaType === 'image'
? `图片${currentMedia.filter(m => m.type === 'image').length}/${maxImageCount}`
: `图片${currentMedia.filter(m => m.type === 'image').length}/${maxImageCount},视频${currentMedia.filter(m => m.type === 'video').length}/${maxVideoCount}`
}>
<div <div
style={{ style={{
width: 48, width: 48,
@@ -1679,7 +1700,10 @@ const AIChatPage: React.FC = () => {
onChange={handleInputChange} onChange={handleInputChange}
onKeyDown={handleInputKeyDown} onKeyDown={handleInputKeyDown}
onKeyPress={handleKeyPress} onKeyPress={handleKeyPress}
placeholder="上传最多4张参考图,输入提示词描述您想生成的画面..." placeholder={mediaType === 'image'
? `上传最多${maxImageCount}张参考图,输入提示词描述您想生成的画面...`
: `上传参考图(最多${maxImageCount}张)和视频(最多${maxVideoCount}个),输入提示词描述您想生成的画面...`
}
autoSize={{ minRows: 1, maxRows: 4 }} autoSize={{ minRows: 1, maxRows: 4 }}
style={{ style={{
flex: 1, flex: 1,