Files
video-gen/video-gen-app/src/pages/HomePage.tsx
T

973 lines
50 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useEffect, useState, useRef } from 'react';
import { Button, Input, Tabs, Tag, Upload, message, Modal } from 'antd';
import {
FileTextOutlined,
ScissorOutlined,
RobotOutlined,
ArrowRightOutlined,
UploadOutlined,
VideoCameraOutlined,
PictureOutlined,
ThunderboltOutlined,
PlayCircleOutlined,
} from '@ant-design/icons';
import { useNavigate } from 'react-router-dom';
import { getmedit ,getHomeCaseHeader,getHomeCaseButton} from '../api';
import hot from '../assets/homebtn1.png';
import mashup from '../assets/homebtn2.png';
import aicreate from '../assets/homebtn3.png';
// 把 ISO 时间格式化成 MM-DD HH:mm(与图片一致)
function formatShortDate(iso?: string | null): string {
if (!iso) return '-';
let s = String(iso).trim();
if (!s.includes('T')) s = s.replace(' ', 'T');
const dotIdx = s.indexOf('.');
if (dotIdx > 0) s = s.slice(0, dotIdx);
s = s.replace(/[+-]\d{2}:?\d{0,2}$/, '').replace(/Z$/, '');
// s 形如 2026-06-18T17:17:00
const [datePart = '', timePart = ''] = s.split('T');
const [, month = '', day = ''] = datePart.split('-');
const hhmm = timePart.slice(0, 5);
if (!month || !day) return '-';
return `${month}-${day} ${hhmm}`;
}
const HomePage: React.FC = () => {
const navigate = useNavigate();
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 () => {
try {
getmedit(5).then((res) => {
for (let item in res) {
res[item].forEach(element => {
// 给每条 element 标记它所属的模块(item 是接口返回的 key)
element.type = item;
// 依次追加到 mockVideos(函数式更新,保证拿到最新 state)
setMockVideos(prev => [...prev, element]);
});
}
// 最终再打印一次汇总(state 异步更新,这里打印的是本次追加的累计长度)
setMockVideos(prev => {
return prev;
});
});
} catch (err) {
}
};
fetchAll();
}, []);
const handleTabChange = (key: string) => {
setActiveTab(key);
};
const aiEntries = [
{
icon: <FileTextOutlined style={{ fontSize: 24, color: '#6366f1' }} />,
title: '爆款开头复刻',
description: '上传参考视频与产品图片,一键复刻爆款视频开头',
action: '立即创作',
path: '/initial',
},
{
icon: <ScissorOutlined style={{ fontSize: 24, color: '#f97316' }} />,
title: '拆镜复刻',
description: '一键拆解画面分镜,助力仿拍或创作',
action: '开始混剪',
path: '/removelens',
},
{
icon: <RobotOutlined style={{ fontSize: 24, color: '#10b981' }} />,
title: 'AI成片',
description: '输入想法、剧本或上传参考,智能生成视频/图片',
action: '立即生成',
path: '/conversation',
},
];
const tabs = [
{ key: 'all', label: '全部' },
{ key: 'project', label: '项目媒体' },
{ key: 'chatAi', label: 'AI成片' },
{ key: 'hotOpeningReplicate', label: '爆款复刻' },
{ key: 'shotReplicate', label: '拆镜复刻' },
];
const filteredVideos = activeTab === 'all'
? mockVideos
: mockVideos.filter(v => v.type === activeTab);
const materialCases = [
'https://trae-api-cn.mchost.guru/api/ide/v1/text_to_image?prompt=modern%20city%20skyline%20night%20view&image_size=landscape_16_9',
'https://trae-api-cn.mchost.guru/api/ide/v1/text_to_image?prompt=nature%20forest%20landscape%20sunlight&image_size=landscape_16_9',
'https://trae-api-cn.mchost.guru/api/ide/v1/text_to_image?prompt=abstract%20technology%20background%20digital&image_size=landscape_16_9',
'https://trae-api-cn.mchost.guru/api/ide/v1/text_to_image?prompt=food%20cooking%20kitchen%20delicious&image_size=landscape_16_9',
'https://trae-api-cn.mchost.guru/api/ide/v1/text_to_image?prompt=fashion%20clothing%20style%20elegant&image_size=landscape_16_9',
];
return (
<div className="content_box">
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16 }}>
<h3>首页工作台</h3>
<p
style={{
cursor: 'pointer',
fontSize: 12, letterSpacing: 0.3,
padding: '10px 16px',
backgroundColor: '#ffffffff',
borderRadius: 30,
boxShadow: '0px 4px 12px 0px rgba(0, 0, 0, 0.1)',
}}
>如需进行账户素材推送:
<span
style={{
color: '#1a50bbff',
}}
onClick={() => {
navigate('/authorization')
}}>
一键推送
<ArrowRightOutlined style={{ marginLeft: 8, transform: 'rotate(0deg)' }} />
</span>
</p>
</div>
{/* ========== 顶部工作台引导区域(三步流程) ========== */}
<div className="animate-fadeInUp" style={{
padding: '24px 28px',
borderRadius: 16,
background: 'linear-gradient(135deg, #f0f9ff 0%, #faf5ff 50%, #fef3c7 100%)',
border: '1px solid rgba(99,102,241,0.10)',
margin: '0 0 20px',
position: 'relative',
overflow: 'hidden',
}}>
{/* 区域标题 */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 18 }}>
<div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<div style={{
width: 4, height: 18, borderRadius: 2,
background: 'linear-gradient(180deg, #6366f1, #a855f7)',
}} />
<div style={{ fontSize: 17, fontWeight: 700, color: '#1f2937', letterSpacing: 0.3 }}>
我的项目
</div>
</div>
</div>
{/* <div >
<span style={{
cursor: 'pointer',
fontSize: 12, color: '#1a50bbff', letterSpacing: 0.3,
}}
onClick={() => {
navigate('/authorization')
}}
>如需进行账户素材推送 一键推送
<ArrowRightOutlined style={{ marginLeft: 8, transform: 'rotate(0deg)' }} />
</span>
</div> */}
</div>
<div style={{
display: 'flex',
alignItems: 'center',
gap: 12,
}}>
<div style={{
flex: 1,
background: '#fff',
borderRadius: 14,
padding: '20px 18px',
display: 'flex',
flexDirection: 'column',
boxShadow: '0 2px 8px rgba(15, 23, 42, 0.04)',
}}>
<div style={{ textAlign: 'center', fontSize: 20, fontWeight: 700, color: '#1e293b', marginBottom: 14, letterSpacing: 1 }}>
1
</div>
{/* 插图占位:项目卡(标题/描述输入框 + 行业分类 chip) */}
<div style={{
flex: 1,
minHeight: 140,
border: '1px dashed #c7d2fe',
borderRadius: 8,
background: '#fafbff',
padding: 10,
display: 'flex',
flexDirection: 'column',
gap: 8,
marginBottom: 12,
}}>
{/* 项目名称占位 */}
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<div style={{ width: 8, height: 8, borderRadius: 2, background: '#6366f1' }} />
<div style={{ flex: 1, height: 22, background: '#fff', border: '1px solid #e2e8f0', borderRadius: 4, display: 'flex', alignItems: 'center', padding: '0 8px', fontSize: 10, color: '#94a3b8' }}>
项目名称...
</div>
</div>
{/* 行业分类 chip */}
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 4 }}>
<div style={{ padding: '2px 8px', background: '#eef2ff', color: '#6366f1', borderRadius: 10, fontSize: 10, fontWeight: 500, border: '1px solid #c7d2fe' }}>美妆</div>
<div style={{ padding: '2px 8px', background: '#fff7ed', color: '#f97316', borderRadius: 10, fontSize: 10, fontWeight: 500, border: '1px solid #fed7aa' }}>美食</div>
<div style={{ padding: '2px 8px', background: '#ecfdf5', color: '#10b981', borderRadius: 10, fontSize: 10, fontWeight: 500, border: '1px solid #a7f3d0' }}>3C数码</div>
<div style={{ padding: '2px 8px', background: '#f5f3ff', color: '#8b5cf6', borderRadius: 10, fontSize: 10, fontWeight: 500, border: '1px solid #ddd6fe' }}>服饰</div>
</div>
{/* 描述占位行 */}
<div style={{ height: 16, background: '#fff', border: '1px solid #e2e8f0', borderRadius: 4 }} />
<div style={{ height: 16, width: '70%', background: '#fff', border: '1px solid #e2e8f0', borderRadius: 4 }} />
</div>
<div style={{
background: '#f1f5f9',
borderRadius: 10,
padding: '14px 12px',
textAlign: 'center',
fontSize: 13,
color: '#475569',
lineHeight: 1.6,
}}>
创建新项目,自定义项目名称并选择对应行业分类
</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', color: '#94a3b8', fontSize: 22 }}>
<ArrowRightOutlined style={{ transform: 'rotate(0deg)' }} />
</div>
<div style={{
flex: 1,
background: '#fff',
borderRadius: 14,
padding: '20px 18px',
display: 'flex',
flexDirection: 'column',
boxShadow: '0 2px 8px rgba(15, 23, 42, 0.04)',
}}>
<div style={{ textAlign: 'center', fontSize: 20, fontWeight: 700, color: '#1e293b', marginBottom: 14, letterSpacing: 1 }}>
2
</div>
{/* 插图占位:图片/视频切换 + 尺寸/时长参数 */}
<div style={{
flex: 1,
minHeight: 140,
border: '1px solid #e2e8f0',
borderRadius: 8,
background: '#fafafa',
padding: 10,
display: 'flex',
flexDirection: 'column',
gap: 8,
marginBottom: 12,
}}>
{/* 图片 / 视频 切换 */}
<div style={{ display: 'flex', background: '#f1f5f9', borderRadius: 6, padding: 2, gap: 2 }}>
<div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 4, padding: '5px 0', background: '#fff', borderRadius: 4, fontSize: 11, fontWeight: 600, color: '#6366f1', boxShadow: '0 1px 3px rgba(99,102,241,0.15)' }}>
<VideoCameraOutlined style={{ fontSize: 11 }} />视频
</div>
<div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 4, padding: '5px 0', fontSize: 11, color: '#94a3b8' }}>
<PictureOutlined style={{ fontSize: 11 }} /> 图片
</div>
</div>
{/* 尺寸参数 */}
<div>
<div style={{ fontSize: 9, color: '#94a3b8', marginBottom: 3 }}>尺寸比例</div>
<div style={{ display: 'flex', gap: 4 }}>
<div style={{ flex: 1, textAlign: 'center', padding: '4px 0', background: '#eef2ff', color: '#6366f1', border: '1px solid #c7d2fe', borderRadius: 4, fontSize: 10, fontWeight: 600 }}>9:16</div>
<div style={{ flex: 1, textAlign: 'center', padding: '4px 0', background: '#fff', color: '#64748b', border: '1px solid #e2e8f0', borderRadius: 4, fontSize: 10 }}>16:9</div>
<div style={{ flex: 1, textAlign: 'center', padding: '4px 0', background: '#fff', color: '#64748b', border: '1px solid #e2e8f0', borderRadius: 4, fontSize: 10 }}>1:1</div>
</div>
</div>
{/* 时长参数 */}
<div>
<div style={{ fontSize: 9, color: '#94a3b8', marginBottom: 3 }}>时长</div>
<div style={{ display: 'flex', gap: 4 }}>
<div style={{ flex: 1, textAlign: 'center', padding: '4px 0', background: '#fff', color: '#64748b', border: '1px solid #e2e8f0', borderRadius: 4, fontSize: 10 }}>5s</div>
<div style={{ flex: 1, textAlign: 'center', padding: '4px 0', background: '#fff7ed', color: '#f97316', border: '1px solid #fed7aa', borderRadius: 4, fontSize: 10, fontWeight: 600 }}>10s</div>
<div style={{ flex: 1, textAlign: 'center', padding: '4px 0', background: '#fff', color: '#64748b', border: '1px solid #e2e8f0', borderRadius: 4, fontSize: 10 }}>15s</div>
</div>
</div>
</div>
<div style={{
background: '#f1f5f9',
borderRadius: 10,
padding: '14px 12px',
textAlign: 'center',
fontSize: 13,
color: '#475569',
lineHeight: 1.6,
}}>
选择生成图片或视频,设置尺寸、时长等参数,点击生成即可一键优化提示词
</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', color: '#94a3b8', fontSize: 22 }}>
<ArrowRightOutlined />
</div>
<div style={{
flex: 1,
background: '#fff',
borderRadius: 14,
padding: '20px 18px',
display: 'flex',
flexDirection: 'column',
boxShadow: '0 2px 8px rgba(15, 23, 42, 0.04)',
}}>
<div style={{ textAlign: 'center', fontSize: 20, fontWeight: 700, color: '#1e293b', marginBottom: 14, letterSpacing: 1 }}>
3
</div>
{/* 插图占位:核对清单(✓ 项)+ 一键生成按钮 */}
<div style={{
flex: 1,
minHeight: 140,
border: '1px solid #e2e8f0',
borderRadius: 8,
background: '#fafafa',
padding: 10,
display: 'flex',
flexDirection: 'column',
gap: 6,
marginBottom: 12,
justifyContent: 'space-between',
}}>
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '5px 8px', background: '#f0fdf4', border: '1px solid #bbf7d0', borderRadius: 5 }}>
<div style={{ width: 14, height: 14, borderRadius: '50%', background: '#10b981', color: '#fff', fontSize: 10, display: 'flex', alignItems: 'center', justifyContent: 'center', fontWeight: 700 }}></div>
<div style={{ fontSize: 10, color: '#065f46', fontWeight: 500 }}>提示词已智能优化</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '5px 8px', background: '#f0fdf4', border: '1px solid #bbf7d0', borderRadius: 5 }}>
<div style={{ width: 14, height: 14, borderRadius: '50%', background: '#10b981', color: '#fff', fontSize: 10, display: 'flex', alignItems: 'center', justifyContent: 'center', fontWeight: 700 }}></div>
<div style={{ fontSize: 10, color: '#065f46', fontWeight: 500 }}>尺寸 9:16 · 时长 5s</div>
</div>
{/* <div style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '5px 8px', background: '#fff7ed', border: '1px solid #fed7aa', borderRadius: 5 }}>
<div style={{ width: 14, height: 14, borderRadius: '50%', background: '#f97316', color: '#fff', fontSize: 10, display: 'flex', alignItems: 'center', justifyContent: 'center', fontWeight: 700 }}>!</div>
<div style={{ fontSize: 10, color: '#9a3412', fontWeight: 500 }}>参考素材 0/3</div>
</div> */}
</div>
{/* 一键生成按钮 */}
<div style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: 4,
padding: '6px 0',
background: 'linear-gradient(135deg, #6366f1, #8b5cf6)',
color: '#fff',
borderRadius: 5,
fontSize: 11,
fontWeight: 600,
boxShadow: '0 2px 6px rgba(99,102,241,0.25)',
}}>
<ThunderboltOutlined style={{ fontSize: 11 }} />
确认并生成素材
</div>
</div>
<div style={{
background: '#f1f5f9',
borderRadius: 10,
padding: '14px 12px',
textAlign: 'center',
fontSize: 13,
color: '#475569',
lineHeight: 1.6,
}}>
核对图片 / 视频参数与优化后的提示词,确认后一键生成素材
</div>
</div>
<div
onClick={() => navigate('/projects')}
onMouseEnter={(e) => {
e.currentTarget.style.transform = 'translateY(-2px) scale(1.05)';
e.currentTarget.style.boxShadow = '0 14px 32px rgba(99,102,241,0.45)';
const arrow = e.currentTarget.querySelector('.round-arrow') as HTMLElement | null;
if (arrow) arrow.style.transform = 'translateX(3px)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.transform = 'translateY(0) scale(1)';
e.currentTarget.style.boxShadow = '0 8px 20px rgba(99,102,241,0.30)';
const arrow = e.currentTarget.querySelector('.round-arrow') as HTMLElement | null;
if (arrow) arrow.style.transform = 'translateX(0)';
}}
style={{
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: 4,
width: 64,
height: 64,
borderRadius: '50%',
textAlign: 'center',
background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 50%, #a855f7 100%)',
color: '#fff',
fontSize: 14,
fontWeight: 600,
letterSpacing: 0.5,
boxShadow: '0 8px 20px rgba(99,102,241,0.30)',
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
userSelect: 'none',
position: 'relative',
}}
>
{/* 高光层(hover 时轻微变亮) */}
<span style={{
position: 'absolute',
inset: 0,
borderRadius: '50%',
background: 'radial-gradient(circle at 30% 25%, rgba(255,255,255,0.35) 0%, transparent 55%)',
pointerEvents: 'none',
}} />
前往
<ArrowRightOutlined
className="round-arrow"
style={{ fontSize: 13, transition: 'transform 0.3s ease' }}
/>
</div>
</div>
</div>
{/* ========== AI 创作入口区域 ========== */}
<div className="animate-fadeInUp stagger-children" style={{
padding: '24px 28px',
borderRadius: 16,
background: '#fff',
border: '1px solid #e2e8f0',
marginBottom: 20,
}}>
<div style={{
display: 'flex',
alignItems: 'center',
gap: 8,
marginBottom: 18,
}}>
<div style={{
width: 4, height: 18, borderRadius: 2,
background: 'linear-gradient(180deg, #6366f1, #a855f7)',
}} />
<div style={{ fontSize: 17, fontWeight: 700, color: '#1f2937', letterSpacing: 0.3 }}>
AI 创作入口
</div>
<div style={{ fontSize: 12, color: '#94a3b8', marginLeft: 8 }}>
一键开启智能创作
</div>
</div>
<div style={{ display: 'flex', gap: 16 }}>
{aiEntries.map((entry, index) => {
// 三个入口用三种不同色调的渐变光晕作为视觉区分,但都保持白底卡片
const accentMap = [
{ color: '#6366f1', light: 'rgba(99,102,241,0.10)', tag: '复刻', bg: hot },
{ color: '#f97316', light: 'rgba(249,115,22,0.10)', tag: '混剪', bg: mashup },
{ color: '#10b981', light: 'rgba(16,185,129,0.10)', tag: '云创', bg: aicreate },
];
const accent = accentMap[index] || accentMap[0];
return (
<div
key={index}
onClick={() => navigate(entry.path)}
className="project-card"
style={{
flex: 1,
padding: '20px 22px',
borderRadius: 14,
background: '#fff',
border: '1px solid #e2e8f0',
cursor: 'pointer',
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
position: 'relative',
overflow: 'hidden',
backgroundImage: `url(${accent.bg})`,
backgroundRepeat: 'no-repeat',
backgroundSize: '100% 100%',
backgroundPosition: 'center',
}}
onMouseEnter={(e) => {
e.currentTarget.style.borderColor = accent.color;
e.currentTarget.style.boxShadow = `0 12px 32px ${accent.light}`;
}}
onMouseLeave={(e) => {
e.currentTarget.style.borderColor = '#e2e8f0';
e.currentTarget.style.boxShadow = 'none';
}}
>
{/* 顶部装饰光带 */}
<div style={{
position: 'absolute',
top: 0, left: 0, right: 0, height: 3,
background: `linear-gradient(90deg, ${accent.color}, ${accent.color}88)`,
}} />
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 14 }}>
<div style={{
width: 48, height: 48,
borderRadius: 12,
background: accent.light,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}>
<span style={{ color: accent.color, fontSize: 22, display: 'flex' }}>{entry.icon}</span>
</div>
<div style={{
fontSize: 11, color: accent.color,
padding: '2px 8px', borderRadius: 6,
background: accent.light, fontWeight: 600,
}}>{accent.tag}</div>
</div>
<div style={{ fontSize: 16, fontWeight: 700, color: '#1f2937', marginBottom: 4 }}>
{entry.title}
</div>
<div style={{ fontSize: 12, color: '#6b7280', marginBottom: 14, lineHeight: 1.5, minHeight: 36 }}>
{entry.description}
</div>
<div style={{
display: 'flex',
alignItems: 'center',
gap: 4,
color: accent.color,
fontSize: 13,
fontWeight: 600,
}}>
{entry.action}
<ArrowRightOutlined style={{ fontSize: 12 }} />
</div>
</div>
);
})}
</div>
</div>
{/* ========== 近期作品区域 ========== */}
<div className="animate-fadeInUp" style={{
padding: '24px 28px',
borderRadius: 16,
background: '#fff',
border: '1px solid #e2e8f0',
marginBottom: 20,
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<div style={{
width: 4, height: 18, borderRadius: 2,
background: 'linear-gradient(180deg, #6366f1, #a855f7)',
}} />
<div style={{ fontSize: 17, fontWeight: 700, color: '#1f2937', letterSpacing: 0.3 }}>
近期作品
</div>
</div>
</div>
{/* Tab切换 */}
<div style={{ marginBottom: 20 }}>
<Tabs
activeKey={activeTab}
onChange={handleTabChange}
items={tabs.map(tab => ({
key: tab.key,
label: tab.label,
}))}
className="homepage-tabs"
/>
</div>
{/* 视频网格 */}
<div className="stagger-children" style={{ display: 'grid', gridTemplateColumns: 'repeat(5, 1fr)', gap: 16 }}>
{filteredVideos.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>
) : filteredVideos.map((video) => (
<div
key={video.id || `${video.type}-${Math.random()}`}
className="project-card"
onClick={() => {
// 按模块分发跳转:
// - 爆款复刻(hot)→ 复刻详情页
// - AI 成片(ai)→ 对话/生成页
// - 项目记录(project)→ 项目详情页
const id = video.moduleProjectId;
if (video.type === 'hotOpeningReplicate' && id != null) {
navigate(`/initial/${id}/initialinfo`);
} else if (video.type === 'shotReplicate' && id != null) {
navigate(`/removelens/${id}/removefenbu`);
} else if (video.type === 'chatAi') {
navigate(`/conversation`);
} else if (video.type === 'project') {
navigate(`/project`);
}
}}
style={{
borderRadius: 12,
overflow: 'hidden',
cursor: 'pointer',
background: '#fff',
border: '1px solid #e2e8f0',
}}
>
<div style={{
position: 'relative',
aspectRatio: '16/9',
// background: '#1a1a2e',
}}>
{(() => {
// 1) 读取后端 API 基础地址;环境变量未配置时降级到本地 8000
const apiBase = (import.meta.env.VITE_API_BASE as string) || 'http://localhost:8000';
// 2) 判断当前作品是否为"图片":
// - 爆款复刻(type === 'hot')始终是视频,不参与图片判断
// - 其他模块(项目记录 / AI 成片)根据 genType 判定
// - genType 可能是字符串 'image',也可能是数字 1(兼容两种后端约定)
const isImage = video.type !== 'hotOpeningReplicate'
&& (
String(video.resourceType ?? '').toLowerCase() === 'image'
|| video.resourceType === 1
|| String(video.resourceType ?? '') === '1'
);
// 3) 根据媒体类型选择对应的资源路径:
// - 图片:后端返回的 imageUrl 已经是带签名的完整相对路径
// 形如 /static/generate/images/2026/06/26/0019f017f5924df4123.png?exp=...&sign=...&w=300&p=50
// - 视频:使用视频封面 videoCoverUrl(这是视频作品的静态缩略图)
// - 爆款复刻(type === 'hot')特殊处理:使用 finalVideoCoverUrl
let rawPath = '';
if (isImage) {
rawPath = '/static' + video.resultUrl + '&w=300&p=50' || '';
} else if (video.type === 'hot') {
rawPath = video.coverUrl || video.resultUrl || video.resultUrl || '';
} else {
rawPath = video.coverUrl || video.resultUrl || video.resultUrl || '';
}
// 4) 拼装最终 src
// - rawPath 为空 → 用空串(让 <img> 走 onError 兜底)
// - 已经是 http(s) 完整 URL → 直接使用(OSS / CDN 场景)
// - 否则视为后端相对路径,前面拼 apiBase
const src = rawPath
? (rawPath.startsWith('http') ? rawPath : apiBase + rawPath)
: '';
return (
<img
src={src}
alt={video.title || video.name || '作品'}
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
/>
);
})()}
{(() => {
const isImage = String(video.resourceType ?? '').toLowerCase() === 'image';
if (isImage) return null;
return (
<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>
);
})()}
</div>
<div style={{
padding: '10px 12px',
background: '#f8fafc',
paddingTop: 0,
}}>
<div style={{
marginTop: 6,
display: 'flex',
alignItems: 'center',
gap: 6,
fontSize: 12,
color: '#64748b',
}}>
{(() => {
// 显示所属模块,而非媒体类型
const moduleMap: Record<string, string> = {
project: '项目媒体',
chatAi: 'AI成片',
hotOpeningReplicate: '爆款复刻',
shotReplicate: '拆镜复刻',
};
const moduleLabel = moduleMap[video.type] || '其他';
return (
<span style={{
display: 'inline-block',
padding: '1px 6px',
border: '1px solid #3b82f6',
borderRadius: 4,
color: '#3b82f6',
fontSize: 11,
fontWeight: 500,
background: '#fff',
lineHeight: 1.4,
whiteSpace: 'nowrap',
}}>
{moduleLabel}
</span>
);
})()}
<span style={{ color: '#94a3b8' }}>·</span>
<span style={{ whiteSpace: 'nowrap' }}>{formatShortDate(video.generatedTime)}</span>
</div>
</div>
</div>
))}
</div>
</div>
{/* ========== 素材案例区域 ========== */}
<div className="animate-fadeInUp" style={{
padding: '24px 28px',
borderRadius: 16,
background: '#fff',
border: '1px solid #e2e8f0',
}}>
<div style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
marginBottom: 18,
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<div style={{
width: 4, height: 18, borderRadius: 2,
background: 'linear-gradient(180deg, #6366f1, #a855f7)',
}} />
<div style={{ fontSize: 17, fontWeight: 700, color: '#1f2937', letterSpacing: 0.3 }}>
素材案例
</div>
<div style={{ fontSize: 12, color: '#94a3b8', marginLeft: 4 }}>
精选优质作品参考
</div>
</div>
{/* <div style={{
fontSize: 13, color: '#6366f1', cursor: 'pointer', fontWeight: 500,
display: 'flex', alignItems: 'center', gap: 2,
}}>
更多案例
<ArrowRightOutlined style={{ fontSize: 11 }} />
</div> */}
</div>
{/* 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={asset.id || index}
className="project-card"
onClick={() => setPreviewAsset(asset)}
style={{
borderRadius: 12,
overflow: 'hidden',
cursor: 'pointer',
background: '#fff',
border: '1px solid #e2e8f0',
}}
>
{/* 媒体区域 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>
);
};
export default HomePage;