Merge branch 'main' of https://gitee.com/wg123/video-gen
This commit is contained in:
+2
-1
@@ -26,4 +26,5 @@ bak/
|
||||
# 忽略特定类型文件但保留目录
|
||||
# *.pyc
|
||||
# !dir/*.pycnode_modules/
|
||||
*.tmp.*
|
||||
*.tmp.*
|
||||
*_上线.py
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 1.2 MiB |
-442
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+442
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Vendored
+37
-37
@@ -1,37 +1,37 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet" />
|
||||
<title>民众智创</title>
|
||||
<script>
|
||||
(function() {
|
||||
var cached = localStorage.getItem('siteInfo');
|
||||
if (cached) {
|
||||
try {
|
||||
var info = JSON.parse(cached);
|
||||
if (info.siteName) {
|
||||
document.title = info.siteName;
|
||||
}
|
||||
if (info.siteLogo) {
|
||||
var link = document.querySelector('link[rel="icon"]');
|
||||
if (link) {
|
||||
link.href = info.siteLogo;
|
||||
link.type = 'image/png';
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<script type="module" crossorigin src="/assets/index-BZEL1bMy.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-D3IaMBsq.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>␍
|
||||
</body>
|
||||
</html>
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet" />
|
||||
<title>民众智创</title>
|
||||
<script>
|
||||
(function() {
|
||||
var cached = localStorage.getItem('siteInfo');
|
||||
if (cached) {
|
||||
try {
|
||||
var info = JSON.parse(cached);
|
||||
if (info.siteName) {
|
||||
document.title = info.siteName;
|
||||
}
|
||||
if (info.siteLogo) {
|
||||
var link = document.querySelector('link[rel="icon"]');
|
||||
if (link) {
|
||||
link.href = info.siteLogo;
|
||||
link.type = 'image/png';
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<script type="module" crossorigin src="/assets/index-C6cOCSi8.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-BhPFzWLH.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -503,7 +503,6 @@ export async function getDefaultPreTest(): Promise<any> {
|
||||
export async function updateHotOpeningVideoPromptSchema(projectId: string, stepId: string, params: { prompt_schema: Record<string, any> }): Promise<any> {
|
||||
return api.put(`/hot-opening-replications/tasks/${projectId}/steps/${stepId}/video-prompt-schema`, params);
|
||||
}
|
||||
|
||||
// 镜头复刻
|
||||
export async function createShotReplication(params: any): Promise<any> {
|
||||
return api.post('/shot-replications/task-sets', params);
|
||||
@@ -519,68 +518,50 @@ export async function getShotReplicationList(page: number, page_size: number, ke
|
||||
export async function getShotReplicationDetail(taskSetId: string): Promise<any> {
|
||||
return api.get(`/shot-replications/task-sets/${taskSetId}`);
|
||||
}
|
||||
|
||||
// ai拆镜
|
||||
export async function createRemoveLens(taskSetId: string, params: any): Promise<any> {
|
||||
return api.post(`/shot-replications/task-sets/${taskSetId}/split-by-ai`, params);
|
||||
}
|
||||
|
||||
// 手动分割
|
||||
export async function splitCustom(taskSetId: string, params: any): Promise<any> {
|
||||
return api.post(`/shot-replications/task-sets/${taskSetId}/split-custom`, params);
|
||||
}
|
||||
|
||||
// 拆镜列表
|
||||
export async function Removelist(taskSetId: string): Promise<any> {
|
||||
return api.get(`/shot-replications/task-sets/${taskSetId}/segments`);
|
||||
}
|
||||
|
||||
// 生成视频
|
||||
export async function removeCreate(recordId: string, params: any): Promise<any> {
|
||||
return api.post(`/shot-replications/segments/${recordId}/replication-projects`, params);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// 获取爆款开头复刻任务详情
|
||||
export async function removeDetail(id: string): Promise<any> {
|
||||
return api.get(`/shot-replications/projects/${id}`);
|
||||
}
|
||||
|
||||
// 第一步,生成提示词
|
||||
export async function removeone(projectId: string, stepId: string): Promise<any> {
|
||||
return api.post(`/shot-replications/projects/${projectId}/steps/${stepId}/generate-image-prompt`);
|
||||
|
||||
}
|
||||
|
||||
// 修改图片提示词
|
||||
export async function updateShotImagePrompt(projectId: string, stepId: string, params: any): Promise<any> {
|
||||
return api.put(`/shot-replications/projects/${projectId}/steps/${stepId}/image-prompt`, params);
|
||||
}
|
||||
|
||||
// 修改视频提示词
|
||||
export async function updateShotVideoPromptSchema(projectId: string, stepId: string, params: any): Promise<any> {
|
||||
return api.put(`/shot-replications/projects/${projectId}/steps/${stepId}/video-prompt-schema`, params);
|
||||
}
|
||||
|
||||
// 第二步,生成图片
|
||||
export async function removetwo(projectId: string, stepId: string ,params: any): Promise<any> {
|
||||
return api.post(`/shot-replications/projects/${projectId}/steps/${stepId}/generate-image`, params);
|
||||
}
|
||||
|
||||
// 第三步,生成视频提示词
|
||||
export async function removethree(projectId: string, stepId: string ,params: any): Promise<any> {
|
||||
return api.post(`/shot-replications/projects/${projectId}/steps/${stepId}/generate-video-prompt`, params);
|
||||
|
||||
|
||||
}
|
||||
// 第四步,生成视频
|
||||
export async function removefour(projectId: string, stepId: string ,params: any): Promise<any> {
|
||||
return api.post(`/shot-replications/projects/${projectId}/steps/${stepId}/generate-video`, params);
|
||||
|
||||
}
|
||||
|
||||
// 获取地区信息
|
||||
export interface GetAreaParams {
|
||||
level?: string;
|
||||
@@ -592,23 +573,21 @@ export async function getArea(params?: GetAreaParams): Promise<any> {
|
||||
if (params?.parent_code) query.set('parent_code', params.parent_code);
|
||||
return api.get(`/pre-test-template/getArea?${query.toString()}`);
|
||||
}
|
||||
|
||||
// ── Upload Material ───────────────────────────────────────
|
||||
|
||||
export interface AsyncBatchUploadTask {
|
||||
advertiser_ids: string[];
|
||||
resource_ids: string[];
|
||||
oauth_id: string;
|
||||
is_pre_test: string;
|
||||
pre_test_template: string;
|
||||
source_model: string;
|
||||
}
|
||||
|
||||
export interface AsyncBatchUploadParams {
|
||||
tasks: AsyncBatchUploadTask[];
|
||||
}
|
||||
|
||||
export async function asyncBatchUploadMaterial(params: AsyncBatchUploadParams): Promise<any> {
|
||||
return api.post('/upload-material/async-batch-upload', params);
|
||||
}
|
||||
|
||||
// 获取上传历史
|
||||
// /api/upload-material/upload-history
|
||||
export interface UploadHistoryParams {
|
||||
@@ -624,7 +603,6 @@ export async function getUploadHistory(params: UploadHistoryParams): Promise<any
|
||||
const query = searchParams.toString();
|
||||
return api.get(`/upload-material/upload-history${query ? `?${query}` : ''}`);
|
||||
}
|
||||
|
||||
export interface UploadFilenames {
|
||||
source_id: string;
|
||||
file_name: string;
|
||||
@@ -636,7 +614,6 @@ export interface UpdateFilenameParams {
|
||||
export async function updateFilename(params: UpdateFilenameParams): Promise<any> {
|
||||
return api.post('/upload-material/batch-update-filename', params);
|
||||
}
|
||||
|
||||
// 查询素材消耗列表
|
||||
export interface MaterialConsumpListParams {
|
||||
advertiser_id?: string;
|
||||
@@ -652,7 +629,6 @@ export async function getMaterialConsumpList(params: MaterialConsumpListParams):
|
||||
if (params.page_size !== undefined) query.set('page_size', String(params.page_size));
|
||||
return api.get(`/material-consumption/list?${query.toString()}`);
|
||||
}
|
||||
|
||||
// 拉取消耗
|
||||
export async function syncMaterialConsumption(params: { date: string; advertiser_id?: string }): Promise<any> {
|
||||
const query = new URLSearchParams();
|
||||
@@ -660,12 +636,10 @@ export async function syncMaterialConsumption(params: { date: string; advertiser
|
||||
if (params.date) query.set('date', params.date);
|
||||
return api.get(`/material-consumption/sync?${query.toString()}`);
|
||||
}
|
||||
|
||||
// 获取消耗字段列表
|
||||
export async function getMaterialConsumptionFields(): Promise<any> {
|
||||
return api.get('/material-consumption/fields');
|
||||
}
|
||||
|
||||
// ── Contact ────────────────────────────────────────────────
|
||||
export interface ContactRequestParams {
|
||||
phone: string;
|
||||
@@ -674,11 +648,9 @@ export interface ContactRequestParams {
|
||||
name: string;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export async function createContactRequest(params: ContactRequestParams): Promise<any> {
|
||||
return api.post('/contact/request', params);
|
||||
}
|
||||
|
||||
// 查询上传素材列表
|
||||
export interface ResourcesMaterialListParams {
|
||||
advertiser_id?: string;
|
||||
|
||||
@@ -27,6 +27,18 @@ const MOCK_USER: User = {
|
||||
username: 'videomaker',
|
||||
email: 'demo@videogen.ai',
|
||||
credits: 2680,
|
||||
resourceCapacity: {
|
||||
enabled: true,
|
||||
source: 'user',
|
||||
hasUserConfig: true,
|
||||
usedBytes: 298161654,
|
||||
availableBytes: 300349549066,
|
||||
totalBytes: 300647710720,
|
||||
usagePercent: 0.1,
|
||||
exceeded: false,
|
||||
limitValue: '280.000',
|
||||
limitUnit: 'GB',
|
||||
},
|
||||
};
|
||||
|
||||
const MOCK_CREDIT_RECORDS: CreditRecord[] = [
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.2 MiB |
@@ -61,10 +61,146 @@ import {
|
||||
} from '@ant-design/icons';
|
||||
import { Outlet, useNavigate, useLocation } from 'react-router-dom';
|
||||
import { useAuthStore } from '../../store/useAuthStore';
|
||||
import { getMenuConfigs, getRechargePackages, getPaymentMethods, createRechargeOrder, getPaymentOrder, cancelPaymentOrder, getSiteInfo, getUnreadCount, createContactRequest } from '../../api';
|
||||
import { getMenuConfigs, getRechargePackages, getPaymentMethods, createRechargeOrder, getPaymentOrder, cancelPaymentOrder, getSiteInfo, getUnreadCount, createContactRequest, getUser } from '../../api';
|
||||
import NotificationPopup from '../NotificationPopup';
|
||||
import './AppLayout.css';
|
||||
|
||||
// ── ResourceCapacity 类型(与 src/types/index.ts 保持一致) ──
|
||||
type ResourceCapacityData = {
|
||||
enabled: boolean;
|
||||
usedBytes: number;
|
||||
totalBytes: number;
|
||||
availableBytes: number;
|
||||
usagePercent: number;
|
||||
exceeded: boolean;
|
||||
limitValue: string;
|
||||
limitUnit: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* 资源存储展示卡片
|
||||
* - enabled === true:显示「已用 / 总额」+ 进度条(超额时变红并提示)
|
||||
* - enabled === false:按 usedBytes 大小自动选 KB / MB / GB / TB 单位显示「当前使用」
|
||||
*/
|
||||
const StorageCard: React.FC<{ data: ResourceCapacityData | null }> = ({ data }) => {
|
||||
if (!data) return null;
|
||||
|
||||
// 单位:后端返回的 limitUnit(KB / MB / GB)
|
||||
const unit = (data.limitUnit || 'GB').toUpperCase();
|
||||
const divisor = unit === 'GB' ? 1024 ** 3 : unit === 'MB' ? 1024 ** 2 : 1024;
|
||||
|
||||
// 百分比(可能 > 100)
|
||||
const rawPercent = Number(data.usagePercent) || 0;
|
||||
// 进度条宽度最多 100%
|
||||
const barPercent = Math.min(100, Math.max(0, rawPercent));
|
||||
|
||||
// 总额:优先用后端 limitValue,兜底 totalBytes / divisor
|
||||
const total = data.limitValue
|
||||
? parseFloat(data.limitValue)
|
||||
: data.totalBytes / divisor;
|
||||
|
||||
// 已用 = 总额 × 百分比 / 100(保证数字与 percent 自洽)
|
||||
const used = (total * rawPercent) / 100;
|
||||
// 剩余 = 总额 - 已用
|
||||
const available = total - used;
|
||||
|
||||
// 超额判断
|
||||
const isOver = data.exceeded || rawPercent >= 100;
|
||||
|
||||
// enabled === false 时按 usedBytes 自动选单位
|
||||
const KB = 1024;
|
||||
const MB = 1024 ** 2;
|
||||
const GB = 1024 ** 3;
|
||||
const TB = 1024 ** 4;
|
||||
const formatUsedAuto = (bytes: number) => {
|
||||
const b = Number(bytes) || 0;
|
||||
if (b >= TB) return { val: (b / TB).toFixed(2), unit: 'TB' };
|
||||
if (b >= GB) return { val: (b / GB).toFixed(2), unit: 'GB' };
|
||||
if (b >= MB) return { val: (b / MB).toFixed(2), unit: 'MB' };
|
||||
return { val: (b / KB).toFixed(2), unit: 'KB' };
|
||||
};
|
||||
const usedAuto = formatUsedAuto(data.usedBytes);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
marginTop: 12,
|
||||
padding: '10px 12px',
|
||||
borderRadius: 10,
|
||||
background: 'linear-gradient(135deg, rgba(248, 250, 252, 0.9) 0%, rgba(241, 245, 249, 0.9) 100%)',
|
||||
border: `1px solid ${isOver ? 'rgba(239, 68, 68, 0.25)' : 'rgba(99, 102, 241, 0.15)'}`,
|
||||
boxShadow: '0 2px 12px rgba(0, 0, 0, 0.04)',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: 6,
|
||||
fontSize: 12,
|
||||
color: '#475569',
|
||||
}}
|
||||
>
|
||||
<span style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
<DatabaseOutlined style={{ fontSize: 12, color: isOver ? '#ef4444' : '#6366f1' }} />
|
||||
资源存储
|
||||
</span>
|
||||
{data.enabled ? (
|
||||
<span style={{ fontWeight: 500, color: isOver ? '#ef4444' : '#1e293b' }}>
|
||||
{used.toFixed(2)} / {total.toFixed(2)} {unit}
|
||||
</span>
|
||||
) : (
|
||||
<span style={{ fontWeight: 500, color: '#1e293b' }}>
|
||||
当前使用 {usedAuto.val} {usedAuto.unit}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{data.enabled && (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
height: 6,
|
||||
background: '#e2e8f0',
|
||||
borderRadius: 3,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: `${barPercent}%`,
|
||||
height: '100%',
|
||||
background: isOver
|
||||
? 'linear-gradient(90deg, #ef4444, #dc2626)'
|
||||
: 'linear-gradient(90deg, #6366f1, #8b5cf6)',
|
||||
borderRadius: 3,
|
||||
transition: 'width 0.4s ease',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
marginTop: 4,
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
fontSize: 11,
|
||||
color: isOver ? '#ef4444' : '#94a3b8',
|
||||
}}
|
||||
>
|
||||
<span>
|
||||
{isOver
|
||||
? `存储超额 · 超用 ${Math.abs(available).toFixed(2)} ${unit}`
|
||||
: `剩余 ${available.toFixed(2)} ${unit}`}
|
||||
</span>
|
||||
<span>{rawPercent.toFixed(1)}%</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface MenuConfig {
|
||||
id: string;
|
||||
key?: string;
|
||||
@@ -186,6 +322,18 @@ const AppLayout: React.FC = () => {
|
||||
const currentOrderNoRef = useRef<string | null>(null);
|
||||
const [enabledMethods, setEnabledMethods] = useState<{ alipay: boolean; wechat: boolean }>({ alipay: false, wechat: false });
|
||||
|
||||
// 资源存储容量(从 getUser().resource_capacity 获取)
|
||||
const [resourceCapacity, setResourceCapacity] = useState<{
|
||||
enabled: boolean;
|
||||
usedBytes: number;
|
||||
totalBytes: number;
|
||||
availableBytes: number;
|
||||
usagePercent: number;
|
||||
exceeded: boolean;
|
||||
limitValue: string;
|
||||
limitUnit: string;
|
||||
} | null>(null);
|
||||
|
||||
const PENDING_ORDER_KEY = 'pending_payment_order';
|
||||
|
||||
useEffect(() => {
|
||||
@@ -212,6 +360,30 @@ const AppLayout: React.FC = () => {
|
||||
|
||||
localStorage.setItem('siteInfo', JSON.stringify({ siteName: name, siteLogo: logo }));
|
||||
}).catch(() => { });
|
||||
|
||||
getUser().then((res: any) => {
|
||||
// console.log('[Storage] getUser 返回:', res);
|
||||
|
||||
const rc = res?.resourceCapacity;
|
||||
if (rc) {
|
||||
setResourceCapacity({
|
||||
enabled: !!rc.enabled,
|
||||
usedBytes: Number(rc.usedBytes) || 0,
|
||||
totalBytes: Number(rc.totalBytes) || 0,
|
||||
availableBytes: Number(rc.availableBytes) || 0,
|
||||
usagePercent: Number(rc.usagePercent) || 0,
|
||||
exceeded: !!rc.exceeded,
|
||||
limitValue: rc.limitValue ?? '',
|
||||
limitUnit: rc.limitUnit || 'GB',
|
||||
});
|
||||
}
|
||||
// 没数据时不显示
|
||||
}).catch((err: any) => {
|
||||
// console.error('[Storage] getUser 失败:', err);
|
||||
// 接口失败也不显示
|
||||
});
|
||||
|
||||
|
||||
}, []);
|
||||
|
||||
const handleContactMouseDown = (e: React.MouseEvent) => {
|
||||
@@ -674,6 +846,9 @@ const AppLayout: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
</Dropdown>
|
||||
|
||||
{/* 资源存储容量展示(来自 getUser.resourceCapacity) */}
|
||||
<StorageCard data={resourceCapacity} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -768,6 +943,9 @@ const AppLayout: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 资源存储容量展示(与桌面端共用 StorageCard) */}
|
||||
<StorageCard data={resourceCapacity} />
|
||||
|
||||
{topLevelItems.map(item => {
|
||||
const menuType = item.menu_type ?? item.menuType;
|
||||
const hasChildren = childMap[item.id] && childMap[item.id].length > 0;
|
||||
|
||||
+564
-158
@@ -3,20 +3,26 @@
|
||||
:root {
|
||||
--font-sans: 'Outfit', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
--nav-bg: #f8f9fb;
|
||||
--nav-surface: rgba(0,0,0,0.02);
|
||||
--nav-surface: rgba(0, 0, 0, 0.02);
|
||||
--nav-border: #e5e7eb;
|
||||
--nav-text: #1f2937;
|
||||
--nav-text-muted: #6b7280;
|
||||
--nav-hover: rgba(0,0,0,0.04);
|
||||
--nav-active: rgba(99,102,241,0.08);
|
||||
--nav-hover: rgba(0, 0, 0, 0.04);
|
||||
--nav-active: rgba(99, 102, 241, 0.08);
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after { box-sizing: border-box; }
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
html, body {
|
||||
margin: 0; padding: 0;
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: var(--font-sans);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
@@ -24,23 +30,68 @@ html, body {
|
||||
color: #1a1a2e;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
#root { min-height: 100vh; }
|
||||
|
||||
|
||||
#root {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* ── Scrollbar ─────────────────────────── */
|
||||
::-webkit-scrollbar { width: 5px; height: 5px; }
|
||||
::-webkit-scrollbar-track { background: transparent; }
|
||||
::-webkit-scrollbar-thumb { background: #d1d5db; border-radius: 3px; }
|
||||
::-webkit-scrollbar-thumb:hover { background: #9ca3af; }
|
||||
::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #d1d5db;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #9ca3af;
|
||||
}
|
||||
|
||||
/* ── Electric Border Animation ─────────── */
|
||||
@keyframes electric-pulse {
|
||||
0%, 100% { opacity: 0.4; filter: blur(1px); }
|
||||
50% { opacity: 1; filter: blur(0); }
|
||||
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.4;
|
||||
filter: blur(1px);
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 1;
|
||||
filter: blur(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes electric-flow {
|
||||
0% { background-position: 0% 50%; }
|
||||
50% { background-position: 100% 50%; }
|
||||
100% { background-position: 0% 50%; }
|
||||
0% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.content_box {
|
||||
margin: -24px -32px -32px;
|
||||
border-radius: 0 0 16px 16px;
|
||||
height: calc(100vh - 34px);
|
||||
overflow: auto;
|
||||
scrollbar-width: none;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.electric-border-card {
|
||||
@@ -51,16 +102,14 @@ html, body {
|
||||
position: absolute;
|
||||
inset: -2px;
|
||||
border-radius: 16px;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
#6366f1,
|
||||
#8b5cf6,
|
||||
#a855f7,
|
||||
#6366f1,
|
||||
#8b5cf6,
|
||||
#a855f7,
|
||||
#6366f1
|
||||
);
|
||||
background: linear-gradient(90deg,
|
||||
#6366f1,
|
||||
#8b5cf6,
|
||||
#a855f7,
|
||||
#6366f1,
|
||||
#8b5cf6,
|
||||
#a855f7,
|
||||
#6366f1);
|
||||
background-size: 300% 100%;
|
||||
animation: electric-flow 3s linear infinite, electric-pulse 2s ease-in-out infinite;
|
||||
z-index: 0;
|
||||
@@ -85,6 +134,7 @@ html, body {
|
||||
0% {
|
||||
background-position: -200% center;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-position: 200% center;
|
||||
}
|
||||
@@ -97,16 +147,14 @@ html, body {
|
||||
.shiny-text {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
#6366f1 0%,
|
||||
#8b5cf6 20%,
|
||||
#a855f7 40%,
|
||||
#c084fc 50%,
|
||||
#a855f7 60%,
|
||||
#8b5cf6 80%,
|
||||
#6366f1 100%
|
||||
);
|
||||
background: linear-gradient(90deg,
|
||||
#6366f1 0%,
|
||||
#8b5cf6 20%,
|
||||
#a855f7 40%,
|
||||
#c084fc 50%,
|
||||
#a855f7 60%,
|
||||
#8b5cf6 80%,
|
||||
#6366f1 100%);
|
||||
background-size: 200% auto;
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
@@ -118,92 +166,225 @@ html, body {
|
||||
|
||||
/* ── Keyframe Animations ───────────────── */
|
||||
@keyframes fadeInUp {
|
||||
from { opacity: 0; transform: translateY(20px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInLeft {
|
||||
from { opacity: 0; transform: translateX(-20px); }
|
||||
to { opacity: 1; transform: translateX(0); }
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-20px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInRight {
|
||||
from { opacity: 0; transform: translateX(20px); }
|
||||
to { opacity: 1; transform: translateX(0); }
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(20px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes float1 {
|
||||
0%, 100% { transform: translate(0, 0) scale(1); }
|
||||
50% { transform: translate(20px, -30px) scale(1.05); }
|
||||
|
||||
0%,
|
||||
100% {
|
||||
transform: translate(0, 0) scale(1);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translate(20px, -30px) scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes float2 {
|
||||
0%, 100% { transform: translate(0, 0) scale(1); }
|
||||
50% { transform: translate(-15px, 20px) scale(0.95); }
|
||||
|
||||
0%,
|
||||
100% {
|
||||
transform: translate(0, 0) scale(1);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translate(-15px, 20px) scale(0.95);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes float3 {
|
||||
0%, 100% { transform: translate(0, 0) rotate(0deg); }
|
||||
50% { transform: translate(10px, -15px) rotate(3deg); }
|
||||
|
||||
0%,
|
||||
100% {
|
||||
transform: translate(0, 0) rotate(0deg);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translate(10px, -15px) rotate(3deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideInCard {
|
||||
from { opacity: 0; transform: translateY(14px) scale(0.97); }
|
||||
to { opacity: 1; transform: translateY(0) scale(1); }
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(14px) scale(0.97);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spinSlow {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-fadeInUp { animation: fadeInUp 0.45s ease-out both; }
|
||||
.animate-fadeInLeft { animation: fadeInLeft 0.55s ease-out both; }
|
||||
.animate-fadeInRight { animation: fadeInRight 0.55s ease-out both; }
|
||||
.animate-slideInCard { animation: slideInCard 0.4s ease-out both; }
|
||||
.animate-float1 { animation: float1 8s ease-in-out infinite; }
|
||||
.animate-float2 { animation: float2 10s ease-in-out infinite; }
|
||||
.animate-float3 { animation: float3 12s ease-in-out infinite; }
|
||||
.animate-fadeInUp {
|
||||
animation: fadeInUp 0.45s ease-out both;
|
||||
}
|
||||
|
||||
.ref-thumb:hover .ref-delete { opacity: 1 !important; }
|
||||
.animate-fadeInLeft {
|
||||
animation: fadeInLeft 0.55s ease-out both;
|
||||
}
|
||||
|
||||
.stagger-children > *:nth-child(1) { animation-delay: 0.04s; }
|
||||
.stagger-children > *:nth-child(2) { animation-delay: 0.10s; }
|
||||
.stagger-children > *:nth-child(3) { animation-delay: 0.16s; }
|
||||
.stagger-children > *:nth-child(4) { animation-delay: 0.22s; }
|
||||
.stagger-children > *:nth-child(5) { animation-delay: 0.28s; }
|
||||
.stagger-children > *:nth-child(6) { animation-delay: 0.34s; }
|
||||
.animate-fadeInRight {
|
||||
animation: fadeInRight 0.55s ease-out both;
|
||||
}
|
||||
|
||||
.animate-slideInCard {
|
||||
animation: slideInCard 0.4s ease-out both;
|
||||
}
|
||||
|
||||
.animate-float1 {
|
||||
animation: float1 8s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.animate-float2 {
|
||||
animation: float2 10s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.animate-float3 {
|
||||
animation: float3 12s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.ref-thumb:hover .ref-delete {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.stagger-children>*:nth-child(1) {
|
||||
animation-delay: 0.04s;
|
||||
}
|
||||
|
||||
.stagger-children>*:nth-child(2) {
|
||||
animation-delay: 0.10s;
|
||||
}
|
||||
|
||||
.stagger-children>*:nth-child(3) {
|
||||
animation-delay: 0.16s;
|
||||
}
|
||||
|
||||
.stagger-children>*:nth-child(4) {
|
||||
animation-delay: 0.22s;
|
||||
}
|
||||
|
||||
.stagger-children>*:nth-child(5) {
|
||||
animation-delay: 0.28s;
|
||||
}
|
||||
|
||||
.stagger-children>*:nth-child(6) {
|
||||
animation-delay: 0.34s;
|
||||
}
|
||||
|
||||
/* ── Ant Design Overrides ──────────────── */
|
||||
.ant-input, .ant-input-affix-wrapper, .ant-select-selector, .ant-input-number {
|
||||
.ant-input,
|
||||
.ant-input-affix-wrapper,
|
||||
.ant-select-selector,
|
||||
.ant-input-number {
|
||||
border-radius: 10px !important;
|
||||
transition: all 0.25s ease !important;
|
||||
}
|
||||
.ant-input:focus, .ant-input-affix-wrapper:focus, .ant-input-affix-wrapper-focused,
|
||||
|
||||
.ant-input:focus,
|
||||
.ant-input-affix-wrapper:focus,
|
||||
.ant-input-affix-wrapper-focused,
|
||||
.ant-select-focused .ant-select-selector {
|
||||
box-shadow: 0 0 0 3px rgba(99,102,241,0.12) !important;
|
||||
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.12) !important;
|
||||
border-color: #6366f1 !important;
|
||||
}
|
||||
|
||||
.ant-btn {
|
||||
border-radius: 10px !important;
|
||||
transition: all 0.25s ease !important;
|
||||
font-weight: 500 !important;
|
||||
}
|
||||
.ant-btn:hover { transform: none; }
|
||||
.ant-btn:active { transform: translateY(0); }
|
||||
.ant-card { border-radius: 16px !important; transition: all 0.3s ease !important; }
|
||||
.ant-modal-content { border-radius: 20px !important; overflow: hidden; }
|
||||
.ant-tag { font-size: 12px; border-radius: 8px !important; }
|
||||
|
||||
.ant-btn:hover {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.ant-btn:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.ant-card {
|
||||
border-radius: 16px !important;
|
||||
transition: all 0.3s ease !important;
|
||||
}
|
||||
|
||||
.ant-modal-content {
|
||||
border-radius: 20px !important;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ant-tag {
|
||||
font-size: 12px;
|
||||
border-radius: 8px !important;
|
||||
}
|
||||
|
||||
/* ── Recharge card hover ───────────────── */
|
||||
.recharge-card {
|
||||
transition: all 0.35s cubic-bezier(0.4, 0, 0.2, 1) !important;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.recharge-card:hover {
|
||||
transform: translateY(-8px) scale(1.02) !important;
|
||||
box-shadow: 0 20px 40px rgba(99,102,241,0.15) !important;
|
||||
box-shadow: 0 20px 40px rgba(99, 102, 241, 0.15) !important;
|
||||
}
|
||||
|
||||
.recharge-card .recharge-price {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.recharge-card:hover .recharge-price {
|
||||
transform: scale(1.08);
|
||||
}
|
||||
.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;
|
||||
@@ -211,13 +392,16 @@ html, body {
|
||||
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;
|
||||
@@ -230,108 +414,249 @@ html, body {
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.project-card:hover {
|
||||
transform: translateY(-4px) !important;
|
||||
box-shadow: 0 16px 40px rgba(99,102,241,0.12) !important;
|
||||
box-shadow: 0 16px 40px rgba(99, 102, 241, 0.12) !important;
|
||||
border-color: #6366f1 !important;
|
||||
}
|
||||
|
||||
/* ── Mobile Bottom Nav ─────────────────── */
|
||||
.mobile-bottom-nav {
|
||||
display: none;
|
||||
position: fixed; bottom: 0; left: 0; right: 0; z-index: 200;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 200;
|
||||
height: 60px;
|
||||
background: rgba(255,255,255,0.95);
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(24px);
|
||||
border-top: 1px solid rgba(0,0,0,0.05);
|
||||
box-shadow: 0 -4px 20px rgba(0,0,0,0.06);
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.05);
|
||||
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.mobile-bottom-nav .nav-item {
|
||||
flex: 1; display: flex; flex-direction: column;
|
||||
align-items: center; justify-content: center; gap: 2px;
|
||||
cursor: pointer; transition: all 0.2s;
|
||||
color: #8b8fa3; font-size: 9px; font-weight: 400;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 2px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
color: #8b8fa3;
|
||||
font-size: 9px;
|
||||
font-weight: 400;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.mobile-bottom-nav .nav-item.active {
|
||||
color: #08080c;
|
||||
}
|
||||
|
||||
.mobile-bottom-nav .nav-item .nav-icon {
|
||||
font-size: 18px; transition: transform 0.2s;
|
||||
font-size: 18px;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.mobile-bottom-nav .nav-item.active .nav-icon {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
/* ── Responsive Breakpoints ────────────── */
|
||||
@media (max-width: 768px) {
|
||||
.desktop-sidebar { display: none !important; }
|
||||
.desktop-content { margin-left: 0 !important; padding: 16px !important; padding-bottom: 80px !important; overflow-x: hidden !important; }
|
||||
.desktop-sidebar-toggle-zone { display: none !important; }
|
||||
.mobile-bottom-nav { display: flex !important; }
|
||||
.desktop-sidebar {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.desktop-content {
|
||||
margin-left: 0 !important;
|
||||
padding: 16px !important;
|
||||
padding-bottom: 80px !important;
|
||||
overflow-x: hidden !important;
|
||||
}
|
||||
|
||||
.desktop-sidebar-toggle-zone {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.mobile-bottom-nav {
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
/* Prevent horizontal overflow globally */
|
||||
html, body { overflow-x: hidden !important; }
|
||||
html,
|
||||
body {
|
||||
overflow-x: hidden !important;
|
||||
}
|
||||
|
||||
.mobile-hide { display: none !important; }
|
||||
.mobile-full { width: 100% !important; max-width: 100% !important; }
|
||||
.mobile-stack { flex-direction: column !important; }
|
||||
.mobile-p-16 { padding: 16px !important; }
|
||||
.mobile-gap-12 { gap: 12px !important; }
|
||||
.mobile-hide {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.mobile-full {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
.mobile-stack {
|
||||
flex-direction: column !important;
|
||||
}
|
||||
|
||||
.mobile-p-16 {
|
||||
padding: 16px !important;
|
||||
}
|
||||
|
||||
.mobile-gap-12 {
|
||||
gap: 12px !important;
|
||||
}
|
||||
|
||||
/* Login page */
|
||||
.login-page { flex-direction: column !important; }
|
||||
.login-left { display: none !important; }
|
||||
.login-right { padding: 24px 16px !important; }
|
||||
.login-card { width: 100% !important; }
|
||||
.login-page {
|
||||
flex-direction: column !important;
|
||||
}
|
||||
|
||||
.login-left {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.login-right {
|
||||
padding: 24px 16px !important;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
/* Project list */
|
||||
.project-item { flex-direction: column !important; align-items: flex-start !important; }
|
||||
.project-item .project-actions { margin-left: 0 !important; margin-top: 12px; width: 100%; justify-content: flex-end; }
|
||||
.project-item {
|
||||
flex-direction: column !important;
|
||||
align-items: flex-start !important;
|
||||
}
|
||||
|
||||
.project-item .project-actions {
|
||||
margin-left: 0 !important;
|
||||
margin-top: 12px;
|
||||
width: 100%;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/* Generate page header */
|
||||
.gen-header { flex-direction: column !important; gap: 12px !important; padding: 16px 18px !important; }
|
||||
.gen-header {
|
||||
flex-direction: column !important;
|
||||
gap: 12px !important;
|
||||
padding: 16px 18px !important;
|
||||
}
|
||||
|
||||
/* Operation flow bar */
|
||||
.gen-flow-bar { flex-direction: column !important; gap: 14px !important; padding: 16px !important; }
|
||||
.gen-flow-divider { display: none !important; }
|
||||
.gen-flow-bar {
|
||||
flex-direction: column !important;
|
||||
gap: 14px !important;
|
||||
padding: 16px !important;
|
||||
}
|
||||
|
||||
.gen-flow-divider {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Config form params row */
|
||||
.gen-form-row { flex-direction: column !important; gap: 0 !important; }
|
||||
.gen-form-row {
|
||||
flex-direction: column !important;
|
||||
gap: 0 !important;
|
||||
}
|
||||
|
||||
/* Credits estimate bar */
|
||||
.gen-credits-bar { flex-direction: column !important; gap: 8px !important; text-align: center; }
|
||||
.gen-credits-bar {
|
||||
flex-direction: column !important;
|
||||
gap: 8px !important;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Prompt rows */
|
||||
.gen-prompt-row { flex-direction: column !important; }
|
||||
.gen-prompt-row > * { width: 100% !important; }
|
||||
.gen-params { flex-wrap: wrap !important; gap: 8px !important; }
|
||||
.gen-params > div { flex: 1 1 45% !important; min-width: 0; }
|
||||
.gen-prompt-row {
|
||||
flex-direction: column !important;
|
||||
}
|
||||
|
||||
.gen-prompt-row>* {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.gen-params {
|
||||
flex-wrap: wrap !important;
|
||||
gap: 8px !important;
|
||||
}
|
||||
|
||||
.gen-params>div {
|
||||
flex: 1 1 45% !important;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* Expanded detail left+right stacks */
|
||||
.gen-detail-row { flex-direction: column !important; }
|
||||
.gen-detail-row > * { width: 100% !important; min-width: 0 !important; flex: 1 1 auto !important; }
|
||||
.gen-detail-row {
|
||||
flex-direction: column !important;
|
||||
}
|
||||
|
||||
.gen-detail-row>* {
|
||||
width: 100% !important;
|
||||
min-width: 0 !important;
|
||||
flex: 1 1 auto !important;
|
||||
}
|
||||
|
||||
/* Record filter bar stacks */
|
||||
.record-filter-bar { flex-direction: column !important; }
|
||||
.record-filter-bar .ant-space { flex-wrap: wrap !important; }
|
||||
.record-filter-bar .ant-select { width: 100% !important; }
|
||||
.record-filter-bar {
|
||||
flex-direction: column !important;
|
||||
}
|
||||
|
||||
.record-filter-bar .ant-space {
|
||||
flex-wrap: wrap !important;
|
||||
}
|
||||
|
||||
.record-filter-bar .ant-select {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
/* Record summary row stacks */
|
||||
.record-item { flex-direction: column !important; align-items: flex-start !important; gap: 8px !important; overflow: hidden !important; }
|
||||
.record-item .record-actions { width: 100% !important; justify-content: flex-end; }
|
||||
.record-item > span[style*="textOverflow"] { white-space: normal !important; overflow: visible !important; text-overflow: unset !important; }
|
||||
.record-item {
|
||||
flex-direction: column !important;
|
||||
align-items: flex-start !important;
|
||||
gap: 8px !important;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
.record-item .record-actions {
|
||||
width: 100% !important;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.record-item>span[style*="textOverflow"] {
|
||||
white-space: normal !important;
|
||||
overflow: visible !important;
|
||||
text-overflow: unset !important;
|
||||
}
|
||||
|
||||
/* Hide less important meta on mobile */
|
||||
.mobile-meta { display: none !important; }
|
||||
.mobile-meta {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Credits page */
|
||||
.credits-balance { flex-direction: column !important; }
|
||||
.recharge-grid { flex-direction: column !important; }
|
||||
.recharge-grid > * { width: 100% !important; }
|
||||
.credits-balance {
|
||||
flex-direction: column !important;
|
||||
}
|
||||
|
||||
.recharge-grid {
|
||||
flex-direction: column !important;
|
||||
}
|
||||
|
||||
.recharge-grid>* {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
/* Video player fits mobile */
|
||||
video { max-width: 100% !important; }
|
||||
video {
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
/* InitialReplication page */
|
||||
.replication-container {
|
||||
@@ -340,17 +665,20 @@ html, body {
|
||||
padding: 16px !important;
|
||||
height: calc(100vh - 64px) !important;
|
||||
}
|
||||
|
||||
.replication-preview {
|
||||
width: 100% !important;
|
||||
height: 30vh !important;
|
||||
border-radius: 12px !important;
|
||||
}
|
||||
|
||||
.replication-form {
|
||||
width: 100% !important;
|
||||
height: auto !important;
|
||||
max-height: calc(70vh - 32px) !important;
|
||||
border-radius: 12px !important;
|
||||
}
|
||||
|
||||
.replication-form-content {
|
||||
padding: 16px !important;
|
||||
max-height: calc(70vh - 64px) !important;
|
||||
@@ -358,56 +686,92 @@ html, body {
|
||||
}
|
||||
|
||||
/* ── Date Display ──────────────────────── */
|
||||
.date-display { font-family: 'SF Mono', 'Menlo', 'Consolas', monospace; }
|
||||
.date-display {
|
||||
font-family: 'SF Mono', 'Menlo', 'Consolas', monospace;
|
||||
}
|
||||
|
||||
/* ── Small Mobile Breakpoint (max-width: 480px) ────────────── */
|
||||
@media (max-width: 480px) {
|
||||
|
||||
/* Base styles */
|
||||
html, body { font-size: 14px !important; }
|
||||
html,
|
||||
body {
|
||||
font-size: 14px !important;
|
||||
}
|
||||
|
||||
/* Login page */
|
||||
.login-card { width: 100% !important; }
|
||||
.login-right { padding: 16px !important; }
|
||||
.login-card {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.login-right {
|
||||
padding: 16px !important;
|
||||
}
|
||||
|
||||
/* Header adjustments */
|
||||
.gen-header { padding: 12px 14px !important; }
|
||||
.gen-header {
|
||||
padding: 12px 14px !important;
|
||||
}
|
||||
|
||||
/* Form elements */
|
||||
.gen-form-row { gap: 8px !important; }
|
||||
|
||||
.gen-form-row {
|
||||
gap: 8px !important;
|
||||
}
|
||||
|
||||
/* Button adjustments */
|
||||
.ant-btn { height: 36px !important; font-size: 13px !important; padding: 0 12px !important; }
|
||||
|
||||
.ant-btn {
|
||||
height: 36px !important;
|
||||
font-size: 13px !important;
|
||||
padding: 0 12px !important;
|
||||
}
|
||||
|
||||
/* Input adjustments */
|
||||
.ant-input, .ant-input-affix-wrapper, .ant-select-selector {
|
||||
height: 36px !important;
|
||||
.ant-input,
|
||||
.ant-input-affix-wrapper,
|
||||
.ant-select-selector {
|
||||
height: 36px !important;
|
||||
font-size: 13px !important;
|
||||
}
|
||||
|
||||
/* Card padding */
|
||||
.ant-card { padding: 12px !important; }
|
||||
.ant-card {
|
||||
padding: 12px !important;
|
||||
}
|
||||
|
||||
/* Mobile bottom nav adjustments */
|
||||
.mobile-bottom-nav { height: 56px !important; }
|
||||
.mobile-bottom-nav .nav-item .nav-icon { font-size: 18px !important; }
|
||||
.mobile-bottom-nav .nav-item { font-size: 9px !important; gap: 2px !important; }
|
||||
.mobile-bottom-nav {
|
||||
height: 56px !important;
|
||||
}
|
||||
|
||||
.mobile-bottom-nav .nav-item .nav-icon {
|
||||
font-size: 18px !important;
|
||||
}
|
||||
|
||||
.mobile-bottom-nav .nav-item {
|
||||
font-size: 9px !important;
|
||||
gap: 2px !important;
|
||||
}
|
||||
|
||||
/* InitialReplication page - Small Mobile */
|
||||
.replication-container {
|
||||
padding: 12px !important;
|
||||
gap: 12px !important;
|
||||
}
|
||||
|
||||
.replication-preview {
|
||||
height: 25vh !important;
|
||||
border-radius: 10px !important;
|
||||
}
|
||||
|
||||
.replication-form-content {
|
||||
padding: 12px !important;
|
||||
}
|
||||
|
||||
.replication-form-content .ant-btn {
|
||||
height: 36px !important;
|
||||
font-size: 13px !important;
|
||||
}
|
||||
|
||||
.replication-form-content .ant-input,
|
||||
.replication-form-content .ant-input-affix-wrapper {
|
||||
height: 36px !important;
|
||||
@@ -415,30 +779,56 @@ html, body {
|
||||
}
|
||||
|
||||
/* Video and image containers */
|
||||
video, img {
|
||||
max-height: 160px !important;
|
||||
video,
|
||||
img {
|
||||
max-height: 160px !important;
|
||||
border-radius: 6px !important;
|
||||
}
|
||||
|
||||
/* Upload areas */
|
||||
.upload-area { padding: 12px !important; }
|
||||
.upload-area {
|
||||
padding: 12px !important;
|
||||
}
|
||||
|
||||
/* Spacing adjustments */
|
||||
.mobile-p-16 { padding: 12px !important; }
|
||||
.mobile-gap-12 { gap: 8px !important; }
|
||||
.mobile-p-16 {
|
||||
padding: 12px !important;
|
||||
}
|
||||
|
||||
.mobile-gap-12 {
|
||||
gap: 8px !important;
|
||||
}
|
||||
|
||||
/* Font size adjustments */
|
||||
h1 { font-size: 18px !important; }
|
||||
h2 { font-size: 16px !important; }
|
||||
h3 { font-size: 14px !important; }
|
||||
p { font-size: 12px !important; }
|
||||
h1 {
|
||||
font-size: 18px !important;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 14px !important;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 12px !important;
|
||||
}
|
||||
|
||||
/* Button text */
|
||||
.btn-text-sm { font-size: 12px !important; }
|
||||
.btn-text-sm {
|
||||
font-size: 12px !important;
|
||||
}
|
||||
|
||||
/* Margin adjustments */
|
||||
.mobile-mb-8 { margin-bottom: 8px !important; }
|
||||
.mobile-mb-12 { margin-bottom: 12px !important; }
|
||||
.mobile-mb-8 {
|
||||
margin-bottom: 8px !important;
|
||||
}
|
||||
|
||||
.mobile-mb-12 {
|
||||
margin-bottom: 12px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Tablet Breakpoint (769px to 1024px) ────────────── */
|
||||
@@ -450,33 +840,49 @@ html, body {
|
||||
*/
|
||||
|
||||
/* Card max-width */
|
||||
.ant-card { max-width: calc(50% - 8px) !important; }
|
||||
.ant-card {
|
||||
max-width: calc(50% - 8px) !important;
|
||||
}
|
||||
|
||||
/* Font adjustments */
|
||||
html, body { font-size: 15px !important; }
|
||||
html,
|
||||
body {
|
||||
font-size: 15px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Large Desktop Breakpoint (min-width: 1200px) ────────────── */
|
||||
@media (min-width: 1200px) {
|
||||
|
||||
/* Max width container */
|
||||
.max-w-container { max-width: 1400px; margin: 0 auto; }
|
||||
.max-w-container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* 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-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);
|
||||
}
|
||||
}
|
||||
@@ -2,15 +2,7 @@ import React, { useEffect, useState } from 'react';
|
||||
import { Button, Table, Tag, Modal, Select, App, Input, Pagination, Typography, Space } from 'antd';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { PlusOutlined, LockOutlined } from '@ant-design/icons';
|
||||
import { getOAuthList, requestOAuth, getOpenTypeAll } from '../api';
|
||||
|
||||
const PORT_TYPE_MAP: Record<number, string> = {
|
||||
1: '巨量',
|
||||
2: '磁力',
|
||||
3: '巨量星图',
|
||||
4: '服务单',
|
||||
5: '腾讯',
|
||||
};
|
||||
import { getOAuthList, requestOAuth, getOpenTypeAll, getDefaultPreTest } from '../api';
|
||||
|
||||
const formatDateTime = (dateStr: string) => {
|
||||
if (!dateStr) return '';
|
||||
@@ -23,7 +15,6 @@ const formatDateTime = (dateStr: string) => {
|
||||
const seconds = String(date.getSeconds()).padStart(2, '0');
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
||||
};
|
||||
|
||||
// 安全拼接URL,避免双斜杠
|
||||
const buildUrl = (path: string): string => {
|
||||
const baseUrl = import.meta.env.VITE_API_BASE || "http://localhost:8000";
|
||||
@@ -37,7 +28,6 @@ const buildUrl = (path: string): string => {
|
||||
const cleanBase = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
|
||||
return `${cleanBase}/${cleanPath}`;
|
||||
};
|
||||
|
||||
interface AuthorizationData {
|
||||
id: string;
|
||||
status: string;
|
||||
@@ -79,8 +69,17 @@ const AuthorizationPage: React.FC = () => {
|
||||
useEffect(() => {
|
||||
loadOAuthList();
|
||||
loadOpenTypeList();
|
||||
list();
|
||||
}, []);
|
||||
|
||||
|
||||
const list = async () => {
|
||||
try {
|
||||
const res = await getDefaultPreTest();
|
||||
console.log(res);
|
||||
} catch (error) {
|
||||
}
|
||||
};
|
||||
const loadOpenTypeList = async () => {
|
||||
try {
|
||||
const res = await getOpenTypeAll();
|
||||
|
||||
@@ -8,127 +8,326 @@ import {
|
||||
PlayCircleOutlined,
|
||||
CheckCircleOutlined,
|
||||
HeartOutlined,
|
||||
EyeOutlined,
|
||||
ArrowRightOutlined,
|
||||
CrownOutlined,
|
||||
} from '@ant-design/icons';
|
||||
|
||||
const CreativePlazaPage: React.FC = () => {
|
||||
const handleUnlock = () => {
|
||||
message.info('请联系客服开通');
|
||||
message.info('请联系客服开通会员');
|
||||
};
|
||||
|
||||
|
||||
// 顶部特性卡片
|
||||
const features = [
|
||||
{
|
||||
icon: <ThunderboltOutlined style={{ fontSize: 28, color: '#8b5cf6' }} />,
|
||||
title: 'AI智能生成',
|
||||
description: '先进的AI技术,一键生成高质量视频内容',
|
||||
icon: <ThunderboltOutlined style={{ fontSize: 26, color: '#8b5cf6' }} />,
|
||||
title: 'AI 智能生成',
|
||||
description: '先进 AI 技术一键产出高质量视频内容',
|
||||
},
|
||||
{
|
||||
icon: <PlayCircleOutlined style={{ fontSize: 28, color: '#ec4899' }} />,
|
||||
icon: <PlayCircleOutlined style={{ fontSize: 26, color: '#ec4899' }} />,
|
||||
title: '多格式输出',
|
||||
description: '支持多种视频格式,满足不同场景需求',
|
||||
description: '支持多种视频格式,满足不同场景需求',
|
||||
},
|
||||
{
|
||||
icon: <CheckCircleOutlined style={{ fontSize: 28, color: '#10b981' }} />,
|
||||
icon: <CheckCircleOutlined style={{ fontSize: 26, color: '#10b981' }} />,
|
||||
title: '品质保证',
|
||||
description: '专业级画质,细节清晰,色彩鲜艳',
|
||||
description: '专业级画质,细节清晰,色彩鲜艳',
|
||||
},
|
||||
{
|
||||
icon: <HeartOutlined style={{ fontSize: 28, color: '#f59e0b' }} />,
|
||||
icon: <HeartOutlined style={{ fontSize: 26, color: '#f59e0b' }} />,
|
||||
title: '创意无限',
|
||||
description: '丰富的模板和风格,激发创作灵感',
|
||||
description: '丰富模板与风格,激发创作灵感',
|
||||
},
|
||||
];
|
||||
|
||||
// 精选案例占位
|
||||
const caseList = [
|
||||
{ title: '美妆直播切片', tag: '热门', tagColor: '#ef4444' },
|
||||
{ title: '3C 数码测评', tag: '推荐', tagColor: '#6366f1' },
|
||||
{ title: '美食探店 vlog', tag: '推荐', tagColor: '#6366f1' },
|
||||
{ title: '服饰穿搭合集', tag: '热门', tagColor: '#ef4444' },
|
||||
{ title: '家居场景展示', tag: '推荐', tagColor: '#6366f1' },
|
||||
{ title: '母婴亲子内容', tag: '热门', tagColor: '#ef4444' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={{ padding: '20px 0' }}>
|
||||
<div style={{ marginBottom: 32 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 12 }}>
|
||||
<div style={{
|
||||
width: 48,
|
||||
height: 48,
|
||||
borderRadius: 14,
|
||||
background: 'linear-gradient(135deg, #8b5cf6 0%, #6366f1 100%)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
boxShadow: '0 4px 16px rgba(139,92,246,0.3)',
|
||||
}}>
|
||||
<GiftOutlined style={{ fontSize: 22, color: '#fff' }} />
|
||||
<div className="content_box">
|
||||
{/* 顶部 Hero 区域 */}
|
||||
<div
|
||||
className="animate-fadeInUp"
|
||||
style={{
|
||||
padding: '28px 32px',
|
||||
borderRadius: 16,
|
||||
background: 'linear-gradient(135deg, #f5f3ff 0%, #ede9fe 50%, #fce7f3 100%)',
|
||||
border: '1px solid rgba(139, 92, 246, 0.12)',
|
||||
marginBottom: 24,
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 14, marginBottom: 12 }}>
|
||||
<div
|
||||
style={{
|
||||
width: 52,
|
||||
height: 52,
|
||||
borderRadius: 14,
|
||||
background: 'linear-gradient(135deg, #8b5cf6 0%, #6366f1 100%)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
boxShadow: '0 6px 20px rgba(139, 92, 246, 0.3)',
|
||||
}}
|
||||
>
|
||||
<GiftOutlined style={{ fontSize: 24, color: '#fff' }} />
|
||||
</div>
|
||||
<div>
|
||||
<Typography.Title level={2} style={{ margin: 0, color: '#1e293b', fontWeight: 700 }}>
|
||||
创意素材案例
|
||||
<span style={{ color: '#6366f1', fontSize: 14, fontWeight: 400, marginLeft: 8 }}>
|
||||
探索AI创作无限可能
|
||||
<div className="shiny-text-container" style={{ marginBottom: 4 }}>
|
||||
<span className="shiny-text" style={{ fontSize: 26, fontWeight: 700 }}>
|
||||
创意素材案例
|
||||
</span>
|
||||
</Typography.Title>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 13, color: '#7c3aed' }}>
|
||||
<CrownOutlined style={{ fontSize: 12 }} />
|
||||
<span>探索 AI 创作无限可能</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Typography.Text style={{ color: '#64748b' }}>
|
||||
精选AI生成的优秀素材案例,展示AI创作的无限潜力与创意灵感
|
||||
<Typography.Text style={{ color: '#64748b', fontSize: 14, lineHeight: 1.7 }}>
|
||||
精选 AI 生成的优秀素材案例,展示 AI 创作的无限潜力与创意灵感
|
||||
</Typography.Text>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 20, marginBottom: 32 }}>
|
||||
{/* 特性卡片 */}
|
||||
{/* <div
|
||||
className="animate-fadeInUp stagger-children"
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(4, 1fr)',
|
||||
gap: 16,
|
||||
marginBottom: 24,
|
||||
}}
|
||||
>
|
||||
{features.map((feature, index) => (
|
||||
<Card
|
||||
key={index}
|
||||
bordered={false}
|
||||
hoverable
|
||||
className="project-card"
|
||||
style={{
|
||||
borderRadius: 16,
|
||||
background: 'linear-gradient(135deg, rgba(139,92,246,0.04) 0%, rgba(99,102,241,0.04) 100%)',
|
||||
border: '1px solid rgba(139,92,246,0.1)',
|
||||
padding: 20,
|
||||
background: '#fff',
|
||||
border: '1px solid #e2e8f0',
|
||||
padding: '4px 0',
|
||||
}}
|
||||
styles={{ body: { padding: 20 } }}
|
||||
>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<div style={{ marginBottom: 16 }}>{feature.icon}</div>
|
||||
<Typography.Title level={4} style={{ marginBottom: 8, color: '#1e293b' }}>
|
||||
<div style={{ textAlign: 'left' }}>
|
||||
<div
|
||||
style={{
|
||||
width: 48,
|
||||
height: 48,
|
||||
borderRadius: 12,
|
||||
background: 'linear-gradient(135deg, rgba(139, 92, 246, 0.08), rgba(99, 102, 241, 0.08))',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginBottom: 14,
|
||||
}}
|
||||
>
|
||||
{feature.icon}
|
||||
</div>
|
||||
<div style={{ fontSize: 15, fontWeight: 600, color: '#1e293b', marginBottom: 6 }}>
|
||||
{feature.title}
|
||||
</Typography.Title>
|
||||
<Typography.Text style={{ color: '#64748b', fontSize: 13 }}>
|
||||
</div>
|
||||
<div style={{ fontSize: 13, color: '#64748b', lineHeight: 1.6 }}>
|
||||
{feature.description}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 20 }}>
|
||||
<Typography.Title level={4} style={{ margin: 0, color: '#1e293b' }}>
|
||||
<StarOutlined style={{ marginRight: 8, color: '#f59e0b' }} />
|
||||
精选案例展示
|
||||
</Typography.Title>
|
||||
<Button
|
||||
type="primary"
|
||||
size="middle"
|
||||
onClick={handleUnlock}
|
||||
style={{
|
||||
borderRadius: 10,
|
||||
background: 'linear-gradient(135deg, #8b5cf6 0%, #a78bfa 100%)',
|
||||
border: 'none',
|
||||
}}
|
||||
>
|
||||
<LockOutlined style={{ marginRight: 6 }} />
|
||||
解锁全部案例
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Card
|
||||
bordered={false}
|
||||
{/* 精选案例展示区域 */}
|
||||
<div
|
||||
className="animate-fadeInUp"
|
||||
style={{
|
||||
padding: '24px 28px',
|
||||
borderRadius: 16,
|
||||
background: '#fafafa',
|
||||
padding: 40,
|
||||
textAlign: 'center',
|
||||
background: '#fff',
|
||||
// border: '1px solid #e2e8f0',
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
<div style={{ color: '#94a3b8', fontSize: 14 }}>
|
||||
敬请期待更多创意案例
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: 20,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<div
|
||||
style={{
|
||||
width: 4,
|
||||
height: 18,
|
||||
background: 'linear-gradient(180deg, #6366f1, #8b5cf6)',
|
||||
borderRadius: 2,
|
||||
marginRight: 10,
|
||||
}}
|
||||
/>
|
||||
<StarOutlined style={{ marginRight: 8, color: '#f59e0b', fontSize: 16 }} />
|
||||
<span style={{ fontSize: 17, fontWeight: 600, color: '#1f2937' }}>精选案例展示</span>
|
||||
|
||||
</div>
|
||||
{/* <Button
|
||||
type="primary"
|
||||
size="middle"
|
||||
onClick={handleUnlock}
|
||||
style={{
|
||||
borderRadius: 10,
|
||||
fontWeight: 500,
|
||||
background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)',
|
||||
border: 'none',
|
||||
boxShadow: '0 4px 14px rgba(99, 102, 241, 0.28)',
|
||||
}}
|
||||
>
|
||||
<LockOutlined style={{ marginRight: 6 }} />
|
||||
解锁全部案例
|
||||
<ArrowRightOutlined style={{ marginLeft: 4, fontSize: 12 }} />
|
||||
</Button> */}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* 案例网格 */}
|
||||
<div
|
||||
className="stagger-children"
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(3, 1fr)',
|
||||
gap: 16,
|
||||
marginBottom: 16,
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
zIndex: 1,
|
||||
background: 'rgb(255,255,255)',
|
||||
// background:
|
||||
// 'linear-gradient(180deg, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.55) 100%)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'column',
|
||||
borderRadius: 14,
|
||||
// boxShadow: '0px 4px 16px rgba(0, 0, 0, 0.08)',
|
||||
gap: 4,
|
||||
color: '#fff',
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
type="primary"
|
||||
size="middle"
|
||||
onClick={handleUnlock}
|
||||
style={{
|
||||
borderRadius: 10,
|
||||
fontWeight: 500,
|
||||
background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)',
|
||||
border: 'none',
|
||||
boxShadow: '0 4px 14px rgba(99, 102, 241, 0.28)',
|
||||
}}
|
||||
>
|
||||
<LockOutlined style={{ marginRight: 6 }} />
|
||||
解锁全部案例
|
||||
<ArrowRightOutlined style={{ marginLeft: 4, fontSize: 12 }} />
|
||||
</Button>
|
||||
{/* <LockOutlined style={{ fontSize: 22, color: '#94a3b8', cursor: 'pointer' }}
|
||||
onClick={handleUnlock}
|
||||
/>
|
||||
<span style={{ fontSize: 16, color: '#1f2937', opacity: 0.9 }}>会员可查看</span> */}
|
||||
</div>
|
||||
|
||||
{caseList.map((item, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="project-card animate-fadeInUp"
|
||||
style={{
|
||||
position: 'relative',
|
||||
borderRadius: 14,
|
||||
overflow: 'hidden',
|
||||
cursor: 'pointer',
|
||||
border: '1px solid #e2e8f0',
|
||||
background: '#fff',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: 'relative',
|
||||
aspectRatio: '16/9',
|
||||
background:
|
||||
'linear-gradient(135deg, #1e1b4b 0%, #312e81 50%, #4c1d95 100%)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
{/* 模糊视频缩略图占位 */}
|
||||
<PlayCircleOutlined style={{ fontSize: 36, color: 'rgba(255,255,255,0.6)' }} />
|
||||
{/* 标签 */}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 10,
|
||||
left: 10,
|
||||
padding: '2px 8px',
|
||||
borderRadius: 6,
|
||||
background: item.tagColor,
|
||||
color: '#fff',
|
||||
fontSize: 11,
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
{item.tag}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
padding: '10px 14px',
|
||||
background: '#f8fafc',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
fontSize: 13,
|
||||
color: '#334155',
|
||||
fontWeight: 500,
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
}}
|
||||
>
|
||||
{item.title}
|
||||
</span>
|
||||
<EyeOutlined style={{ fontSize: 13, color: '#94a3b8' }} />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{/* 锁定遮罩 */}
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreativePlazaPage;
|
||||
export default CreativePlazaPage;
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
|
||||
import bg1 from '../assets/bg1.png';
|
||||
import bg2 from '../assets/bg2.png';
|
||||
import bg3 from '../assets/bg3.png';
|
||||
import text from '../assets/testb.png';
|
||||
|
||||
|
||||
@@ -44,6 +45,7 @@ import {
|
||||
SettingOutlined,
|
||||
LayoutOutlined,
|
||||
ArrowUpOutlined,
|
||||
DownloadOutlined,
|
||||
|
||||
} from '@ant-design/icons';
|
||||
|
||||
@@ -111,11 +113,7 @@ const AIChatPage: React.FC = () => {
|
||||
const [conversations, setConversations] = useState<Conversation[]>([
|
||||
]);
|
||||
|
||||
|
||||
|
||||
const [inputValue, setInputValue] = useState<string>('');
|
||||
|
||||
// 从 store 读取生成配置状态
|
||||
// 从 store 读取生成配置状态(包括输入框内容)
|
||||
const {
|
||||
mediaType,
|
||||
countType,
|
||||
@@ -128,6 +126,7 @@ const AIChatPage: React.FC = () => {
|
||||
videoResolution,
|
||||
engineOptions,
|
||||
enginesele,
|
||||
inputValue,
|
||||
setMediaType,
|
||||
setCountType,
|
||||
setSelectedRatio,
|
||||
@@ -139,6 +138,7 @@ const AIChatPage: React.FC = () => {
|
||||
setVideoResolution,
|
||||
setEngineOptions,
|
||||
setEnginesele,
|
||||
setInputValue,
|
||||
} = useAppStore();
|
||||
|
||||
const [uploading, setUploading] = useState<boolean>(false);
|
||||
@@ -857,7 +857,7 @@ const AIChatPage: React.FC = () => {
|
||||
borderRadius: 20,
|
||||
height: 'calc(100vh - 34px)',
|
||||
background: 'linear-gradient(135deg, #f8fafc 0%, #f0f4ff 50%, #faf5ff 100%)',
|
||||
backgroundImage: `url(${bg2})`,
|
||||
backgroundImage: `url(${bg3})`,
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundSize: '100% 100%',
|
||||
backgroundPosition: 'center',
|
||||
@@ -2223,6 +2223,7 @@ const AIChatPage: React.FC = () => {
|
||||
<span style={{ fontSize: 12, color: '#9ca3af' }}>秒</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/* 选择分辨率 */}
|
||||
@@ -2298,46 +2299,30 @@ const AIChatPage: React.FC = () => {
|
||||
{/* 图片/视频预览弹窗 */}
|
||||
<Modal
|
||||
open={previewVisible}
|
||||
onCancel={handleClosePreview}
|
||||
footer={[
|
||||
<Button key="download" type="primary" onClick={handleDownload} style={{ background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', borderRadius: 10, border: 'none' }}>
|
||||
下载
|
||||
</Button>,
|
||||
]}
|
||||
width={800}
|
||||
centered
|
||||
closeIcon={
|
||||
<button
|
||||
onClick={handleClosePreview}
|
||||
style={{
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: '50%',
|
||||
border: 'none',
|
||||
background: 'rgba(99, 102, 241, 0.1)',
|
||||
cursor: 'pointer',
|
||||
fontSize: 16,
|
||||
color: '#6366f1',
|
||||
fontWeight: 'bold',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
transition: 'all 0.2s',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.background = 'rgba(239, 68, 68, 0.1)';
|
||||
e.currentTarget.style.color = '#ef4444';
|
||||
e.currentTarget.style.transform = 'scale(1.1)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.background = 'rgba(99, 102, 241, 0.1)';
|
||||
e.currentTarget.style.color = '#6366f1';
|
||||
e.currentTarget.style.transform = 'scale(1)';
|
||||
}}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
title={
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||
<div style={{ width: 4, height: 20, background: 'linear-gradient(180deg, #6366f1 0%, #8b5cf6 100%)', borderRadius: 2 }} />
|
||||
<span style={{ fontSize: 16, fontWeight: 700, background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent', backgroundClip: 'text' }}>
|
||||
预览
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
onCancel={handleClosePreview}
|
||||
width={800}
|
||||
footer={
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 12, padding: '16px 24px', background: 'rgba(255,255,255,0.6)', borderTop: '1px solid rgba(99, 102, 241, 0.08)' }}>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<DownloadOutlined />}
|
||||
onClick={handleDownload}
|
||||
style={{ borderRadius: 8, background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', border: 'none', boxShadow: '0 4px 12px rgba(99, 102, 241, 0.3)' }}
|
||||
disabled={isMediaExpired(previewUrl)}
|
||||
>
|
||||
{isMediaExpired(previewUrl) ? '资源已过期' : '下载'}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
centered
|
||||
style={{ borderRadius: 16 }}
|
||||
styles={{
|
||||
body: {
|
||||
@@ -2346,21 +2331,13 @@ const AIChatPage: React.FC = () => {
|
||||
justifyContent: 'center',
|
||||
minHeight: '400px',
|
||||
},
|
||||
header: { background: 'rgba(255,255,255,0.6)', backdropFilter: 'blur(10px)', borderBottom: '1px solid rgba(99, 102, 241, 0.08)', padding: '16px 24px' },
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', width: '100%', height: '100%' }}>
|
||||
{isMediaExpired(previewUrl) ? (
|
||||
<div style={{ textAlign: 'center', padding: '40px' }}>
|
||||
{/* <div style={{ fontSize: 48, marginBottom: 16 }}>⚠️</div> */}
|
||||
<p style={{ fontSize: 16, color: '#ff4d4f', marginBottom: 16 }}>图片/视频资源已过期,请刷新重新加载~</p>
|
||||
{/* <Button
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
window.location.reload();
|
||||
}}
|
||||
>
|
||||
刷新页面重新加载
|
||||
</Button> */}
|
||||
</div>
|
||||
) : previewType === 'image' ? (
|
||||
<img
|
||||
@@ -2382,54 +2359,37 @@ const AIChatPage: React.FC = () => {
|
||||
{/* 附件预览弹窗(独立弹窗) */}
|
||||
<Modal
|
||||
open={attachmentPreviewVisible}
|
||||
onCancel={() => setAttachmentPreviewVisible(false)}
|
||||
footer={[
|
||||
<Button key="download" type="primary" onClick={() => {
|
||||
if (!attachmentPreviewUrl) return;
|
||||
const link = document.createElement('a');
|
||||
link.href = `${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${attachmentPreviewUrl}&download=1`;
|
||||
link.download = attachmentPreviewName || (attachmentPreviewType === 'image' ? 'image.png' : 'video.mp4');
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}} style={{ background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', borderRadius: 10, border: 'none' }}>
|
||||
下载
|
||||
</Button>,
|
||||
]}
|
||||
width={800}
|
||||
centered
|
||||
closeIcon={
|
||||
<button
|
||||
onClick={() => setAttachmentPreviewVisible(false)}
|
||||
style={{
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: '50%',
|
||||
border: 'none',
|
||||
background: 'rgba(99, 102, 241, 0.1)',
|
||||
cursor: 'pointer',
|
||||
fontSize: 16,
|
||||
color: '#6366f1',
|
||||
fontWeight: 'bold',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
transition: 'all 0.2s',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.background = 'rgba(239, 68, 68, 0.1)';
|
||||
e.currentTarget.style.color = '#ef4444';
|
||||
e.currentTarget.style.transform = 'scale(1.1)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.background = 'rgba(99, 102, 241, 0.1)';
|
||||
e.currentTarget.style.color = '#6366f1';
|
||||
e.currentTarget.style.transform = 'scale(1)';
|
||||
}}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
title={
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||
<div style={{ width: 4, height: 20, background: 'linear-gradient(180deg, #6366f1 0%, #8b5cf6 100%)', borderRadius: 2 }} />
|
||||
<span style={{ fontSize: 16, fontWeight: 700, background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent', backgroundClip: 'text' }}>
|
||||
附件预览
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
onCancel={() => setAttachmentPreviewVisible(false)}
|
||||
width={800}
|
||||
footer={
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 12, padding: '16px 24px', background: 'rgba(255,255,255,0.6)', borderTop: '1px solid rgba(99, 102, 241, 0.08)' }}>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<DownloadOutlined />}
|
||||
onClick={() => {
|
||||
if (!attachmentPreviewUrl) return;
|
||||
const link = document.createElement('a');
|
||||
link.href = `${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${attachmentPreviewUrl}&download=1`;
|
||||
link.download = attachmentPreviewName || (attachmentPreviewType === 'image' ? 'image.png' : 'video.mp4');
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}}
|
||||
style={{ borderRadius: 8, background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', border: 'none', boxShadow: '0 4px 12px rgba(99, 102, 241, 0.3)' }}
|
||||
>
|
||||
下载
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
centered
|
||||
style={{ borderRadius: 16 }}
|
||||
styles={{
|
||||
body: {
|
||||
@@ -2438,6 +2398,7 @@ const AIChatPage: React.FC = () => {
|
||||
justifyContent: 'center',
|
||||
minHeight: '400px',
|
||||
},
|
||||
header: { background: 'rgba(255,255,255,0.6)', backdropFilter: 'blur(10px)', borderBottom: '1px solid rgba(99, 102, 241, 0.08)', padding: '16px 24px' },
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', width: '100%', height: '100%' }}>
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
PlusOutlined,
|
||||
PictureOutlined,
|
||||
SwapOutlined,
|
||||
XOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { useAppStore } from "../store/useAppStore";
|
||||
@@ -4583,7 +4584,7 @@ const GeneratePage: React.FC = () => {
|
||||
e.currentTarget.style.background = "rgba(0,0,0,0.6)";
|
||||
}}
|
||||
>
|
||||
<CloseCircleOutlined style={{ color: "#fff", fontSize: 16 }} />
|
||||
<XOutlined style={{ color: "#fff", fontSize: 16 }} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useState, useLayoutEffect, useRef, useCallback } from 'react';
|
||||
import { Button, Empty, Input, Select, Space, Typography, Tag, message, Modal, Table, DatePicker, Tabs, Transfer } from 'antd';
|
||||
import { Button, Empty, Input, Select, Space, Typography, Tag, message, Modal, Table, DatePicker, Tabs, Transfer, Tooltip } from 'antd';
|
||||
import dayjs from 'dayjs';
|
||||
import JSZip from 'jszip';
|
||||
import {
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
UploadOutlined,
|
||||
|
||||
} from '@ant-design/icons';
|
||||
import { gethistory, gethistoryItems, getOAuthList, asyncBatchUploadMaterial, updateFilename, getUploadHistory, getAllOAuthAccountList, getOpenTypeAll } from '../api';
|
||||
import { gethistory, gethistoryItems, getOAuthList, asyncBatchUploadMaterial, updateFilename, getUploadHistory, getAllOAuthAccountList, getOpenTypeAll, getPreTestList } from '../api';
|
||||
|
||||
const { Search } = Input;
|
||||
const { Text } = Typography;
|
||||
@@ -68,6 +68,11 @@ const GeneratedRecord: React.FC = () => {
|
||||
const [selectedHistoryAccounts, setSelectedHistoryAccounts] = useState<any[]>([]);
|
||||
const [openTypeMap, setOpenTypeMap] = useState<Record<number, string>>({});
|
||||
|
||||
const [isPreTest, setIsPreTest] = useState<string>('2');
|
||||
const [preTestTemplate, setPreTestTemplate] = useState<string>('');
|
||||
const [preTestTemplates, setPreTestTemplates] = useState<any[]>([]);
|
||||
const [preTestTemplatesLoading, setPreTestTemplatesLoading] = useState(false);
|
||||
|
||||
// 推送任务历史弹窗相关状态
|
||||
const [uploadHistoryModalVisible, setUploadHistoryModalVisible] = useState(false);
|
||||
const [uploadHistoryList, setUploadHistoryList] = useState<any[]>([]);
|
||||
@@ -211,6 +216,7 @@ const GeneratedRecord: React.FC = () => {
|
||||
const [isError, setIsError] = useState(false);
|
||||
const [isExpired, setIsExpired] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const placeholderRef = useRef<HTMLDivElement>(null);
|
||||
const mediaRef = useRef<HTMLImageElement | HTMLVideoElement>(null);
|
||||
const errorTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
@@ -294,12 +300,14 @@ const GeneratedRecord: React.FC = () => {
|
||||
}}
|
||||
onClick={!isSelectionMode ? onClick : undefined}
|
||||
onMouseEnter={(e) => {
|
||||
setIsHovered(true);
|
||||
if (!isSelectionMode) {
|
||||
(e.currentTarget as HTMLElement).style.transform = 'scale(1.05)';
|
||||
(e.currentTarget as HTMLElement).style.boxShadow = '0 4px 16px rgba(0,0,0,0.2)';
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
setIsHovered(false);
|
||||
if (!isSelectionMode) {
|
||||
(e.currentTarget as HTMLElement).style.transform = 'scale(1)';
|
||||
(e.currentTarget as HTMLElement).style.boxShadow = '0 2px 8px rgba(0,0,0,0.1)';
|
||||
@@ -479,6 +487,56 @@ const GeneratedRecord: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 下载按钮 - hover时显示 */}
|
||||
{!isSelectionMode && !isExpired && !isError && isLoaded && (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 8,
|
||||
right: 8,
|
||||
zIndex: 10,
|
||||
opacity: isHovered ? 1 : 0,
|
||||
transition: 'opacity 0.2s',
|
||||
pointerEvents: isHovered ? 'auto' : 'none',
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
const url = `${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${item.videoUrl || item.imageUrl}&download=1`;
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = '';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}}
|
||||
>
|
||||
<Tooltip title="下载">
|
||||
<div
|
||||
style={{
|
||||
width: 28,
|
||||
height: 28,
|
||||
borderRadius: '50%',
|
||||
background: 'rgba(0,0,0,0.5)',
|
||||
backdropFilter: 'blur(4px)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
cursor: 'pointer',
|
||||
transition: 'background 0.15s',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.background = 'rgba(0,0,0,0.7)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.background = 'rgba(0,0,0,0.5)';
|
||||
}}
|
||||
>
|
||||
<DownloadOutlined style={{ color: '#fff', fontSize: 14 }} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 选择模式下的选择框 */}
|
||||
{isSelectionMode && (
|
||||
<div
|
||||
@@ -669,6 +727,18 @@ const GeneratedRecord: React.FC = () => {
|
||||
message.destroy('downloadProgress');
|
||||
message.success(`下载完成,共 ${successCount} 个文件`);
|
||||
};
|
||||
const loadPreTestTemplates = async () => {
|
||||
setPreTestTemplatesLoading(true);
|
||||
try {
|
||||
const res = await getPreTestList({ page: 1, pageSize: 100 });
|
||||
setPreTestTemplates(res.data?.data || res.data || []);
|
||||
} catch (error) {
|
||||
console.error('加载前测模板失败:', error);
|
||||
} finally {
|
||||
setPreTestTemplatesLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBatchUploadSelected = () => {
|
||||
if (selectedItems.size === 0) {
|
||||
message.warning('请先选择要推送的媒体');
|
||||
@@ -677,6 +747,9 @@ const GeneratedRecord: React.FC = () => {
|
||||
setAccountIdLists([[]]);
|
||||
setAccountIdInputs(['']);
|
||||
setSelectedOauthItems([undefined]);
|
||||
setIsPreTest('2');
|
||||
setPreTestTemplate('');
|
||||
loadPreTestTemplates();
|
||||
setUploadConfigModalVisible(true);
|
||||
};
|
||||
const handleSinglePushToMedia = () => {
|
||||
@@ -688,6 +761,9 @@ const GeneratedRecord: React.FC = () => {
|
||||
setSelectedOauthItems([undefined]);
|
||||
setMaterialFileNames(new Map());
|
||||
setUnifiedFileName('');
|
||||
setIsPreTest('2');
|
||||
setPreTestTemplate('');
|
||||
loadPreTestTemplates();
|
||||
setUploadConfigModalVisible(true);
|
||||
};
|
||||
const loadOAuthList = async (page: number, pageSize: number) => {
|
||||
@@ -779,6 +855,8 @@ const GeneratedRecord: React.FC = () => {
|
||||
let tasks: {
|
||||
advertiser_ids: string[];
|
||||
resource_ids: string[];
|
||||
is_pre_test: string;
|
||||
pre_test_template: string;
|
||||
oauth_id: string;
|
||||
source_model: string;
|
||||
}[] = [];
|
||||
@@ -815,6 +893,8 @@ const GeneratedRecord: React.FC = () => {
|
||||
resource_ids: resourceIds,
|
||||
oauth_id: oauthItem.value,
|
||||
source_model: sourceModel,
|
||||
is_pre_test: isPreTest,
|
||||
pre_test_template: preTestTemplate,
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -844,6 +924,8 @@ const GeneratedRecord: React.FC = () => {
|
||||
resource_ids: resourceIds,
|
||||
oauth_id: String(account.oauthId),
|
||||
source_model: sourceModel,
|
||||
is_pre_test: isPreTest,
|
||||
pre_test_template: preTestTemplate,
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -868,6 +950,8 @@ const GeneratedRecord: React.FC = () => {
|
||||
setSelectedHistoryAccounts([]);
|
||||
setMaterialFileNames(new Map());
|
||||
setUnifiedFileName('');
|
||||
setIsPreTest('2');
|
||||
setPreTestTemplate('');
|
||||
} catch (error: any) {
|
||||
console.error('批量推送失败:', error);
|
||||
message.error(error.message || '批量推送失败');
|
||||
@@ -1202,7 +1286,6 @@ const GeneratedRecord: React.FC = () => {
|
||||
}}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||
<Typography.Text style={{ color: '#94a3b8', fontSize: 14 }}>媒体类型:</Typography.Text>
|
||||
|
||||
<Space>
|
||||
<Button
|
||||
type={filterMedia === 'video' ? 'primary' : 'default'}
|
||||
@@ -1512,7 +1595,41 @@ const GeneratedRecord: React.FC = () => {
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div style={{ display: 'flex', gap: 16, marginBottom: 16 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<Typography.Text style={{ fontSize: 14, color: '#475569' }}>是否开启前测:</Typography.Text>
|
||||
<Select
|
||||
value={isPreTest}
|
||||
onChange={(value) => {
|
||||
setIsPreTest(value);
|
||||
if (value !== '1') {
|
||||
setPreTestTemplate('');
|
||||
}
|
||||
}}
|
||||
style={{ width: 120 }}
|
||||
options={[
|
||||
{ value: '1', label: '是' },
|
||||
{ value: '2', label: '否' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<Typography.Text style={{ fontSize: 14, color: '#475569' }}>前测模板:</Typography.Text>
|
||||
<Select
|
||||
value={preTestTemplate}
|
||||
onChange={(value) => setPreTestTemplate(value)}
|
||||
style={{ width: 200 }}
|
||||
disabled={isPreTest !== '1'}
|
||||
loading={preTestTemplatesLoading}
|
||||
placeholder={isPreTest !== '1' ? '请先开启前测' : '选择前测模板'}
|
||||
options={preTestTemplates.map((item) => ({
|
||||
value: item.id,
|
||||
label: item.name,
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Tabs
|
||||
size="small"
|
||||
activeKey={accountTab}
|
||||
@@ -1533,7 +1650,6 @@ const GeneratedRecord: React.FC = () => {
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
{accountTab === 'new' && (
|
||||
<div>
|
||||
{selectedOauthItems.map((oauthItem, index) => (
|
||||
@@ -1728,7 +1844,6 @@ const GeneratedRecord: React.FC = () => {
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{accountTab === 'history' && (
|
||||
<div style={{ padding: 8 }}>
|
||||
{historyOAuthLoading ? (
|
||||
@@ -1968,77 +2083,67 @@ const GeneratedRecord: React.FC = () => {
|
||||
|
||||
{/* 预览弹窗 */}
|
||||
{previewVisible && previewItem && (
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
background: 'rgba(0,0,0,0.85)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
zIndex: 1000,
|
||||
padding: 16,
|
||||
boxSizing: 'border-box',
|
||||
overflow: 'auto',
|
||||
}}
|
||||
onClick={handleClosePreview}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
background: '#fff',
|
||||
borderRadius: 16,
|
||||
padding: 0,
|
||||
width: '100%',
|
||||
maxWidth: '1200px',
|
||||
maxHeight: '95vh',
|
||||
minHeight: '300px',
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
boxShadow: '0 20px 60px rgba(0,0,0,0.3)',
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* 头部 */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
padding: '12px 16px',
|
||||
borderBottom: '1px solid #f0f0f0',
|
||||
flexShrink: 0,
|
||||
}}>
|
||||
<Typography.Title level={5} style={{ margin: 0, color: '#1a1a2e', fontSize: 16 }}>
|
||||
<Modal
|
||||
title={
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||
<div style={{ width: 4, height: 20, background: 'linear-gradient(180deg, #6366f1 0%, #8b5cf6 100%)', borderRadius: 2 }} />
|
||||
<span style={{ fontSize: 16, fontWeight: 700, background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent', backgroundClip: 'text' }}>
|
||||
{previewItem.title || '预览'}
|
||||
</Typography.Title>
|
||||
<Button
|
||||
icon={<XOutlined />}
|
||||
onClick={handleClosePreview}
|
||||
style={{
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
color: '#94a3b8',
|
||||
fontSize: 16,
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 内容区域 */}
|
||||
<div style={{
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
height: '500px',
|
||||
gap: 20,
|
||||
padding: 20,
|
||||
overflow: 'auto',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
}}>
|
||||
}
|
||||
open={previewVisible}
|
||||
onCancel={handleClosePreview}
|
||||
width={1200}
|
||||
footer={
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 12, padding: '16px 24px', background: 'rgba(255,255,255,0.6)', borderTop: '1px solid rgba(99, 102, 241, 0.08)' }}>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<DownloadOutlined />}
|
||||
onClick={() => {
|
||||
if (videoRef.current) {
|
||||
videoRef.current.pause();
|
||||
}
|
||||
const url = `${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${previewItem.videoUrl || previewItem.imageUrl}&download=1`;
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = '';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}}
|
||||
style={{ borderRadius: 8, background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', border: 'none', boxShadow: '0 4px 12px rgba(99, 102, 241, 0.3)' }}
|
||||
disabled={isMediaExpired(previewItem.videoUrl || previewItem.imageUrl)}
|
||||
>
|
||||
{isMediaExpired(previewItem.videoUrl || previewItem.imageUrl) ? '资源已过期' : '下载'}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSinglePushToMedia}
|
||||
style={{ borderRadius: 8 }}
|
||||
disabled={isMediaExpired(previewItem.videoUrl || previewItem.imageUrl)}
|
||||
>
|
||||
推送媒体后台
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
style={{ borderRadius: 20 }}
|
||||
styles={{
|
||||
body: { height: 460, display: 'flex', flexDirection: 'column', padding: 0 },
|
||||
header: { background: 'rgba(255,255,255,0.6)', backdropFilter: 'blur(10px)', borderBottom: '1px solid rgba(99, 102, 241, 0.08)', padding: '16px 24px' },
|
||||
}}
|
||||
>
|
||||
{/* 内容区域 */}
|
||||
<div style={{
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
height: '460px',
|
||||
gap: 20,
|
||||
padding: 20,
|
||||
overflow: 'auto',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
}}>
|
||||
{/* 媒体预览 */}
|
||||
<div style={{
|
||||
flex: 1,
|
||||
@@ -2252,47 +2357,9 @@ const GeneratedRecord: React.FC = () => {
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
{/* 操作按钮 */}
|
||||
<div >
|
||||
<div style={{ display: 'flex', gap: 12, marginTop: 20 }}>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<DownloadOutlined />}
|
||||
onClick={() => {
|
||||
// 暂停视频
|
||||
if (videoRef.current) {
|
||||
videoRef.current.pause();
|
||||
}
|
||||
const url = `${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${previewItem.videoUrl || previewItem.imageUrl}&download=1`;
|
||||
window.open(url, '_blank');
|
||||
}}
|
||||
style={{ flex: 1, borderRadius: 8 }}
|
||||
disabled={isMediaExpired(previewItem.videoUrl || previewItem.imageUrl)}
|
||||
>
|
||||
{isMediaExpired(previewItem.videoUrl || previewItem.imageUrl) ? '资源已过期' : '下载'}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleClosePreview}
|
||||
style={{ flex: 1, borderRadius: 8 }}
|
||||
>
|
||||
关闭
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={handleSinglePushToMedia}
|
||||
style={{ width: '100%', borderRadius: 8, marginTop: 20 }}
|
||||
disabled={isMediaExpired(previewItem.videoUrl || previewItem.imageUrl)}
|
||||
>
|
||||
推送媒体后台
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -45,7 +45,6 @@ const HomePage: React.FC = () => {
|
||||
try {
|
||||
|
||||
getmedit(5).then((res) => {
|
||||
console.log('getmedit:', res);
|
||||
for (let item in res) {
|
||||
res[item].forEach(element => {
|
||||
// 给每条 element 标记它所属的模块(item 是接口返回的 key)
|
||||
@@ -56,13 +55,11 @@ const HomePage: React.FC = () => {
|
||||
}
|
||||
// 最终再打印一次汇总(state 异步更新,这里打印的是本次追加的累计长度)
|
||||
setMockVideos(prev => {
|
||||
console.log('mockVideos 最终条数:', prev.length);
|
||||
return prev;
|
||||
});
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
console.error('HomePage fetch error:', err);
|
||||
}
|
||||
};
|
||||
fetchAll();
|
||||
@@ -72,7 +69,7 @@ const HomePage: React.FC = () => {
|
||||
setActiveTab(key);
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
const aiEntries = [
|
||||
{
|
||||
@@ -120,21 +117,43 @@ const HomePage: React.FC = () => {
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
margin: '-24px -32px -32px',
|
||||
borderRadius: 20,
|
||||
height: 'calc(100vh - 34px)',
|
||||
overflow: 'auto',
|
||||
scrollbarWidth: 'none',
|
||||
padding: '0 28px 32px',
|
||||
}}>
|
||||
<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: '24px 0 20px',
|
||||
margin: '0 0 20px',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
}}>
|
||||
@@ -151,7 +170,7 @@ const HomePage: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div >
|
||||
{/* <div >
|
||||
<span style={{
|
||||
cursor: 'pointer',
|
||||
fontSize: 12, color: '#1a50bbff', letterSpacing: 0.3,
|
||||
@@ -159,11 +178,11 @@ const HomePage: React.FC = () => {
|
||||
onClick={() => {
|
||||
navigate('/authorization')
|
||||
}}
|
||||
>一键授权
|
||||
>如需进行账户素材推送 一键推送
|
||||
<ArrowRightOutlined style={{ marginLeft: 8, transform: 'rotate(0deg)' }} />
|
||||
|
||||
</span>
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
</div>
|
||||
|
||||
@@ -553,7 +572,7 @@ const HomePage: React.FC = () => {
|
||||
近期作品
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
{/* Tab切换 */}
|
||||
<div style={{ marginBottom: 20 }}>
|
||||
@@ -679,17 +698,8 @@ const HomePage: React.FC = () => {
|
||||
<div style={{
|
||||
padding: '10px 12px',
|
||||
background: '#f8fafc',
|
||||
paddingTop: 0,
|
||||
}}>
|
||||
{/* <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',
|
||||
@@ -739,7 +749,7 @@ const HomePage: React.FC = () => {
|
||||
|
||||
|
||||
{/* ========== 素材案例区域 ========== */}
|
||||
<div className="animate-fadeInUp" style={{
|
||||
{/* <div className="animate-fadeInUp" style={{
|
||||
padding: '24px 28px',
|
||||
borderRadius: 16,
|
||||
background: '#fff',
|
||||
@@ -800,7 +810,7 @@ const HomePage: React.FC = () => {
|
||||
alt={`素材案例 ${index + 1}`}
|
||||
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
||||
/>
|
||||
{/* 案例悬停遮罩(由父级 hover 触发) */}
|
||||
案例悬停遮罩(由父级 hover 触发)
|
||||
<div
|
||||
className="case-overlay"
|
||||
style={{
|
||||
@@ -824,7 +834,7 @@ const HomePage: React.FC = () => {
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -31,6 +31,8 @@ function InitialInfo() {
|
||||
// 引擎和视频参数相关状态
|
||||
const [enginesele, setEnginesele] = useState<any>({});
|
||||
const [countType, setCountType] = useState('');
|
||||
// 图片引擎 id:取自 getEngine().engine.image[0].id
|
||||
const [imageEngineId, setImageEngineId] = useState<string>('');
|
||||
const [showEngineModal, setShowEngineModal] = useState(false);
|
||||
const [showVideoSettingsModal, setShowVideoSettingsModal] = useState(false);
|
||||
const [videoDuration, setVideoDuration] = useState(5);
|
||||
@@ -160,6 +162,10 @@ function InitialInfo() {
|
||||
durations: firstEngine.supportedDurations || [5, 8, 10, 12, 15],
|
||||
});
|
||||
}
|
||||
// 默认选中第一个图片引擎
|
||||
if (data.engine?.image && data.engine.image.length > 0) {
|
||||
setImageEngineId(data.engine.image[0].id);
|
||||
}
|
||||
})
|
||||
.catch((error: any) => {
|
||||
});
|
||||
@@ -335,7 +341,7 @@ function InitialInfo() {
|
||||
|
||||
const createimage = (stepId: number) => {
|
||||
let params = {
|
||||
engine_id: "0019e3dac0b795b925b",
|
||||
engine_id: imageEngineId,
|
||||
image_proportion: "1:1",
|
||||
image_px: "2048x2048",
|
||||
image_size: "2K"
|
||||
@@ -349,7 +355,7 @@ function InitialInfo() {
|
||||
const newcreateimage = () => {
|
||||
// console.log('下一步:', stepId);
|
||||
let params = {
|
||||
engine_id: "0019e3dac0b795b925b",
|
||||
engine_id: imageEngineId,
|
||||
image_proportion: "1:1",
|
||||
image_px: "2048x2048",
|
||||
image_size: "2K"
|
||||
|
||||
@@ -852,6 +852,7 @@ const GenerateConver: React.FC = () => {
|
||||
</div>
|
||||
) : (
|
||||
<Upload
|
||||
style={{ width: '100%' }}
|
||||
beforeUpload={beforeVideoUpload}
|
||||
showUploadList={false}
|
||||
accept="video/mp4,video/quicktime,.mp4,.mov"
|
||||
@@ -967,6 +968,8 @@ const GenerateConver: React.FC = () => {
|
||||
</div>
|
||||
) : (
|
||||
<Upload
|
||||
style={{ width: '100%' }}
|
||||
|
||||
beforeUpload={beforeImageUpload}
|
||||
showUploadList={false}
|
||||
accept="image/jpeg,image/jpg,image/png,.jpg,.jpeg,.png"
|
||||
|
||||
@@ -250,8 +250,15 @@ const MaterialListPage: React.FC = () => {
|
||||
dataIndex: 'note',
|
||||
key: 'note',
|
||||
width: 150,
|
||||
ellipsis: true,
|
||||
render: (text: string) => <span style={{ color: '#94a3b8' }}>{text || '-'}</span>,
|
||||
render: (text: string) => (
|
||||
<Input.TextArea
|
||||
value={text || ''}
|
||||
readOnly
|
||||
autoSize={{ minRows: 1, maxRows: 4 }}
|
||||
style={{ color: '#94a3b8', resize: 'none', border: 'none', background: 'transparent', padding: 0 }}
|
||||
placeholder="-"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '用户ID',
|
||||
|
||||
@@ -1,115 +1,655 @@
|
||||
import React from 'react';
|
||||
import { Button, Card, message, Typography } from 'antd';
|
||||
import { Button, message, Typography } from 'antd';
|
||||
import {
|
||||
StarOutlined,
|
||||
CrownOutlined,
|
||||
LockOutlined,
|
||||
ThunderboltOutlined,
|
||||
GiftOutlined,
|
||||
BarChartOutlined,
|
||||
FireOutlined,
|
||||
TrophyOutlined,
|
||||
LockOutlined,
|
||||
ArrowRightOutlined,
|
||||
PlayCircleOutlined,
|
||||
EyeOutlined,
|
||||
LikeOutlined,
|
||||
CommentOutlined,
|
||||
ShareAltOutlined,
|
||||
RiseOutlined,
|
||||
ThunderboltOutlined,
|
||||
} from '@ant-design/icons';
|
||||
|
||||
const PopularPage: React.FC = () => {
|
||||
const handleUnlock = () => {
|
||||
message.info('请联系客服开通');
|
||||
message.info('请联系客服开通会员');
|
||||
};
|
||||
|
||||
// 顶部统计 KPI
|
||||
const stats = [
|
||||
{
|
||||
icon: <FireOutlined style={{ fontSize: 22, color: '#f59e0b' }} />,
|
||||
label: '今日爆款',
|
||||
value: '1,286',
|
||||
unit: '条',
|
||||
trend: '+12.5%',
|
||||
color: '#f59e0b',
|
||||
},
|
||||
{
|
||||
icon: <EyeOutlined style={{ fontSize: 22, color: '#6366f1' }} />,
|
||||
label: '总浏览量',
|
||||
value: '428',
|
||||
unit: '万',
|
||||
trend: '+8.2%',
|
||||
color: '#6366f1',
|
||||
},
|
||||
{
|
||||
icon: <LikeOutlined style={{ fontSize: 22, color: '#ec4899' }} />,
|
||||
label: '总互动数',
|
||||
value: '76.4',
|
||||
unit: '万',
|
||||
trend: '+15.7%',
|
||||
color: '#ec4899',
|
||||
},
|
||||
{
|
||||
icon: <ThunderboltOutlined style={{ fontSize: 22, color: '#10b981' }} />,
|
||||
label: '上升最快',
|
||||
value: '美妆',
|
||||
unit: '赛道',
|
||||
trend: '+28.3%',
|
||||
color: '#10b981',
|
||||
},
|
||||
];
|
||||
|
||||
// 行业热度排行(假数据)
|
||||
const industryRank = [
|
||||
{ rank: 1, name: '美妆个护', hot: 98.5, change: 'up' as const, value: 98234 },
|
||||
{ rank: 2, name: '服饰穿搭', hot: 95.2, change: 'up' as const, value: 87643 },
|
||||
{ rank: 3, name: '美食探店', hot: 91.7, change: 'up' as const, value: 78921 },
|
||||
{ rank: 4, name: '3C 数码', hot: 87.3, change: 'down' as const, value: 65432 },
|
||||
{ rank: 5, name: '家居生活', hot: 82.6, change: 'up' as const, value: 54321 },
|
||||
{ rank: 6, name: '母婴亲子', hot: 78.1, change: 'flat' as const, value: 43210 },
|
||||
{ rank: 7, name: '运动健身', hot: 72.4, change: 'up' as const, value: 38765 },
|
||||
];
|
||||
|
||||
// 爆款素材榜单
|
||||
const popularList = [
|
||||
{
|
||||
rank: 1,
|
||||
title: '夏日美妆教程:3 步打造奶油肌',
|
||||
author: '美妆博主·小雅',
|
||||
views: '128.4w',
|
||||
likes: '12.6w',
|
||||
comments: '8,234',
|
||||
tag: 'NO.1',
|
||||
tagColor: '#f59e0b',
|
||||
},
|
||||
{
|
||||
rank: 2,
|
||||
title: '平价穿搭 | 学生党一周不重样',
|
||||
author: '穿搭达人·Miki',
|
||||
views: '96.2w',
|
||||
likes: '9.8w',
|
||||
comments: '6,128',
|
||||
tag: 'NO.2',
|
||||
tagColor: '#a78bfa',
|
||||
},
|
||||
{
|
||||
rank: 3,
|
||||
title: '探店 vlog | 网红咖啡馆测评',
|
||||
author: '美食探店·阿伦',
|
||||
views: '78.5w',
|
||||
likes: '7.4w',
|
||||
comments: '4,892',
|
||||
tag: 'NO.3',
|
||||
tagColor: '#6366f1',
|
||||
},
|
||||
{
|
||||
rank: 4,
|
||||
title: 'iPhone 16 Pro 一周深度体验',
|
||||
author: '数码评测·老王',
|
||||
views: '65.7w',
|
||||
likes: '6.1w',
|
||||
comments: '3,847',
|
||||
tag: 'NO.4',
|
||||
tagColor: '#94a3b8',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={{ padding: '20px 0' }}>
|
||||
<div style={{ marginBottom: 32 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 12 }}>
|
||||
<div style={{
|
||||
width: 48,
|
||||
height: 48,
|
||||
borderRadius: 14,
|
||||
background: 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
boxShadow: '0 4px 16px rgba(245,158,11,0.3)',
|
||||
}}>
|
||||
<BarChartOutlined style={{ fontSize: 22, color: '#fff' }} />
|
||||
<div className="content_box">
|
||||
{/* Hero 区域 - 橙金主题(区别创意素材案例的紫粉) */}
|
||||
<div
|
||||
className="animate-fadeInUp"
|
||||
style={{
|
||||
padding: '28px 32px',
|
||||
borderRadius: 16,
|
||||
background: 'linear-gradient(135deg, #fffbeb 0%, #fef3c7 50%, #fed7aa 100%)',
|
||||
border: '1px solid rgba(245, 158, 11, 0.18)',
|
||||
marginBottom: 24,
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 14, marginBottom: 12 }}>
|
||||
<div
|
||||
style={{
|
||||
width: 52,
|
||||
height: 52,
|
||||
borderRadius: 14,
|
||||
background: 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
boxShadow: '0 6px 20px rgba(245, 158, 11, 0.3)',
|
||||
}}
|
||||
>
|
||||
<BarChartOutlined style={{ fontSize: 24, color: '#fff' }} />
|
||||
</div>
|
||||
<div>
|
||||
<Typography.Title level={2} style={{ margin: 0, color: '#1e293b', fontWeight: 700 }}>
|
||||
行业爆款大盘
|
||||
<span style={{ color: '#6366f1', fontSize: 14, fontWeight: 400, marginLeft: 8 }}>
|
||||
发现热门素材趋势
|
||||
<div className="shiny-text-container" style={{ marginBottom: 4 }}>
|
||||
<span
|
||||
className="shiny-text"
|
||||
style={{
|
||||
fontSize: 26,
|
||||
fontWeight: 700,
|
||||
backgroundImage:
|
||||
'linear-gradient(90deg, #f59e0b 0%, #d97706 30%, #ea580c 50%, #d97706 70%, #f59e0b 100%)',
|
||||
}}
|
||||
>
|
||||
行业爆款大盘
|
||||
</span>
|
||||
</Typography.Title>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 13, color: '#b45309' }}>
|
||||
<RiseOutlined style={{ fontSize: 12 }} />
|
||||
<span>实时追踪全网热门素材趋势</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Typography.Text style={{ color: '#64748b' }}>
|
||||
实时追踪各行业爆款素材,把握创作风向,打造热门内容
|
||||
<Typography.Text style={{ color: '#78716c', fontSize: 14, lineHeight: 1.7 }}>
|
||||
实时追踪各行业爆款素材,把握创作风向,打造热门内容
|
||||
</Typography.Text>
|
||||
</div>
|
||||
|
||||
<Card
|
||||
bordered={false}
|
||||
{/* 4 个 KPI 统计卡片 */}
|
||||
{/* <div
|
||||
className="animate-fadeInUp stagger-children"
|
||||
style={{
|
||||
borderRadius: 20,
|
||||
background: 'linear-gradient(135deg, rgba(245,158,11,0.04) 0%, rgba(217,119,6,0.04) 100%)',
|
||||
border: '1px solid rgba(245,158,11,0.1)',
|
||||
padding: 10,
|
||||
marginBottom: 32,
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(4, 1fr)',
|
||||
gap: 16,
|
||||
marginBottom: 24,
|
||||
}}
|
||||
>
|
||||
<Typography.Title level={4} style={{ marginBottom: 20, color: '#1e293b' }}>
|
||||
<FireOutlined style={{ marginRight: 8, color: '#f59e0b' }} />
|
||||
行业热度排行
|
||||
</Typography.Title>
|
||||
<Card
|
||||
bordered={false}
|
||||
style={{
|
||||
borderRadius: 16,
|
||||
background: '#fafafa',
|
||||
padding: 40,
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
<div style={{ color: '#94a3b8', fontSize: 14 }}>
|
||||
敬请期待行业热度排行
|
||||
{stats.map((item, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="project-card"
|
||||
style={{
|
||||
padding: 20,
|
||||
borderRadius: 16,
|
||||
background: '#fff',
|
||||
border: '1px solid #e2e8f0',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: 14,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 44,
|
||||
height: 44,
|
||||
borderRadius: 12,
|
||||
background: `linear-gradient(135deg, ${item.color}15, ${item.color}08)`,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
{item.icon}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 2,
|
||||
fontSize: 12,
|
||||
fontWeight: 600,
|
||||
color: '#10b981',
|
||||
background: 'rgba(16, 185, 129, 0.08)',
|
||||
padding: '2px 8px',
|
||||
borderRadius: 6,
|
||||
}}
|
||||
>
|
||||
<RiseOutlined style={{ fontSize: 10 }} />
|
||||
{item.trend}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'baseline', gap: 4, marginBottom: 4 }}>
|
||||
<span style={{ fontSize: 24, fontWeight: 700, color: '#1e293b' }}>{item.value}</span>
|
||||
<span style={{ fontSize: 13, color: '#94a3b8' }}>{item.unit}</span>
|
||||
</div>
|
||||
<div style={{ fontSize: 13, color: '#64748b' }}>{item.label}</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Card>
|
||||
))}
|
||||
</div> */}
|
||||
{/* 行业热度排行 */}
|
||||
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 20 }}>
|
||||
<Typography.Title level={4} style={{ margin: 0, color: '#1e293b' }}>
|
||||
<TrophyOutlined style={{ marginRight: 8, color: '#6366f1' }} />
|
||||
爆款素材榜单
|
||||
</Typography.Title>
|
||||
<Button
|
||||
type="primary"
|
||||
size="middle"
|
||||
onClick={handleUnlock}
|
||||
<div style={{ position: 'relative' }}>
|
||||
{/* <div
|
||||
className="animate-fadeInUp"
|
||||
style={{
|
||||
borderRadius: 10,
|
||||
background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)',
|
||||
border: 'none',
|
||||
padding: '24px 28px',
|
||||
borderRadius: 16,
|
||||
background: '#fff',
|
||||
border: '1px solid #e2e8f0',
|
||||
marginBottom: 24,
|
||||
}}
|
||||
>
|
||||
<LockOutlined style={{ marginRight: 6 }} />
|
||||
立即解锁全部
|
||||
</Button>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: 20,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<div
|
||||
style={{
|
||||
width: 4,
|
||||
height: 18,
|
||||
background: 'linear-gradient(180deg, #f59e0b, #d97706)',
|
||||
borderRadius: 2,
|
||||
marginRight: 10,
|
||||
}}
|
||||
/>
|
||||
<FireOutlined style={{ marginRight: 8, color: '#f59e0b', fontSize: 16 }} />
|
||||
<span style={{ fontSize: 17, fontWeight: 600, color: '#1f2937' }}>行业热度排行</span>
|
||||
<span
|
||||
style={{
|
||||
marginLeft: 10,
|
||||
fontSize: 12,
|
||||
fontWeight: 400,
|
||||
color: '#94a3b8',
|
||||
}}
|
||||
>
|
||||
基于近 7 天数据 · 实时更新
|
||||
</span>
|
||||
</div>
|
||||
<span style={{ fontSize: 12, color: '#94a3b8' }}>更新时间: 10 分钟前</span>
|
||||
</div>
|
||||
<div style={{position: 'relative', height: 300}}>
|
||||
<Typography.Text style={{ width: '100%', textAlign: 'center', color: '#64748b', display: 'block', height: 100, background: '#f3f4f6', marginTop: 20, marginBottom: 20, padding: 20, }}>
|
||||
实时追踪各行业爆款素材,把握创作风向,打造热门内容
|
||||
</Typography.Text>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
// background:
|
||||
// ' linear-gradient(180deg, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.7) 100%)',
|
||||
background: 'rgb(255,255,255)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'column',
|
||||
gap: 2,
|
||||
color: '#fff',
|
||||
borderRadius: 6,
|
||||
// boxShadow: '0 0 10px rgba(0,0,0,0.5)',
|
||||
|
||||
}}
|
||||
>
|
||||
<LockOutlined style={{ fontSize: 18, color: '#94a3b8', cursor: 'pointer' }}
|
||||
onClick={handleUnlock}
|
||||
/>
|
||||
<span style={{ fontSize: 16, color: '#1f2937', opacity: 0.9 }}>立即解锁全部</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
<div className="stagger-children">
|
||||
{industryRank.map((item) => {
|
||||
const rankColors: Record<number, string> = {
|
||||
1: 'linear-gradient(135deg, #f59e0b, #d97706)',
|
||||
2: 'linear-gradient(135deg, #94a3b8, #64748b)',
|
||||
3: 'linear-gradient(135deg, #ea580c, #c2410c)',
|
||||
};
|
||||
const rankColor = rankColors[item.rank] || '#cbd5e1';
|
||||
const trendIcon =
|
||||
item.change === 'up' ? (
|
||||
<RiseOutlined style={{ fontSize: 11, color: '#10b981' }} />
|
||||
) : item.change === 'down' ? (
|
||||
<RiseOutlined
|
||||
style={{ fontSize: 11, color: '#ef4444', transform: 'rotate(180deg)' }}
|
||||
/>
|
||||
) : (
|
||||
<span style={{ fontSize: 11, color: '#94a3b8' }}>—</span>
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.rank}
|
||||
className="project-card animate-fadeInUp"
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '12px 16px',
|
||||
borderRadius: 12,
|
||||
marginBottom: 8,
|
||||
background: '#f8fafc',
|
||||
border: '1px solid transparent',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 8,
|
||||
background: rankColor,
|
||||
color: '#fff',
|
||||
fontSize: 14,
|
||||
fontWeight: 700,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginRight: 14,
|
||||
boxShadow: item.rank <= 3 ? '0 2px 8px rgba(0,0,0,0.15)' : 'none',
|
||||
}}
|
||||
>
|
||||
{item.rank}
|
||||
</div>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 14,
|
||||
fontWeight: 600,
|
||||
color: '#1e293b',
|
||||
marginBottom: 6,
|
||||
}}
|
||||
>
|
||||
{item.name}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
height: 6,
|
||||
background: '#e2e8f0',
|
||||
borderRadius: 3,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: `${item.hot}%`,
|
||||
height: '100%',
|
||||
background:
|
||||
item.rank === 1
|
||||
? 'linear-gradient(90deg, #f59e0b, #d97706)'
|
||||
: item.rank === 2
|
||||
? 'linear-gradient(90deg, #94a3b8, #64748b)'
|
||||
: item.rank === 3
|
||||
? 'linear-gradient(90deg, #ea580c, #c2410c)'
|
||||
: 'linear-gradient(90deg, #6366f1, #8b5cf6)',
|
||||
borderRadius: 3,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
width: 80,
|
||||
textAlign: 'right',
|
||||
fontSize: 14,
|
||||
fontWeight: 600,
|
||||
color: '#1e293b',
|
||||
marginLeft: 16,
|
||||
}}
|
||||
>
|
||||
{item.hot}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
width: 80,
|
||||
textAlign: 'right',
|
||||
fontSize: 12,
|
||||
color: '#64748b',
|
||||
marginLeft: 12,
|
||||
}}
|
||||
>
|
||||
{item.value.toLocaleString()} 素材
|
||||
</div>
|
||||
<div style={{ width: 24, marginLeft: 12, textAlign: 'center' }}>{trendIcon}</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
{/* 爆款素材榜单 */}
|
||||
<div
|
||||
className="animate-fadeInUp"
|
||||
style={{
|
||||
padding: '24px 28px',
|
||||
borderRadius: 16,
|
||||
background: '#fff',
|
||||
// border: '1px solid #e2e8f0',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: 20,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<div
|
||||
style={{
|
||||
width: 4,
|
||||
height: 18,
|
||||
background: 'linear-gradient(180deg, #6366f1, #8b5cf6)',
|
||||
borderRadius: 2,
|
||||
marginRight: 10,
|
||||
}}
|
||||
/>
|
||||
<TrophyOutlined style={{ marginRight: 8, color: '#6366f1', fontSize: 16 }} />
|
||||
<span style={{ fontSize: 17, fontWeight: 600, color: '#1f2937' }}>爆款素材榜单</span>
|
||||
<span
|
||||
style={{
|
||||
marginLeft: 10,
|
||||
fontSize: 12,
|
||||
fontWeight: 400,
|
||||
color: '#94a3b8',
|
||||
}}
|
||||
>
|
||||
{/* TOP 100 */}
|
||||
</span>
|
||||
</div>
|
||||
{/* <Button
|
||||
type="primary"
|
||||
size="middle"
|
||||
onClick={handleUnlock}
|
||||
style={{
|
||||
borderRadius: 10,
|
||||
fontWeight: 500,
|
||||
background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)',
|
||||
border: 'none',
|
||||
boxShadow: '0 4px 14px rgba(99, 102, 241, 0.28)',
|
||||
}}
|
||||
>
|
||||
<LockOutlined style={{ marginRight: 6 }} />
|
||||
立即解锁全部
|
||||
<ArrowRightOutlined style={{ marginLeft: 4, fontSize: 12 }} />
|
||||
</Button> */}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="stagger-children"
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(2, 1fr)',
|
||||
gap: 16,
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
zIndex: 1,
|
||||
inset: 0,
|
||||
// background:
|
||||
// ' linear-gradient(180deg, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.7) 100%)',
|
||||
background: 'rgb(255,255,255)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'column',
|
||||
gap: 2,
|
||||
color: '#fff',
|
||||
borderRadius: 6,
|
||||
// boxShadow: '0 0 10px rgba(0,0,0,0.5)',
|
||||
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
type="primary"
|
||||
size="middle"
|
||||
onClick={handleUnlock}
|
||||
style={{
|
||||
borderRadius: 10,
|
||||
fontWeight: 500,
|
||||
background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)',
|
||||
border: 'none',
|
||||
boxShadow: '0 4px 14px rgba(99, 102, 241, 0.28)',
|
||||
}}
|
||||
>
|
||||
<LockOutlined style={{ marginRight: 6 }} />
|
||||
立即解锁全部
|
||||
<ArrowRightOutlined style={{ marginLeft: 4, fontSize: 12 }} />
|
||||
</Button>
|
||||
{/* <LockOutlined style={{ fontSize: 18, color: '#94a3b8', cursor: 'pointer' }}
|
||||
onClick={handleUnlock}
|
||||
/>
|
||||
<span style={{ fontSize: 16, color: '#1f2937', opacity: 0.9 }}>立即解锁全部</span> */}
|
||||
</div>
|
||||
|
||||
{popularList.map((item) => (
|
||||
<div
|
||||
key={item.rank}
|
||||
className="project-card animate-fadeInUp"
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: 14,
|
||||
padding: 14,
|
||||
borderRadius: 14,
|
||||
background: '#fff',
|
||||
border: '1px solid #e2e8f0',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: 'relative',
|
||||
width: 140,
|
||||
height: 80,
|
||||
flexShrink: 0,
|
||||
borderRadius: 10,
|
||||
overflow: 'hidden',
|
||||
background: 'linear-gradient(135deg, #1e1b4b 0%, #312e81 50%, #4c1d95 100%)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<PlayCircleOutlined style={{ fontSize: 28, color: 'rgba(255,255,255,0.6)' }} />
|
||||
{/* 排名徽章 */}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 6,
|
||||
left: 6,
|
||||
padding: '2px 8px',
|
||||
borderRadius: 6,
|
||||
background: item.tagColor,
|
||||
color: '#fff',
|
||||
fontSize: 11,
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
{item.tag}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div style={{ flex: 1, minWidth: 0, display: 'flex', flexDirection: 'column' }}>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 14,
|
||||
fontWeight: 600,
|
||||
color: '#1e293b',
|
||||
marginBottom: 4,
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
}}
|
||||
>
|
||||
{item.title}
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: '#94a3b8', marginBottom: 8 }}>
|
||||
{item.author}
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 12, fontSize: 12, color: '#64748b' }}>
|
||||
<span>
|
||||
<EyeOutlined style={{ marginRight: 3 }} />
|
||||
{item.views}
|
||||
</span>
|
||||
<span>
|
||||
<LikeOutlined style={{ marginRight: 3 }} />
|
||||
{item.likes}
|
||||
</span>
|
||||
<span>
|
||||
<CommentOutlined style={{ marginRight: 3 }} />
|
||||
{item.comments}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{/* 锁定遮罩 */}
|
||||
{/* <div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
background:
|
||||
' linear-gradient(to right, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.7) 100%)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'column',
|
||||
gap: 2,
|
||||
color: '#fff',
|
||||
borderRadius: 6,
|
||||
|
||||
}}
|
||||
>
|
||||
<LockOutlined style={{ fontSize: 18 }} />
|
||||
<span style={{ fontSize: 10, opacity: 0.9 }}>会员可看</span>
|
||||
</div> */}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<Card
|
||||
bordered={false}
|
||||
style={{
|
||||
borderRadius: 16,
|
||||
background: '#fafafa',
|
||||
padding: 40,
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
<div style={{ color: '#94a3b8', fontSize: 14 }}>
|
||||
敬请期待更多爆款素材
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PopularPage;
|
||||
export default PopularPage;
|
||||
|
||||
+486
-371
@@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Table, Tag, Button, Typography, Space, Modal, Form, Input, Select, Cascader, App, Pagination } from 'antd';
|
||||
import { PlusOutlined, EditOutlined, FileTextOutlined } from '@ant-design/icons';
|
||||
import { getPreTestList, createPreTest, getArea } from '../api';
|
||||
import { PlusOutlined, EditOutlined, FileTextOutlined, DeleteOutlined, EyeOutlined } from '@ant-design/icons';
|
||||
import { getPreTestList, createPreTest, getArea, getPreTestDetail, updatePreTest, deletePreTest } from '../api';
|
||||
|
||||
interface PreTestRecord {
|
||||
id: string;
|
||||
@@ -19,13 +19,6 @@ interface PreTestField {
|
||||
description?: string;
|
||||
}
|
||||
|
||||
const statusConfig = {
|
||||
completed: { label: '已完成', color: 'success' },
|
||||
pending: { label: '待测试', color: 'default' },
|
||||
testing: { label: '测试中', color: 'processing' },
|
||||
failed: { label: '测试失败', color: 'error' },
|
||||
};
|
||||
|
||||
const PRETEST_FIELDS: PreTestField[] = [
|
||||
{ id: 'AD_APP_ACTIVATE', name: 'AD_APP_ACTIVATE', label: '应用-激活', description: '应用激活行为' },
|
||||
{ id: 'AD_APP_AUTH', name: 'AD_APP_AUTH', label: '应用-授信', description: '应用授信行为' },
|
||||
@@ -129,171 +122,17 @@ const PRETEST_FIELDS: PreTestField[] = [
|
||||
{ id: 'QC_PRODUCT_ROI', name: 'QC_PRODUCT_ROI', label: '商品投放-商品支付roi', description: '商品支付roi' },
|
||||
];
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '前测模板ID',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
width: 110,
|
||||
render: (text: string) => <Typography.Text strong style={{ color: '#1e293b', fontSize: 13 }}>{text}</Typography.Text>,
|
||||
},
|
||||
{
|
||||
title: '模板名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
ellipsis: true,
|
||||
render: (text: string) => (
|
||||
<span style={{ color: '#1e293b', fontWeight: 500, fontSize: 13 }}>{text}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '平台',
|
||||
dataIndex: 'platform',
|
||||
key: 'platform',
|
||||
render: (text: string) => (
|
||||
<Tag color={text === 'AD' ? 'blue' : text === 'QIANCHUAN' ? 'green' : 'orange'} style={{ borderRadius: 6, fontSize: 12 }}>
|
||||
{text === 'AD' ? 'AD' : text === 'QIANCHUAN' ? '千川' : '本地推'}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '转化目标',
|
||||
dataIndex: 'externalAction',
|
||||
key: 'externalAction',
|
||||
ellipsis: true,
|
||||
width: 200,
|
||||
render: (text: string) => {
|
||||
if (!text) return <span style={{ color: '#94a3b8', fontSize: 13 }}>-</span>;
|
||||
const actionArray = text.split(',').filter(item => item.trim());
|
||||
const labels = actionArray.map(action => {
|
||||
const field = PRETEST_FIELDS.find(f => f.name === action.trim());
|
||||
return field ? field.label : action;
|
||||
});
|
||||
return (
|
||||
<span style={{ color: '#1e293b', fontSize: 13 }}>{labels.join(', ')}</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '目标转化成本',
|
||||
dataIndex: 'cpaBid',
|
||||
key: 'cpaBid',
|
||||
width: 120,
|
||||
render: (text: number) => (
|
||||
<span style={{ color: '#1e293b', fontSize: 13 }}>{text !== undefined ? text : '-'}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '受众性别',
|
||||
dataIndex: 'audienceGender',
|
||||
key: 'audienceGender',
|
||||
width: 90,
|
||||
render: (text: string) => (
|
||||
<span style={{ color: '#1e293b', fontSize: 13 }}>{text === 'ALL' ? '不限' : text === 'MALE' ? '男' : text === 'FEMALE' ? '女' : text || '-'}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '受众年龄',
|
||||
dataIndex: 'audienceAge',
|
||||
key: 'audienceAge',
|
||||
width: 90,
|
||||
render: (text: string[]) => (
|
||||
<span style={{ color: '#1e293b', fontSize: 13 }}>{Array.isArray(text) ? text.join(', ') : text || '-'}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '受众地区',
|
||||
dataIndex: 'audience_region',
|
||||
key: 'audience_region',
|
||||
ellipsis: true,
|
||||
width: 150,
|
||||
render: (text: string[]) => (
|
||||
<span style={{ color: '#1e293b', fontSize: 13 }}>{Array.isArray(text) ? text.join(', ') : text || '-'}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '网络类型',
|
||||
dataIndex: 'audienceNetwork',
|
||||
key: 'audienceNetwork',
|
||||
width: 90,
|
||||
render: (text: string[]) => (
|
||||
<span style={{ color: '#1e293b', fontSize: 13 }}>{Array.isArray(text) ? text.join(', ') : text || '-'}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '客户主体名称',
|
||||
dataIndex: 'cusName',
|
||||
key: 'cusName',
|
||||
ellipsis: true,
|
||||
width: 120,
|
||||
render: (text: string) => (
|
||||
<span style={{ color: '#1e293b', fontSize: 13 }}>{text || '-'}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '出价类型',
|
||||
dataIndex: 'pricingType',
|
||||
key: 'pricingType',
|
||||
width: 90,
|
||||
render: (text: string) => (
|
||||
<span style={{ color: '#1e293b', fontSize: 13 }}>{text || '-'}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '目标点击成本',
|
||||
dataIndex: 'cpcBid',
|
||||
key: 'cpcBid',
|
||||
width: 120,
|
||||
render: (text: number) => (
|
||||
<span style={{ color: '#1e293b', fontSize: 13 }}>{text !== undefined ? text : '-'}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '预算金额',
|
||||
dataIndex: 'budget',
|
||||
key: 'budget',
|
||||
width: 100,
|
||||
render: (text: number) => (
|
||||
<span style={{ color: '#1e293b', fontSize: 13 }}>{text !== undefined ? text : '-'}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '默认模板',
|
||||
dataIndex: 'isDefault',
|
||||
key: 'isDefault',
|
||||
width: 90,
|
||||
render: (text: boolean) => (
|
||||
<Tag color={text ? 'green' : 'default'} style={{ borderRadius: 6, fontSize: 12 }}>
|
||||
{text ? '是' : '否'}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'createdAt',
|
||||
key: 'createdAt',
|
||||
width: 160,
|
||||
render: (text: string) => <Typography.Text style={{ color: '#94a3b8', fontSize: 12 }}>{text}</Typography.Text>,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'operation',
|
||||
key: 'operation',
|
||||
width: 140,
|
||||
render: (_: any, record: PreTestRecord) => (
|
||||
<Space size={4}>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<EditOutlined />}
|
||||
style={{ color: '#6366f1', fontSize: 12 }}
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
const formatDateTime = (dateStr: string) => {
|
||||
if (!dateStr) return '';
|
||||
const date = new Date(dateStr);
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const hours = String(date.getHours()).padStart(2, '0');
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
const seconds = String(date.getSeconds()).padStart(2, '0');
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
||||
};
|
||||
|
||||
const PreTest: React.FC = () => {
|
||||
const { message } = App.useApp();
|
||||
@@ -301,6 +140,8 @@ const PreTest: React.FC = () => {
|
||||
const [total, setTotal] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [modalMode, setModalMode] = useState<'create' | 'edit' | 'detail'>('create');
|
||||
const [currentRecord, setCurrentRecord] = useState<PreTestRecord | null>(null);
|
||||
const [form] = Form.useForm();
|
||||
const [submitLoading, setSubmitLoading] = useState(false);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
@@ -320,7 +161,6 @@ const PreTest: React.FC = () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await getPreTestList({ page: currentPage, pageSize });
|
||||
console.log(res);
|
||||
setRecords(res.data || []);
|
||||
setTotal(res.pagination?.total || 0);
|
||||
setCurrentPage(res.pagination?.page || 1);
|
||||
@@ -349,7 +189,6 @@ const PreTest: React.FC = () => {
|
||||
const loadCityOptions = async (selectedOptions: any) => {
|
||||
const targetOption = selectedOptions[selectedOptions.length - 1];
|
||||
targetOption.loading = true;
|
||||
|
||||
try {
|
||||
const res = await getArea({ level: 'TWO_LEVEL', parent_code: targetOption.value });
|
||||
targetOption.children = (res.data || []).map((item: any) => ({
|
||||
@@ -372,12 +211,263 @@ const PreTest: React.FC = () => {
|
||||
key: item.id,
|
||||
}));
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '前测模板编号',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
width: 120,
|
||||
render: (text: string) => <Typography.Text strong style={{ color: '#1e293b', fontSize: 13 }}>{text}</Typography.Text>,
|
||||
},
|
||||
{
|
||||
title: '模板名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
ellipsis: true,
|
||||
render: (text: string) => (
|
||||
<span style={{ color: '#1e293b', fontWeight: 500, fontSize: 13 }}>{text}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '平台',
|
||||
dataIndex: 'platform',
|
||||
key: 'platform',
|
||||
render: (text: string) => (
|
||||
<Tag color={text === 'AD' ? 'blue' : text === 'QIANCHUAN' ? 'green' : 'orange'} style={{ borderRadius: 6, fontSize: 12 }}>
|
||||
{text === 'AD' ? 'AD' : text === 'QIANCHUAN' ? '千川' : '本地推'}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '转化目标',
|
||||
dataIndex: 'externalAction',
|
||||
key: 'externalAction',
|
||||
ellipsis: true,
|
||||
width: 200,
|
||||
render: (text: string) => {
|
||||
if (!text) return <span style={{ color: '#94a3b8', fontSize: 13 }}>-</span>;
|
||||
const field = PRETEST_FIELDS.find(f => f.name === text.trim());
|
||||
return (
|
||||
<span style={{ color: '#1e293b', fontSize: 13 }}>{field ? field.label : text}</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '目标转化成本',
|
||||
dataIndex: 'cpaBid',
|
||||
key: 'cpaBid',
|
||||
width: 120,
|
||||
render: (text: number) => (
|
||||
<span style={{ color: '#1e293b', fontSize: 13 }}>{text !== undefined ? text : '-'}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '受众性别',
|
||||
dataIndex: 'audienceGender',
|
||||
key: 'audienceGender',
|
||||
width: 90,
|
||||
render: (text: string) => (
|
||||
<span style={{ color: '#1e293b', fontSize: 13 }}>{text === 'ALL' ? '不限' : text === 'MALE' ? '男' : text === 'FEMALE' ? '女' : text || '-'}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '受众年龄',
|
||||
dataIndex: 'audienceAge',
|
||||
key: 'audienceAge',
|
||||
width: 90,
|
||||
render: (text: string[]) => (
|
||||
<span style={{ color: '#1e293b', fontSize: 13 }}>{Array.isArray(text) ? text.join(', ') : text || '-'}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '受众地区',
|
||||
dataIndex: 'audience_region',
|
||||
key: 'audience_region',
|
||||
ellipsis: true,
|
||||
width: 150,
|
||||
render: (text: string[]) => (
|
||||
<span style={{ color: '#1e293b', fontSize: 13 }}>{Array.isArray(text) ? text.join(', ') : text || '-'}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '网络类型',
|
||||
dataIndex: 'audienceNetwork',
|
||||
key: 'audienceNetwork',
|
||||
width: 90,
|
||||
render: (text: string[]) => (
|
||||
<span style={{ color: '#1e293b', fontSize: 13 }}>{Array.isArray(text) ? text.join(', ') : text || '-'}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '客户主体名称',
|
||||
dataIndex: 'cusName',
|
||||
key: 'cusName',
|
||||
ellipsis: true,
|
||||
width: 120,
|
||||
render: (text: string) => (
|
||||
<span style={{ color: '#1e293b', fontSize: 13 }}>{text || '-'}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '出价类型',
|
||||
dataIndex: 'pricingType',
|
||||
key: 'pricingType',
|
||||
width: 90,
|
||||
render: (text: string) => (
|
||||
<span style={{ color: '#1e293b', fontSize: 13 }}>{text || '-'}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '目标点击成本',
|
||||
dataIndex: 'cpcBid',
|
||||
key: 'cpcBid',
|
||||
width: 120,
|
||||
render: (text: number) => (
|
||||
<span style={{ color: '#1e293b', fontSize: 13 }}>{text !== undefined ? text : '-'}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '预算金额',
|
||||
dataIndex: 'budget',
|
||||
key: 'budget',
|
||||
width: 100,
|
||||
render: (text: number) => (
|
||||
<span style={{ color: '#1e293b', fontSize: 13 }}>{text !== undefined ? text : '-'}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '默认模板',
|
||||
dataIndex: 'isDefault',
|
||||
key: 'isDefault',
|
||||
width: 90,
|
||||
render: (text: boolean) => (
|
||||
<Tag color={text ? 'green' : 'default'} style={{ borderRadius: 6, fontSize: 12 }}>
|
||||
{text ? '是' : '否'}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'createdAt',
|
||||
key: 'createdAt',
|
||||
width: 160,
|
||||
render: (text: string) => <Typography.Text style={{ color: '#94a3b8', fontSize: 12 }}>{formatDateTime(text)}</Typography.Text>,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'operation',
|
||||
fixed: 'right' as const,
|
||||
key: 'operation',
|
||||
render: (_: any, record: PreTestRecord) => (
|
||||
<Space size={4}>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<EyeOutlined />}
|
||||
style={{ color: '#6366f1', fontSize: 12 }}
|
||||
onClick={() => handleDetail(record)}
|
||||
>
|
||||
详情
|
||||
</Button>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<EditOutlined />}
|
||||
style={{ color: '#6366f1', fontSize: 12 }}
|
||||
onClick={() => handleEdit(record)}
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<DeleteOutlined />}
|
||||
style={{ color: '#ef4444', fontSize: 12 }}
|
||||
onClick={() => handleDelete(record)}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const handleOpenModal = () => {
|
||||
form.resetFields();
|
||||
setPlatform('AD');
|
||||
setModalMode('create');
|
||||
setCurrentRecord(null);
|
||||
setModalOpen(true);
|
||||
};
|
||||
|
||||
const fillFormWithDetail = (data: any) => {
|
||||
setCurrentRecord(data);
|
||||
setPlatform(data.platform || 'AD');
|
||||
const regionCodes = data.audience_region || [];
|
||||
const regionPaths = regionCodes.map((code: any) => [String(code)]);
|
||||
form.setFieldsValue({
|
||||
name: data.name,
|
||||
note: data.note,
|
||||
platform: data.platform,
|
||||
external_action: data.externalAction,
|
||||
cpa_bid: data.cpaBid,
|
||||
audience_gender: data.audienceGender,
|
||||
audience_age: data.audienceAge || [],
|
||||
audience_region: regionPaths,
|
||||
audience_network: data.audienceNetwork || [],
|
||||
cus_name: data.cusName,
|
||||
pricing_type: data.pricingType,
|
||||
cost_cap: data.costCap !== undefined ? data.costCap : false,
|
||||
target_cost: data.targetCost !== undefined ? data.targetCost : false,
|
||||
nobid: data.nobid !== undefined ? data.nobid : false,
|
||||
cpc_bid: data.cpcBid,
|
||||
budget: data.budget,
|
||||
is_default: data.isDefault !== undefined ? data.isDefault : false,
|
||||
});
|
||||
};
|
||||
|
||||
const handleDetail = async (record: PreTestRecord) => {
|
||||
try {
|
||||
const res = await getPreTestDetail(record.id);
|
||||
const data = res.data;
|
||||
setModalMode('detail');
|
||||
fillFormWithDetail(data);
|
||||
setModalOpen(true);
|
||||
} catch (error) {
|
||||
message.error('获取详情失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleEdit = async (record: PreTestRecord) => {
|
||||
try {
|
||||
const res = await getPreTestDetail(record.id);
|
||||
const data = res.data;
|
||||
setModalMode('edit');
|
||||
fillFormWithDetail(data);
|
||||
setModalOpen(true);
|
||||
} catch (error) {
|
||||
message.error('获取详情失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = (record: PreTestRecord) => {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: '确定要删除该前测模板吗?',
|
||||
okText: '确定',
|
||||
cancelText: '取消',
|
||||
onOk: async () => {
|
||||
try {
|
||||
await deletePreTest(record.id);
|
||||
message.success('删除成功');
|
||||
loadRecords();
|
||||
} catch (error) {
|
||||
message.error('删除失败');
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleCloseModal = () => {
|
||||
setModalOpen(false);
|
||||
form.resetFields();
|
||||
@@ -389,12 +479,11 @@ const PreTest: React.FC = () => {
|
||||
const values = await form.validateFields();
|
||||
setSubmitLoading(true);
|
||||
const regionCodes = (values.audience_region || []).map((path: string[]) => path[path.length - 1]);
|
||||
const externalActionStr = (values.external_action || []).join(',');
|
||||
const result = await createPreTest({
|
||||
const params = {
|
||||
name: values.name,
|
||||
note: values.note,
|
||||
platform: values.platform,
|
||||
external_action: externalActionStr,
|
||||
external_action: values.external_action,
|
||||
cpa_bid: values.cpa_bid || 0,
|
||||
audience_gender: values.audience_gender,
|
||||
audience_age: values.audience_age || [],
|
||||
@@ -408,9 +497,17 @@ const PreTest: React.FC = () => {
|
||||
cpc_bid: values.cpc_bid || 0,
|
||||
budget: values.budget || 0,
|
||||
is_default: values.is_default || false,
|
||||
});
|
||||
};
|
||||
|
||||
let result;
|
||||
if (modalMode === 'edit' && currentRecord) {
|
||||
result = await updatePreTest(currentRecord.id, params);
|
||||
message.success('前测模板更新成功');
|
||||
} else {
|
||||
result = await createPreTest(params);
|
||||
message.success('前测模板创建成功');
|
||||
}
|
||||
|
||||
message.success('前测模板创建成功');
|
||||
setModalOpen(false);
|
||||
form.resetFields();
|
||||
loadRecords();
|
||||
@@ -420,7 +517,7 @@ const PreTest: React.FC = () => {
|
||||
} else if (error?.response?.data?.message) {
|
||||
message.error(error.response.data.message);
|
||||
} else {
|
||||
message.error('创建失败,请重试');
|
||||
message.error(modalMode === 'edit' ? '更新失败,请重试' : '创建失败,请重试');
|
||||
}
|
||||
} finally {
|
||||
setSubmitLoading(false);
|
||||
@@ -498,138 +595,170 @@ const PreTest: React.FC = () => {
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
title={<Space>新增前测模板</Space>}
|
||||
title={<Space>{modalMode === 'create' ? '新增前测模板' : modalMode === 'edit' ? '编辑前测模板' : '前测模板详情'}</Space>}
|
||||
open={modalOpen}
|
||||
onCancel={handleCloseModal}
|
||||
onOk={handleSubmit}
|
||||
okText="提交"
|
||||
okText={modalMode === 'detail' ? '' : '提交'}
|
||||
cancelText="取消"
|
||||
width={720}
|
||||
height={600}
|
||||
confirmLoading={submitLoading}
|
||||
style={{ borderRadius: 16 }}
|
||||
footer={modalMode === 'detail' ? null : undefined}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
style={{ marginTop: 16 }}
|
||||
disabled={modalMode === 'detail'}
|
||||
>
|
||||
<Form.Item
|
||||
name="name"
|
||||
label="模板名称"
|
||||
rules={[{ required: true, message: '请输入模板名称' }]}
|
||||
>
|
||||
<Input
|
||||
placeholder="请输入模板名称"
|
||||
style={{ borderRadius: 8, height: 40 }}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="platform"
|
||||
label="平台"
|
||||
initialValue="AD"
|
||||
rules={[{ required: true, message: '请选择平台' }]}
|
||||
>
|
||||
<Select
|
||||
placeholder="请选择平台"
|
||||
style={{ borderRadius: 8, height: 40 }}
|
||||
onChange={(value) => {
|
||||
setPlatform(value);
|
||||
}}
|
||||
options={[
|
||||
{ value: 'AD', label: 'AD' },
|
||||
{ value: 'QIANCHUAN', label: '千川' },
|
||||
{ value: 'LOCAL', label: '本地推' },
|
||||
<div style={{ display: 'flex', gap: 16 }}>
|
||||
<Form.Item
|
||||
style={{ flex: 1 }}
|
||||
name="name"
|
||||
label="模板名称"
|
||||
rules={[{ required: true, message: '请输入模板名称' }]}
|
||||
>
|
||||
<Input
|
||||
placeholder="请输入模板名称"
|
||||
style={{ borderRadius: 8, height: 40 }}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
style={{ flex: 1 }}
|
||||
name="platform"
|
||||
label="平台"
|
||||
initialValue="AD"
|
||||
rules={[{ required: true, message: '请选择平台' }]}
|
||||
>
|
||||
<Select
|
||||
placeholder="请选择平台"
|
||||
style={{ borderRadius: 8, height: 40 }}
|
||||
onChange={(value) => {
|
||||
setPlatform(value);
|
||||
}}
|
||||
options={[
|
||||
{ value: 'AD', label: 'AD' },
|
||||
{ value: 'QIANCHUAN', label: '千川' },
|
||||
{ value: 'LOCAL', label: '本地推' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
{platform !== 'LOCAL' && (
|
||||
<Form.Item
|
||||
name="external_action"
|
||||
label="转化目标"
|
||||
>
|
||||
<Select
|
||||
placeholder="请选择转化目标"
|
||||
style={{ borderRadius: 8, height: 40 }}
|
||||
options={PRETEST_FIELDS.filter(field => {
|
||||
if (platform === 'AD') {
|
||||
return field.name.startsWith('AD_');
|
||||
} else if (platform === 'QIANCHUAN') {
|
||||
return field.name.startsWith('QC_');
|
||||
} else if (platform === 'LOCAL') {
|
||||
return field.name.startsWith('LOCAL_');
|
||||
}
|
||||
return true;
|
||||
}).map(field => ({
|
||||
value: field.name,
|
||||
label: (
|
||||
<span>
|
||||
{field.label}
|
||||
{field.description && (
|
||||
<Typography.Text style={{ color: '#94a3b8', fontSize: 12, marginLeft: 4 }}>
|
||||
({field.description})
|
||||
</Typography.Text>
|
||||
)}
|
||||
</span>
|
||||
),
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
<div style={{ display: 'flex', gap: 16 }}>
|
||||
<Form.Item
|
||||
style={{ flex: 1 }}
|
||||
name="cpa_bid"
|
||||
label="目标转化成本"
|
||||
rules={[
|
||||
{ type: 'number', min: 1, max: 10000, message: '取值范围: 1-10000' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
getValueFromEvent={(e) => parseFloat(e.target.value) || undefined}
|
||||
>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="取值范围: 1-10000,精确到小数点后2位"
|
||||
style={{ borderRadius: 8, height: 40 }}
|
||||
min={1}
|
||||
max={10000}
|
||||
step={0.01}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="external_action"
|
||||
label="转化目标"
|
||||
>
|
||||
<Select
|
||||
mode="multiple"
|
||||
placeholder="请选择转化目标(可多选)"
|
||||
style={{ borderRadius: 8, minHeight: 40 }}
|
||||
maxTagCount="responsive"
|
||||
options={PRETEST_FIELDS.filter(field => {
|
||||
if (platform === 'AD') {
|
||||
return field.name.startsWith('AD_');
|
||||
} else if (platform === 'QIANCHUAN') {
|
||||
return field.name.startsWith('QC_');
|
||||
} else if (platform === 'LOCAL') {
|
||||
return field.name.startsWith('LOCAL_');
|
||||
}
|
||||
return true;
|
||||
}).map(field => ({
|
||||
value: field.name,
|
||||
label: (
|
||||
<span>
|
||||
{field.label}
|
||||
{field.description && (
|
||||
<Typography.Text style={{ color: '#94a3b8', fontSize: 12, marginLeft: 4 }}>
|
||||
({field.description})
|
||||
</Typography.Text>
|
||||
)}
|
||||
</span>
|
||||
),
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="cpa_bid"
|
||||
label="目标转化成本"
|
||||
rules={[
|
||||
{ type: 'number', min: 1, max: 10000, message: '取值范围: 1-10000' },
|
||||
]}
|
||||
getValueFromEvent={(e) => parseFloat(e.target.value) || undefined}
|
||||
>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="取值范围: 1-10000,精确到小数点后2位"
|
||||
style={{ borderRadius: 8, height: 40 }}
|
||||
min={1}
|
||||
max={10000}
|
||||
step={0.01}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="audience_gender"
|
||||
label="受众性别"
|
||||
>
|
||||
<Select
|
||||
placeholder="请选择受众性别"
|
||||
style={{ borderRadius: 8, height: 40 }}
|
||||
options={[
|
||||
{ value: 'ALL', label: '不限' },
|
||||
{ value: 'MALE', label: '男' },
|
||||
{ value: 'FEMALE', label: '女' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="audience_age"
|
||||
label="受众年龄"
|
||||
>
|
||||
<Select
|
||||
mode="multiple"
|
||||
placeholder="请选择受众年龄(可多选)"
|
||||
style={{ borderRadius: 8, minHeight: 40 }}
|
||||
maxTagCount="responsive"
|
||||
options={[
|
||||
{ value: 'ALL', label: '不限' },
|
||||
{ value: '18-23', label: '18-23岁' },
|
||||
{ value: '24-30', label: '24-30岁' },
|
||||
{ value: '31-40', label: '31-40岁' },
|
||||
{ value: '41-49', label: '41-49岁' },
|
||||
{ value: '50', label: '50岁以上' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
style={{ flex: 1 }}
|
||||
name="audience_gender"
|
||||
label="受众性别"
|
||||
>
|
||||
<Select
|
||||
placeholder="请选择受众性别"
|
||||
style={{ borderRadius: 8, height: 40 }}
|
||||
options={[
|
||||
{ value: 'ALL', label: '不限' },
|
||||
{ value: 'MALE', label: '男' },
|
||||
{ value: 'FEMALE', label: '女' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 16 }}>
|
||||
<Form.Item
|
||||
style={{ flex: 1 }}
|
||||
name="audience_age"
|
||||
label="受众年龄"
|
||||
>
|
||||
<Select
|
||||
mode="multiple"
|
||||
placeholder="请选择受众年龄(可多选)"
|
||||
style={{ borderRadius: 8, minHeight: 40 }}
|
||||
maxTagCount="responsive"
|
||||
options={[
|
||||
{ value: 'ALL', label: '不限' },
|
||||
{ value: '18-23', label: '18-23岁' },
|
||||
{ value: '24-30', label: '24-30岁' },
|
||||
{ value: '31-40', label: '31-40岁' },
|
||||
{ value: '41-49', label: '41-49岁' },
|
||||
{ value: '50', label: '50岁以上' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
style={{ flex: 1 }}
|
||||
name="audience_network"
|
||||
label="网络类型"
|
||||
>
|
||||
<Select
|
||||
mode="multiple"
|
||||
placeholder="请选择网络类型(可多选)"
|
||||
style={{ borderRadius: 8, minHeight: 40 }}
|
||||
maxTagCount="responsive"
|
||||
options={[
|
||||
{ value: 'ALL', label: '不限' },
|
||||
{ value: '5G', label: '5G' },
|
||||
{ value: '4G', label: '4G' },
|
||||
{ value: '3G', label: '3G' },
|
||||
{ value: '2G', label: '2G' },
|
||||
{ value: 'WIFI', label: 'wifi' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
<Form.Item
|
||||
name="audience_region"
|
||||
label="受众地区"
|
||||
@@ -644,56 +773,39 @@ const PreTest: React.FC = () => {
|
||||
maxTagCount="responsive"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="audience_network"
|
||||
label="网络类型"
|
||||
>
|
||||
<Select
|
||||
mode="multiple"
|
||||
placeholder="请选择网络类型(可多选)"
|
||||
style={{ borderRadius: 8, minHeight: 40 }}
|
||||
maxTagCount="responsive"
|
||||
options={[
|
||||
{ value: 'ALL', label: '不限' },
|
||||
{ value: '5G', label: '5G' },
|
||||
{ value: '4G', label: '4G' },
|
||||
{ value: '3G', label: '3G' },
|
||||
{ value: '2G', label: '2G' },
|
||||
{ value: 'WIFI', label: 'wifi' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="cus_name"
|
||||
label="客户主体名称"
|
||||
>
|
||||
<Input
|
||||
placeholder="请输入客户主体名称"
|
||||
style={{ borderRadius: 8, height: 40 }}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="pricing_type"
|
||||
label="出价类型"
|
||||
>
|
||||
<Select
|
||||
placeholder="请选择出价类型"
|
||||
style={{ borderRadius: 8, height: 40 }}
|
||||
options={[
|
||||
{ value: 'OCPC', label: 'OCPC' },
|
||||
{ value: 'CPA', label: 'CPA' },
|
||||
{ value: 'OCPM', label: 'OCPM' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
<div style={{ display: 'flex', gap: 16 }}>
|
||||
<Form.Item
|
||||
style={{ flex: 1 }}
|
||||
name="cus_name"
|
||||
label="客户主体名称"
|
||||
>
|
||||
<Input
|
||||
placeholder="请输入客户主体名称"
|
||||
style={{ borderRadius: 8, height: 40 }}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
style={{ flex: 1 }}
|
||||
name="pricing_type"
|
||||
label="出价类型"
|
||||
>
|
||||
<Select
|
||||
placeholder="请选择出价类型"
|
||||
style={{ borderRadius: 8, height: 40 }}
|
||||
options={[
|
||||
{ value: 'OCPC', label: 'OCPC' },
|
||||
{ value: 'CPA', label: 'CPA' },
|
||||
{ value: 'OCPM', label: 'OCPM' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
{platform == 'AD' && (
|
||||
<>
|
||||
<div style={{ display: 'flex', gap: 16 }}>
|
||||
<Form.Item
|
||||
style={{ flex: 1 }}
|
||||
name="cost_cap"
|
||||
label="是否最优成本出价"
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Select
|
||||
placeholder="是否最优成本出价"
|
||||
@@ -706,9 +818,9 @@ const PreTest: React.FC = () => {
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
style={{ flex: 1 }}
|
||||
name="target_cost"
|
||||
label="是否稳定成本出价"
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Select
|
||||
placeholder="是否稳定成本出价"
|
||||
@@ -721,9 +833,9 @@ const PreTest: React.FC = () => {
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
style={{ flex: 1 }}
|
||||
name="nobid"
|
||||
label="是否最大转化出价"
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Select
|
||||
placeholder="是否最大转化出价"
|
||||
@@ -734,38 +846,42 @@ const PreTest: React.FC = () => {
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
<Form.Item
|
||||
name="cpc_bid"
|
||||
label="目标点击成本"
|
||||
getValueFromEvent={(e) => parseFloat(e.target.value) || undefined}
|
||||
>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="请输入目标点击成本"
|
||||
style={{ borderRadius: 8, height: 40 }}
|
||||
/>
|
||||
</Form.Item>
|
||||
<div style={{ display: 'flex', gap: 16 }}>
|
||||
<Form.Item
|
||||
style={{ flex: 1 }}
|
||||
name="cpc_bid"
|
||||
label="目标点击成本"
|
||||
getValueFromEvent={(e) => parseFloat(e.target.value) || undefined}
|
||||
>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="请输入目标点击成本"
|
||||
style={{ borderRadius: 8, height: 40 }}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="budget"
|
||||
label="预算金额"
|
||||
getValueFromEvent={(e) => parseFloat(e.target.value) || undefined}
|
||||
>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="请输入预算金额"
|
||||
style={{ borderRadius: 8, height: 40 }}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
style={{ flex: 1 }}
|
||||
name="budget"
|
||||
label="预算金额"
|
||||
getValueFromEvent={(e) => parseFloat(e.target.value) || undefined}
|
||||
>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="请输入预算金额"
|
||||
style={{ borderRadius: 8, height: 40 }}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
<Form.Item
|
||||
name="note"
|
||||
label="备注"
|
||||
>
|
||||
<Input.TextArea
|
||||
placeholder="请输入备注"
|
||||
rows={3}
|
||||
rows={2}
|
||||
style={{ borderRadius: 8 }}
|
||||
/>
|
||||
</Form.Item>
|
||||
@@ -773,7 +889,6 @@ const PreTest: React.FC = () => {
|
||||
name="is_default"
|
||||
label="默认模板"
|
||||
initialValue={false}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Select
|
||||
placeholder="请选择默认模板"
|
||||
|
||||
@@ -116,7 +116,6 @@ function RemoveInfo() {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('轮询获取拆镜列表失败:', error);
|
||||
}
|
||||
}, 30000); // 30秒轮询
|
||||
}
|
||||
@@ -155,7 +154,6 @@ function RemoveInfo() {
|
||||
if (!taskDetail) return;
|
||||
|
||||
if (taskDetail.analysisStatus === 'processing') {
|
||||
console.log("开始轮询");
|
||||
|
||||
analysisPollingRef.current = window.setInterval(async () => {
|
||||
try {
|
||||
@@ -236,7 +234,6 @@ function RemoveInfo() {
|
||||
material_image_url: buildAssetUrl(productImage),
|
||||
idempotency_key: `replication_${Date.now()}`,
|
||||
};
|
||||
console.log(currentSegment,params);
|
||||
|
||||
|
||||
setLoading(true);
|
||||
@@ -245,10 +242,8 @@ function RemoveInfo() {
|
||||
message.success('视频生成任务创建成功');
|
||||
handleCloseDrawer();
|
||||
Removelist(creatID).then((res: any) => {
|
||||
console.log('123123123123',res);
|
||||
|
||||
const targetItem = res.items.find((item: any) => item.id === currentSegment);
|
||||
console.log(targetItem);
|
||||
message.loading('创建中...', 3);
|
||||
|
||||
setTimeout(() => {
|
||||
@@ -355,7 +350,6 @@ function RemoveInfo() {
|
||||
render: (_: any, record: any) => (
|
||||
<div style={{ position: 'relative', width: 120, height: 80, borderRadius: 6, overflow: 'hidden', background: '#f1f5f9', cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
console.log('video clicked:', record.segmentVideoUrl);
|
||||
if (record.segmentVideoUrl) {
|
||||
setPreviewVideoUrl(buildAssetUrl(record.segmentVideoUrl));
|
||||
setPreviewModalVisible(true);
|
||||
|
||||
@@ -31,6 +31,8 @@ function InitialInfo() {
|
||||
// 引擎和视频参数相关状态
|
||||
const [enginesele, setEnginesele] = useState<any>({});
|
||||
const [countType, setCountType] = useState('');
|
||||
// 图片引擎 id:取自 getEngine().engine.image[0].id
|
||||
const [imageEngineId, setImageEngineId] = useState<string>('');
|
||||
const [showEngineModal, setShowEngineModal] = useState(false);
|
||||
const [showVideoSettingsModal, setShowVideoSettingsModal] = useState(false);
|
||||
const [videoDuration, setVideoDuration] = useState(5);
|
||||
@@ -155,6 +157,10 @@ function InitialInfo() {
|
||||
durations: firstEngine.supportedDurations || [5, 8, 10, 12, 15],
|
||||
});
|
||||
}
|
||||
// 默认选中第一个图片引擎
|
||||
if (data.engine?.image && data.engine.image.length > 0) {
|
||||
setImageEngineId(data.engine.image[0].id);
|
||||
}
|
||||
})
|
||||
.catch((error: any) => {
|
||||
});
|
||||
@@ -188,7 +194,6 @@ function InitialInfo() {
|
||||
// 获取任务详情数据
|
||||
useEffect(() => {
|
||||
|
||||
console.log('creatID', creatID);
|
||||
|
||||
if (creatID) {
|
||||
removeDetail(creatID).then((res: any) => {
|
||||
@@ -341,7 +346,7 @@ function InitialInfo() {
|
||||
|
||||
const createimage = (stepId: number) => {
|
||||
let params = {
|
||||
engine_id: "0019e3dac0b795b925b",
|
||||
engine_id: imageEngineId,
|
||||
image_proportion: "1:1",
|
||||
image_px: "2048x2048",
|
||||
image_size: "2K"
|
||||
@@ -355,7 +360,7 @@ function InitialInfo() {
|
||||
const newcreateimage = () => {
|
||||
// console.log('下一步:', stepId);
|
||||
let params = {
|
||||
engine_id: "0019e3dac0b795b925b",
|
||||
engine_id: imageEngineId,
|
||||
image_proportion: "1:1",
|
||||
image_px: "2048x2048",
|
||||
image_size: "2K"
|
||||
|
||||
@@ -31,6 +31,7 @@ interface AppState {
|
||||
durations: number[];
|
||||
};
|
||||
enginesele: any;
|
||||
inputValue: string;
|
||||
|
||||
fetchProjects: () => Promise<void>;
|
||||
createProject: (name: string, industry: Industry) => Promise<Project>;
|
||||
@@ -56,6 +57,7 @@ interface AppState {
|
||||
setVideoAspectRatio: (aspectRatio: string) => void;
|
||||
setVideoResolution: (resolution: string) => void;
|
||||
setEnginesele: (enginesele: any) => void;
|
||||
setInputValue: (inputValue: string) => void;
|
||||
resetGenerationConfig: () => void;
|
||||
}
|
||||
|
||||
@@ -80,6 +82,7 @@ export const useAppStore = create<AppState>((set, get) => ({
|
||||
durations: [5, 8, 10, 12, 15],
|
||||
},
|
||||
enginesele: [],
|
||||
inputValue: '',
|
||||
|
||||
fetchProjects: async () => {
|
||||
set({ loading: true });
|
||||
@@ -168,6 +171,7 @@ export const useAppStore = create<AppState>((set, get) => ({
|
||||
setVideoAspectRatio: (aspectRatio) => set({ videoAspectRatio: aspectRatio }),
|
||||
setVideoResolution: (resolution) => set({ videoResolution: resolution }),
|
||||
setEnginesele: (enginesele) => set({ enginesele }),
|
||||
setInputValue: (inputValue) => set({ inputValue }),
|
||||
resetGenerationConfig: () => set({
|
||||
mediaType: 'image',
|
||||
countType: '请选择',
|
||||
@@ -184,5 +188,6 @@ export const useAppStore = create<AppState>((set, get) => ({
|
||||
durations: [5, 8, 10, 12, 15],
|
||||
},
|
||||
enginesele: [],
|
||||
inputValue: '',
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -1,10 +1,27 @@
|
||||
export interface ResourceCapacity {
|
||||
enabled: boolean;
|
||||
source?: string;
|
||||
hasUserConfig?: boolean;
|
||||
usedBytes: number;
|
||||
availableBytes: number;
|
||||
totalBytes: number;
|
||||
usagePercent: number;
|
||||
exceeded: boolean;
|
||||
limitValue?: string;
|
||||
limitUnit: string; // 'KB' | 'MB' | 'GB' 等
|
||||
}
|
||||
|
||||
export interface User {
|
||||
id: string;
|
||||
username: string;
|
||||
email: string;
|
||||
avatar?: string;
|
||||
credits: number;
|
||||
isAdmin?: boolean;
|
||||
phone?: string;
|
||||
allowedMenus?: string[] | null;
|
||||
mustSetPassword?: boolean;
|
||||
resourceCapacity?: ResourceCapacity;
|
||||
}
|
||||
|
||||
export interface CreditRecord {
|
||||
|
||||
Reference in New Issue
Block a user