File size: 6,149 Bytes
a43ac26 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 | /**
* Schema UI 表单组件
*
* 按 ui_entry.schema.fields 渲染表单,并按 ui_entry.submit 契约提交。
* 只允许使用后端 ui_entry.submit 指定的提交契约,不能由前端猜 endpoint。
* 使用 console 语义 token 保持视觉一致。
*/
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 {
// 只使用 ui_entry.submit 契约
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();
// 按 success_path 读取结果
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>
);
}
|