| |
| |
| |
| |
| |
| |
| |
|
|
| import { useState } from 'react'; |
| import { AlertCircle, Loader2, CheckCircle } from 'lucide-react'; |
| import { UISchemaDefinition, UISubmitContract } from '@typings/plugin'; |
| import { Button } from '@components/ui/button'; |
| import { Input } from '@components/ui/input'; |
| import { Label } from '@components/ui/label'; |
|
|
| interface SchemaPluginFormProps { |
| pluginName: string; |
| schema: UISchemaDefinition; |
| submit: UISubmitContract; |
| } |
|
|
| export function SchemaPluginForm({ pluginName: _pluginName, schema, submit }: SchemaPluginFormProps) { |
| const [formData, setFormData] = useState<Record<string, any>>({}); |
| const [isSubmitting, setIsSubmitting] = useState(false); |
| const [result, setResult] = useState<any>(null); |
| const [error, setError] = useState<string | null>(null); |
|
|
| const handleFieldChange = (name: string, value: any) => { |
| setFormData((prev) => ({ ...prev, [name]: value })); |
| }; |
|
|
| const handleSubmit = async () => { |
| setIsSubmitting(true); |
| setError(null); |
| setResult(null); |
|
|
| try { |
| |
| if (submit.method !== 'POST') { |
| throw new Error(`不支持的提交方法: ${submit.method}`); |
| } |
|
|
| const response = await fetch(submit.path, { |
| method: submit.method, |
| headers: { |
| 'Content-Type': submit.content_type || 'application/json', |
| }, |
| body: JSON.stringify(formData), |
| }); |
|
|
| if (!response.ok) { |
| const errorData = await response.json().catch(() => null); |
| const errorMessage = |
| errorData?.message || errorData?.detail || `请求失败: ${response.status}`; |
| throw new Error(errorMessage); |
| } |
|
|
| const data = await response.json(); |
|
|
| |
| if (submit.success_path) { |
| const successResult = submit.success_path.split('.').reduce((obj, key) => obj?.[key], data); |
| setResult(successResult); |
| } else { |
| setResult(data); |
| } |
| } catch (err: any) { |
| setError(err.message || '提交失败'); |
| } finally { |
| setIsSubmitting(false); |
| } |
| }; |
|
|
| return ( |
| <div className="space-y-4"> |
| {/* 表单标题 */} |
| <div> |
| <h3 className="text-sm font-medium text-console-text-primary">{schema.title}</h3> |
| {schema.description && ( |
| <p className="text-sm text-console-text-secondary mt-1">{schema.description}</p> |
| )} |
| </div> |
| |
| {/* 表单字段 */} |
| <div className="space-y-4"> |
| <h4 className="text-sm font-medium text-console-text-primary">参数</h4> |
| {schema.fields.map((field) => ( |
| <div key={field.name} className="space-y-1.5"> |
| <Label htmlFor={field.name} className="text-sm text-console-text-primary"> |
| {field.label} |
| {field.required && <span className="text-console-status-error ml-1">*</span>} |
| </Label> |
| {field.type === 'string' && field.name.includes('content') ? ( |
| <textarea |
| id={field.name} |
| placeholder={field.placeholder} |
| value={formData[field.name] || ''} |
| onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => handleFieldChange(field.name, e.target.value)} |
| className="flex min-h-[80px] w-full rounded-console-md border border-console-border bg-console-surface-base px-3 py-2 text-sm text-console-text-primary placeholder:text-console-text-muted focus:outline-none focus:ring-2 focus:ring-console-status-info/30 focus:border-console-status-info disabled:cursor-not-allowed disabled:opacity-50" |
| /> |
| ) : ( |
| <Input |
| id={field.name} |
| type={field.type === 'number' ? 'number' : 'text'} |
| placeholder={field.placeholder} |
| value={formData[field.name] || ''} |
| onChange={(e) => |
| handleFieldChange( |
| field.name, |
| field.type === 'number' ? Number(e.target.value) : e.target.value |
| ) |
| } |
| /> |
| )} |
| {field.help_text && ( |
| <p className="text-xs text-console-text-muted">{field.help_text}</p> |
| )} |
| </div> |
| ))} |
| <Button onClick={handleSubmit} disabled={isSubmitting} className="w-full"> |
| {isSubmitting ? ( |
| <> |
| <Loader2 className="h-4 w-4 mr-2 animate-spin" /> |
| 提交中... |
| </> |
| ) : ( |
| '提交' |
| )} |
| </Button> |
| </div> |
| |
| {/* 错误信息 */} |
| {error && ( |
| <div className="p-3 rounded-console-md border border-console-status-error bg-console-status-error-bg"> |
| <div className="flex items-start gap-2"> |
| <AlertCircle className="h-4 w-4 text-console-status-error mt-0.5 shrink-0" /> |
| <div> |
| <p className="text-sm font-medium text-console-status-error">提交失败</p> |
| <p className="text-sm text-console-text-secondary mt-1">{error}</p> |
| </div> |
| </div> |
| </div> |
| )} |
| |
| {/* 结果 */} |
| {result !== null && ( |
| <div className="p-3 rounded-console-md border border-console-status-enabled bg-console-status-enabled-bg"> |
| <div className="flex items-start gap-2"> |
| <CheckCircle className="h-4 w-4 text-console-status-enabled mt-0.5 shrink-0" /> |
| <div className="min-w-0"> |
| <p className="text-sm font-medium text-console-status-enabled">提交成功</p> |
| <pre className="text-sm bg-console-surface-base text-console-text-primary p-3 rounded-console-sm mt-2 overflow-x-auto"> |
| {typeof result === 'string' ? result : JSON.stringify(result, null, 2)} |
| </pre> |
| </div> |
| </div> |
| </div> |
| )} |
| </div> |
| ); |
| } |
|
|