解决冲突

This commit is contained in:
Lrd
2026-07-01 17:59:05 +08:00
46 changed files with 3656 additions and 474 deletions
+8 -1
View File
@@ -709,8 +709,15 @@ export async function deleteOAuthAccount(params: DeleteOAuthAccountParams): Prom
export async function getAllOAuthAccountList(): Promise<any> {
return api.post(`/upload-material/oauth_account_list`);
}
// 获取用户全部授权账户列表 resources_material_ids pre_test_template_id
export async function submitPreTest(params: { resources_material_ids: any[]; pre_test_template_id: string }): Promise<any> {
return api.post(`/resources-material/pre-commit`, params);
}
// 首页素材案例头
export async function getHomeCaseHeader(): Promise<any> {
return api.get(`/home-materials/categories`);
}
// 首页素材按钮资源
export async function getHomeCaseButton(id: string,limit:number=5): Promise<any> {
return api.get(`/home-materials?category_id=${id}&limit_per_category=${limit}&include_empty_categories=false&response_mode=grouped&page=1&page_size=20`);
}
+183 -135
View File
@@ -143,16 +143,17 @@ const StorageCard: React.FC<{ data: ResourceCapacityData | null }> = ({ data })
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' }} />
{rawPercent.toFixed(1)}%
</span>
</span> */}
{data.enabled ? (
<span style={{ fontWeight: 500, color: isOver ? '#ef4444' : '#1e293b' }}>
{used.toFixed(2)} / {total.toFixed(2)} {unit}
</span>
<></>
) : (
<span style={{ fontWeight: 500, color: '#1e293b' }}>
<DatabaseOutlined style={{ fontSize: 12, color: isOver ? '#ef4444' : '#6366f1', marginRight: 4 }} />
使 {usedAuto.val} {usedAuto.unit}
</span>
)}
@@ -162,23 +163,61 @@ const StorageCard: React.FC<{ data: ResourceCapacityData | null }> = ({ data })
<div
style={{
width: '100%',
height: 6,
height: 20,
background: '#e2e8f0',
borderRadius: 3,
borderRadius: 2,
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: isOver ? '#ffffffff' : '#000000ff',
padding: '0 8px',
}}>
<span style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
<DatabaseOutlined style={{ fontSize: 12, color: isOver ? '#ffffffff' :'#000000ff' }} />
{rawPercent.toFixed(1)}%
</span>
{data.enabled ? (
<span style={{ fontWeight: 500, color: isOver ? '#ffffffff' : '#000000ff' }}>
{used.toFixed(2)} / {total.toFixed(2)} {unit}
</span>
) : (
<span style={{ fontWeight: 500, color: '#1e293b' }}>
使 {usedAuto.val} {usedAuto.unit}
</span>
)}
</div>
<div
style={{
width: `${barPercent}%`,
height: '100%',
background: isOver
? 'linear-gradient(90deg, #ef4444, #dc2626)'
: 'linear-gradient(90deg, #6366f1, #8b5cf6)',
borderRadius: 3,
background: rawPercent < 50
? 'linear-gradient(90deg, #279951, #b7fad0)'
: rawPercent < 85
? 'linear-gradient(90deg, #ffd759, #fff6d4)'
: 'linear-gradient(90deg, #ef4444, #dc2626)',
borderRadius: 2,
transition: 'width 0.4s ease',
}}
/>
</div>
<div
style={{
@@ -283,9 +322,9 @@ const AppLayout: React.FC = () => {
const [contactHovered, setContactHovered] = useState(false);
const [contactForm] = Form.useForm();
const [submittingContact, setSubmittingContact] = useState(false);
const [contactPosition, setContactPosition] = useState<{ x: number; y: number }>(() => ({
x: 24,
y: window.innerHeight * 0.75
const [contactPosition, setContactPosition] = useState<{ x: number; y: number }>(() => ({
x: 24,
y: window.innerHeight * 0.75
}));
const [isDragging, setIsDragging] = useState(false);
const hasMovedRef = useRef(false);
@@ -334,6 +373,7 @@ const AppLayout: React.FC = () => {
limitValue: string;
limitUnit: string;
} | null>(null);
const [isMobile, setIsMobile] = useState(false);
const PENDING_ORDER_KEY = 'pending_payment_order';
@@ -387,6 +427,13 @@ const AppLayout: React.FC = () => {
}, []);
useEffect(() => {
const checkMobile = () => setIsMobile(window.innerWidth <= 767);
checkMobile();
window.addEventListener('resize', checkMobile);
return () => window.removeEventListener('resize', checkMobile);
}, []);
const handleContactMouseDown = (e: React.MouseEvent) => {
if (e.button === 0) {
setIsDragging(true);
@@ -400,16 +447,16 @@ const AppLayout: React.FC = () => {
const handleContactMouseMove = (e: MouseEvent) => {
if (!isDragging) return;
const deltaX = Math.abs(e.clientX - dragStartRef.current.x);
const deltaY = Math.abs(e.clientY - dragStartRef.current.y);
if (deltaX > 5 || deltaY > 5) {
hasMovedRef.current = true;
}
const newY = Math.max(60, Math.min(window.innerHeight - 60, e.clientY - (dragStartRef.current.y - contactPosition.y)));
setContactPosition(prev => ({ x: prev.x, y: newY }));
};
@@ -417,7 +464,7 @@ const AppLayout: React.FC = () => {
const moved = hasMovedRef.current;
setIsDragging(false);
hasMovedRef.current = false;
if (!moved) {
setContactModalOpen(true);
}
@@ -672,12 +719,12 @@ const AppLayout: React.FC = () => {
return (
<div key={item.id}>
<div
<div
onClick={() => {
if (!(isGroup || hasChildren) && item.path) {
navigate(item.path);
}
}}
}}
style={{
display: 'flex', alignItems: 'center',
justifyContent: 'flex-start',
@@ -712,7 +759,7 @@ const AppLayout: React.FC = () => {
{item.label}
</span>
</div>
{(isGroup || hasChildren) && (
<div style={{ overflow: 'hidden' }}>
{(childMap[item.id] || []).map(c => renderMenuItem(c, depth + 1))}
@@ -752,7 +799,7 @@ const AppLayout: React.FC = () => {
boxShadow: '0 4px 16px rgba(99, 102, 241, 0.35)',
overflow: 'hidden',
}}
>
{siteLogo ? (
<img src={siteLogo} alt="logo" style={{ width: 28, height: 28, objectFit: 'contain' }} />
@@ -803,9 +850,9 @@ const AppLayout: React.FC = () => {
<span></span>
</div>
{/* 资源存储容量展示(来自 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>
<div style={{
display: 'flex', alignItems: 'center',
@@ -832,7 +879,7 @@ const AppLayout: React.FC = () => {
flexShrink: 0,
boxShadow: '0 4px 12px rgba(99, 102, 241, 0.3)',
}} />
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ color: '#1e293b', fontSize: 16, fontWeight: 600, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', letterSpacing: -0.01 }}>
{user?.username}
@@ -851,7 +898,7 @@ const AppLayout: React.FC = () => {
</div>
</Dropdown>
</div>
</div>
@@ -876,7 +923,7 @@ const AppLayout: React.FC = () => {
padding: '24px 32px 32px',
border: '1px solid rgba(0, 0, 0, 0.06)',
}}>
<Outlet />
{!isMobile && <Outlet />}
</div>
</div>
@@ -896,7 +943,7 @@ const AppLayout: React.FC = () => {
</div>
<div className="mobile-content">
<Outlet />
{isMobile && <Outlet />}
</div>
<Drawer
@@ -967,7 +1014,7 @@ const AppLayout: React.FC = () => {
{menuIcon}
</span>
)}
<span className="mobile-menu-label" style={{
<span className="mobile-menu-label" style={{
paddingLeft: menuType === 'group' ? 0 : 0,
color: menuType === 'group' ? '#94a3b8' : (isActive ? '#4f46e5' : '#475569'),
fontWeight: menuType === 'group' ? 600 : (isActive ? 600 : 400),
@@ -1002,7 +1049,7 @@ const AppLayout: React.FC = () => {
<span className="mobile-menu-icon" style={{ color: childActive ? '#6366f1' : '#94a3b8' }}>
{childIcon}
</span>
<span className="mobile-menu-label" style={{
<span className="mobile-menu-label" style={{
color: childActive ? '#4f46e5' : '#64748b',
fontWeight: childActive ? 600 : 400,
fontSize: 14,
@@ -1091,7 +1138,7 @@ const AppLayout: React.FC = () => {
</span>
<span className="mobile-menu-label" style={{ color: '#fff', fontWeight: 600 }}></span>
</div>
<div
className="mobile-menu-item"
onClick={() => {
@@ -1133,109 +1180,10 @@ const AppLayout: React.FC = () => {
<Modal title={<Space><GiftOutlined /></Space>} open={rechargeModalOpen}
onCancel={() => { setRechargeModalOpen(false); setSelectedPlan(null); }}
footer={null} width={680}
width={680}
className="recharge-modal"
styles={{ body: { maxHeight: '70vh', overflowY: 'auto' } }}>
<div style={{ marginTop: 16 }}>
<Space style={{ marginBottom: 16 }}>
<WalletOutlined style={{ color: '#6366f1' }} />
<Typography.Text style={{ color: '#64748b', letterSpacing: 0 }}></Typography.Text>
<Typography.Text strong style={{ color: '#6366f1', fontSize: 20, fontWeight: 600 }}>{user?.credits ?? 0}</Typography.Text>
</Space>
<div style={{
padding: '12px 16px',
background: 'rgba(99, 102, 241, 0.06)',
borderRadius: 10,
marginBottom: 16,
display: 'flex',
alignItems: 'center',
gap: 8,
}}>
<InfoOutlined style={{ color: '#6366f1', fontSize: 14 }} />
<Typography.Text style={{ color: '#64748b', fontSize: 13 }}>
/
<Typography.Text
style={{
color: '#ff0000ff',
cursor: 'pointer',
textDecoration: 'underline',
}}
onClick={() => { setRechargeModalOpen(false); setContactModalOpen(true); }}
></Typography.Text>
</Typography.Text>
</div>
<div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}>
{rechargeOptions.map((opt, idx) => {
const g = GRADIENTS[idx % GRADIENTS.length];
const totalCredits = (opt.credits || 0) + (opt.bonus_credits || opt.bonusCredits || 0);
return (
<div key={opt.id} onClick={() => setSelectedPlan(opt.id)} style={{
flex: '1 1 45%', minWidth: 200, borderRadius: 16, padding: '20px 16px',
background: selectedPlan === opt.id ? 'rgba(99,102,241,0.04)' : '#fafbff',
border: selectedPlan === opt.id ? '2px solid #6366f1' : '1px solid #f0f0f5',
cursor: 'pointer', position: 'relative', transition: 'all 0.2s',
}}>
{opt.description && (
<Tag color="purple" style={{ position: 'absolute', top: -10, left: '50%', transform: 'translateX(-50%)', borderRadius: 8, fontSize: 11 }}>{opt.description}</Tag>
)}
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<div style={{
width: 44, height: 44, borderRadius: 12, flexShrink: 0,
background: g.gradient, display: 'flex', alignItems: 'center', justifyContent: 'center',
fontSize: 18, color: '#fff', boxShadow: `0 6px 16px ${g.shadow}`,
}}>{g.icon}</div>
<div style={{ flex: 1 }}>
<div style={{ display: 'flex', alignItems: 'baseline', gap: 6 }}>
<Typography.Text strong style={{ fontSize: 15 }}>{opt.name}</Typography.Text>
<Typography.Text style={{ fontSize: 14, fontWeight: 600, color: '#6366f1' }}>{totalCredits.toLocaleString()} </Typography.Text>
</div>
<div style={{
fontSize: 22, fontWeight: 800, marginTop: 4,
background: g.gradient, WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent',
}}>¥{opt.price}</div>
</div>
</div>
</div>
);
})}
</div>
{(!enabledMethods.alipay && !enabledMethods.wechat) ? (
<div style={{ marginTop: 20, marginBottom: 8, padding: 16, background: '#fef2f2', borderRadius: 12, border: '1px solid #fecaca' }}>
<Typography.Text style={{ color: '#dc2626', fontSize: 13 }}>
</Typography.Text>
</div>
) : (
<div style={{ marginTop: 20, marginBottom: 8 }}>
<Typography.Text style={{ color: '#64748b', fontSize: 13, marginBottom: 8, display: 'block' }}></Typography.Text>
<Radio.Group value={paymentMethod} onChange={(e) => setPaymentMethod(e.target.value)}
style={{ display: 'flex', gap: 12 }}>
{enabledMethods.alipay && (
<Radio.Button value="alipay" style={{
flex: 1, textAlign: 'center', borderRadius: 10, height: 44, lineHeight: '42px',
borderColor: paymentMethod === 'alipay' ? '#1677ff' : undefined,
color: paymentMethod === 'alipay' ? '#1677ff' : undefined,
}}>
<AlipayCircleOutlined style={{ fontSize: 16, marginRight: 6 }} />
</Radio.Button>
)}
{enabledMethods.wechat && (
<Radio.Button value="wechat" style={{
flex: 1, textAlign: 'center', borderRadius: 10, height: 44, lineHeight: '42px',
borderColor: paymentMethod === 'wechat' ? '#07c160' : undefined,
color: paymentMethod === 'wechat' ? '#07c160' : undefined,
}}>
<WechatOutlined style={{ fontSize: 16, marginRight: 6 }} />
</Radio.Button>
)}
</Radio.Group>
</div>
)}
<div style={{ marginTop: 16, display: 'flex', justifyContent: 'flex-end' }}>
footer={
<div style={{ display: 'flex', justifyContent: 'flex-end', padding: '12px 0 0', borderTop: '1px solid #f0f0f0' }}>
<Button size="large" onClick={() => { setRechargeModalOpen(false); setSelectedPlan(null); }} style={{ borderRadius: 10, marginRight: 12 }}></Button>
<Button type="primary" size="large" disabled={!selectedPlan || (!enabledMethods.alipay && !enabledMethods.wechat)} loading={paying}
onClick={async () => {
@@ -1289,6 +1237,106 @@ const AppLayout: React.FC = () => {
</Button>
</div>
}>
<div style={{ marginTop: 16 }}>
<Space style={{ marginBottom: 16 }}>
<WalletOutlined style={{ color: '#6366f1' }} />
<Typography.Text style={{ color: '#64748b', letterSpacing: 0 }}></Typography.Text>
<Typography.Text strong style={{ color: '#6366f1', fontSize: 20, fontWeight: 600 }}>{user?.credits ?? 0}</Typography.Text>
</Space>
{(!enabledMethods.alipay && !enabledMethods.wechat) ? (
<div style={{ marginBottom: 16, padding: 16, background: '#fef2f2', borderRadius: 12, border: '1px solid #fecaca' }}>
<Typography.Text style={{ color: '#dc2626', fontSize: 13 }}>
</Typography.Text>
</div>
) : (
<div style={{ marginBottom: 16 }}>
<Typography.Text style={{ color: '#64748b', fontSize: 13, marginBottom: 8, display: 'block' }}></Typography.Text>
<Radio.Group value={paymentMethod} onChange={(e) => setPaymentMethod(e.target.value)}
style={{ display: 'flex', gap: 12 }}>
{enabledMethods.alipay && (
<Radio.Button value="alipay" style={{
flex: 1, textAlign: 'center', borderRadius: 10, height: 44, lineHeight: '42px',
borderColor: paymentMethod === 'alipay' ? '#1677ff' : undefined,
color: paymentMethod === 'alipay' ? '#1677ff' : undefined,
}}>
<AlipayCircleOutlined style={{ fontSize: 16, marginRight: 6 }} />
</Radio.Button>
)}
{enabledMethods.wechat && (
<Radio.Button value="wechat" style={{
flex: 1, textAlign: 'center', borderRadius: 10, height: 44, lineHeight: '42px',
borderColor: paymentMethod === 'wechat' ? '#07c160' : undefined,
color: paymentMethod === 'wechat' ? '#07c160' : undefined,
}}>
<WechatOutlined style={{ fontSize: 16, marginRight: 6 }} />
</Radio.Button>
)}
</Radio.Group>
</div>
)}
<div style={{
padding: '12px 16px',
background: 'rgba(99, 102, 241, 0.06)',
borderRadius: 10,
marginBottom: 16,
display: 'flex',
alignItems: 'center',
gap: 8,
}}>
<InfoOutlined style={{ color: '#6366f1', fontSize: 14 }} />
<Typography.Text style={{ color: '#64748b', fontSize: 13 }}>
/
<Typography.Text
style={{
color: '#ff0000ff',
cursor: 'pointer',
textDecoration: 'underline',
}}
onClick={() => { setRechargeModalOpen(false); setContactModalOpen(true); }}
></Typography.Text>
</Typography.Text>
</div>
<div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}>
{rechargeOptions.map((opt, idx) => {
const g = GRADIENTS[idx % GRADIENTS.length];
const totalCredits = (opt.credits || 0) + (opt.bonus_credits || opt.bonusCredits || 0);
return (
<div key={opt.id} onClick={() => setSelectedPlan(opt.id)} style={{
flex: '1 1 45%', minWidth: 200, borderRadius: 16, padding: '20px 16px',
background: selectedPlan === opt.id ? 'rgba(99,102,241,0.04)' : '#fafbff',
border: selectedPlan === opt.id ? '2px solid #6366f1' : '1px solid #f0f0f5',
cursor: 'pointer', position: 'relative', transition: 'all 0.2s',
}}>
{opt.description && (
<Tag color="purple" style={{ position: 'absolute', top: -10, left: '50%', transform: 'translateX(-50%)', borderRadius: 8, fontSize: 11 }}>{opt.description}</Tag>
)}
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<div style={{
width: 44, height: 44, borderRadius: 12, flexShrink: 0,
background: g.gradient, display: 'flex', alignItems: 'center', justifyContent: 'center',
fontSize: 18, color: '#fff', boxShadow: `0 6px 16px ${g.shadow}`,
}}>{g.icon}</div>
<div style={{ flex: 1 }}>
<div style={{ display: 'flex', alignItems: 'baseline', gap: 6 }}>
<Typography.Text strong style={{ fontSize: 15 }}>{opt.name}</Typography.Text>
<Typography.Text style={{ fontSize: 14, fontWeight: 600, color: '#6366f1' }}>{totalCredits.toLocaleString()} </Typography.Text>
</div>
<div style={{
fontSize: 22, fontWeight: 800, marginTop: 4,
background: g.gradient, WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent',
}}>¥{opt.price}</div>
</div>
</div>
</div>
);
})}
</div>
</div>
</Modal>
@@ -1436,7 +1484,7 @@ const AppLayout: React.FC = () => {
<NotificationPopup />
<div
<div
className="contact-button-wrapper"
style={{
right: `${contactPosition.x}px`,
+325 -67
View File
@@ -46,6 +46,7 @@ import {
LayoutOutlined,
ArrowUpOutlined,
DownloadOutlined,
ReloadOutlined,
} from '@ant-design/icons';
@@ -139,13 +140,52 @@ const AIChatPage: React.FC = () => {
setEngineOptions,
setEnginesele,
setInputValue,
currentMedia,
setCurrentMedia,
} = 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 [loading, setLoading] = useState<boolean>(false);
const [currentMedia, setCurrentMedia] = useState<{ name: string; type: 'image' | 'video'; url: string }[]>([]);
// @ 提及相关状态
const [mentionVisible, setMentionVisible] = useState(false);
const mentionInputRef = useRef<any>(null);
const [mentionPosition, setMentionPosition] = useState({ top: 0, left: 0 });
// 数字转中文数字(一、二、三、四)
const numberToChinese = (num: number): string => {
const map = ['零', '一', '二', '三', '四', '五', '六', '七', '八', '九', '十'];
if (num <= 10) return map[num];
if (num < 20) return '十' + map[num % 10];
if (num < 100) {
const tens = Math.floor(num / 10);
const ones = num % 10;
return map[tens] + '十' + (ones ? map[ones] : '');
}
return String(num);
};
// 根据媒体列表生成标签(图片一、图片二、视频一、视频二...)
const generateMediaLabels = (media: { type: 'image' | 'video' }[]) => {
let imgCount = 0;
let vidCount = 0;
return media.map((m) => {
if (m.type === 'image') {
imgCount++;
return `图片${imgCount}`;
} else {
vidCount++;
return `视频${vidCount}`;
}
});
};
const [previewVisible, setPreviewVisible] = useState<boolean>(false);
const [previewUrl, setPreviewUrl] = useState<string>('');
@@ -393,44 +433,20 @@ const AIChatPage: React.FC = () => {
setCountType(targetEngine.id);
}
}
})
.catch(() => {
});
getCreditRatios()
.then((data: any) => {
// console.log('积分', data);
setCreditRatios(data.video || []);
setCimage(data.image || []);
})
.catch((error) => {
});
calculateCredits().then((data: any) => {
// console.log('积分计算', data);
// 保存积分计算数据
setCreditCalculationData(data);
})
getgen_list(Pagebreak).then((data: any) => {
let mess_list = data.items
let total = data.total
setGen_list(mess_list)
setTotalnumber(total)
})
getParameters()
.then((data) => {
let supportedSizes = (data as any).items?.[0]?.supportedSizes || {};
let supportedResolutions = [];
let twokwidth = [];
let fourkwidth = [];
let supportedSizes = (data as any).engine?.image?.[0]?.supported_sizes || {};
let supportedResolutions: string[] = [];
let twokwidth: string[] = [];
let fourkwidth: string[] = [];
for (let key in supportedSizes["2K"]) {
supportedResolutions.push(key);
twokwidth.push(supportedSizes["2K"][key]);
}
const newRatioOptions = supportedResolutions.map((res: any, index: number) => ({
const newRatioOptions = supportedResolutions.map((res: any) => ({
value: res,
label: String(index),
label: res,
}));
setRatioOptions(newRatioOptions);
@@ -444,8 +460,67 @@ const AIChatPage: React.FC = () => {
{ value: '2K', label: '高清 2K' },
{ value: '4K', label: '超清 4K' },
]);
})
.catch(() => { });
.catch(() => {
});
// getCreditRatios()
// .then((data: any) => {
// // console.log('积分', data);
// setCreditRatios(data.video || []);
// setCimage(data.image || []);
// })
// .catch((error) => {
// });
calculateCredits().then((data: any) => {
// console.log('积分计算', data);
// 保存积分计算数据
setCreditCalculationData(data);
})
getgen_list(Pagebreak).then((data: any) => {
let mess_list = data.items
let total = data.total
setGen_list(mess_list)
setTotalnumber(total)
})
// getParameters()
// .then((data) => {
// let supportedSizes = (data as any).items?.[0]?.supportedSizes || {};
// let supportedResolutions = [];
// let twokwidth = [];
// let fourkwidth = [];
// for (let key in supportedSizes["2K"]) {
// supportedResolutions.push(key);
// twokwidth.push(supportedSizes["2K"][key]);
// }
// const newRatioOptions = supportedResolutions.map((res: any, index: number) => ({
// value: res,
// label: String(index),
// }));
// setRatioOptions(newRatioOptions);
// for (let key in supportedSizes["4K"]) {
// fourkwidth.push(supportedSizes["4K"][key]);
// }
// const newWidthandHeight = [twokwidth, fourkwidth];
// setWidthandHeight(newWidthandHeight);
// setResolutionOptions([
// { value: '2K', label: '高清 2K' },
// { value: '4K', label: '超清 4K' },
// ]);
// })
// .catch(() => { });
}, []);
@@ -770,6 +845,18 @@ const AIChatPage: React.FC = () => {
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;
if (file.size / 1024 / 1024 > maxMB) {
@@ -777,17 +864,17 @@ const AIChatPage: React.FC = () => {
return false;
}
// 验证图片数量(最多4张)
// 验证图片数量
const imageCount = currentMedia.filter((m) => m.type === 'image').length;
if (isImage && imageCount >= 4) {
message.error('最多上传4张图片');
if (isImage && imageCount >= maxImage) {
message.error(`该引擎最多上传${maxImage}张图片`);
return false;
}
// 验证视频数量(最多1个)
// 验证视频数量
const videoCount = currentMedia.filter((m) => m.type === 'video').length;
if (isVideo && videoCount >= 1) {
message.error('最多上传1个视频');
if (isVideo && videoCount >= maxVideo) {
message.error(`该引擎最多上传${maxVideo}个视频`);
return false;
}
@@ -799,11 +886,15 @@ const AIChatPage: React.FC = () => {
const uploadFn = isImage ? uploadImage : uploadVideo;
const res = await uploadFn(file);
// 添加到统一的媒体列表
setCurrentMedia((prev) => [...prev, {
const mediaType: 'image' | 'video' = isImage ? 'image' : 'video';
const newList = [...currentMedia, {
name: file.name,
type: isImage ? 'image' : 'video',
type: mediaType,
url: res.url,
}]);
label: '',
}];
const labels = generateMediaLabels(newList);
setCurrentMedia(newList.map((m, i) => ({ ...m, label: labels[i] })));
message.success(`${isImage ? '图片' : '视频'}上传成功`);
} catch (error) {
message.error('上传失败');
@@ -817,7 +908,9 @@ const AIChatPage: React.FC = () => {
const handleRemoveMedia = (index: number) => {
setCurrentMedia((prev) => prev.filter((_, i) => i !== index));
const newList = currentMedia.filter((_, i) => i !== index);
const labels = generateMediaLabels(newList);
setCurrentMedia(newList.map((m, i) => ({ ...m, label: labels[i] })));
};
@@ -828,6 +921,63 @@ const AIChatPage: React.FC = () => {
}
};
// 检测光标前的 @ 符号
const checkMention = (textarea: HTMLTextAreaElement, value: string) => {
const cursorPos = textarea.selectionStart;
const textBeforeCursor = value.slice(0, cursorPos);
const atMatch = textBeforeCursor.match(/@([^@\s]*)$/);
if (atMatch && currentMedia.length > 0) {
setMentionVisible(true);
return true;
}
setMentionVisible(false);
return false;
};
// 输入框变化处理
const handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
const value = e.target.value;
setInputValue(value);
const textarea = e.target;
checkMention(textarea, value);
};
// 键盘事件处理(ESC 关闭提及、Tab/Enter 选择)
const handleInputKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (mentionVisible) {
if (e.key === 'Escape') {
setMentionVisible(false);
}
}
};
// 插入 @ 提及
const insertMention = (label: string) => {
const textarea = mentionInputRef.current?.resizableTextArea?.textArea;
if (!textarea) return;
const cursorPos = textarea.selectionStart;
const textBefore = inputValue.slice(0, cursorPos);
const textAfter = inputValue.slice(cursorPos);
const atIndex = textBefore.lastIndexOf('@');
if (atIndex === -1) {
setMentionVisible(false);
return;
}
const newValue = textBefore.slice(0, atIndex) + `@${label} ` + textAfter;
setInputValue(newValue);
setMentionVisible(false);
setTimeout(() => {
const pos = atIndex + label.length + 2;
textarea.focus();
textarea.setSelectionRange(pos, pos);
}, 0);
};
const handleClosePreview = () => {
@@ -961,9 +1111,9 @@ const AIChatPage: React.FC = () => {
border: '1px solid rgba(99, 102, 241, 0.08)',
position: 'relative', overflow: 'hidden', flexWrap: 'wrap', gap: 12,
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
<div style={{ width: 32, height: 2, background: 'linear-gradient(90deg, transparent, #6366f1, #8b5cf6, transparent)', borderRadius: 1 }} />
<div>
<div style={{ width: '100%', display: 'flex', alignItems: 'center', gap: 16 }}>
<div style={{ width: '15%', height: 2, background: 'linear-gradient(90deg, transparent, #6366f1, #8b5cf6, transparent)', borderRadius: 1 }} />
<div style={{ flex: 1, minWidth: 0, textAlign: 'center' }}>
<h2 style={{
margin: 0,
fontSize: 18,
@@ -971,7 +1121,7 @@ const AIChatPage: React.FC = () => {
background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)',
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
backgroundClip: 'text'
backgroundClip: 'text',
}}>
AI创作
</h2>
@@ -979,7 +1129,7 @@ const AIChatPage: React.FC = () => {
/
</p>
</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>
@@ -1125,8 +1275,55 @@ const AIChatPage: React.FC = () => {
{msg.genType === 'image' ? '图片生成' : '视频生成'}
</span>
</div>
{/* 删除按钮 - 右上角 */}
<div style={{ position: 'absolute', top: 8, right: 8, zIndex: 100 }}>
{/* 附件详情 - 右上角 */}
{msg.mediaReferences && msg.mediaReferences.length > 0 && (
<div style={{ position: 'absolute', top: 8, right: 8, zIndex: 100 }}>
<span style={{ padding: '4px 12px', borderRadius: 16, color: '#6366f1', cursor: 'pointer', fontWeight: 500, border: '1px solid rgba(99, 102, 241, 0.2)', background: 'rgba(99, 102, 241, 0.04)' }} onClick={(e) => { e.stopPropagation(); const target = e.currentTarget as HTMLElement; const rect = target.getBoundingClientRect(); setAttachmentPopupPosition({ x: rect.left, y: rect.top - 10 }); setAttachmentPopupMessageId(msg.id); setAttachmentPopupVisible(true); }}></span>
</div>
)}
{/* 操作按钮 - 右下角 */}
<div style={{ position: 'absolute', bottom: 8, right: 8, zIndex: 100, display: 'flex', gap: 6 }}>
<button
onClick={(e) => {
e.stopPropagation();
setInputValue(msg.originalPrompt || '');
if (msg.mediaReferences && msg.mediaReferences.length > 0) {
setCurrentMedia(msg.mediaReferences.map((ref: any) => ({
name: ref.name,
type: ref.type,
url: ref.url,
label: ref.label || '',
})));
} else {
setCurrentMedia([]);
}
msgApi.success('已加载到编辑区');
}}
style={{
height: 28,
padding: '0 10px',
borderRadius: 8,
border: 'none',
background: 'rgba(99, 102, 241, 0.08)',
color: '#6366f1',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
transition: 'all 0.2s ease',
gap: 4,
fontSize: 12,
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = 'rgba(99, 102, 241, 0.15)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'rgba(99, 102, 241, 0.08)';
}}
>
<ReloadOutlined style={{ fontSize: 12 }} />
</button>
<Popconfirm
title="确定要删除吗?"
onConfirm={async () => {
@@ -1148,8 +1345,8 @@ const AIChatPage: React.FC = () => {
<button
onClick={(e) => e.stopPropagation()}
style={{
width: 28,
height: 28,
padding: '0 10px',
borderRadius: 8,
border: 'none',
background: 'rgba(99, 102, 241, 0.08)',
@@ -1159,7 +1356,8 @@ const AIChatPage: React.FC = () => {
alignItems: 'center',
justifyContent: 'center',
transition: 'all 0.2s ease',
padding: 0,
gap: 4,
fontSize: 12,
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = 'rgba(239, 68, 68, 0.1)';
@@ -1171,9 +1369,9 @@ const AIChatPage: React.FC = () => {
}}
>
<DeleteOutlined style={{ fontSize: 12 }} />
</button>
</Popconfirm>
</div>
{/* 文本内容 */}
@@ -1261,7 +1459,10 @@ const AIChatPage: React.FC = () => {
<div style={{ width: 48, height: 48, borderRadius: '50%', background: 'rgba(254, 226, 226, 0.8)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<WarningOutlined style={{ color: '#ef4444', fontSize: 22 }} />
</div>
<span style={{ fontSize: 14, color: '#94a3b8' }}></span>
<span style={{ fontSize: 14, color: '#94a3b8' }}>退</span>
{msg.errorMessage && (
<span style={{ fontSize: 13, color: '#ef4444', textAlign: 'center', padding: '0 8px', lineHeight: 1.5 }}>{msg.errorMessage}</span>
)}
</div>
) : (
<>
@@ -1300,11 +1501,6 @@ const AIChatPage: React.FC = () => {
</div>
)}
<div style={{ textAlign: 'right' }}>
{msg.mediaReferences && msg.mediaReferences.length > 0 && (
<span style={{ padding: '4px 12px', borderRadius: 16, color: '#6366f1', cursor: 'pointer', fontWeight: 500, border: '1px solid rgba(99, 102, 241, 0.2)', background: 'rgba(99, 102, 241, 0.04)' }} onClick={(e) => { e.stopPropagation(); const target = e.currentTarget as HTMLElement; const rect = target.getBoundingClientRect(); setAttachmentPopupPosition({ x: rect.left, y: rect.top - 10 }); setAttachmentPopupMessageId(msg.id); setAttachmentPopupVisible(true); }}></span>
)}
</div>
</div>
@@ -1444,7 +1640,7 @@ const AIChatPage: React.FC = () => {
{currentMedia.length > 0 && (
<div style={{ display: 'flex', gap: 8, marginBottom: 12, overflowX: 'auto' }}>
{currentMedia.map((media, idx) => (
<div key={`media-${idx}`} style={{ position: 'relative', width: media.type === 'video' ? 120 : 80, flexShrink: 0 }}>
<div key={`media-${idx}`} style={{ position: 'relative', width: media.type === 'video' ? 120 : 80, flexShrink: 0, display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 4 }}>
{media.type === 'image' ? (
<img
src={`${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${media.url}`}
@@ -1457,6 +1653,9 @@ const AIChatPage: React.FC = () => {
style={{ width: '100%', height: 80, objectFit: 'cover', borderRadius: 8 }}
/>
)}
<span style={{ fontSize: 11, color: '#64748b', fontWeight: 500 }}>
{media.label}
</span>
{/* 删除已上传媒体按钮 */}
<button
onClick={() => handleRemoveMedia(idx)}
@@ -1495,14 +1694,18 @@ const AIChatPage: React.FC = () => {
border: '1px solid rgba(99, 102, 241, 0.08)',
boxShadow: '0 2px 8px rgba(99, 102, 241, 0.04)',
transition: 'all 0.2s ease',
position: 'relative',
}}>
{/* 上传按钮 */}
<Upload
accept="image/*,video/*"
accept={mediaType === 'image' ? 'image/*' : 'image/*,video/*'}
showUploadList={false}
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
style={{
width: 48,
@@ -1539,10 +1742,15 @@ const AIChatPage: React.FC = () => {
{/* 文本输入框 */}
<TextArea
ref={mentionInputRef}
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onChange={handleInputChange}
onKeyDown={handleInputKeyDown}
onKeyPress={handleKeyPress}
placeholder="上传最多4张参考图,输入提示词描述您想生成的画面..."
placeholder={mediaType === 'image'
? `上传最多${maxImageCount}张参考图,输入提示词描述您想生成的画面...`
: `上传参考图(最多${maxImageCount}张)和视频(最多${maxVideoCount}个),输入提示词描述您想生成的画面...`
}
autoSize={{ minRows: 1, maxRows: 4 }}
style={{
flex: 1,
@@ -1554,14 +1762,64 @@ const AIChatPage: React.FC = () => {
fontSize: 14,
lineHeight: 1.5,
color: '#1e293b',
// resize: 'none',
// boxShadow: 'none',
// padding: '8px 12px',
}}
disabled={loading}
/>
{/* @ 提及下拉列表 */}
{mentionVisible && currentMedia.length > 0 && (
<div
style={{
position: 'absolute',
bottom: '100%',
left: 70,
marginBottom: 8,
zIndex: 1000,
background: '#fff',
borderRadius: 12,
boxShadow: '0 8px 28px rgba(0,0,0,0.12), 0 2px 8px rgba(0,0,0,0.06)',
border: '1px solid #e2e8f0',
padding: 6,
minWidth: 160,
maxHeight: 260,
overflowY: 'auto',
}}
>
{currentMedia.map((media, idx) => (
<div
key={idx}
onClick={() => insertMention(media.label)}
style={{
padding: '8px 12px',
borderRadius: 6,
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
gap: 8,
fontSize: 13,
color: '#334155',
transition: 'all 0.15s',
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = 'rgba(99, 102, 241, 0.08)';
e.currentTarget.style.color = '#6366f1';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'transparent';
e.currentTarget.style.color = '#334155';
}}
>
{media.type === 'image' ? (
<PictureOutlined style={{ fontSize: 14, color: '#6366f1' }} />
) : (
<VideoCameraOutlined style={{ fontSize: 14, color: '#8b5cf6' }} />
)}
<span>{media.label}</span>
</div>
))}
</div>
)}
{/* 发送按钮 */}
<Button
type="primary"
+181 -51
View File
@@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react';
import { Button, Input, Tabs, Tag, Upload, message } from 'antd';
import React, { useEffect, useState, useRef } from 'react';
import { Button, Input, Tabs, Tag, Upload, message, Modal } from 'antd';
import {
FileTextOutlined,
ScissorOutlined,
@@ -9,9 +9,10 @@ import {
VideoCameraOutlined,
PictureOutlined,
ThunderboltOutlined,
PlayCircleOutlined,
} from '@ant-design/icons';
import { useNavigate } from 'react-router-dom';
import { getmedit } from '../api';
import { getmedit ,getHomeCaseHeader,getHomeCaseButton} from '../api';
import hot from '../assets/homebtn1.png';
import mashup from '../assets/homebtn2.png';
@@ -39,6 +40,29 @@ const HomePage: React.FC = () => {
const [activeTab, setActiveTab] = useState('project');
const [inputValue, setInputValue] = useState('');
const [mockVideos, setMockVideos] = useState<any[]>([]);
const [caseHeader, setCaseHeader] = useState<any[]>([]);
const [activeCaseTab, setActiveCaseTab] = useState<string>('');
const [caseAssets, setCaseAssets] = useState<any[]>([]);
const [previewAsset, setPreviewAsset] = useState<any>(null);
const previewVideoRef = useRef<HTMLVideoElement>(null);
useEffect(() => {
getHomeCaseHeader().then((res: any) => {
// console.log('caseHeader:', res);
if (res?.items?.length > 0) {
setCaseHeader(res.items);
const firstId = res.items[0].id;
setActiveCaseTab(firstId);
// 初始请求第一个 tab 的数据
getHomeCaseButton(firstId).then((btnRes: any) => {
console.log('caseButton:', btnRes);
if (btnRes?.categories?.[0]?.assets) {
setCaseAssets(btnRes.categories[0].assets);
}
});
}
});
}, []);
useEffect(() => {
const fetchAll = async () => {
@@ -75,21 +99,21 @@ const HomePage: React.FC = () => {
{
icon: <FileTextOutlined style={{ fontSize: 24, color: '#6366f1' }} />,
title: '爆款开头复刻',
description: '一键复刻热门视频开篇,快速替换自有商品素材',
description: '上传参考视频与产品图片,一键复刻爆款视频开',
action: '立即创作',
path: '/initial',
},
{
icon: <ScissorOutlined style={{ fontSize: 24, color: '#f97316' }} />,
title: '批量混剪',
description: '多素材批量自动剪辑,智能筛选高清优质镜头片段',
title: '拆镜复刻',
description: '一键拆解画面分镜,助力仿拍或创作',
action: '开始混剪',
path: '/removelens',
},
{
icon: <RobotOutlined style={{ fontSize: 24, color: '#10b981' }} />,
title: 'AI成片',
description: 'AI全自动快速出片,支持文案生成/上传参考素材制作',
description: '输入想法、剧本或上传参考,智能生成视频/图片',
action: '立即生成',
path: '/conversation',
},
@@ -707,8 +731,6 @@ const HomePage: React.FC = () => {
gap: 6,
fontSize: 12,
color: '#64748b',
}}>
{(() => {
// 显示所属模块,而非媒体类型
@@ -773,68 +795,176 @@ const HomePage: React.FC = () => {
</div>
</div>
<div style={{
{/* <div style={{
fontSize: 13, color: '#6366f1', cursor: 'pointer', fontWeight: 500,
display: 'flex', alignItems: 'center', gap: 2,
}}>
更多案例
<ArrowRightOutlined style={{ fontSize: 11 }} />
</div>
</div> */}
</div>
<div className="stagger-children" style={{ display: 'flex', gap: 14 }}>
{materialCases.map((caseUrl, index) => (
{/* Tab切换 */}
<div style={{ marginBottom: 20 }}>
<Tabs
activeKey={activeCaseTab}
onChange={(key) => {
setActiveCaseTab(key);
getHomeCaseButton(key).then((btnRes: any) => {
if (btnRes?.categories?.[0]?.assets) {
setCaseAssets(btnRes.categories[0].assets);
} else {
setCaseAssets([]);
}
});
}}
items={caseHeader.map((item: any) => ({
key: item.id,
label: item.name,
}))}
className="homepage-tabs"
/>
</div>
{/* ========== 素材案例列表(与近期作品一致) ========== */}
<div className="stagger-children" style={{ display: 'grid', gridTemplateColumns: 'repeat(5, 1fr)', gap: 16 }}>
{caseAssets.length === 0 ? (
<div style={{
gridColumn: '1 / -1',
padding: '60px 0',
textAlign: 'center',
color: '#94a3b8',
fontSize: 14,
}}>
<PictureOutlined style={{ fontSize: 36, color: '#cbd5e1', marginBottom: 8 }} />
<div></div>
</div>
) : caseAssets.map((asset: any, index: number) => (
<div
key={index}
key={asset.id || index}
className="project-card"
onClick={() => setPreviewAsset(asset)}
style={{
flex: 1,
aspectRatio: '16/9',
borderRadius: 12,
overflow: 'hidden',
cursor: 'pointer',
background: '#f1f5f9',
position: 'relative',
}}
onMouseEnter={(e) => {
const overlay = e.currentTarget.querySelector('.case-overlay') as HTMLElement | null;
if (overlay) overlay.style.opacity = '1';
}}
onMouseLeave={(e) => {
const overlay = e.currentTarget.querySelector('.case-overlay') as HTMLElement | null;
if (overlay) overlay.style.opacity = '0';
background: '#fff',
border: '1px solid #e2e8f0',
}}
>
<img
src={caseUrl}
alt={`素材案例 ${index + 1}`}
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
/>
hover
<div
className="case-overlay"
style={{
position: 'absolute',
inset: 0,
background: 'linear-gradient(180deg, transparent 40%, rgba(99,102,241,0.75) 100%)',
opacity: 0,
transition: 'opacity 0.3s',
display: 'flex',
alignItems: 'flex-end',
justifyContent: 'center',
padding: 14,
color: '#fff',
fontSize: 13,
fontWeight: 600,
letterSpacing: 0.5,
}}
>
{index + 1}
{/* 媒体区域 16:9 */}
<div style={{ position: 'relative', aspectRatio: '16/9', }}>
{asset.mediaType === 'video' ? (
<>
<video
src={`${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${asset.url}`}
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
muted
playsInline
/>
<div style={{
position: 'absolute',
inset: 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: 'rgba(0,0,0,0.2)',
}}>
<VideoCameraOutlined style={{ fontSize: 28, color: '#fff' }} />
</div>
</>
) : (
<img
src={`${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${asset.url}`}
alt={asset.title || `素材 ${index + 1}`}
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
/>
)}
</div>
{/* 底部信息栏 */}
<div style={{
padding: '10px 12px',
background: '#f8fafc',
}}>
<div style={{
fontSize: 12,
color: '#64748b',
textAlign: 'center',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}>
{asset.title || `素材 ${index + 1}`}
</div>
</div>
</div>
))}
</div>
</div>
{/* ========== 预览弹窗 ========== */}
<Modal
open={!!previewAsset}
onCancel={() => {
previewVideoRef.current?.pause();
setPreviewAsset(null);
}}
footer={null}
width={760}
centered
className="preview-modal"
bodyStyle={{
padding: 0,
background: '#fff',
borderRadius: 16,
overflow: 'hidden',
}}
>
{/* 固定比例容器 16:9 */}
<div style={{
width: '100%',
paddingTop: '56.25%',
position: 'relative',
// background: '#000',
}}>
<div style={{
position: 'absolute',
inset: 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}>
{previewAsset?.mediaType === 'video' ? (
<video
ref={previewVideoRef}
src={`${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${previewAsset.url}`}
controls
autoPlay
style={{ width: '100%', height: '100%', objectFit: 'contain' }}
/>
) : (
<img
src={`${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${previewAsset?.url}`}
alt={previewAsset?.title}
style={{ width: '100%', height: '100%', objectFit: 'contain' }}
/>
)}
</div>
</div>
{/* 底部标题栏 */}
{previewAsset?.title && (
<div style={{
padding: '14px 20px',
fontSize: 14,
color: '#4b5563',
fontWeight: 500,
borderTop: '1px solid #f1f5f9',
textAlign: 'center',
}}>
{previewAsset.title}
</div>
)}
</Modal>
</div>
);
};
@@ -459,7 +459,8 @@ const GenerateConver: React.FC = () => {
background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)',
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
backgroundClip: 'text'
backgroundClip: 'text',
textAlign: 'center',
}}>
</h2>
+2 -1
View File
@@ -125,7 +125,8 @@ const ProjectsPage: React.FC = () => {
background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)',
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
backgroundClip: 'text'
backgroundClip: 'text',
textAlign: 'center',
}}>
</h2>
+2 -1
View File
@@ -186,7 +186,8 @@ export default function VideoFrameExtractor() {
background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)',
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
backgroundClip: 'text'
backgroundClip: 'text',
textAlign: 'center',
}}>
AI视频拆镜工作台
</h2>
+5
View File
@@ -32,6 +32,7 @@ interface AppState {
};
enginesele: any;
inputValue: string;
currentMedia: { name: string; type: 'image' | 'video'; url: string; label: string }[];
fetchProjects: () => Promise<void>;
createProject: (name: string, industry: Industry) => Promise<Project>;
@@ -58,6 +59,7 @@ interface AppState {
setVideoResolution: (resolution: string) => void;
setEnginesele: (enginesele: any) => void;
setInputValue: (inputValue: string) => void;
setCurrentMedia: (currentMedia: { name: string; type: 'image' | 'video'; url: string; label: string }[]) => void;
resetGenerationConfig: () => void;
}
@@ -83,6 +85,7 @@ export const useAppStore = create<AppState>((set, get) => ({
},
enginesele: [],
inputValue: '',
currentMedia: [],
fetchProjects: async () => {
set({ loading: true });
@@ -172,6 +175,7 @@ export const useAppStore = create<AppState>((set, get) => ({
setVideoResolution: (resolution) => set({ videoResolution: resolution }),
setEnginesele: (enginesele) => set({ enginesele }),
setInputValue: (inputValue) => set({ inputValue }),
setCurrentMedia: (currentMedia) => set({ currentMedia }),
resetGenerationConfig: () => set({
mediaType: 'image',
countType: '请选择',
@@ -189,5 +193,6 @@ export const useAppStore = create<AppState>((set, get) => ({
},
enginesele: [],
inputValue: '',
currentMedia: [],
}),
}));