This commit is contained in:
孙佳艺
2026-06-26 17:25:11 +08:00
parent 3c3797a734
commit f2dbfbf62f
18 changed files with 1051 additions and 123 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -28,8 +28,8 @@
} }
})(); })();
</script> </script>
<script type="module" crossorigin src="/assets/index-COTSz9Xd.js"></script> <script type="module" crossorigin src="/assets/index-BpyoCjCy.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-eLRg4pQk.css"> <link rel="stylesheet" crossorigin href="/assets/index-eN1cM2Wc.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
+2
View File
@@ -22,6 +22,7 @@ import AuthorizationPage from './pages/AuthorizationPage';
import MaterialListPage from './pages/MaterialListPage'; import MaterialListPage from './pages/MaterialListPage';
import RemoveInfo from './pages/RemoveInfo'; import RemoveInfo from './pages/RemoveInfo';
import RemoveRw from './pages/RemoveRw'; import RemoveRw from './pages/RemoveRw';
import HomePage from './pages/HomePage';
// import RemoveFenbu from './pages/RemoveFenbu'; // import RemoveFenbu from './pages/RemoveFenbu';
import ConsumePage from './pages/ConsumePage'; import ConsumePage from './pages/ConsumePage';
import { useAuthStore } from './store/useAuthStore'; import { useAuthStore } from './store/useAuthStore';
@@ -99,6 +100,7 @@ const App = () => {
<Route path="order-records" element={<UserCenterPage />} /> <Route path="order-records" element={<UserCenterPage />} />
<Route path="user-center" element={<UserCenterPage />} /> <Route path="user-center" element={<UserCenterPage />} />
<Route path="messages" element={<MessagesPage />} /> <Route path="messages" element={<MessagesPage />} />
<Route path="home" element={<HomePage />} />
<Route path="conversation" element={<GenerateConver />} /> <Route path="conversation" element={<GenerateConver />} />
<Route path="initial" element={<InitialReplication />} /> <Route path="initial" element={<InitialReplication />} />
<Route path="initial/:creatID/initialinfo" element={<InitialInfo />} /> <Route path="initial/:creatID/initialinfo" element={<InitialInfo />} />
+10 -1
View File
@@ -197,7 +197,7 @@ export async function verifySms(phone: string, code: string): Promise<{ token: s
return api.post('/sms/verify', { phone, code }, false); return api.post('/sms/verify', { phone, code }, false);
} }
// ── Notifications ───────────────────────────────────────── // ── Notifications ─────────────────────────────────────────
export async function getNotifications(page = 1, pageSize = 20, isRead?: boolean): Promise<{ items: AdminNotification[], total: number }> { export async function getNotifications(page = 1, pageSize = 20, isRead?: boolean): Promise<{ items: AdminNotification[], total: number, credits?: { balance: number } }> {
if (USE_MOCK) { if (USE_MOCK) {
const items = await mock.mockGetAdminNotifications(); const items = await mock.mockGetAdminNotifications();
return { items, total: items.length }; return { items, total: items.length };
@@ -679,3 +679,12 @@ export async function getResourcesMaterialList(params: ResourcesMaterialListPara
if (params.page_size !== undefined) query.set('page_size', String(params.page_size)); if (params.page_size !== undefined) query.set('page_size', String(params.page_size));
return api.get(`/resources-material/list?${query.toString()}`); return api.get(`/resources-material/list?${query.toString()}`);
} }
// home 获取各模块媒体
export async function getmedit(limit:number): Promise<any> {
return api.get(`/recent-generations?limit=${limit}&modules=project&modules=chat_ai&modules=hot_opening_replicate&modules=shot_replicate`);
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 MiB

@@ -413,14 +413,19 @@ const AppLayout: React.FC = () => {
flexShrink: 0, flexShrink: 0,
background: 'linear-gradient(135deg, rgba(99, 102, 241, 0.06) 0%, rgba(139, 92, 246, 0.04) 100%)', background: 'linear-gradient(135deg, rgba(99, 102, 241, 0.06) 0%, rgba(139, 92, 246, 0.04) 100%)',
}}> }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 14 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 14, cursor: 'pointer' }}
onClick={() => {
navigate('/home');
}}>
<div style={{ <div style={{
width: 42, height: 42, borderRadius: 14, flexShrink: 0, width: 42, height: 42, borderRadius: 14, flexShrink: 0,
background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 50%, #a78bfa 100%)', background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 50%, #a78bfa 100%)',
display: 'flex', alignItems: 'center', justifyContent: 'center', display: 'flex', alignItems: 'center', justifyContent: 'center',
boxShadow: '0 4px 16px rgba(99, 102, 241, 0.35)', boxShadow: '0 4px 16px rgba(99, 102, 241, 0.35)',
overflow: 'hidden', overflow: 'hidden',
}}> }}
>
{siteLogo ? ( {siteLogo ? (
<img src={siteLogo} alt="logo" style={{ width: 28, height: 28, objectFit: 'contain' }} /> <img src={siteLogo} alt="logo" style={{ width: 28, height: 28, objectFit: 'contain' }} />
) : ( ) : (
@@ -24,18 +24,30 @@ const NotificationPopup: React.FC = () => {
const [notifications, setNotifications] = useState<Notification[]>([]); const [notifications, setNotifications] = useState<Notification[]>([]);
const [currentIndex, setCurrentIndex] = useState(0); const [currentIndex, setCurrentIndex] = useState(0);
const refreshUser = useAuthStore((state) => state.refreshUser); const refreshUser = useAuthStore((state) => state.refreshUser);
const setUserCredits = useAuthStore((state) => state.setUserCredits);
const fetchNotifications = useCallback(async () => { const fetchNotifications = useCallback(async () => {
try { try {
const result = await getNotifications(1, 20, false); const result = await getNotifications(1, 20, false);
// 接口返回结构:{ credits: { balance: 4317.23 }, items: [...], total: N }
const data = result.items || []; const data = result.items || [];
// 同步积分:result.credits.balance 与当前 user.credits 不一致时,直接替换
const newBalance = result?.credits?.balance;
if (typeof newBalance === 'number') {
const currentUser = useAuthStore.getState().user;
if (currentUser && currentUser.credits !== newBalance) {
setUserCredits(newBalance);
}
}
setNotifications(data); setNotifications(data);
if (data.length > 0 && !visible) { if (data.length > 0 && !visible) {
setVisible(true); setVisible(true);
setCurrentIndex(0); setCurrentIndex(0);
} }
} catch { /* ignore */ } } catch { /* ignore */ }
}, [visible]); }, [visible, setUserCredits]);
useEffect(() => { useEffect(() => {
fetchNotifications(); fetchNotifications();
+40
View File
@@ -200,6 +200,30 @@ html, body {
.recharge-card .recharge-price { transition: all 0.3s ease; } .recharge-card .recharge-price { transition: all 0.3s ease; }
.recharge-card:hover .recharge-price { transform: scale(1.08); } .recharge-card:hover .recharge-price { transform: scale(1.08); }
/* ── Homepage tabs ────────────────────── */
.homepage-tabs .ant-tabs-nav {
margin-bottom: 8px !important;
}
.homepage-tabs .ant-tabs-tab {
padding: 8px 16px !important;
font-size: 13px !important;
font-weight: 500 !important;
color: #6b7280 !important;
transition: all 0.25s !important;
}
.homepage-tabs .ant-tabs-tab:hover {
color: #6366f1 !important;
}
.homepage-tabs .ant-tabs-tab-active .ant-tabs-tab-btn {
color: #6366f1 !important;
font-weight: 600 !important;
}
.homepage-tabs .ant-tabs-ink-bar {
background: linear-gradient(90deg, #6366f1, #a855f7) !important;
height: 3px !important;
border-radius: 2px !important;
}
/* ── Project card hover ────────────────── */ /* ── Project card hover ────────────────── */
.project-card { .project-card {
transition: all 0.35s cubic-bezier(0.4, 0, 0.2, 1) !important; transition: all 0.35s cubic-bezier(0.4, 0, 0.2, 1) !important;
@@ -440,3 +464,19 @@ html, body {
/* Card grid */ /* Card grid */
.card-grid { grid-template-columns: repeat(3, 1fr) !important; } .card-grid { grid-template-columns: repeat(3, 1fr) !important; }
} }
/* ── HomePage Tabs ────────────── */
.homepage-tabs .ant-tabs-nav { margin-bottom: 0; }
.homepage-tabs .ant-tabs-tab {
font-size: 14px;
font-weight: 500;
padding: 8px 20px;
margin-right: 4px;
}
.homepage-tabs .ant-tabs-tab-active {
color: #6366f1;
font-weight: 600;
}
.homepage-tabs .ant-tabs-ink-bar {
background: linear-gradient(135deg, #6366f1, #8b5cf6);
}
+5 -4
View File
@@ -1603,14 +1603,15 @@ const AIChatPage: React.FC = () => {
}} }}
size="middle" size="middle"
> >
<Option value="image">
<PictureOutlined style={{ marginRight: 6, fontSize: 14 }} />
<span style={{ fontSize: 14, fontWeight: 500, color: '#64748b' }}></span>
</Option>
<Option value="video"> <Option value="video">
<VideoCameraOutlined style={{ marginRight: 6, fontSize: 14 }} /> <VideoCameraOutlined style={{ marginRight: 6, fontSize: 14 }} />
<span style={{ fontSize: 14, fontWeight: 500, color: '#64748b' }}></span> <span style={{ fontSize: 14, fontWeight: 500, color: '#64748b' }}></span>
</Option> </Option>
<Option value="image">
<PictureOutlined style={{ marginRight: 6, fontSize: 14 }} />
<span style={{ fontSize: 14, fontWeight: 500, color: '#64748b' }}></span>
</Option>
</Select> </Select>
{/* </div> */} {/* </div> */}
+17 -9
View File
@@ -247,7 +247,7 @@ const GeneratePage: React.FC = () => {
const [lastTextTokens, setLastTextTokens] = useState(0); const [lastTextTokens, setLastTextTokens] = useState(0);
// Media type: 1 for image, 2 for video // Media type: 1 for image, 2 for video
const [mediaType, setMediaType] = useState<any>("image"); const [mediaType, setMediaType] = useState<any>("video");
// Image parameters // Image parameters
const [selectedRatio, setSelectedRatio] = useState<string>("1:1"); const [selectedRatio, setSelectedRatio] = useState<string>("1:1");
@@ -1400,7 +1400,14 @@ const GeneratePage: React.FC = () => {
}; };
return ( return (
<div> <div
style={{
margin: '-24px -32px -32px',
borderRadius: 20,
height: 'calc(100vh - 34px)',
padding:'24px'
}}>
{/* Header */} {/* Header */}
<div <div
className="gen-header animate-fadeInUp" className="gen-header animate-fadeInUp"
@@ -1531,16 +1538,10 @@ const GeneratePage: React.FC = () => {
{/* 单选按钮组 */} {/* 单选按钮组 */}
<Radio.Group <Radio.Group
defaultValue="image" defaultValue="video"
buttonStyle="solid" buttonStyle="solid"
onChange={handleMediaTypeChange} onChange={handleMediaTypeChange}
> >
{/* 图片选项 */}
<Radio.Button value="image" style={{ zIndex: 0 }}>
<PictureOutlined style={{ marginRight: 6, fontSize: 14 }} />{" "}
{/* 图片图标 */}
</Radio.Button>
{/* 视频选项 */} {/* 视频选项 */}
<Radio.Button value="video"> <Radio.Button value="video">
<VideoCameraOutlined <VideoCameraOutlined
@@ -1549,6 +1550,13 @@ const GeneratePage: React.FC = () => {
{/* 视频图标 */} {/* 视频图标 */}
</Radio.Button> </Radio.Button>
{/* 图片选项 */}
<Radio.Button value="image" style={{ zIndex: 0 }}>
<PictureOutlined style={{ marginRight: 6, fontSize: 14 }} />{" "}
{/* 图片图标 */}
</Radio.Button>
</Radio.Group> </Radio.Group>
</div> </div>
{mediaType === "video" && {mediaType === "video" &&
+832
View File
@@ -0,0 +1,832 @@
import React, { useEffect, useState } from 'react';
import { Button, Input, Tabs, Tag, Upload, message } from 'antd';
import {
FileTextOutlined,
ScissorOutlined,
RobotOutlined,
ArrowRightOutlined,
UploadOutlined,
VideoCameraOutlined,
PictureOutlined,
ThunderboltOutlined,
} from '@ant-design/icons';
import { useNavigate } from 'react-router-dom';
import { getmedit } 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[]>([]);
useEffect(() => {
const fetchAll = async () => {
try {
getmedit(5).then((res) => {
console.log('getmedit:', 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 => {
console.log('mockVideos 最终条数:', prev.length);
return prev;
});
});
} catch (err) {
console.error('HomePage fetch error:', 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: 'AI全自动快速出片,支持文案生成/上传参考素材制作',
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 style={{
margin: '-24px -32px -32px',
borderRadius: 20,
height: 'calc(100vh - 34px)',
overflow: 'auto',
scrollbarWidth: 'none',
padding: '0 28px 32px',
}}>
{/* ========== 顶部工作台引导区域(三步流程) ========== */}
<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: '24px 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',
}}>
{/* <div style={{
fontSize: 13,
color: '#334155',
fontWeight: 500,
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
}}>
{video.title || video.name || video.originalPrompt || video.prompt || '未命名作品'}
</div> */}
<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>
<div className="stagger-children" style={{ display: 'flex', gap: 14 }}>
{materialCases.map((caseUrl, index) => (
<div
key={index}
className="project-card"
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';
}}
>
<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}
</div>
</div>
))}
</div>
</div>
</div>
);
};
export default HomePage;
+3 -3
View File
@@ -77,7 +77,7 @@ const LoginPage: React.FC = () => {
await login(values.phone, values.password, undefined, values.rememberMe); await login(values.phone, values.password, undefined, values.rememberMe);
message.success('登录成功,欢迎回来'); message.success('登录成功,欢迎回来');
await checkAuth(); await checkAuth();
navigate('/projects'); navigate('/home');
} catch (error: any) { } catch (error: any) {
const errorMsg = error?.response?.data?.detail || error?.response?.data?.message || error?.message || '登录失败'; const errorMsg = error?.response?.data?.detail || error?.response?.data?.message || error?.message || '登录失败';
message.error(errorMsg); message.error(errorMsg);
@@ -98,7 +98,7 @@ const LoginPage: React.FC = () => {
await phonelogin(values.phone, values.code); await phonelogin(values.phone, values.code);
message.success('登录成功,欢迎回来'); message.success('登录成功,欢迎回来');
await checkAuth(); await checkAuth();
navigate('/projects'); navigate('/home');
} catch (error: any) { } catch (error: any) {
const errorMsg = error?.response?.data?.detail || error?.response?.data?.message || error?.message || '登录失败'; const errorMsg = error?.response?.data?.detail || error?.response?.data?.message || error?.message || '登录失败';
message.error(errorMsg); message.error(errorMsg);
@@ -115,7 +115,7 @@ const LoginPage: React.FC = () => {
const user = await register(values.phone, values.regCode, values.password); const user = await register(values.phone, values.regCode, values.password);
message.success('注册成功'); message.success('注册成功');
await checkAuth(); await checkAuth();
navigate('/projects'); navigate('/home');
} catch (error: any) { } catch (error: any) {
const errorMsg = error?.response?.data?.detail || error?.response?.data?.message || error?.message || '注册失败'; const errorMsg = error?.response?.data?.detail || error?.response?.data?.message || error?.message || '注册失败';
message.error(errorMsg); message.error(errorMsg);
+13 -1
View File
@@ -97,7 +97,15 @@ const ProjectsPage: React.FC = () => {
}; };
return ( return (
<div> <div
style={
{
margin: '-24px -32px -32px',
borderRadius: 20,
height: 'calc(100vh - 34px)',
padding:'24px'
}
}>
{/* Header banner */} {/* Header banner */}
<div className="animate-fadeInUp" style={{ <div className="animate-fadeInUp" style={{
display: 'flex', justifyContent: 'space-between', alignItems: 'center', display: 'flex', justifyContent: 'space-between', alignItems: 'center',
@@ -116,6 +124,10 @@ const ProjectsPage: React.FC = () => {
background: 'rgba(255,255,255,0.15)', border: '1px solid rgba(255,255,255,0.3)', background: 'rgba(255,255,255,0.15)', border: '1px solid rgba(255,255,255,0.3)',
color: '#fff', fontWeight: 600, backdropFilter: 'blur(10px)', borderRadius: 10, height: 42, color: '#fff', fontWeight: 600, backdropFilter: 'blur(10px)', borderRadius: 10, height: 42,
}}></Button> }}></Button>
<Button icon={<PlusOutlined />} onClick={() => navigate('/records')} size="large" style={{
background: 'rgba(255,255,255,0.15)', border: '1px solid rgba(255,255,255,0.3)',
color: '#fff', fontWeight: 600, backdropFilter: 'blur(10px)', borderRadius: 10, height: 42,
}}></Button>
</div> </div>
{projects.length === 0 ? ( {projects.length === 0 ? (
+1 -1
View File
@@ -65,7 +65,7 @@ export const useAppStore = create<AppState>((set, get) => ({
loading: false, loading: false,
// 生成配置状态初始值 // 生成配置状态初始值
mediaType: 'image', mediaType: 'video',
countType: '请选择', countType: '请选择',
selectedRatio: '1:1', selectedRatio: '1:1',
selectedResolution: '2K', selectedResolution: '2K',
+7
View File
@@ -10,6 +10,7 @@ interface AuthState {
checkAuth: () => Promise<void>; checkAuth: () => Promise<void>;
changePassword: (oldPwd: string, newPwd: string) => Promise<void>; changePassword: (oldPwd: string, newPwd: string) => Promise<void>;
refreshUser: () => Promise<void>; refreshUser: () => Promise<void>;
setUserCredits: (credits: number) => void;
} }
export const useAuthStore = create<AuthState>((set) => ({ export const useAuthStore = create<AuthState>((set) => ({
@@ -52,4 +53,10 @@ export const useAuthStore = create<AuthState>((set) => ({
console.error('Failed to refresh user:', error); console.error('Failed to refresh user:', error);
} }
}, },
// 用通知接口返回的 credits.balance 直接覆盖当前 user.credits
// 避免再请求一次 getUser 接口
setUserCredits: (credits: number) => {
set((state) => (state.user ? { user: { ...state.user, credits } } : {}));
},
})); }));