This commit is contained in:
2026-06-30 17:06:56 +08:00
27 changed files with 2963 additions and 1465 deletions
+2 -1
View File
@@ -26,4 +26,5 @@ bak/
# 忽略特定类型文件但保留目录
# *.pyc
# !dir/*.pycnode_modules/
*.tmp.*
*.tmp.*
*_上线.py
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+37 -37
View File
@@ -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>
+3 -31
View File
@@ -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;
+12
View File
@@ -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;
// 单位:后端返回的 limitUnitKB / 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
View File
@@ -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);
}
}
+10 -11
View File
@@ -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();
+271 -72
View File
@@ -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;
+62 -101
View File
@@ -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%' }}>
+2 -1
View File
@@ -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>
</>
)}
+181 -114
View File
@@ -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>
);
+40 -30
View File
@@ -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>
);
};
+8 -2
View File
@@ -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"
+9 -2
View File
@@ -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',
+621 -81
View File
@@ -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
View File
@@ -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="请选择默认模板"
-6
View File
@@ -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);
+8 -3
View File
@@ -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"
+5
View File
@@ -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: '',
}),
}));
+17
View File
@@ -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 {