|
|
@@ -2,8 +2,8 @@
|
|
|
* ProjectForm Component
|
|
|
*
|
|
|
* Reusable form component for creating and editing projects.
|
|
|
- * Supports template selection for quick project setup.
|
|
|
- * Requirements: 1.2, 1.4, 4.4, 4.6
|
|
|
+ * Supports template selection and config wizard for quick project setup.
|
|
|
+ * Requirements: 1.2, 1.4, 4.4, 4.6, 6.1, 6.6
|
|
|
*/
|
|
|
import React, { useState, useEffect, useCallback } from 'react';
|
|
|
import { Button } from '@humansignal/ui';
|
|
|
@@ -14,8 +14,13 @@ import {
|
|
|
Settings,
|
|
|
Check,
|
|
|
Grid3X3,
|
|
|
+ Wand2,
|
|
|
+ Code,
|
|
|
} from 'lucide-react';
|
|
|
import { TemplateGallery } from '../template-gallery';
|
|
|
+import { TaskTypeSelector, type TaskType } from '../task-type-selector';
|
|
|
+import { LabelEditor, type LabelConfig } from '../label-editor';
|
|
|
+import { XMLGenerator } from '../../services/xml-generator';
|
|
|
import type { Template } from '../../services/api';
|
|
|
import styles from './project-form.module.scss';
|
|
|
|
|
|
@@ -23,6 +28,7 @@ export interface ProjectFormData {
|
|
|
name: string;
|
|
|
description: string;
|
|
|
config: string;
|
|
|
+ taskType?: TaskType;
|
|
|
}
|
|
|
|
|
|
export interface ProjectFormProps {
|
|
|
@@ -35,7 +41,8 @@ export interface ProjectFormProps {
|
|
|
showTemplateStep?: boolean;
|
|
|
}
|
|
|
|
|
|
-type FormStep = 'template' | 'details';
|
|
|
+type FormStep = 'template' | 'details' | 'config';
|
|
|
+type ConfigMode = 'wizard' | 'advanced';
|
|
|
|
|
|
export const ProjectForm: React.FC<ProjectFormProps> = ({
|
|
|
initialData,
|
|
|
@@ -50,11 +57,18 @@ export const ProjectForm: React.FC<ProjectFormProps> = ({
|
|
|
showTemplateStep ? 'template' : 'details'
|
|
|
);
|
|
|
const [selectedTemplate, setSelectedTemplate] = useState<Template | null>(null);
|
|
|
+
|
|
|
+ // Config wizard state
|
|
|
+ const [configMode, setConfigMode] = useState<ConfigMode>('wizard');
|
|
|
+ const [taskType, setTaskType] = useState<TaskType | null>(null);
|
|
|
+ const [labels, setLabels] = useState<LabelConfig[]>([]);
|
|
|
+ const [choiceType, setChoiceType] = useState<'single' | 'multiple'>('single');
|
|
|
|
|
|
const [formData, setFormData] = useState<ProjectFormData>({
|
|
|
name: initialData?.name || '',
|
|
|
description: initialData?.description || '',
|
|
|
config: initialData?.config || '',
|
|
|
+ taskType: initialData?.taskType,
|
|
|
});
|
|
|
|
|
|
const [formErrors, setFormErrors] = useState<Record<string, string>>({});
|
|
|
@@ -66,10 +80,23 @@ export const ProjectForm: React.FC<ProjectFormProps> = ({
|
|
|
name: initialData.name || '',
|
|
|
description: initialData.description || '',
|
|
|
config: initialData.config || '',
|
|
|
+ taskType: initialData.taskType,
|
|
|
});
|
|
|
}
|
|
|
}, [initialData]);
|
|
|
|
|
|
+ // 当向导配置变化时更新 XML
|
|
|
+ useEffect(() => {
|
|
|
+ if (configMode === 'wizard' && taskType && labels.length > 0) {
|
|
|
+ try {
|
|
|
+ const xml = XMLGenerator.generate({ taskType, labels, choiceType });
|
|
|
+ setFormData(prev => ({ ...prev, config: xml, taskType }));
|
|
|
+ } catch {
|
|
|
+ // 忽略生成错误
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }, [configMode, taskType, labels, choiceType]);
|
|
|
+
|
|
|
// Handle template selection
|
|
|
const handleTemplateSelect = useCallback((template: Template) => {
|
|
|
setSelectedTemplate(template);
|
|
|
@@ -78,11 +105,14 @@ export const ProjectForm: React.FC<ProjectFormProps> = ({
|
|
|
...prev,
|
|
|
config: template.config,
|
|
|
}));
|
|
|
+ // 使用模板时切换到高级模式
|
|
|
+ setConfigMode('advanced');
|
|
|
}, []);
|
|
|
|
|
|
- // Skip template selection
|
|
|
+ // Skip template selection - use wizard mode
|
|
|
const handleSkipTemplate = useCallback(() => {
|
|
|
setSelectedTemplate(null);
|
|
|
+ setConfigMode('wizard');
|
|
|
setCurrentStep('details');
|
|
|
}, []);
|
|
|
|
|
|
@@ -90,6 +120,8 @@ export const ProjectForm: React.FC<ProjectFormProps> = ({
|
|
|
const handleNextStep = useCallback(() => {
|
|
|
if (currentStep === 'template') {
|
|
|
setCurrentStep('details');
|
|
|
+ } else if (currentStep === 'details') {
|
|
|
+ setCurrentStep('config');
|
|
|
}
|
|
|
}, [currentStep]);
|
|
|
|
|
|
@@ -97,9 +129,30 @@ export const ProjectForm: React.FC<ProjectFormProps> = ({
|
|
|
const handlePrevStep = useCallback(() => {
|
|
|
if (currentStep === 'details' && showTemplateStep) {
|
|
|
setCurrentStep('template');
|
|
|
+ } else if (currentStep === 'config') {
|
|
|
+ setCurrentStep('details');
|
|
|
}
|
|
|
}, [currentStep, showTemplateStep]);
|
|
|
|
|
|
+ // 检查详情步骤是否可以继续
|
|
|
+ const canProceedFromDetails = (): boolean => {
|
|
|
+ const nameValid = formData.name && formData.name.trim().length >= 2;
|
|
|
+ const descValid = formData.description && formData.description.trim().length >= 5;
|
|
|
+ return !!(nameValid && descValid);
|
|
|
+ };
|
|
|
+
|
|
|
+ // 检查配置步骤是否完成
|
|
|
+ const isConfigComplete = (): boolean => {
|
|
|
+ if (configMode === 'wizard') {
|
|
|
+ return taskType !== null && labels.length > 0;
|
|
|
+ }
|
|
|
+ // 高级模式:检查 XML 是否有效
|
|
|
+ return formData.config.trim().length > 0;
|
|
|
+ };
|
|
|
+
|
|
|
+ // 判断是否为分类任务
|
|
|
+ const isClassificationTask = taskType === 'text_classification' || taskType === 'image_classification';
|
|
|
+
|
|
|
const validateForm = (): boolean => {
|
|
|
const errors: Record<string, string> = {};
|
|
|
|
|
|
@@ -149,6 +202,10 @@ export const ProjectForm: React.FC<ProjectFormProps> = ({
|
|
|
setFormData({ name: '', description: '', config: '' });
|
|
|
setFormErrors({});
|
|
|
setSelectedTemplate(null);
|
|
|
+ setTaskType(null);
|
|
|
+ setLabels([]);
|
|
|
+ setChoiceType('single');
|
|
|
+ setConfigMode('wizard');
|
|
|
if (showTemplateStep) {
|
|
|
setCurrentStep('template');
|
|
|
}
|
|
|
@@ -209,25 +266,36 @@ export const ProjectForm: React.FC<ProjectFormProps> = ({
|
|
|
const renderStepIndicator = () => {
|
|
|
if (!showTemplateStep) return null;
|
|
|
|
|
|
+ const steps = [
|
|
|
+ { id: 'template', label: '选择模板', icon: Grid3X3 },
|
|
|
+ { id: 'details', label: '项目详情', icon: Settings },
|
|
|
+ { id: 'config', label: '配置标注', icon: Wand2 },
|
|
|
+ ];
|
|
|
+
|
|
|
+ const currentIndex = steps.findIndex(s => s.id === currentStep);
|
|
|
+
|
|
|
return (
|
|
|
<div className={styles.stepIndicator}>
|
|
|
- <div
|
|
|
- className={`${styles.step} ${currentStep === 'template' ? styles.stepActive : styles.stepCompleted}`}
|
|
|
- >
|
|
|
- <div className={styles.stepIcon}>
|
|
|
- {currentStep === 'details' ? <Check size={16} /> : <Grid3X3 size={16} />}
|
|
|
- </div>
|
|
|
- <span className={styles.stepLabel}>选择模板</span>
|
|
|
- </div>
|
|
|
- <div className={styles.stepConnector} />
|
|
|
- <div
|
|
|
- className={`${styles.step} ${currentStep === 'details' ? styles.stepActive : ''}`}
|
|
|
- >
|
|
|
- <div className={styles.stepIcon}>
|
|
|
- <Settings size={16} />
|
|
|
- </div>
|
|
|
- <span className={styles.stepLabel}>项目详情</span>
|
|
|
- </div>
|
|
|
+ {steps.map((step, index) => {
|
|
|
+ const Icon = step.icon;
|
|
|
+ const isCompleted = index < currentIndex;
|
|
|
+ const isActive = index === currentIndex;
|
|
|
+ return (
|
|
|
+ <React.Fragment key={step.id}>
|
|
|
+ <div
|
|
|
+ className={`${styles.step} ${isActive ? styles.stepActive : ''} ${isCompleted ? styles.stepCompleted : ''}`}
|
|
|
+ >
|
|
|
+ <div className={styles.stepIcon}>
|
|
|
+ {isCompleted ? <Check size={16} /> : <Icon size={16} />}
|
|
|
+ </div>
|
|
|
+ <span className={styles.stepLabel}>{step.label}</span>
|
|
|
+ </div>
|
|
|
+ {index < steps.length - 1 && (
|
|
|
+ <div className={`${styles.stepConnector} ${isCompleted ? styles.stepConnectorCompleted : ''}`} />
|
|
|
+ )}
|
|
|
+ </React.Fragment>
|
|
|
+ );
|
|
|
+ })}
|
|
|
</div>
|
|
|
);
|
|
|
};
|
|
|
@@ -268,96 +336,61 @@ export const ProjectForm: React.FC<ProjectFormProps> = ({
|
|
|
|
|
|
// Render details step
|
|
|
const renderDetailsStep = () => (
|
|
|
- <form onSubmit={handleSubmit} className={styles.form}>
|
|
|
- {/* Name field */}
|
|
|
- <div className={styles.field}>
|
|
|
- <label htmlFor="project-name" className={styles.label}>
|
|
|
- 项目名称 <span className={styles.required}>*</span>
|
|
|
- </label>
|
|
|
- <input
|
|
|
- id="project-name"
|
|
|
- type="text"
|
|
|
- value={formData.name}
|
|
|
- onChange={(e) => handleFieldChange('name', e.target.value)}
|
|
|
- onBlur={() => handleFieldBlur('name')}
|
|
|
- className={`${styles.input} ${formErrors.name ? styles.error : ''}`}
|
|
|
- placeholder="输入项目名称"
|
|
|
- disabled={isSubmitting}
|
|
|
- aria-invalid={!!formErrors.name}
|
|
|
- aria-describedby={formErrors.name ? 'name-error' : undefined}
|
|
|
- maxLength={100}
|
|
|
- />
|
|
|
- {formErrors.name && (
|
|
|
- <span id="name-error" className={styles.errorMessage}>
|
|
|
- {formErrors.name}
|
|
|
- </span>
|
|
|
- )}
|
|
|
- </div>
|
|
|
-
|
|
|
- {/* Description field */}
|
|
|
- <div className={styles.field}>
|
|
|
- <label htmlFor="project-description" className={styles.label}>
|
|
|
- 项目描述 <span className={styles.required}>*</span>
|
|
|
- </label>
|
|
|
- <textarea
|
|
|
- id="project-description"
|
|
|
- value={formData.description}
|
|
|
- onChange={(e) => handleFieldChange('description', e.target.value)}
|
|
|
- onBlur={() => handleFieldBlur('description')}
|
|
|
- className={`${styles.textarea} ${formErrors.description ? styles.error : ''}`}
|
|
|
- placeholder="输入项目描述"
|
|
|
- rows={3}
|
|
|
- disabled={isSubmitting}
|
|
|
- aria-invalid={!!formErrors.description}
|
|
|
- aria-describedby={
|
|
|
- formErrors.description ? 'description-error' : undefined
|
|
|
- }
|
|
|
- maxLength={500}
|
|
|
- />
|
|
|
- {formErrors.description && (
|
|
|
- <span id="description-error" className={styles.errorMessage}>
|
|
|
- {formErrors.description}
|
|
|
- </span>
|
|
|
- )}
|
|
|
- </div>
|
|
|
+ <div className={styles.detailsStep}>
|
|
|
+ <div className={styles.form}>
|
|
|
+ {/* Name field */}
|
|
|
+ <div className={styles.field}>
|
|
|
+ <label htmlFor="project-name" className={styles.label}>
|
|
|
+ 项目名称 <span className={styles.required}>*</span>
|
|
|
+ </label>
|
|
|
+ <input
|
|
|
+ id="project-name"
|
|
|
+ type="text"
|
|
|
+ value={formData.name}
|
|
|
+ onChange={(e) => handleFieldChange('name', e.target.value)}
|
|
|
+ onBlur={() => handleFieldBlur('name')}
|
|
|
+ className={`${styles.input} ${formErrors.name ? styles.error : ''}`}
|
|
|
+ placeholder="输入项目名称"
|
|
|
+ disabled={isSubmitting}
|
|
|
+ aria-invalid={!!formErrors.name}
|
|
|
+ aria-describedby={formErrors.name ? 'name-error' : undefined}
|
|
|
+ maxLength={100}
|
|
|
+ />
|
|
|
+ {formErrors.name && (
|
|
|
+ <span id="name-error" className={styles.errorMessage}>
|
|
|
+ {formErrors.name}
|
|
|
+ </span>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
|
|
|
- {/* Config field */}
|
|
|
- <div className={styles.field}>
|
|
|
- <label htmlFor="project-config" className={styles.label}>
|
|
|
- 标注配置 <span className={styles.required}>*</span>
|
|
|
- {selectedTemplate && (
|
|
|
- <span className={styles.templateBadge}>
|
|
|
- 来自模板: {selectedTemplate.name}
|
|
|
+ {/* Description field */}
|
|
|
+ <div className={styles.field}>
|
|
|
+ <label htmlFor="project-description" className={styles.label}>
|
|
|
+ 项目描述 <span className={styles.required}>*</span>
|
|
|
+ </label>
|
|
|
+ <textarea
|
|
|
+ id="project-description"
|
|
|
+ value={formData.description}
|
|
|
+ onChange={(e) => handleFieldChange('description', e.target.value)}
|
|
|
+ onBlur={() => handleFieldBlur('description')}
|
|
|
+ className={`${styles.textarea} ${formErrors.description ? styles.error : ''}`}
|
|
|
+ placeholder="输入项目描述"
|
|
|
+ rows={3}
|
|
|
+ disabled={isSubmitting}
|
|
|
+ aria-invalid={!!formErrors.description}
|
|
|
+ aria-describedby={
|
|
|
+ formErrors.description ? 'description-error' : undefined
|
|
|
+ }
|
|
|
+ maxLength={500}
|
|
|
+ />
|
|
|
+ {formErrors.description && (
|
|
|
+ <span id="description-error" className={styles.errorMessage}>
|
|
|
+ {formErrors.description}
|
|
|
</span>
|
|
|
)}
|
|
|
- </label>
|
|
|
- <textarea
|
|
|
- id="project-config"
|
|
|
- value={formData.config}
|
|
|
- onChange={(e) => handleFieldChange('config', e.target.value)}
|
|
|
- onBlur={() => handleFieldBlur('config')}
|
|
|
- className={`${styles.textarea} ${styles.textareaCode} ${formErrors.config ? styles.error : ''}`}
|
|
|
- placeholder='输入 Label Studio 配置 XML,例如:<View><Text name="text" value="$text"/></View>'
|
|
|
- rows={10}
|
|
|
- disabled={isSubmitting}
|
|
|
- aria-invalid={!!formErrors.config}
|
|
|
- aria-describedby={formErrors.config ? 'config-error' : undefined}
|
|
|
- />
|
|
|
- {formErrors.config && (
|
|
|
- <span id="config-error" className={styles.errorMessage}>
|
|
|
- {formErrors.config}
|
|
|
- </span>
|
|
|
- )}
|
|
|
- <span className={styles.hint}>
|
|
|
- 请输入有效的 Label Studio XML 配置,可根据需要修改模板配置
|
|
|
- </span>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
|
|
|
- {/* Submit error */}
|
|
|
- {formErrors.submit && (
|
|
|
- <div className={styles.submitError}>{formErrors.submit}</div>
|
|
|
- )}
|
|
|
-
|
|
|
{/* Form actions */}
|
|
|
<div className={styles.actions}>
|
|
|
{showTemplateStep && (
|
|
|
@@ -382,21 +415,188 @@ export const ProjectForm: React.FC<ProjectFormProps> = ({
|
|
|
>
|
|
|
取消
|
|
|
</Button>
|
|
|
- <Button type="submit" variant="primary" disabled={isSubmitting}>
|
|
|
+ <Button
|
|
|
+ type="button"
|
|
|
+ variant="primary"
|
|
|
+ onClick={handleNextStep}
|
|
|
+ disabled={!canProceedFromDetails() || isSubmitting}
|
|
|
+ >
|
|
|
+ 下一步
|
|
|
+ <ChevronRight size={16} />
|
|
|
+ </Button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+
|
|
|
+ // Render config step (wizard or advanced mode)
|
|
|
+ const renderConfigStep = () => (
|
|
|
+ <div className={styles.configStep}>
|
|
|
+ {/* Mode toggle */}
|
|
|
+ <div className={styles.modeToggle}>
|
|
|
+ <button
|
|
|
+ type="button"
|
|
|
+ className={`${styles.modeButton} ${configMode === 'wizard' ? styles.modeButtonActive : ''}`}
|
|
|
+ onClick={() => setConfigMode('wizard')}
|
|
|
+ disabled={isSubmitting}
|
|
|
+ >
|
|
|
+ <Wand2 size={16} />
|
|
|
+ 向导模式
|
|
|
+ </button>
|
|
|
+ <button
|
|
|
+ type="button"
|
|
|
+ className={`${styles.modeButton} ${configMode === 'advanced' ? styles.modeButtonActive : ''}`}
|
|
|
+ onClick={() => setConfigMode('advanced')}
|
|
|
+ disabled={isSubmitting}
|
|
|
+ >
|
|
|
+ <Code size={16} />
|
|
|
+ 高级模式
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {configMode === 'wizard' ? (
|
|
|
+ <div className={styles.wizardContent}>
|
|
|
+ {/* Task type selection */}
|
|
|
+ <div className={styles.wizardSection}>
|
|
|
+ <h3 className={styles.wizardSectionTitle}>选择任务类型</h3>
|
|
|
+ <TaskTypeSelector
|
|
|
+ selectedType={taskType}
|
|
|
+ onSelect={setTaskType}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* Labels configuration */}
|
|
|
+ {taskType && (
|
|
|
+ <div className={styles.wizardSection}>
|
|
|
+ <h3 className={styles.wizardSectionTitle}>配置标签</h3>
|
|
|
+ {isClassificationTask && (
|
|
|
+ <div className={styles.choiceTypeSelector}>
|
|
|
+ <span className={styles.choiceTypeLabel}>选择模式:</span>
|
|
|
+ <div className={styles.choiceTypeOptions}>
|
|
|
+ <label className={styles.choiceTypeOption}>
|
|
|
+ <input
|
|
|
+ type="radio"
|
|
|
+ name="choiceType"
|
|
|
+ value="single"
|
|
|
+ checked={choiceType === 'single'}
|
|
|
+ onChange={() => setChoiceType('single')}
|
|
|
+ />
|
|
|
+ <span>单选</span>
|
|
|
+ </label>
|
|
|
+ <label className={styles.choiceTypeOption}>
|
|
|
+ <input
|
|
|
+ type="radio"
|
|
|
+ name="choiceType"
|
|
|
+ value="multiple"
|
|
|
+ checked={choiceType === 'multiple'}
|
|
|
+ onChange={() => setChoiceType('multiple')}
|
|
|
+ />
|
|
|
+ <span>多选</span>
|
|
|
+ </label>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ <LabelEditor
|
|
|
+ labels={labels}
|
|
|
+ onChange={setLabels}
|
|
|
+ taskType={taskType}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+
|
|
|
+ {/* XML Preview */}
|
|
|
+ {taskType && labels.length > 0 && (
|
|
|
+ <div className={styles.wizardSection}>
|
|
|
+ <h3 className={styles.wizardSectionTitle}>生成的配置预览</h3>
|
|
|
+ <div className={styles.xmlPreview}>
|
|
|
+ <pre>{formData.config}</pre>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ ) : (
|
|
|
+ <div className={styles.advancedContent}>
|
|
|
+ {/* Config field */}
|
|
|
+ <div className={styles.field}>
|
|
|
+ <label htmlFor="project-config" className={styles.label}>
|
|
|
+ 标注配置 <span className={styles.required}>*</span>
|
|
|
+ {selectedTemplate && (
|
|
|
+ <span className={styles.templateBadge}>
|
|
|
+ 来自模板: {selectedTemplate.name}
|
|
|
+ </span>
|
|
|
+ )}
|
|
|
+ </label>
|
|
|
+ <textarea
|
|
|
+ id="project-config"
|
|
|
+ value={formData.config}
|
|
|
+ onChange={(e) => handleFieldChange('config', e.target.value)}
|
|
|
+ onBlur={() => handleFieldBlur('config')}
|
|
|
+ className={`${styles.textarea} ${styles.textareaCode} ${formErrors.config ? styles.error : ''}`}
|
|
|
+ placeholder='输入 Label Studio 配置 XML,例如:<View><Text name="text" value="$text"/></View>'
|
|
|
+ rows={15}
|
|
|
+ disabled={isSubmitting}
|
|
|
+ aria-invalid={!!formErrors.config}
|
|
|
+ aria-describedby={formErrors.config ? 'config-error' : undefined}
|
|
|
+ />
|
|
|
+ {formErrors.config && (
|
|
|
+ <span id="config-error" className={styles.errorMessage}>
|
|
|
+ {formErrors.config}
|
|
|
+ </span>
|
|
|
+ )}
|
|
|
+ <span className={styles.hint}>
|
|
|
+ 请输入有效的 Label Studio XML 配置
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+
|
|
|
+ {/* Submit error */}
|
|
|
+ {formErrors.submit && (
|
|
|
+ <div className={styles.submitError}>{formErrors.submit}</div>
|
|
|
+ )}
|
|
|
+
|
|
|
+ {/* Form actions */}
|
|
|
+ <div className={styles.actions}>
|
|
|
+ <Button
|
|
|
+ type="button"
|
|
|
+ variant="neutral"
|
|
|
+ look="outlined"
|
|
|
+ onClick={handlePrevStep}
|
|
|
+ disabled={isSubmitting}
|
|
|
+ >
|
|
|
+ <ChevronLeft size={16} />
|
|
|
+ 上一步
|
|
|
+ </Button>
|
|
|
+ <div className={styles.actionsRight}>
|
|
|
+ <Button
|
|
|
+ type="button"
|
|
|
+ variant="neutral"
|
|
|
+ look="outlined"
|
|
|
+ onClick={onCancel}
|
|
|
+ disabled={isSubmitting}
|
|
|
+ >
|
|
|
+ 取消
|
|
|
+ </Button>
|
|
|
+ <Button
|
|
|
+ type="button"
|
|
|
+ variant="primary"
|
|
|
+ onClick={handleSubmit}
|
|
|
+ disabled={!isConfigComplete() || isSubmitting}
|
|
|
+ >
|
|
|
{isSubmitting ? '提交中...' : submitLabel}
|
|
|
</Button>
|
|
|
</div>
|
|
|
</div>
|
|
|
- </form>
|
|
|
+ </div>
|
|
|
);
|
|
|
|
|
|
return (
|
|
|
<div className={styles.root}>
|
|
|
{renderStepIndicator()}
|
|
|
<div className={styles.content}>
|
|
|
- {currentStep === 'template' && showTemplateStep
|
|
|
- ? renderTemplateStep()
|
|
|
- : renderDetailsStep()}
|
|
|
+ {currentStep === 'template' && showTemplateStep && renderTemplateStep()}
|
|
|
+ {currentStep === 'details' && renderDetailsStep()}
|
|
|
+ {currentStep === 'config' && renderConfigStep()}
|
|
|
</div>
|
|
|
</div>
|
|
|
);
|