合并
This commit is contained in:
BIN
Binary file not shown.
|
After Width: | Height: | Size: 3.3 MiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 3.0 MiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 3.4 MiB |
+183
-32
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Vendored
+2
-2
@@ -28,8 +28,8 @@
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<script type="module" crossorigin src="/assets/index-CEkBiD-W.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-TmZWd4w9.css">
|
||||
<script type="module" crossorigin src="/assets/index-1RNaKjB1.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-xCZbcxht.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -22,6 +22,7 @@ import AuthorizationPage from './pages/AuthorizationPage';
|
||||
import MaterialListPage from './pages/MaterialListPage';
|
||||
import RemoveInfo from './pages/RemoveInfo';
|
||||
import RemoveRw from './pages/RemoveRw';
|
||||
import HomePage from './pages/HomePage';
|
||||
// import RemoveFenbu from './pages/RemoveFenbu';
|
||||
import ConsumePage from './pages/ConsumePage';
|
||||
import AuthorizationWaitingPage from './pages/AuthorizationWaitingPage';
|
||||
@@ -102,6 +103,7 @@ const App = () => {
|
||||
<Route path="order-records" element={<UserCenterPage />} />
|
||||
<Route path="user-center" element={<UserCenterPage />} />
|
||||
<Route path="messages" element={<MessagesPage />} />
|
||||
<Route path="home" element={<HomePage />} />
|
||||
<Route path="conversation" element={<GenerateConver />} />
|
||||
<Route path="initial" element={<InitialReplication />} />
|
||||
<Route path="initial/:creatID/initialinfo" element={<InitialInfo />} />
|
||||
|
||||
@@ -197,7 +197,7 @@ export async function verifySms(phone: string, code: string): Promise<{ token: s
|
||||
return api.post('/sms/verify', { phone, code }, false);
|
||||
}
|
||||
// ── 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) {
|
||||
const items = await mock.mockGetAdminNotifications();
|
||||
return { items, total: items.length };
|
||||
@@ -701,6 +701,12 @@ export async function getResourcesMaterialList(params: ResourcesMaterialListPara
|
||||
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`);
|
||||
}
|
||||
export interface OpenTypeItem {
|
||||
id: string;
|
||||
openType: number;
|
||||
@@ -711,4 +717,5 @@ export interface OpenTypeItem {
|
||||
|
||||
export async function getOpenTypeAll(): Promise<{ data: OpenTypeItem[] }> {
|
||||
return api.get('/open-type/open_type_all');
|
||||
|
||||
}
|
||||
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 |
@@ -531,14 +531,19 @@ const AppLayout: React.FC = () => {
|
||||
flexShrink: 0,
|
||||
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={{
|
||||
width: 42, height: 42, borderRadius: 14, flexShrink: 0,
|
||||
background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 50%, #a78bfa 100%)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
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' }} />
|
||||
) : (
|
||||
|
||||
@@ -24,18 +24,30 @@ const NotificationPopup: React.FC = () => {
|
||||
const [notifications, setNotifications] = useState<Notification[]>([]);
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
const refreshUser = useAuthStore((state) => state.refreshUser);
|
||||
const setUserCredits = useAuthStore((state) => state.setUserCredits);
|
||||
|
||||
const fetchNotifications = useCallback(async () => {
|
||||
try {
|
||||
const result = await getNotifications(1, 20, false);
|
||||
// 接口返回结构:{ credits: { balance: 4317.23 }, items: [...], total: N }
|
||||
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);
|
||||
if (data.length > 0 && !visible) {
|
||||
setVisible(true);
|
||||
setCurrentIndex(0);
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
}, [visible]);
|
||||
}, [visible, setUserCredits]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchNotifications();
|
||||
|
||||
@@ -200,6 +200,30 @@ html, body {
|
||||
.recharge-card .recharge-price { transition: all 0.3s ease; }
|
||||
.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 {
|
||||
transition: all 0.35s cubic-bezier(0.4, 0, 0.2, 1) !important;
|
||||
@@ -440,3 +464,19 @@ html, body {
|
||||
/* Card grid */
|
||||
.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);
|
||||
}
|
||||
|
||||
@@ -1603,14 +1603,15 @@ const AIChatPage: React.FC = () => {
|
||||
}}
|
||||
size="middle"
|
||||
>
|
||||
<Option value="image">
|
||||
<PictureOutlined style={{ marginRight: 6, fontSize: 14 }} />
|
||||
<span style={{ fontSize: 14, fontWeight: 500, color: '#64748b' }}>图片</span>
|
||||
</Option>
|
||||
<Option value="video">
|
||||
<VideoCameraOutlined style={{ marginRight: 6, fontSize: 14 }} />
|
||||
<span style={{ fontSize: 14, fontWeight: 500, color: '#64748b' }}>视频</span>
|
||||
</Option>
|
||||
<Option value="image">
|
||||
<PictureOutlined style={{ marginRight: 6, fontSize: 14 }} />
|
||||
<span style={{ fontSize: 14, fontWeight: 500, color: '#64748b' }}>图片</span>
|
||||
</Option>
|
||||
|
||||
</Select>
|
||||
{/* </div> */}
|
||||
|
||||
|
||||
@@ -247,7 +247,7 @@ const GeneratePage: React.FC = () => {
|
||||
const [lastTextTokens, setLastTextTokens] = useState(0);
|
||||
|
||||
// Media type: 1 for image, 2 for video
|
||||
const [mediaType, setMediaType] = useState<any>("image");
|
||||
const [mediaType, setMediaType] = useState<any>("video");
|
||||
|
||||
// Image parameters
|
||||
const [selectedRatio, setSelectedRatio] = useState<string>("1:1");
|
||||
@@ -1400,7 +1400,14 @@ const GeneratePage: React.FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
margin: '-24px -32px -32px',
|
||||
borderRadius: 20,
|
||||
height: 'calc(100vh - 34px)',
|
||||
padding:'24px'
|
||||
|
||||
}}>
|
||||
{/* Header */}
|
||||
<div
|
||||
className="gen-header animate-fadeInUp"
|
||||
@@ -1531,16 +1538,10 @@ const GeneratePage: React.FC = () => {
|
||||
|
||||
{/* 单选按钮组 */}
|
||||
<Radio.Group
|
||||
defaultValue="image"
|
||||
defaultValue="video"
|
||||
buttonStyle="solid"
|
||||
onChange={handleMediaTypeChange}
|
||||
>
|
||||
{/* 图片选项 */}
|
||||
<Radio.Button value="image" style={{ zIndex: 0 }}>
|
||||
<PictureOutlined style={{ marginRight: 6, fontSize: 14 }} />{" "}
|
||||
{/* 图片图标 */}
|
||||
图片
|
||||
</Radio.Button>
|
||||
{/* 视频选项 */}
|
||||
<Radio.Button value="video">
|
||||
<VideoCameraOutlined
|
||||
@@ -1549,6 +1550,13 @@ const GeneratePage: React.FC = () => {
|
||||
{/* 视频图标 */}
|
||||
视频
|
||||
</Radio.Button>
|
||||
{/* 图片选项 */}
|
||||
<Radio.Button value="image" style={{ zIndex: 0 }}>
|
||||
<PictureOutlined style={{ marginRight: 6, fontSize: 14 }} />{" "}
|
||||
{/* 图片图标 */}
|
||||
图片
|
||||
</Radio.Button>
|
||||
|
||||
</Radio.Group>
|
||||
</div>
|
||||
{mediaType === "video" &&
|
||||
|
||||
@@ -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;
|
||||
@@ -77,7 +77,7 @@ const LoginPage: React.FC = () => {
|
||||
await login(values.phone, values.password, undefined, values.rememberMe);
|
||||
message.success('登录成功,欢迎回来');
|
||||
await checkAuth();
|
||||
navigate('/projects');
|
||||
navigate('/home');
|
||||
} catch (error: any) {
|
||||
const errorMsg = error?.response?.data?.detail || error?.response?.data?.message || error?.message || '登录失败';
|
||||
message.error(errorMsg);
|
||||
@@ -97,7 +97,7 @@ const LoginPage: React.FC = () => {
|
||||
await phonelogin(values.phone, values.code);
|
||||
message.success('登录成功,欢迎回来');
|
||||
await checkAuth();
|
||||
navigate('/projects');
|
||||
navigate('/home');
|
||||
} catch (error: any) {
|
||||
const errorMsg = error?.response?.data?.detail || error?.response?.data?.message || error?.message || '登录失败';
|
||||
message.error(errorMsg);
|
||||
@@ -114,7 +114,7 @@ const LoginPage: React.FC = () => {
|
||||
const user = await register(values.phone, values.regCode, values.password);
|
||||
message.success('注册成功');
|
||||
await checkAuth();
|
||||
navigate('/projects');
|
||||
navigate('/home');
|
||||
} catch (error: any) {
|
||||
const errorMsg = error?.response?.data?.detail || error?.response?.data?.message || error?.message || '注册失败';
|
||||
message.error(errorMsg);
|
||||
|
||||
@@ -97,7 +97,15 @@ const ProjectsPage: React.FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
style={
|
||||
{
|
||||
margin: '-24px -32px -32px',
|
||||
borderRadius: 20,
|
||||
height: 'calc(100vh - 34px)',
|
||||
padding:'24px'
|
||||
}
|
||||
}>
|
||||
{/* Header banner */}
|
||||
<div className="animate-fadeInUp" style={{
|
||||
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)',
|
||||
color: '#fff', fontWeight: 600, backdropFilter: 'blur(10px)', borderRadius: 10, height: 42,
|
||||
}}>新建项目</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>
|
||||
|
||||
{projects.length === 0 ? (
|
||||
|
||||
@@ -65,7 +65,7 @@ export const useAppStore = create<AppState>((set, get) => ({
|
||||
loading: false,
|
||||
|
||||
// 生成配置状态初始值
|
||||
mediaType: 'image',
|
||||
mediaType: 'video',
|
||||
countType: '请选择',
|
||||
selectedRatio: '1:1',
|
||||
selectedResolution: '2K',
|
||||
|
||||
@@ -10,6 +10,7 @@ interface AuthState {
|
||||
checkAuth: () => Promise<void>;
|
||||
changePassword: (oldPwd: string, newPwd: string) => Promise<void>;
|
||||
refreshUser: () => Promise<void>;
|
||||
setUserCredits: (credits: number) => void;
|
||||
}
|
||||
|
||||
export const useAuthStore = create<AuthState>((set) => ({
|
||||
@@ -52,4 +53,10 @@ export const useAuthStore = create<AuthState>((set) => ({
|
||||
console.error('Failed to refresh user:', error);
|
||||
}
|
||||
},
|
||||
|
||||
// 用通知接口返回的 credits.balance 直接覆盖当前 user.credits
|
||||
// 避免再请求一次 getUser 接口
|
||||
setUserCredits: (credits: number) => {
|
||||
set((state) => (state.user ? { user: { ...state.user, credits } } : {}));
|
||||
},
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user