Merge branch 'main' of https://gitee.com/wg123/video-gen
This commit is contained in:
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+36
-36
@@ -1,37 +1,37 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<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.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet" />
|
||||
<title>后台管理</title>
|
||||
<script>
|
||||
(function() {
|
||||
var cached = localStorage.getItem('siteInfo');
|
||||
if (cached) {
|
||||
try {
|
||||
var info = JSON.parse(cached);
|
||||
if (info.siteName) {
|
||||
document.title = info.siteName + ' - 管理后台';
|
||||
}
|
||||
if (info.siteLogo) {
|
||||
var link = document.querySelector('link[rel="icon"]');
|
||||
if (link) {
|
||||
link.href = info.siteLogo;
|
||||
link.type = 'image/png';
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<script type="module" crossorigin src="/assets/index-BhcMFup1.js"></script>
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<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.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet" />
|
||||
<title>后台管理</title>
|
||||
<script>
|
||||
(function() {
|
||||
var cached = localStorage.getItem('siteInfo');
|
||||
if (cached) {
|
||||
try {
|
||||
var info = JSON.parse(cached);
|
||||
if (info.siteName) {
|
||||
document.title = info.siteName + ' - 管理后台';
|
||||
}
|
||||
if (info.siteLogo) {
|
||||
var link = document.querySelector('link[rel="icon"]');
|
||||
if (link) {
|
||||
link.href = info.siteLogo;
|
||||
link.type = 'image/png';
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<script type="module" crossorigin src="/assets/index-DQ1LXqt7.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-D7ShJUt4.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>␍
|
||||
</body>
|
||||
</html>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -258,8 +258,8 @@ const AdminGenerationAiRecords: React.FC = () => {
|
||||
userId: queryUserId || undefined,
|
||||
userName: queryUserName || undefined,
|
||||
engineId: queryEngineId || undefined,
|
||||
createdStart: queryCreatedRange?.[0]?.toISOString?.(),
|
||||
createdEnd: queryCreatedRange?.[1]?.toISOString?.(),
|
||||
createdStart: queryCreatedRange?.[0]?.format?.('YYYY-MM-DDTHH:mm:ss'),
|
||||
createdEnd: queryCreatedRange?.[1]?.format?.('YYYY-MM-DDTHH:mm:ss'),
|
||||
page,
|
||||
pageSize: PAGE_SIZE,
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import random
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
import httpx
|
||||
from sqlalchemy import select, func
|
||||
@@ -136,8 +136,8 @@ async def get_juliang_token(app_id: str, secret: str, code: str, open_type: int,
|
||||
data = data.get("data", {})
|
||||
access_token = data.get("access_token", "")
|
||||
refresh_token = data.get("refresh_token", "")
|
||||
expires_in = datetime.now() + timedelta(seconds=data.get("expires_in", 0))
|
||||
refresh_token_expires_in = datetime.now() + timedelta(seconds=data.get("refresh_token_expires_in", 0))
|
||||
expires_in = datetime.now(timezone.utc) + timedelta(seconds=data.get("expires_in", 0))
|
||||
refresh_token_expires_in = datetime.now(timezone.utc) + timedelta(seconds=data.get("refresh_token_expires_in", 0))
|
||||
#2.获取已授权角色账户,一个授权可能有多个角色账户
|
||||
url = "https://api.oceanengine.com/open_api/oauth2/advertiser/get/"
|
||||
response = await client.get(
|
||||
@@ -222,7 +222,7 @@ async def get_juliang_token(app_id: str, secret: str, code: str, open_type: int,
|
||||
|
||||
# 1. 软删除已消失的账户
|
||||
for account_id in old_account_ids - new_account_ids:
|
||||
existing_accounts[account_id].deleted_at = datetime.now()
|
||||
existing_accounts[account_id].deleted_at = datetime.now(timezone.utc)
|
||||
|
||||
# 2. 更新或新增账户
|
||||
for account in account_list:
|
||||
|
||||
@@ -93,8 +93,8 @@ async def refresh_juliang_token(oauth: UserOAuth, app: UserOAuthApp, db: AsyncSe
|
||||
data = data.get("data", {})
|
||||
new_access_token = data.get("access_token", "")
|
||||
new_refresh_token = data.get("refresh_token", "")
|
||||
expires_in = datetime.now(tz=oauth.access_token_expired.tzinfo) + timedelta(seconds=data.get("expires_in", 0))
|
||||
refresh_token_expires_in = datetime.now(tz=oauth.refresh_token_expired.tzinfo) + timedelta(seconds=data.get("refresh_token_expires_in", 0))
|
||||
expires_in = datetime.now(timezone.utc) + timedelta(seconds=data.get("expires_in", 0))
|
||||
refresh_token_expires_in = datetime.now(timezone.utc) + timedelta(seconds=data.get("refresh_token_expires_in", 0))
|
||||
|
||||
from sqlalchemy import update
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ class DouyinRequest:
|
||||
token = cache.get("token")
|
||||
expired_at_str = cache.get("expired_at")
|
||||
if token and expired_at_str:
|
||||
expired_at = datetime.fromisoformat(expired_at_str)
|
||||
expired_at = datetime.fromisoformat(expired_at_str).replace(tzinfo=timezone.utc)
|
||||
if expired_at > datetime.now(timezone.utc):
|
||||
return token
|
||||
except Exception as e:
|
||||
|
||||
+90
-90
File diff suppressed because one or more lines are too long
Vendored
+36
-36
@@ -1,37 +1,37 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<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.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet" />
|
||||
<title>民众智创</title>
|
||||
<script>
|
||||
(function() {
|
||||
var cached = localStorage.getItem('siteInfo');
|
||||
if (cached) {
|
||||
try {
|
||||
var info = JSON.parse(cached);
|
||||
if (info.siteName) {
|
||||
document.title = info.siteName;
|
||||
}
|
||||
if (info.siteLogo) {
|
||||
var link = document.querySelector('link[rel="icon"]');
|
||||
if (link) {
|
||||
link.href = info.siteLogo;
|
||||
link.type = 'image/png';
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<script type="module" crossorigin src="/assets/index-DxvrbZVm.js"></script>
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<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.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet" />
|
||||
<title>民众智创</title>
|
||||
<script>
|
||||
(function() {
|
||||
var cached = localStorage.getItem('siteInfo');
|
||||
if (cached) {
|
||||
try {
|
||||
var info = JSON.parse(cached);
|
||||
if (info.siteName) {
|
||||
document.title = info.siteName;
|
||||
}
|
||||
if (info.siteLogo) {
|
||||
var link = document.querySelector('link[rel="icon"]');
|
||||
if (link) {
|
||||
link.href = info.siteLogo;
|
||||
link.type = 'image/png';
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<script type="module" crossorigin src="/assets/index-BRAajPcM.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-D9_3MPsN.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>␍
|
||||
</body>
|
||||
</html>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -50,6 +50,7 @@ import {
|
||||
ArrowUpOutlined,
|
||||
DownloadOutlined,
|
||||
ReloadOutlined,
|
||||
AudioOutlined,
|
||||
|
||||
} from '@ant-design/icons';
|
||||
|
||||
@@ -274,7 +275,7 @@ const AIChatPage: React.FC = () => {
|
||||
// 附件预览弹窗状态(独立弹窗)
|
||||
const [attachmentPreviewVisible, setAttachmentPreviewVisible] = useState<boolean>(false);
|
||||
const [attachmentPreviewUrl, setAttachmentPreviewUrl] = useState<string>('');
|
||||
const [attachmentPreviewType, setAttachmentPreviewType] = useState<'image' | 'video'>('image');
|
||||
const [attachmentPreviewType, setAttachmentPreviewType] = useState<'image' | 'video' | 'audio'>('image');
|
||||
const [attachmentPreviewName, setAttachmentPreviewName] = useState<string>('');
|
||||
const attachmentPreviewVideoRef = useRef<HTMLVideoElement>(null);
|
||||
|
||||
@@ -292,6 +293,7 @@ const AIChatPage: React.FC = () => {
|
||||
const [fblindex, setFBlindex] = useState<any>(0);
|
||||
|
||||
const [showEngineModal, setShowEngineModal] = useState(false);
|
||||
const [showMediaTypeModal, setShowMediaTypeModal] = useState(false);
|
||||
|
||||
const [creditRatios, setCreditRatios] = useState<any[]>([]);
|
||||
const [cimage, setCimage] = useState<any[]>([]);
|
||||
@@ -428,6 +430,10 @@ const AIChatPage: React.FC = () => {
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (e: MouseEvent) => {
|
||||
const target = e.target as HTMLElement;
|
||||
// 关闭媒体类型选择弹窗
|
||||
if (showMediaTypeModal && !target.closest('.image-settings-popover') && !target.closest('.image-settings-trigger')) {
|
||||
setShowMediaTypeModal(false);
|
||||
}
|
||||
// 关闭引擎选择弹窗
|
||||
if (showEngineModal && !target.closest('.image-settings-popover') && !target.closest('.image-settings-trigger')) {
|
||||
setShowEngineModal(false);
|
||||
@@ -446,7 +452,7 @@ const AIChatPage: React.FC = () => {
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, [showEngineModal, showImageSettingsModal, showVideoSettingsModal]);
|
||||
}, [showMediaTypeModal, showEngineModal, showImageSettingsModal, showVideoSettingsModal]);
|
||||
|
||||
useEffect(() => {
|
||||
if (mediaType !== 'video') return;
|
||||
@@ -788,12 +794,8 @@ const AIChatPage: React.FC = () => {
|
||||
const isFirstLastFrameMode = mediaType === 'video' && referenceMode === 'first_last_frame';
|
||||
|
||||
if (isFirstLastFrameMode) {
|
||||
if (!inputValue.trim() && !firstFrame) {
|
||||
message.warning('请输入内容或上传首帧图片');
|
||||
return;
|
||||
}
|
||||
if (!firstFrame) {
|
||||
message.warning('请上传首帧图片');
|
||||
if (!inputValue.trim()) {
|
||||
message.warning('请输入内容');
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
@@ -2094,152 +2096,7 @@ const AIChatPage: React.FC = () => {
|
||||
<div style={{ display: 'flex', gap: isFirstLastFrameComposer ? 18 : 18, alignItems: 'flex-end', marginBottom: 14 }}>
|
||||
{/* 左侧附件区域 */}
|
||||
<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' ? (
|
||||
@@ -2345,63 +2202,216 @@ const AIChatPage: React.FC = () => {
|
||||
</div>) : (
|
||||
<>
|
||||
|
||||
{/* 上传按钮 - 非首尾帧模式且还有上传空间时始终显示 */}
|
||||
{/* 上传区域 - 图片堆叠展示,右下角圆形+按钮 */}
|
||||
{!(mediaType === 'video' && referenceMode === 'first_last_frame') && (
|
||||
<div style={{ width: '100%', display: 'flex', justifyContent: 'center', minHeight: 84 }}>
|
||||
<Upload
|
||||
accept={mediaType === 'image' ? 'image/*' : 'image/*,video/*,audio/*'}
|
||||
showUploadList={false}
|
||||
beforeUpload={handleUpload}
|
||||
>
|
||||
<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
|
||||
style={{
|
||||
width: 54,
|
||||
height: 74,
|
||||
borderRadius: 7,
|
||||
border: '1px solid rgba(231, 234, 240, 0.95)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.25s ease',
|
||||
background: '#ffffff',
|
||||
flexDirection: 'column',
|
||||
gap: 5,
|
||||
transform: 'rotate(-7deg)',
|
||||
boxShadow: '0 9px 20px rgba(47, 52, 64, 0.10), inset 0 1px 0 rgba(255,255,255,0.95)',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.borderColor = '#D7DDE7';
|
||||
e.currentTarget.style.background = 'linear-gradient(180deg, #ffffff 0%, #F7F8FA 100%)';
|
||||
e.currentTarget.style.transform = 'rotate(0deg) translateY(-2px)';
|
||||
e.currentTarget.style.boxShadow = '0 14px 28px rgba(47, 52, 64, 0.15), inset 0 1px 0 rgba(255,255,255,0.98)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.borderColor = 'rgba(231, 234, 240, 0.95)';
|
||||
e.currentTarget.style.background = 'linear-gradient(180deg, #ffffff 0%, #FAFBFC 100%)';
|
||||
e.currentTarget.style.transform = 'rotate(-7deg)';
|
||||
e.currentTarget.style.boxShadow = '0 9px 20px rgba(47, 52, 64, 0.10), inset 0 1px 0 rgba(255,255,255,0.95)';
|
||||
}}
|
||||
>
|
||||
{uploading ? (
|
||||
<LoadingOutlined style={{ fontSize: 17, color: '#667085' }} />
|
||||
) : (
|
||||
<>
|
||||
<PlusOutlined style={{ fontSize: 18, color: '#667085', lineHeight: 1 }} />
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 14px)', columnGap: 2, justifyContent: 'center', color: '#344054', fontSize: 12, fontWeight: 700, lineHeight: 1.05, letterSpacing: 0 }}>
|
||||
<span style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 1 }}><span>参</span><span>考</span></span>
|
||||
<span style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 1 }}><span>内</span><span>容</span></span>
|
||||
<div
|
||||
style={{
|
||||
position: 'relative',
|
||||
width: 64,
|
||||
height: 72,
|
||||
marginTop: '-10',
|
||||
}}
|
||||
onMouseEnter={openMediaStackTray}
|
||||
onMouseLeave={closeMediaStackTray}
|
||||
>
|
||||
{/* 没有上传时的卡片样式 */}
|
||||
{currentMedia.length === 0 && (
|
||||
<Upload
|
||||
accept={mediaType === 'image' ? 'image/*' : 'image/*,video/*,audio/*'}
|
||||
showUploadList={false}
|
||||
beforeUpload={handleUpload}
|
||||
>
|
||||
<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
|
||||
style={{
|
||||
width: 54,
|
||||
height: 74,
|
||||
borderRadius: 7,
|
||||
border: '1px solid rgba(231, 234, 240, 0.95)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.25s ease',
|
||||
background: '#ffffff',
|
||||
flexDirection: 'column',
|
||||
gap: 5,
|
||||
transform: 'rotate(-7deg)',
|
||||
boxShadow: '0 9px 20px rgba(47, 52, 64, 0.10), inset 0 1px 0 rgba(255,255,255,0.95)',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.borderColor = '#D7DDE7';
|
||||
e.currentTarget.style.background = 'linear-gradient(180deg, #ffffff 0%, #F7F8FA 100%)';
|
||||
e.currentTarget.style.transform = 'rotate(0deg) translateY(-2px)';
|
||||
e.currentTarget.style.boxShadow = '0 14px 28px rgba(47, 52, 64, 0.15), inset 0 1px 0 rgba(255,255,255,0.98)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.borderColor = 'rgba(231, 234, 240, 0.95)';
|
||||
e.currentTarget.style.background = 'linear-gradient(180deg, #ffffff 0%, #FAFBFC 100%)';
|
||||
e.currentTarget.style.transform = 'rotate(-7deg)';
|
||||
e.currentTarget.style.boxShadow = '0 9px 20px rgba(47, 52, 64, 0.10), inset 0 1px 0 rgba(255,255,255,0.95)';
|
||||
}}
|
||||
>
|
||||
{uploading ? (
|
||||
<LoadingOutlined style={{ fontSize: 17, color: '#667085' }} />
|
||||
) : (
|
||||
<>
|
||||
<PlusOutlined style={{ fontSize: 18, color: '#667085', lineHeight: 1 }} />
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 14px)', columnGap: 2, justifyContent: 'center', color: '#344054', fontSize: 12, fontWeight: 700, lineHeight: 1.05, letterSpacing: 0 }}>
|
||||
<span style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 1 }}><span>参</span><span>考</span></span>
|
||||
<span style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 1 }}><span>内</span><span>容</span></span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Upload>
|
||||
)}
|
||||
|
||||
{/* 层叠附件展示 - 鼠标移入向右排列展开 */}
|
||||
{currentMedia.length > 0 && (
|
||||
<>
|
||||
{currentMedia.map((media, idx) => {
|
||||
const reversedIdx = Math.min(currentMedia.length, 3) - 1 - idx;
|
||||
const offsetX = mediaStackHovered ? idx * 58 : (idx < 3 ? idx * 6 : 18);
|
||||
const isVisible = !mediaStackHovered ? idx < 3 : true;
|
||||
return (
|
||||
<div
|
||||
key={`stack-${idx}`}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: offsetX,
|
||||
bottom: 0,
|
||||
zIndex: idx + 1,
|
||||
transition: 'all 0.28s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
transform: mediaStackHovered ? `scale(1) rotate(0deg)` : `scale(${idx < 3 ? (1 - reversedIdx * 0.08) : 0}) rotate(${idx < 3 ? (-3 + idx * 1.5) : 0}deg)`,
|
||||
opacity: isVisible ? (mediaStackHovered ? 1 : (1 - reversedIdx * 0.15)) : 0,
|
||||
pointerEvents: isVisible ? 'auto' : 'none',
|
||||
}}
|
||||
>
|
||||
<div style={{ position: 'relative', width: 52, height: 60 }}>
|
||||
{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: 52, height: 60, objectFit: 'cover', borderRadius: 10, cursor: 'pointer', border: '1px solid rgba(255,255,255,0.98)', boxShadow: '0 4px 12px rgba(31,41,55,0.15)' }}
|
||||
/>
|
||||
) : media.type === 'video' ? (
|
||||
<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: 52, height: 60, objectFit: 'cover', borderRadius: 10, cursor: 'pointer', border: '1px solid rgba(255,255,255,0.98)', boxShadow: '0 4px 12px rgba(31,41,55,0.15)' }}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
onClick={() => {
|
||||
setAttachmentPreviewUrl(media.url);
|
||||
setAttachmentPreviewType('audio');
|
||||
setAttachmentPreviewName(media.name);
|
||||
setAttachmentPreviewVisible(true);
|
||||
}}
|
||||
style={{ width: 52, height: 60, objectFit: 'cover', borderRadius: 10, cursor: 'pointer', border: '1px solid rgba(255,255,255,0.98)', boxShadow: '0 4px 12px rgba(31,41,55,0.15)', background: 'linear-gradient(135deg, #8b5cf6 0%, #a78bfa 100%)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}
|
||||
>
|
||||
<AudioOutlined style={{ fontSize: 20, color: '#fff' }} />
|
||||
</div>
|
||||
)}
|
||||
{/* 右上角删除按钮 */}
|
||||
<button
|
||||
onClick={() => handleRemoveMedia(idx)}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: -6,
|
||||
right: -6,
|
||||
width: 20,
|
||||
height: 20,
|
||||
borderRadius: 50,
|
||||
background: '#1f2937',
|
||||
border: 'none',
|
||||
color: '#fff',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
boxShadow: '0 2px 6px rgba(0,0,0,0.2)',
|
||||
zIndex: 10,
|
||||
transition: 'all 0.2s ease',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.background = '#dc2626';
|
||||
e.currentTarget.style.transform = 'scale(1.1)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.background = '#1f2937';
|
||||
e.currentTarget.style.transform = 'scale(1)';
|
||||
}}
|
||||
>
|
||||
<DeleteOutlined style={{ fontSize: 10 }} />
|
||||
</button>
|
||||
</div>
|
||||
{/* <span style={{ fontSize: 9, color: '#98a2b3', fontWeight: 600, lineHeight: 1 }}>{currentMedia.length}/{maxImageCount + maxVideoCount}</span> */}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Upload>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 右下角圆形+上传按钮 */}
|
||||
{currentMedia.length > 0 && (
|
||||
<Upload
|
||||
accept={mediaType === 'image' ? 'image/*' : 'image/*,video/*,audio/*'}
|
||||
showUploadList={false}
|
||||
beforeUpload={handleUpload}
|
||||
>
|
||||
<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
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
width: 28,
|
||||
height: 28,
|
||||
borderRadius: 50,
|
||||
background: '#ffffff',
|
||||
border: '1px solid rgba(231, 234, 240, 0.95)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s ease',
|
||||
boxShadow: '0 2px 8px rgba(47, 52, 64, 0.08)',
|
||||
zIndex: 100,
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.borderColor = '#8b5cf6';
|
||||
e.currentTarget.style.boxShadow = '0 4px 12px rgba(139, 92, 246, 0.2)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.borderColor = 'rgba(231, 234, 240, 0.95)';
|
||||
e.currentTarget.style.boxShadow = '0 2px 8px rgba(47, 52, 64, 0.08)';
|
||||
}}
|
||||
>
|
||||
<PlusOutlined style={{ fontSize: 14, color: '#8b5cf6', lineHeight: 1 }} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Upload>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
@@ -2521,36 +2531,144 @@ const AIChatPage: React.FC = () => {
|
||||
创作类型:
|
||||
</span> */}
|
||||
{/* 文件类型选择器 */}
|
||||
<Select
|
||||
className="ai-composer-select"
|
||||
value={mediaType}
|
||||
onChange={(val) => setMediaType(val)}
|
||||
style={{
|
||||
minWidth: 118,
|
||||
padding: '6px 14px',
|
||||
height: 32,
|
||||
outline: 'none',
|
||||
border: '1px solid #E7EAF0',
|
||||
backgroundColor: '#ffffff',
|
||||
borderRadius: 10,
|
||||
boxShadow: '0 1px 2px rgba(47, 52, 64, 0.025)',
|
||||
}}
|
||||
size="middle"
|
||||
>
|
||||
<Option value="video">
|
||||
<VideoCameraOutlined style={{ marginRight: 6, fontSize: 14, color: '#8b5cf6' }} />
|
||||
<span style={{ fontSize: 13, fontWeight: 650, color: '#475467' }}>视频生成</span>
|
||||
</Option>
|
||||
<Option value="image">
|
||||
<PictureOutlined style={{ marginRight: 6, fontSize: 14, color: '#8b5cf6' }} />
|
||||
<span style={{ fontSize: 13, fontWeight: 650, color: '#475467' }}>图片生成</span>
|
||||
</Option>
|
||||
</Select>
|
||||
<div style={{ position: 'relative', display: 'inline-block' }}>
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowEngineModal(false);
|
||||
setShowImageSettingsModal(false);
|
||||
setShowVideoSettingsModal(false);
|
||||
setReferenceModeDropdownVisible(false);
|
||||
setShowMediaTypeModal(prev => !prev);
|
||||
}}
|
||||
className="image-settings-trigger"
|
||||
style={{
|
||||
minWidth: 118,
|
||||
padding: '5px 12px',
|
||||
height: 32,
|
||||
borderRadius: 10,
|
||||
border: '1px solid #E7EAF0',
|
||||
backgroundColor: '#ffffff',
|
||||
cursor: 'pointer',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
transition: 'all 0.2s ease',
|
||||
boxShadow: '0 1px 2px rgba(47, 52, 64, 0.025)',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.backgroundColor = '#FFFFFF';
|
||||
e.currentTarget.style.borderColor = '#D7DDE7';
|
||||
e.currentTarget.style.boxShadow = '0 2px 8px rgba(47, 52, 64, 0.06)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.backgroundColor = '#ffffff';
|
||||
e.currentTarget.style.borderColor = '#E7EAF0';
|
||||
e.currentTarget.style.boxShadow = '0 1px 2px rgba(47, 52, 64, 0.03)';
|
||||
}}
|
||||
>
|
||||
{mediaType === 'video' ? (
|
||||
<VideoCameraOutlined style={{ fontSize: 14, color: '#8b5cf6' }} />
|
||||
) : (
|
||||
<PictureOutlined style={{ fontSize: 14, color: '#8b5cf6' }} />
|
||||
)}
|
||||
<Text style={{
|
||||
fontSize: 13,
|
||||
fontWeight: 500,
|
||||
color: '#2f3440',
|
||||
}}>
|
||||
{mediaType === 'video' ? '视频生成' : '图片生成'}
|
||||
</Text>
|
||||
<CaretDownOutlined style={{ fontSize: 10, color: '#8b5cf6', marginLeft: 'auto' }} />
|
||||
</button>
|
||||
|
||||
{showMediaTypeModal && (
|
||||
<div
|
||||
className="image-settings-popover"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: 'calc(100% + 8px)',
|
||||
left: 0,
|
||||
width: 300,
|
||||
backgroundColor: '#ffffff',
|
||||
backdropFilter: 'blur(16px)',
|
||||
borderRadius: 16,
|
||||
boxShadow: '0 20px 48px rgba(47, 52, 64, 0.12)',
|
||||
padding: 16,
|
||||
border: '1px solid #E7EAF0',
|
||||
zIndex: 9999,
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
<Text style={{
|
||||
display: 'block',
|
||||
marginBottom: 8,
|
||||
fontSize: 12,
|
||||
fontWeight: 600,
|
||||
color: '#475467',
|
||||
}}>
|
||||
创作类型
|
||||
</Text>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 4,
|
||||
}}>
|
||||
{[
|
||||
{ value: 'video', label: '视频生成', icon: VideoCameraOutlined },
|
||||
{ value: 'image', label: '图片生成', icon: PictureOutlined },
|
||||
].map((item) => (
|
||||
<button
|
||||
key={item.value}
|
||||
onClick={() => {
|
||||
setMediaType(item.value);
|
||||
setShowMediaTypeModal(false);
|
||||
}}
|
||||
style={{
|
||||
flex: 1,
|
||||
minHeight: 48,
|
||||
borderRadius: 10,
|
||||
border: mediaType === item.value
|
||||
? '1px solid #8b5cf6'
|
||||
: '1px solid #E7EAF0',
|
||||
backgroundColor: mediaType === item.value
|
||||
? '#f5f3ff'
|
||||
: '#ffffff',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '8px 12px',
|
||||
transition: 'all 0.2s',
|
||||
textAlign: 'left',
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
<item.icon style={{ fontSize: 14, color: mediaType === item.value ? '#8b5cf6' : '#98a2b3' }} />
|
||||
<span style={{
|
||||
fontSize: 14,
|
||||
fontWeight: mediaType === item.value ? 600 : 500,
|
||||
color: mediaType === item.value ? '#8b5cf6' : '#475467',
|
||||
}}>
|
||||
{item.label}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 引擎选择器 */}
|
||||
<div style={{ position: 'relative', display: 'inline-block' }}>
|
||||
<button
|
||||
onClick={() => setShowEngineModal(prev => !prev)}
|
||||
onClick={() => {
|
||||
setShowMediaTypeModal(false);
|
||||
setShowImageSettingsModal(false);
|
||||
setShowVideoSettingsModal(false);
|
||||
setReferenceModeDropdownVisible(false);
|
||||
setShowEngineModal(prev => !prev);
|
||||
}}
|
||||
className="image-settings-trigger"
|
||||
style={{
|
||||
minWidth: 174,
|
||||
@@ -2692,7 +2810,13 @@ const AIChatPage: React.FC = () => {
|
||||
{mediaType === 'video' && (supportsFirstLastFrame || supportsUniversalReference) && (
|
||||
<div style={{ position: 'relative', display: 'inline-block' }}>
|
||||
<button
|
||||
onClick={() => setReferenceModeDropdownVisible(!referenceModeDropdownVisible)}
|
||||
onClick={() => {
|
||||
setShowMediaTypeModal(false);
|
||||
setShowEngineModal(false);
|
||||
setShowImageSettingsModal(false);
|
||||
setShowVideoSettingsModal(false);
|
||||
setReferenceModeDropdownVisible(!referenceModeDropdownVisible);
|
||||
}}
|
||||
className="image-settings-trigger"
|
||||
style={{
|
||||
minWidth: 118,
|
||||
@@ -2731,127 +2855,117 @@ const AIChatPage: React.FC = () => {
|
||||
</button>
|
||||
|
||||
{referenceModeDropdownVisible && (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
zIndex: 9998,
|
||||
}}
|
||||
onClick={() => setReferenceModeDropdownVisible(false)}
|
||||
/>
|
||||
<div
|
||||
className="image-settings-popover"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: 'calc(100% + 8px)',
|
||||
left: 0,
|
||||
minWidth: 160,
|
||||
backgroundColor: '#ffffff',
|
||||
backdropFilter: 'blur(16px)',
|
||||
borderRadius: 16,
|
||||
boxShadow: '0 20px 48px rgba(47, 52, 64, 0.12)',
|
||||
padding: 8,
|
||||
border: '1px solid #E7EAF0',
|
||||
zIndex: 9999,
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<button
|
||||
onClick={() => handleReferenceModeChange('universal')}
|
||||
disabled={!supportsUniversalReference}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '10px 14px',
|
||||
borderRadius: 10,
|
||||
border: referenceMode === 'universal'
|
||||
? '1px solid rgba(139, 92, 246, 0.3)'
|
||||
: '1px solid transparent',
|
||||
backgroundColor: referenceMode === 'universal'
|
||||
? 'rgba(139, 92, 246, 0.08)'
|
||||
: 'transparent',
|
||||
cursor: supportsUniversalReference ? 'pointer' : 'not-allowed',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
transition: 'all 0.2s',
|
||||
textAlign: 'left',
|
||||
opacity: supportsUniversalReference ? 1 : 0.4,
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (supportsUniversalReference && referenceMode !== 'universal') {
|
||||
e.currentTarget.style.backgroundColor = 'rgba(139, 92, 246, 0.04)';
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (referenceMode !== 'universal') {
|
||||
e.currentTarget.style.backgroundColor = 'transparent';
|
||||
}
|
||||
}}
|
||||
>
|
||||
<PictureOutlined style={{ fontSize: 14, color: '#8b5cf6' }} />
|
||||
<span style={{
|
||||
fontSize: 13,
|
||||
fontWeight: referenceMode === 'universal' ? 600 : 500,
|
||||
color: referenceMode === 'universal' ? '#8b5cf6' : '#475467',
|
||||
flex: 1,
|
||||
}}>
|
||||
全能参考
|
||||
</span>
|
||||
{!supportsUniversalReference && (
|
||||
<span style={{ fontSize: 10, color: '#98a2b3' }}>不支持</span>
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleReferenceModeChange('first_last_frame')}
|
||||
disabled={!supportsFirstLastFrame}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '10px 14px',
|
||||
borderRadius: 10,
|
||||
border: referenceMode === 'first_last_frame'
|
||||
? '1px solid rgba(139, 92, 246, 0.3)'
|
||||
: '1px solid transparent',
|
||||
backgroundColor: referenceMode === 'first_last_frame'
|
||||
? 'rgba(139, 92, 246, 0.08)'
|
||||
: 'transparent',
|
||||
cursor: supportsFirstLastFrame ? 'pointer' : 'not-allowed',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
transition: 'all 0.2s',
|
||||
textAlign: 'left',
|
||||
opacity: supportsFirstLastFrame ? 1 : 0.4,
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (supportsFirstLastFrame && referenceMode !== 'first_last_frame') {
|
||||
e.currentTarget.style.backgroundColor = 'rgba(139, 92, 246, 0.04)';
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (referenceMode !== 'first_last_frame') {
|
||||
e.currentTarget.style.backgroundColor = 'transparent';
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SwapOutlined style={{ fontSize: 14, color: '#8b5cf6' }} />
|
||||
<span style={{
|
||||
fontSize: 13,
|
||||
fontWeight: referenceMode === 'first_last_frame' ? 600 : 500,
|
||||
color: referenceMode === 'first_last_frame' ? '#8b5cf6' : '#475467',
|
||||
flex: 1,
|
||||
}}>
|
||||
首尾帧
|
||||
</span>
|
||||
{!supportsFirstLastFrame && (
|
||||
<span style={{ fontSize: 10, color: '#98a2b3' }}>不支持</span>
|
||||
)}
|
||||
</button>
|
||||
<div
|
||||
className="image-settings-popover"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: 'calc(100% + 8px)',
|
||||
left: 0,
|
||||
width: 300,
|
||||
backgroundColor: '#ffffff',
|
||||
backdropFilter: 'blur(16px)',
|
||||
borderRadius: 16,
|
||||
boxShadow: '0 20px 48px rgba(47, 52, 64, 0.12)',
|
||||
padding: 16,
|
||||
border: '1px solid #E7EAF0',
|
||||
zIndex: 9999,
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
<Text style={{
|
||||
display: 'block',
|
||||
marginBottom: 8,
|
||||
fontSize: 12,
|
||||
fontWeight: 600,
|
||||
color: '#475467',
|
||||
}}>
|
||||
参考模式
|
||||
</Text>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 4,
|
||||
}}>
|
||||
<button
|
||||
onClick={() => {
|
||||
handleReferenceModeChange('universal');
|
||||
setReferenceModeDropdownVisible(false);
|
||||
}}
|
||||
disabled={!supportsUniversalReference}
|
||||
style={{
|
||||
flex: 1,
|
||||
minHeight: 48,
|
||||
borderRadius: 10,
|
||||
border: referenceMode === 'universal'
|
||||
? '1px solid #8b5cf6'
|
||||
: '1px solid #E7EAF0',
|
||||
backgroundColor: referenceMode === 'universal'
|
||||
? '#f5f3ff'
|
||||
: '#ffffff',
|
||||
cursor: supportsUniversalReference ? 'pointer' : 'not-allowed',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '8px 12px',
|
||||
transition: 'all 0.2s',
|
||||
textAlign: 'left',
|
||||
gap: 8,
|
||||
opacity: supportsUniversalReference ? 1 : 0.4,
|
||||
}}
|
||||
>
|
||||
<PictureOutlined style={{ fontSize: 14, color: referenceMode === 'universal' ? '#8b5cf6' : '#98a2b3' }} />
|
||||
<span style={{
|
||||
fontSize: 14,
|
||||
fontWeight: referenceMode === 'universal' ? 600 : 500,
|
||||
color: referenceMode === 'universal' ? '#8b5cf6' : '#475467',
|
||||
}}>
|
||||
全能参考
|
||||
</span>
|
||||
{!supportsUniversalReference && (
|
||||
<span style={{ fontSize: 10, color: '#98a2b3', marginLeft: 'auto' }}>不支持</span>
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
handleReferenceModeChange('first_last_frame');
|
||||
setReferenceModeDropdownVisible(false);
|
||||
}}
|
||||
disabled={!supportsFirstLastFrame}
|
||||
style={{
|
||||
flex: 1,
|
||||
minHeight: 48,
|
||||
borderRadius: 10,
|
||||
border: referenceMode === 'first_last_frame'
|
||||
? '1px solid #8b5cf6'
|
||||
: '1px solid #E7EAF0',
|
||||
backgroundColor: referenceMode === 'first_last_frame'
|
||||
? '#f5f3ff'
|
||||
: '#ffffff',
|
||||
cursor: supportsFirstLastFrame ? 'pointer' : 'not-allowed',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '8px 12px',
|
||||
transition: 'all 0.2s',
|
||||
textAlign: 'left',
|
||||
gap: 8,
|
||||
opacity: supportsFirstLastFrame ? 1 : 0.4,
|
||||
}}
|
||||
>
|
||||
<SwapOutlined style={{ fontSize: 14, color: referenceMode === 'first_last_frame' ? '#8b5cf6' : '#98a2b3' }} />
|
||||
<span style={{
|
||||
fontSize: 14,
|
||||
fontWeight: referenceMode === 'first_last_frame' ? 600 : 500,
|
||||
color: referenceMode === 'first_last_frame' ? '#8b5cf6' : '#475467',
|
||||
}}>
|
||||
首尾帧
|
||||
</span>
|
||||
{!supportsFirstLastFrame && (
|
||||
<span style={{ fontSize: 10, color: '#98a2b3', marginLeft: 'auto' }}>不支持</span>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
@@ -2860,7 +2974,13 @@ const AIChatPage: React.FC = () => {
|
||||
{mediaType === 'image' && (
|
||||
<div style={{ position: 'relative', display: 'inline-block' }}>
|
||||
<button
|
||||
onClick={() => setShowImageSettingsModal(!showImageSettingsModal)}
|
||||
onClick={() => {
|
||||
setShowMediaTypeModal(false);
|
||||
setShowEngineModal(false);
|
||||
setShowVideoSettingsModal(false);
|
||||
setReferenceModeDropdownVisible(false);
|
||||
setShowImageSettingsModal(!showImageSettingsModal);
|
||||
}}
|
||||
className="image-settings-trigger"
|
||||
style={{
|
||||
minWidth: 156,
|
||||
@@ -3164,7 +3284,13 @@ const AIChatPage: React.FC = () => {
|
||||
{mediaType === 'video' && (
|
||||
<div style={{ position: 'relative', display: 'inline-block' }}>
|
||||
<button
|
||||
onClick={() => setShowVideoSettingsModal(!showVideoSettingsModal)}
|
||||
onClick={() => {
|
||||
setShowMediaTypeModal(false);
|
||||
setShowEngineModal(false);
|
||||
setShowImageSettingsModal(false);
|
||||
setReferenceModeDropdownVisible(false);
|
||||
setShowVideoSettingsModal(!showVideoSettingsModal);
|
||||
}}
|
||||
className="image-settings-trigger"
|
||||
style={{
|
||||
minWidth: 156,
|
||||
|
||||
Reference in New Issue
Block a user