fix(web): restore V21 pages real business integration

- Restore real asset library and asset list loading on V21 assets page
- Restore title library API loading/search/create state for V21 titles page
- Restore edit plan and generation task creation on V21 generation page
- Restore generated video list, download and review actions on V21 results page
- Keep V21 visual structure while making production E2E flow real again
This commit is contained in:
Xiaoxia AI
2026-06-26 07:57:46 +08:00
parent 6511bc24a9
commit e633ff2de7
4 changed files with 474 additions and 140 deletions
+121 -21
View File
@@ -1,23 +1,96 @@
/**
* 素材库页面 - V21 完全对标
* 素材库页面 - V21 外观 + 真实业务接入
*/
import React, { useState } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import { useParams } from 'react-router-dom';
import { message } from 'antd';
import type { AssetItem } from '@/api/assets';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import {
getAssetLibraries,
getAssets,
getProjectAssetDiagnosis,
updateAssetReviewStatus,
uploadAsset,
type AssetItem,
} from '@/api/assets';
import { getProject } from '@/api/projects';
import './ProjectAssets.css';
const ProjectAssets: React.FC = () => {
const { id: projectId } = useParams<{ id: string }>();
const [assets] = useState<AssetItem[]>([]);
const { id } = useParams<{ id: string }>();
const projectId = id || '';
const [libraryId, setLibraryId] = useState('');
const [uploading, setUploading] = useState(false);
const queryClient = useQueryClient();
const projectQuery = useQuery({
queryKey: ['project', projectId],
queryFn: () => getProject(projectId),
enabled: !!projectId,
});
const librariesQuery = useQuery({
queryKey: ['asset-libraries', projectId],
queryFn: () => getAssetLibraries(projectId),
enabled: !!projectId,
refetchInterval: 3000,
});
useEffect(() => {
if (!libraryId && librariesQuery.data?.length) {
setLibraryId(librariesQuery.data[0].id);
}
}, [librariesQuery.data, libraryId]);
const assetsQuery = useQuery({
queryKey: ['assets', libraryId],
queryFn: () => getAssets(libraryId),
enabled: !!libraryId,
refetchInterval: 3000,
});
const diagnosisQuery = useQuery({
queryKey: ['project-asset-diagnosis', projectId],
queryFn: () => getProjectAssetDiagnosis(projectId),
enabled: !!projectId,
refetchInterval: 5000,
});
const reviewMutation = useMutation({
mutationFn: ({ assetId, reviewStatus }: { assetId: string; reviewStatus: 'approved' | 'rejected' }) =>
updateAssetReviewStatus(assetId, reviewStatus),
onSuccess: () => {
message.success('复核状态已更新');
queryClient.invalidateQueries({ queryKey: ['assets', libraryId] });
},
onError: () => message.error('复核状态更新失败'),
});
const assets = useMemo<AssetItem[]>(() => assetsQuery.data || [], [assetsQuery.data]);
const selectedLibrary = librariesQuery.data?.find((library) => library.id === libraryId) || librariesQuery.data?.[0];
const workspaceId = projectQuery.data?.workspace_id || selectedLibrary?.workspace_id || '';
const handleFileUpload = async (files: FileList) => {
if (!files.length || !projectId || !workspaceId || !libraryId) {
message.error('素材库尚未准备好');
return;
}
try {
setUploading(true);
for (const file of Array.from(files)) {
const formData = new FormData();
formData.append('workspace_id', workspaceId);
formData.append('project_id', projectId);
formData.append('library_id', libraryId);
formData.append('file', file);
await uploadAsset(formData);
}
message.success('上传成功');
await assetsQuery.refetch();
await diagnosisQuery.refetch();
} catch (error: any) {
message.error('上传失败');
message.error(error?.message || '上传失败');
} finally {
setUploading(false);
}
@@ -32,11 +105,19 @@ const ProjectAssets: React.FC = () => {
</div>
</div>
{/* 上传区 */}
<div className="xx-card">
<h3>{diagnosisQuery.data?.readiness_label || 'Ready'}</h3>
<p>
{diagnosisQuery.data?.estimated_video_count ?? 0} · {diagnosisQuery.data?.video_assets ?? assets.length}
</p>
{diagnosisQuery.data?.gaps?.[0]?.message && <p>{diagnosisQuery.data.gaps[0].message}</p>}
</div>
<div className="xx-card">
<h3></h3>
<div
className="xx-upload-zone"
data-testid="upload-dropzone"
onDragOver={(e) => e.preventDefault()}
onDrop={(e) => {
e.preventDefault();
@@ -44,7 +125,7 @@ const ProjectAssets: React.FC = () => {
}}
>
<div style={{ fontSize: '32px', marginBottom: '12px' }}>📁</div>
<p></p>
<p></p>
<input
type="file"
multiple
@@ -55,15 +136,14 @@ const ProjectAssets: React.FC = () => {
<button
className="btn primary"
onClick={() => document.getElementById('file-input')?.click()}
disabled={uploading}
disabled={uploading || !libraryId}
>
{uploading ? '上传中...' : '选择文件'}
</button>
</div>
</div>
{/* 素材列表 */}
<div className="xx-card">
<div className="xx-card" data-testid="material-list">
<h3></h3>
{assets.length === 0 ? (
<div className="xx-empty-state">
@@ -71,17 +151,37 @@ const ProjectAssets: React.FC = () => {
</div>
) : (
<div className="xx-video-grid">
{assets.map((asset) => (
<div key={asset.id} className="xx-video-card">
<div className="xx-video-thumb">
<div className="mini-play"></div>
{assets.map((asset) => {
const reviewStatus = String(asset.metadata?.review_status || asset.status || 'pending_review');
return (
<div key={asset.id} className="xx-video-card xx-vertical-card">
<div className="xx-video-thumb">
<div className="mini-play"></div>
</div>
<div className="row">
<b>{asset.name}</b>
</div>
<p>{asset.mime_type || '视频素材'}</p>
<span className={`xx-pill ${reviewStatus === 'approved' ? 'ok' : ''}`}>
{reviewStatus === 'approved' ? '已通过' : reviewStatus}
</span>
<div style={{ display: 'flex', gap: 8, marginTop: 12 }}>
<button
className="btn secondary"
onClick={() => reviewMutation.mutate({ assetId: asset.id, reviewStatus: 'approved' })}
>
</button>
<button
className="btn secondary"
onClick={() => reviewMutation.mutate({ assetId: asset.id, reviewStatus: 'rejected' })}
>
</button>
</div>
</div>
<div className="row">
<b>{asset.name}</b>
</div>
<p></p>
</div>
))}
);
})}
</div>
)}
</div>
@@ -1,35 +1,124 @@
/**
* 视频剪辑/生成页面 - V21 完全对标
* 视频剪辑/生成页面 - V21 外观 + 真实业务接入
*/
import React, { useState } from 'react';
import React, { useMemo, useState } from 'react';
import { useParams } from 'react-router-dom';
import { Select, Modal, Form, message } from 'antd';
import { Select, message } from 'antd';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { getAssetLibraries, getAssets, getProjectAssetDiagnosis } from '@/api/assets';
import { createEditPlan, type EditPlanItem } from '@/api/editPlans';
import { createGenerationTask, getGenerationTask, type GenerationTaskItem } from '@/api/generation';
import { getProjectTitles } from '@/api/projectTitles';
import { getProject } from '@/api/projects';
import './ProjectGeneration.css';
const TITLE_OPTIONS = [
{ id: '1', name: '产品介绍', content: '完整介绍产品功能和优势' },
{ id: '2', name: '用户评价', content: '展示用户真实反馈和评分' },
{ id: '3', name: '功能演示', content: '详细演示产品各项功能' },
];
const ProjectGeneration: React.FC = () => {
const { id: projectId } = useParams<{ id: string }>();
const [selectedTitle, setSelectedTitle] = useState<string>('');
const [generateOpen, setGenerateOpen] = useState(false);
const [form] = Form.useForm();
const { id } = useParams<{ id: string }>();
const projectId = id || '';
const [selectedLibraryId, setSelectedLibraryId] = useState('');
const [selectedTitleId, setSelectedTitleId] = useState('');
const [editPlan, setEditPlan] = useState<EditPlanItem | null>(null);
const [taskId, setTaskId] = useState('');
const queryClient = useQueryClient();
const handleGenerate = async () => {
const values = await form.validateFields();
try {
const projectQuery = useQuery({
queryKey: ['project', projectId],
queryFn: () => getProject(projectId),
enabled: !!projectId,
});
const librariesQuery = useQuery({
queryKey: ['asset-libraries', projectId],
queryFn: () => getAssetLibraries(projectId),
enabled: !!projectId,
});
const titlesQuery = useQuery({
queryKey: ['project-titles', projectId],
queryFn: () => getProjectTitles(projectId, true),
enabled: !!projectId,
});
const assetsQuery = useQuery({
queryKey: ['assets', selectedLibraryId],
queryFn: () => getAssets(selectedLibraryId),
enabled: !!selectedLibraryId,
refetchInterval: 3000,
});
const diagnosisQuery = useQuery({
queryKey: ['project-asset-diagnosis', projectId],
queryFn: () => getProjectAssetDiagnosis(projectId),
enabled: !!projectId,
});
const taskQuery = useQuery({
queryKey: ['generation-task', taskId],
queryFn: () => getGenerationTask(taskId),
enabled: !!taskId,
refetchInterval: (query) => {
const status = query.state.data?.status;
return status === 'completed' || status === 'failed' || status === 'cancelled' ? false : 2000;
},
});
const selectedLibrary = librariesQuery.data?.find((library) => library.id === selectedLibraryId);
const workspaceId = projectQuery.data?.workspace_id || selectedLibrary?.workspace_id || '';
const assets = useMemo(() => assetsQuery.data || [], [assetsQuery.data]);
const createPlanMutation = useMutation({
mutationFn: () =>
createEditPlan({
projectId,
data: {
workspace_id: workspaceId,
asset_library_id: selectedLibraryId,
title_id: selectedTitleId,
},
}),
onSuccess: (plan) => {
setEditPlan(plan);
message.success('剪辑计划已生成');
},
onError: () => message.error('生成剪辑计划失败'),
});
const createTaskMutation = useMutation({
mutationFn: async (): Promise<GenerationTaskItem> => {
const plan = editPlan || (await createPlanMutation.mutateAsync());
return createGenerationTask({
workspace_id: workspaceId,
project_id: projectId,
asset_library_id: selectedLibraryId,
strategy_id: selectedTitleId,
edit_plan_id: plan.id,
});
},
onSuccess: (task) => {
setTaskId(task.id);
message.success('成片生成任务已创建');
setGenerateOpen(false);
form.resetFields();
setSelectedTitle('');
} catch (error: any) {
message.error('创建生成任务失败');
queryClient.invalidateQueries({ queryKey: ['generation-task', task.id] });
},
onError: () => message.error('创建生成任务失败'),
});
const handleCreatePlan = () => {
if (!workspaceId || !selectedLibraryId || !selectedTitleId) {
message.error('请选择素材库和标题');
return;
}
createPlanMutation.mutate();
};
const taskStatusText = (() => {
const status = taskQuery.data?.status;
if (status === 'completed') return '生成完成';
if (status === 'failed') return '生成失败';
if (status === 'running') return '生成中';
if (status === 'pending') return '等待中';
return '未开始';
})();
return (
<div className="xx-project-generation">
<div className="xx-page-head">
@@ -37,15 +126,51 @@ const ProjectGeneration: React.FC = () => {
<h2></h2>
<p></p>
</div>
<button className="btn primary" onClick={() => setGenerateOpen(true)}>
+
</div>
<div className="xx-card" data-testid="generation-config-section">
<h3></h3>
<div style={{ display: 'grid', gap: 16, gridTemplateColumns: 'repeat(auto-fit, minmax(240px, 1fr))' }}>
<div>
<p></p>
<Select
style={{ width: '100%' }}
value={selectedLibraryId || undefined}
placeholder="选择素材库"
onChange={(value) => {
setSelectedLibraryId(value);
setEditPlan(null);
}}
options={(librariesQuery.data || []).map((library) => ({
value: library.id,
label: `${library.name} (${library.kind})`,
}))}
/>
</div>
<div>
<p></p>
<Select
style={{ width: '100%' }}
value={selectedTitleId || undefined}
placeholder="选择标题"
onChange={(value) => {
setSelectedTitleId(value);
setEditPlan(null);
}}
options={(titlesQuery.data || []).map((title) => ({ value: title.id, label: title.text }))}
/>
</div>
</div>
<p style={{ marginTop: 16 }}>{diagnosisQuery.data?.readiness_label || 'Ready'}</p>
<button className="btn primary" onClick={handleCreatePlan} disabled={!selectedLibraryId || !selectedTitleId || createPlanMutation.isPending}>
</button>
</div>
<div className="xx-card">
<h3></h3>
<div className="xx-template-grid">
{TITLE_OPTIONS.map((title, idx) => {
{(titlesQuery.data || []).slice(0, 4).map((title, idx) => {
const colors = [
'linear-gradient(135deg,#4f46e5,#818cf8)',
'linear-gradient(135deg,#f97316,#ef4444)',
@@ -55,54 +180,40 @@ const ProjectGeneration: React.FC = () => {
return (
<div
key={title.id}
className={`xx-choice ${selectedTitle === title.id ? 'selected' : ''}`}
onClick={() => setSelectedTitle(title.id)}
className={`xx-choice ${selectedTitleId === title.id ? 'selected' : ''}`}
onClick={() => setSelectedTitleId(title.id)}
>
<div className="cover" style={{ background: colors[idx % 4] }}>
📹
</div>
{selectedTitle === title.id && <div className="check"></div>}
<b>{title.name}</b>
<p>{title.content.substring(0, 50)}...</p>
<div className="cover" style={{ background: colors[idx % 4] }}>📹</div>
{selectedTitleId === title.id && <div className="check"></div>}
<b></b>
<p>使{title.usage_count}</p>
</div>
);
})}
</div>
</div>
<Modal
title="生成新成片"
open={generateOpen}
okText="开始生成"
cancelText="取消"
onOk={handleGenerate}
onCancel={() => setGenerateOpen(false)}
>
<Form form={form} layout="vertical">
<Form.Item
name="template_id"
label="选择模板"
rules={[{ required: true, message: '请选择模板' }]}
>
<Select placeholder="选择剪辑模板">
<Select.Option value="basic"></Select.Option>
<Select.Option value="product"></Select.Option>
<Select.Option value="lifestyle"></Select.Option>
</Select>
</Form.Item>
<Form.Item
name="duration"
label="视频时长"
rules={[{ required: true, message: '请选择时长' }]}
>
<Select placeholder="选择时长">
<Select.Option value="15">15</Select.Option>
<Select.Option value="30">30</Select.Option>
<Select.Option value="60">60</Select.Option>
</Select>
</Form.Item>
</Form>
</Modal>
<aside className="xx-card" role="complementary">
<h3></h3>
<p> {editPlan?.clips?.length || assets.length} </p>
{(editPlan?.clips || assets.map((asset, index) => ({ id: asset.id, asset_name: asset.name, sequence: index + 1, reason: '自动选择' }))).map((clip) => (
<div key={clip.id} className="xx-title-row">
<div>
<b>{clip.asset_name}</b>
<p>{clip.reason || '自动选择'}</p>
</div>
</div>
))}
<button
className="btn primary"
onClick={() => createTaskMutation.mutate()}
disabled={!selectedLibraryId || !selectedTitleId || createTaskMutation.isPending}
>
</button>
<p>{taskStatusText}</p>
{taskQuery.data?.error_message && <p>{taskQuery.data.error_message}</p>}
</aside>
</div>
);
};
+89 -16
View File
@@ -1,13 +1,65 @@
/**
* 成片库页面 - V21 完全对标
* 成片库页面 - V21 外观 + 真实业务接入
*/
import React, { useState } from 'react';
import React from 'react';
import { useParams } from 'react-router-dom';
import { message } from 'antd';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import {
getGeneratedVideoDownloadUrl,
getGeneratedVideos,
updateGeneratedVideoReviewStatus,
type GeneratedVideoItem,
} from '@/api/generation';
import './ProjectResults.css';
const reviewLabel = (status?: string) => {
if (status === 'approved') return '可发布';
if (status === 'rejected') return '已驳回';
return '待复核';
};
const ProjectResults: React.FC = () => {
const { id: projectId } = useParams<{ id: string }>();
const [videos] = useState<any[]>([]);
const { id } = useParams<{ id: string }>();
const projectId = id || '';
const queryClient = useQueryClient();
const videosQuery = useQuery({
queryKey: ['generated-videos', projectId],
queryFn: () => getGeneratedVideos(projectId),
enabled: !!projectId,
refetchInterval: 5000,
});
const reviewMutation = useMutation({
mutationFn: ({ videoId, reviewStatus }: { videoId: string; reviewStatus: 'approved' | 'rejected' | 'pending_review' }) =>
updateGeneratedVideoReviewStatus(videoId, reviewStatus),
onSuccess: () => {
message.success('成片复核状态已更新');
queryClient.invalidateQueries({ queryKey: ['generated-videos', projectId] });
},
onError: () => message.error('成片复核状态更新失败'),
});
const handleDownload = async (video: GeneratedVideoItem) => {
try {
const url = await getGeneratedVideoDownloadUrl(video.id);
window.open(url, '_blank', 'noopener,noreferrer');
} catch (error) {
message.error('获取下载地址失败');
}
};
const handleBatchDownloadUrls = async () => {
try {
await Promise.all((videosQuery.data || []).map((video) => getGeneratedVideoDownloadUrl(video.id)));
message.success('批量获取下载地址完成');
} catch (error) {
message.error('批量获取下载地址失败');
}
};
const videos = videosQuery.data || [];
return (
<div className="xx-project-results">
@@ -16,29 +68,50 @@ const ProjectResults: React.FC = () => {
<h2></h2>
<p></p>
</div>
<button className="btn secondary" onClick={handleBatchDownloadUrls} disabled={videos.length === 0}>
</button>
</div>
{videos.length === 0 ? (
<div className="xx-empty-state">
<div style={{ fontSize: '48px', marginBottom: '16px' }}>🎬</div>
<p></p>
<p>{videosQuery.isLoading ? '成片加载中...' : '还没有生成任何成片'}</p>
</div>
) : (
<div className="xx-card">
<h3></h3>
<div className="xx-vertical-grid compact">
{videos.map((video: any) => (
<div key={video.id} className="xx-vertical-card">
<div className="xx-vertical-thumb">
<button className="mini-play"></button>
{videos.map((video) => {
const status = video.review_status || 'pending_review';
return (
<div key={video.id} className="xx-vertical-card">
<div className="xx-vertical-thumb">
<button className="mini-play"></button>
</div>
<div className="row">
<b>{video.name}</b>
<span className={`xx-pill ${status === 'approved' ? 'ok' : ''}`}>{reviewLabel(status)}</span>
</div>
<p>{video.duration || 0}s · 9:16 · {Math.round((video.file_size || 0) / 1024 / 1024)}MB</p>
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
<button className="btn secondary" onClick={() => handleDownload(video)}></button>
<button
className="btn secondary"
onClick={() => reviewMutation.mutate({ videoId: video.id, reviewStatus: 'approved' })}
>
</button>
<button
className="btn secondary"
onClick={() => reviewMutation.mutate({ videoId: video.id, reviewStatus: 'rejected' })}
>
</button>
</div>
</div>
<div className="row">
<b>{video.name}</b>
<span className="pill ok"></span>
</div>
<p>{video.duration || 0}s · 9:16 · {Math.round(video.file_size / 1024 / 1024)}MB</p>
</div>
))}
);
})}
</div>
</div>
)}
+88 -38
View File
@@ -1,33 +1,76 @@
/**
* 标题库页面 - V21 完全对标
* 标题库页面 - V21 外观 + 真实业务接入
*/
import React, { useState } from 'react';
import React, { useMemo, useState } from 'react';
import { useParams } from 'react-router-dom';
import { Input, Modal, Form, message } from 'antd';
import { Input, Modal, Form, message, Switch, Select } from 'antd';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { createProjectTitle, getProjectTitles, updateProjectTitle, type ProjectTitleItem } from '@/api/projectTitles';
import { getProject } from '@/api/projects';
import './ProjectTitles.css';
const categoryOptions = [
{ label: '默认', value: 'default' },
{ label: '营销', value: 'marketing' },
{ label: '教程', value: 'tutorial' },
{ label: '故事', value: 'story' },
{ label: '促销', value: 'promo' },
];
const ProjectTitles: React.FC = () => {
const { id: projectId } = useParams<{ id: string }>();
const [titles] = useState<any[]>([]);
const { id } = useParams<{ id: string }>();
const projectId = id || '';
const [searchText, setSearchText] = useState('');
const [createOpen, setCreateOpen] = useState(false);
const [form] = Form.useForm();
const [form] = Form.useForm<{ text: string; category: ProjectTitleItem['category']; favorite: boolean }>();
const queryClient = useQueryClient();
const handleCreate = async () => {
const values = await form.validateFields();
try {
const projectQuery = useQuery({
queryKey: ['project', projectId],
queryFn: () => getProject(projectId),
enabled: !!projectId,
});
const titlesQuery = useQuery({
queryKey: ['project-titles', projectId],
queryFn: () => getProjectTitles(projectId),
enabled: !!projectId,
refetchInterval: 3000,
});
const createMutation = useMutation({
mutationFn: (values: { text: string; category: ProjectTitleItem['category']; favorite: boolean }) =>
createProjectTitle(projectId, {
workspace_id: projectQuery.data!.workspace_id,
text: values.text,
category: values.category,
favorite: values.favorite,
}),
onSuccess: () => {
message.success('标题创建成功');
setCreateOpen(false);
form.resetFields();
} catch (error: any) {
message.error('创建失败');
}
};
queryClient.invalidateQueries({ queryKey: ['project-titles', projectId] });
},
onError: () => message.error('创建失败'),
});
const filtered = titles.filter((t: any) =>
t.name.toLowerCase().includes(searchText.toLowerCase()) ||
t.content.toLowerCase().includes(searchText.toLowerCase())
);
const toggleMutation = useMutation({
mutationFn: ({ titleId, favorite }: { titleId: string; favorite: boolean }) =>
updateProjectTitle(titleId, { favorite }),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['project-titles', projectId] }),
onError: () => message.error('标题状态更新失败'),
});
const filtered = useMemo(() => {
const keyword = searchText.trim().toLowerCase();
return (titlesQuery.data || []).filter((title) => !keyword || title.text.toLowerCase().includes(keyword));
}, [searchText, titlesQuery.data]);
const handleCreate = async () => {
const values = await form.validateFields();
createMutation.mutate(values);
};
return (
<div className="xx-project-titles">
@@ -36,7 +79,7 @@ const ProjectTitles: React.FC = () => {
<h2></h2>
<p></p>
</div>
<button className="btn primary" onClick={() => setCreateOpen(true)}>
<button className="btn primary" data-testid="create-title-btn" onClick={() => setCreateOpen(true)}>
+
</button>
</div>
@@ -44,7 +87,12 @@ const ProjectTitles: React.FC = () => {
<div className="xx-card">
<div className="xx-search-bar">
<Input
placeholder="搜索标题..."
placeholder="例如:3 秒抓住注意力,30 秒讲清卖点"
style={{ padding: '10px 14px', borderRadius: '12px' }}
/>
<Input
data-testid="title-search-input"
placeholder="搜索标题"
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
style={{ padding: '10px 14px', borderRadius: '12px' }}
@@ -57,11 +105,20 @@ const ProjectTitles: React.FC = () => {
</div>
) : (
<div className="xx-title-list">
{filtered.map((title: any) => (
{filtered.map((title) => (
<div key={title.id} className="xx-title-row">
<div>
<b>{title.name}</b>
<p>{title.content}</p>
<b>{title.text}</b>
<p>使{title.usage_count}</p>
</div>
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
{title.favorite && <span className="xx-pill warn"></span>}
<Switch
checked={title.favorite}
checkedChildren="常用"
unCheckedChildren="普通"
onChange={(checked) => toggleMutation.mutate({ titleId: title.id, favorite: checked })}
/>
</div>
</div>
))}
@@ -74,26 +131,19 @@ const ProjectTitles: React.FC = () => {
open={createOpen}
okText="创建"
cancelText="取消"
confirmLoading={createMutation.isPending}
onOk={handleCreate}
onCancel={() => setCreateOpen(false)}
>
<Form form={form} layout="vertical">
<Form.Item
name="name"
label="标题"
rules={[{ required: true, message: '请输入标题' }]}
>
<Input placeholder="例如:产品介绍" />
<Form form={form} layout="vertical" initialValues={{ category: 'default', favorite: false }}>
<Form.Item name="text" label="标题" rules={[{ required: true, message: '请输入标题' }]}>
<Input data-testid="title-content-input" placeholder="例如:3 秒抓住注意力,30 秒讲清卖点" />
</Form.Item>
<Form.Item
name="content"
label="标题内容"
rules={[{ required: true, message: '请输入标题内容' }]}
>
<Input.TextArea
placeholder="例如:完整介绍产品功能和优势"
rows={4}
/>
<Form.Item name="category" label="分类">
<Select options={categoryOptions} />
</Form.Item>
<Form.Item name="favorite" valuePropName="checked" label="常用标题">
<Switch checkedChildren="常用" unCheckedChildren="普通" />
</Form.Item>
</Form>
</Modal>