fix(web): connect project context in media flow
This commit is contained in:
@@ -12,6 +12,10 @@ export interface AssetItem {
|
||||
storage_key: string;
|
||||
mime_type: string;
|
||||
metadata: Record<string, unknown>;
|
||||
file_size?: number;
|
||||
status?: string;
|
||||
classification_status?: string | null;
|
||||
quality_score?: number | null;
|
||||
}
|
||||
|
||||
export interface AssetLibraryItem {
|
||||
@@ -20,6 +24,8 @@ export interface AssetLibraryItem {
|
||||
project_id: string;
|
||||
name: string;
|
||||
kind: 'video' | 'voice' | 'image';
|
||||
asset_count?: number;
|
||||
total_size?: number;
|
||||
}
|
||||
|
||||
export interface IngestJob {
|
||||
@@ -55,7 +61,7 @@ export const createAssetLibrary = async (data: {
|
||||
workspace_id: string;
|
||||
project_id: string;
|
||||
name: string;
|
||||
kind: 'video' | 'voice';
|
||||
kind: 'video' | 'voice' | 'image';
|
||||
}): Promise<AssetLibraryItem> => {
|
||||
const response = await apiClient.post('/asset-libraries', data);
|
||||
return response.data;
|
||||
|
||||
@@ -5,12 +5,12 @@ export interface GenerationTaskItem {
|
||||
workspace_id: string;
|
||||
project_id: string;
|
||||
asset_library_id: string;
|
||||
strategy_id: string;
|
||||
voice_library_id: string;
|
||||
strategy_id?: string | null;
|
||||
voice_library_id?: string | null;
|
||||
status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled';
|
||||
progress: number;
|
||||
result_count: number;
|
||||
error_message: string;
|
||||
error_message?: string | null;
|
||||
}
|
||||
|
||||
export interface GeneratedVideoItem {
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import apiClient from './client';
|
||||
|
||||
export interface ProjectItem {
|
||||
id: string;
|
||||
workspace_id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface CreateProjectRequest {
|
||||
workspace_id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export const getProjects = async (workspaceId: string): Promise<ProjectItem[]> => {
|
||||
const response = await apiClient.get('/projects', { params: { workspace_id: workspaceId } });
|
||||
return response.data.items;
|
||||
};
|
||||
|
||||
export const createProject = async (data: CreateProjectRequest): Promise<ProjectItem> => {
|
||||
const response = await apiClient.post('/projects', data);
|
||||
return response.data;
|
||||
};
|
||||
@@ -39,6 +39,13 @@ export interface InviteMemberRequest {
|
||||
role: 'admin' | 'member' | 'viewer';
|
||||
}
|
||||
|
||||
export interface InviteMemberResponse {
|
||||
invitation_id: string;
|
||||
invitee_email: string;
|
||||
role: 'admin' | 'member' | 'viewer';
|
||||
expires_at: string;
|
||||
}
|
||||
|
||||
type WorkspaceApiResponse = Omit<Workspace, 'id' | 'workspace_id'> & {
|
||||
id?: string;
|
||||
workspace_id?: string;
|
||||
@@ -60,10 +67,10 @@ const normalizeWorkspace = (workspace: WorkspaceApiResponse): Workspace => {
|
||||
};
|
||||
|
||||
const normalizeMember = (member: WorkspaceMemberApiResponse): WorkspaceMember => {
|
||||
const memberId = member.id || member.member_id || member.user_id || '';
|
||||
const memberId = member.member_id || member.id || '';
|
||||
return {
|
||||
...member,
|
||||
id: memberId,
|
||||
id: member.user_id || memberId,
|
||||
member_id: memberId,
|
||||
};
|
||||
};
|
||||
@@ -97,7 +104,7 @@ export const getMembers = async (workspaceId: string): Promise<WorkspaceMember[]
|
||||
export const inviteMember = async (
|
||||
workspaceId: string,
|
||||
data: InviteMemberRequest
|
||||
): Promise<{ message: string }> => {
|
||||
): Promise<InviteMemberResponse> => {
|
||||
const response = await apiClient.post(`/workspaces/${workspaceId}/members/invite`, data);
|
||||
return response.data;
|
||||
};
|
||||
@@ -105,23 +112,21 @@ export const inviteMember = async (
|
||||
export const removeMember = async (
|
||||
workspaceId: string,
|
||||
memberId: string
|
||||
): Promise<{ message: string }> => {
|
||||
const response = await apiClient.delete(`/workspaces/${workspaceId}/members/${memberId}`);
|
||||
return response.data;
|
||||
): Promise<void> => {
|
||||
await apiClient.delete(`/workspaces/${workspaceId}/members/${memberId}`);
|
||||
};
|
||||
|
||||
export const updateMemberRole = async (
|
||||
workspaceId: string,
|
||||
memberId: string,
|
||||
role: 'admin' | 'member' | 'viewer'
|
||||
): Promise<{ message: string }> => {
|
||||
const response = await apiClient.patch(`/workspaces/${workspaceId}/members/${memberId}`, {
|
||||
): Promise<{ user_id: string; old_role: string; new_role: string }> => {
|
||||
const response = await apiClient.patch(`/workspaces/${workspaceId}/members/${memberId}/role`, {
|
||||
role,
|
||||
});
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const leaveWorkspace = async (workspaceId: string): Promise<{ message: string }> => {
|
||||
const response = await apiClient.post(`/workspaces/${workspaceId}/leave`);
|
||||
return response.data;
|
||||
export const leaveWorkspace = async (_workspaceId: string): Promise<{ message: string }> => {
|
||||
throw new Error('主动退出工作空间暂未开放');
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useLocation, useParams } from 'react-router-dom';
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
@@ -81,9 +81,12 @@ const defaultBatchProgress: BatchProgressState = {
|
||||
|
||||
const ProjectAssets: React.FC = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const location = useLocation();
|
||||
const projectId = id || '';
|
||||
const routedWorkspaceId = (location.state as { workspaceId?: string } | null)?.workspaceId || '';
|
||||
const storedWorkspaceId = projectId ? sessionStorage.getItem(`project-workspace:${projectId}`) || '' : '';
|
||||
const workspaceId = routedWorkspaceId || storedWorkspaceId;
|
||||
const [libraryId, setLibraryId] = useState<string>('');
|
||||
const [workspaceId, setWorkspaceId] = useState('demo-workspace');
|
||||
const [createLibraryOpen, setCreateLibraryOpen] = useState(false);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [ingestJobId, setIngestJobId] = useState<string>('');
|
||||
@@ -98,6 +101,12 @@ const ProjectAssets: React.FC = () => {
|
||||
const [batchFailedIds, setBatchFailedIds] = useState<string[]>([]);
|
||||
const [form] = Form.useForm();
|
||||
|
||||
useEffect(() => {
|
||||
if (projectId && routedWorkspaceId) {
|
||||
sessionStorage.setItem(`project-workspace:${projectId}`, routedWorkspaceId);
|
||||
}
|
||||
}, [projectId, routedWorkspaceId]);
|
||||
|
||||
const librariesQuery = useQuery({
|
||||
queryKey: ['asset-libraries', projectId],
|
||||
queryFn: () => getAssetLibraries(projectId),
|
||||
@@ -191,7 +200,9 @@ const ProjectAssets: React.FC = () => {
|
||||
if (pendingCount === 0) {
|
||||
assetsQuery.refetch();
|
||||
}
|
||||
} catch {
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '批量分类轮询失败');
|
||||
setBatchProgress((current) => ({ ...current, running: false, tracking: false }));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -213,7 +224,7 @@ const ProjectAssets: React.FC = () => {
|
||||
setLibraryId(library.id);
|
||||
form.resetFields();
|
||||
},
|
||||
onError: () => message.error('素材库创建失败'),
|
||||
onError: (error: any) => message.error(error.response?.data?.detail || '素材库创建失败'),
|
||||
});
|
||||
|
||||
const classificationMutation = useMutation({
|
||||
@@ -222,9 +233,9 @@ const ProjectAssets: React.FC = () => {
|
||||
setClassificationJobId(job.id);
|
||||
message.info('已发起素材分类任务');
|
||||
},
|
||||
onError: () => {
|
||||
onError: (error) => {
|
||||
setClassifyingAssetId('');
|
||||
message.error('发起分类失败');
|
||||
message.error((error as any).response?.data?.detail || '发起分类失败');
|
||||
},
|
||||
});
|
||||
|
||||
@@ -397,7 +408,7 @@ const ProjectAssets: React.FC = () => {
|
||||
} catch (error) {
|
||||
setUploading(false);
|
||||
onError(error);
|
||||
message.error('上传失败');
|
||||
message.error((error as any).response?.data?.detail || '上传失败');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -436,6 +447,32 @@ const ProjectAssets: React.FC = () => {
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
{!workspaceId && (
|
||||
<Alert
|
||||
style={{ marginBottom: 16 }}
|
||||
type="error"
|
||||
showIcon
|
||||
message="缺少工作空间上下文"
|
||||
description="请从工作空间详情页进入项目素材管理,不要直接打开项目地址。"
|
||||
/>
|
||||
)}
|
||||
|
||||
{librariesQuery.isError && (
|
||||
<Alert style={{ marginBottom: 16 }} type="error" showIcon message="素材库加载失败" />
|
||||
)}
|
||||
|
||||
{assetsQuery.isError && (
|
||||
<Alert style={{ marginBottom: 16 }} type="error" showIcon message="素材列表加载失败" />
|
||||
)}
|
||||
|
||||
{ingestJobQuery.isError && (
|
||||
<Alert style={{ marginBottom: 16 }} type="error" showIcon message="导入任务查询失败" />
|
||||
)}
|
||||
|
||||
{classificationJobQuery.isError && (
|
||||
<Alert style={{ marginBottom: 16 }} type="error" showIcon message="分类任务查询失败" />
|
||||
)}
|
||||
|
||||
{ingestJobQuery.data && (
|
||||
<Alert
|
||||
style={{ marginBottom: 16 }}
|
||||
@@ -467,7 +504,7 @@ const ProjectAssets: React.FC = () => {
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<Dragger name="file" multiple={false} customRequest={customUpload} showUploadList={false} disabled={uploading} style={{ marginBottom: 24 }}>
|
||||
<Dragger name="file" multiple={false} customRequest={customUpload} showUploadList={false} disabled={uploading || !workspaceId} style={{ marginBottom: 24 }}>
|
||||
<p className="ant-upload-drag-icon"><InboxOutlined /></p>
|
||||
<p className="ant-upload-text">点击或拖拽文件到这里上传素材</p>
|
||||
<p className="ant-upload-hint">支持视频、音频、图片文件;上传后自动进入导入任务</p>
|
||||
@@ -488,7 +525,7 @@ const ProjectAssets: React.FC = () => {
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Modal title="新建素材库" open={createLibraryOpen} onCancel={() => setCreateLibraryOpen(false)} onOk={() => form.submit()} confirmLoading={createLibraryMutation.isPending}>
|
||||
<Modal title="新建素材库" open={createLibraryOpen} onCancel={() => setCreateLibraryOpen(false)} onOk={() => form.submit()} confirmLoading={createLibraryMutation.isPending} okButtonProps={{ disabled: !workspaceId }}>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
@@ -501,10 +538,9 @@ const ProjectAssets: React.FC = () => {
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Form.Item label="Workspace ID"><Input value={workspaceId} onChange={(e) => setWorkspaceId(e.target.value)} /></Form.Item>
|
||||
<Form.Item label="素材库名称" name="name" rules={[{ required: true, message: '请输入素材库名称' }]}><Input placeholder="例如:项目视频库" /></Form.Item>
|
||||
<Form.Item label="素材库类型" name="kind" initialValue="video" rules={[{ required: true }]}>
|
||||
<Select options={[{ label: '视频库', value: 'video' }, { label: '配音库', value: 'voice' }]} />
|
||||
<Select options={[{ label: '视频库', value: 'video' }, { label: '配音库', value: 'voice' }, { label: '图片库', value: 'image' }]} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
@@ -1,17 +1,28 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { useLocation, useParams } from 'react-router-dom';
|
||||
import { Alert, Button, Card, Form, Input, Select, Space, Typography, message } from 'antd';
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import { createGenerationTask, getGenerationResults, getGenerationTask } from '@/api/generation';
|
||||
import { getAssetLibraries } from '@/api/assets';
|
||||
import { useAuthStore } from '@/store/authStore';
|
||||
|
||||
const ProjectGeneration: React.FC = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const location = useLocation();
|
||||
const user = useAuthStore((state) => state.user);
|
||||
const projectId = id || '';
|
||||
const [workspaceId, setWorkspaceId] = useState('demo-workspace');
|
||||
const routedWorkspaceId = (location.state as { workspaceId?: string } | null)?.workspaceId || '';
|
||||
const storedWorkspaceId = projectId ? sessionStorage.getItem(`project-workspace:${projectId}`) || '' : '';
|
||||
const workspaceId = routedWorkspaceId || storedWorkspaceId;
|
||||
const [taskId, setTaskId] = useState('');
|
||||
const [form] = Form.useForm();
|
||||
|
||||
useEffect(() => {
|
||||
if (projectId && routedWorkspaceId) {
|
||||
sessionStorage.setItem(`project-workspace:${projectId}`, routedWorkspaceId);
|
||||
}
|
||||
}, [projectId, routedWorkspaceId]);
|
||||
|
||||
const librariesQuery = useQuery({
|
||||
queryKey: ['generation-libraries', projectId],
|
||||
queryFn: () => getAssetLibraries(projectId),
|
||||
@@ -40,9 +51,15 @@ const ProjectGeneration: React.FC = () => {
|
||||
setTaskId(task.id);
|
||||
message.success('生成任务已创建');
|
||||
},
|
||||
onError: () => message.error('创建生成任务失败'),
|
||||
onError: (error: any) => message.error(error.response?.data?.detail || '创建生成任务失败'),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (taskQuery.data?.status === 'completed') {
|
||||
resultsQuery.refetch();
|
||||
}
|
||||
}, [taskQuery.data?.status, resultsQuery]);
|
||||
|
||||
const assetLibraryOptions = useMemo(
|
||||
() => (librariesQuery.data || []).filter((item) => item.kind === 'video' || item.kind === 'image').map((item) => ({ label: `${item.name} (${item.kind})`, value: item.id })),
|
||||
[librariesQuery.data]
|
||||
@@ -56,6 +73,28 @@ const ProjectGeneration: React.FC = () => {
|
||||
return (
|
||||
<div style={{ padding: 24 }}>
|
||||
<Card title="项目生成任务">
|
||||
{(!workspaceId || !user?.id) && (
|
||||
<Alert
|
||||
style={{ marginBottom: 16 }}
|
||||
type="error"
|
||||
showIcon
|
||||
message="缺少生成上下文"
|
||||
description="请从工作空间详情页进入项目生成,并确认登录态有效。"
|
||||
/>
|
||||
)}
|
||||
|
||||
{librariesQuery.isError && (
|
||||
<Alert style={{ marginBottom: 16 }} type="error" showIcon message="素材库加载失败" />
|
||||
)}
|
||||
|
||||
{taskQuery.isError && (
|
||||
<Alert style={{ marginBottom: 16 }} type="error" showIcon message="生成任务查询失败" />
|
||||
)}
|
||||
|
||||
{resultsQuery.isError && (
|
||||
<Alert style={{ marginBottom: 16 }} type="error" showIcon message="生成结果查询失败" />
|
||||
)}
|
||||
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
@@ -66,13 +105,10 @@ const ProjectGeneration: React.FC = () => {
|
||||
asset_library_id: values.asset_library_id,
|
||||
voice_library_id: values.voice_library_id || '',
|
||||
strategy_id: values.strategy_id || '',
|
||||
created_by_user_id: values.created_by_user_id || '',
|
||||
created_by_user_id: user?.id || '',
|
||||
})
|
||||
}
|
||||
>
|
||||
<Form.Item label="Workspace ID">
|
||||
<Input value={workspaceId} onChange={(e) => setWorkspaceId(e.target.value)} />
|
||||
</Form.Item>
|
||||
<Form.Item label="素材库" name="asset_library_id" rules={[{ required: true, message: '请选择素材库' }]}>
|
||||
<Select options={assetLibraryOptions} placeholder="选择视频/图片素材库" loading={librariesQuery.isLoading} />
|
||||
</Form.Item>
|
||||
@@ -82,11 +118,8 @@ const ProjectGeneration: React.FC = () => {
|
||||
<Form.Item label="策略 ID" name="strategy_id">
|
||||
<Input placeholder="可选:例如 default-strategy" />
|
||||
</Form.Item>
|
||||
<Form.Item label="创建者 ID" name="created_by_user_id">
|
||||
<Input placeholder="可选:例如 user-1" />
|
||||
</Form.Item>
|
||||
<Space>
|
||||
<Button type="primary" htmlType="submit" loading={generationMutation.isPending}>发起生成</Button>
|
||||
<Button type="primary" htmlType="submit" loading={generationMutation.isPending} disabled={!workspaceId || !user?.id}>发起生成</Button>
|
||||
</Space>
|
||||
</Form>
|
||||
|
||||
@@ -103,7 +136,12 @@ const ProjectGeneration: React.FC = () => {
|
||||
<Card size="small" style={{ marginTop: 16 }} title="最新生成结果">
|
||||
<Space direction="vertical">
|
||||
{resultsQuery.data.map((item) => (
|
||||
<Typography.Text key={item.id}>{item.name} - {item.file_url}</Typography.Text>
|
||||
<Space key={item.id}>
|
||||
<Typography.Text>{item.name}</Typography.Text>
|
||||
<Button type="link" href={item.file_url} target="_blank" rel="noreferrer">
|
||||
下载/查看
|
||||
</Button>
|
||||
</Space>
|
||||
))}
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
@@ -3,11 +3,12 @@
|
||||
*/
|
||||
import React, { useState } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { Tabs, Button, Descriptions, Progress, Card, Row, Col } from 'antd';
|
||||
import { Tabs, Button, Descriptions, Progress, Card, Row, Col, List, Modal, Form, Input, message, Alert } from 'antd';
|
||||
import { PlusOutlined, TeamOutlined, ProjectOutlined } from '@ant-design/icons';
|
||||
import { useWorkspace } from '@/hooks/useWorkspace';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { getQuotaStatus } from '@/api/subscription';
|
||||
import { createProject, getProjects } from '@/api/projects';
|
||||
import MemberList from '@/components/business/MemberList';
|
||||
import InviteMemberModal from '@/components/business/InviteMemberModal';
|
||||
import PermissionMatrix from '@/components/business/PermissionMatrix';
|
||||
@@ -21,7 +22,39 @@ const WorkspaceDetail: React.FC = () => {
|
||||
queryFn: () => getQuotaStatus(id!),
|
||||
enabled: !!id,
|
||||
});
|
||||
const queryClient = useQueryClient();
|
||||
const [inviteModalOpen, setInviteModalOpen] = useState(false);
|
||||
const [createProjectOpen, setCreateProjectOpen] = useState(false);
|
||||
const [projectForm] = Form.useForm<{ name: string; description?: string }>();
|
||||
|
||||
const projectsQuery = useQuery({
|
||||
queryKey: ['projects', id],
|
||||
queryFn: () => getProjects(id!),
|
||||
enabled: !!id,
|
||||
});
|
||||
|
||||
const createProjectMutation = useMutation({
|
||||
mutationFn: createProject,
|
||||
onSuccess: (project) => {
|
||||
message.success('项目创建成功');
|
||||
setCreateProjectOpen(false);
|
||||
projectForm.resetFields();
|
||||
queryClient.invalidateQueries({ queryKey: ['projects', id] });
|
||||
navigate(`/projects/${project.id}/assets`, { state: { workspaceId: id } });
|
||||
},
|
||||
onError: (error: any) => {
|
||||
message.error(error.response?.data?.detail || '项目创建失败,请稍后重试');
|
||||
},
|
||||
});
|
||||
|
||||
const handleCreateProject = async () => {
|
||||
const values = await projectForm.validateFields();
|
||||
createProjectMutation.mutate({
|
||||
workspace_id: id!,
|
||||
name: values.name,
|
||||
description: values.description || '',
|
||||
});
|
||||
};
|
||||
|
||||
if (isLoading) return <div>加载中...</div>;
|
||||
if (!workspace) return <div>工作空间不存在</div>;
|
||||
@@ -46,8 +79,8 @@ const WorkspaceDetail: React.FC = () => {
|
||||
title="工作空间信息"
|
||||
style={{ marginBottom: 16 }}
|
||||
extra={
|
||||
<Button type="primary" onClick={() => navigate(`/projects/${id}/assets`)}>
|
||||
进入素材管理
|
||||
<Button type="primary" onClick={() => setCreateProjectOpen(true)}>
|
||||
创建项目
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
@@ -63,6 +96,34 @@ const WorkspaceDetail: React.FC = () => {
|
||||
</Descriptions>
|
||||
</Card>
|
||||
|
||||
<Card title="项目" style={{ marginBottom: 16 }}>
|
||||
{projectsQuery.isError && (
|
||||
<Alert type="error" showIcon message="项目列表加载失败" style={{ marginBottom: 16 }} />
|
||||
)}
|
||||
<List
|
||||
loading={projectsQuery.isLoading}
|
||||
dataSource={projectsQuery.data || []}
|
||||
locale={{ emptyText: '还没有项目,请先创建项目再上传素材' }}
|
||||
renderItem={(project) => (
|
||||
<List.Item
|
||||
actions={[
|
||||
<Button key="assets" type="link" onClick={() => navigate(`/projects/${project.id}/assets`, { state: { workspaceId: id } })}>
|
||||
素材管理
|
||||
</Button>,
|
||||
<Button key="generation" type="link" onClick={() => navigate(`/projects/${project.id}/generation`, { state: { workspaceId: id } })}>
|
||||
视频生成
|
||||
</Button>,
|
||||
<Button key="results" type="link" onClick={() => navigate(`/projects/${project.id}/results`, { state: { workspaceId: id } })}>
|
||||
生成结果
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<List.Item.Meta title={project.name} description={project.description || '暂无描述'} />
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
{quota && (
|
||||
<Card title="配额使用情况">
|
||||
<Row gutter={16}>
|
||||
@@ -130,6 +191,31 @@ const WorkspaceDetail: React.FC = () => {
|
||||
<div>
|
||||
<h1>{workspace.name}</h1>
|
||||
<Tabs items={items} />
|
||||
<Modal
|
||||
title="创建项目"
|
||||
open={createProjectOpen}
|
||||
okText="创建"
|
||||
cancelText="取消"
|
||||
confirmLoading={createProjectMutation.isPending}
|
||||
onOk={handleCreateProject}
|
||||
onCancel={() => setCreateProjectOpen(false)}
|
||||
>
|
||||
<Form form={projectForm} layout="vertical" preserve={false}>
|
||||
<Form.Item
|
||||
name="name"
|
||||
label="项目名称"
|
||||
rules={[
|
||||
{ required: true, message: '请输入项目名称' },
|
||||
{ max: 100, message: '项目名称最多 100 个字符' },
|
||||
]}
|
||||
>
|
||||
<Input placeholder="例如:618 宣传片" />
|
||||
</Form.Item>
|
||||
<Form.Item name="description" label="项目描述">
|
||||
<Input.TextArea placeholder="可选" rows={3} maxLength={500} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
<InviteMemberModal
|
||||
workspaceId={id!}
|
||||
open={inviteModalOpen}
|
||||
|
||||
Reference in New Issue
Block a user