表单设计器

This commit is contained in:
dengqichen 2025-10-23 22:43:20 +08:00
parent 93adb8e71e
commit 90d0792f0d
5 changed files with 277 additions and 38 deletions

View File

@ -25,6 +25,7 @@ import {
import { UploadOutlined } from '@ant-design/icons';
import type { FieldConfig } from '../types';
import GridField from './GridField';
import { useFieldOptions } from '../hooks/useFieldOptions';
import '../styles.css';
const { Text } = Typography;
@ -52,6 +53,9 @@ const FieldRenderer: React.FC<FieldRendererProps> = ({
onDeleteField,
labelAlign = 'right',
}) => {
// 获取字段选项(支持静态和动态数据源)
const options = useFieldOptions(field);
// 布局组件特殊处理
if (field.type === 'divider') {
return <Divider>{field.label}</Divider>;
@ -119,7 +123,7 @@ const FieldRenderer: React.FC<FieldRendererProps> = ({
<Select
placeholder={field.placeholder}
disabled={field.disabled}
options={field.options}
options={options}
value={value}
onChange={onChange}
style={{ width: '100%' }}
@ -129,7 +133,7 @@ const FieldRenderer: React.FC<FieldRendererProps> = ({
case 'radio':
return (
<Radio.Group
options={field.options}
options={options}
disabled={field.disabled}
value={value}
onChange={(e) => onChange?.(e.target.value)}
@ -139,7 +143,7 @@ const FieldRenderer: React.FC<FieldRendererProps> = ({
case 'checkbox':
return (
<Checkbox.Group
options={field.options}
options={options}
disabled={field.disabled}
value={value}
onChange={onChange}
@ -225,7 +229,7 @@ const FieldRenderer: React.FC<FieldRendererProps> = ({
style={{ width: '100%' }}
placeholder={field.placeholder}
disabled={field.disabled}
options={field.options as any}
options={options as any}
value={value}
onChange={onChange}
/>

View File

@ -15,6 +15,7 @@ import {
Tabs,
Typography,
Divider,
Select,
} from 'antd';
import { PlusOutlined, DeleteOutlined } from '@ant-design/icons';
import type { FieldConfig, FormConfig } from '../types';
@ -39,13 +40,60 @@ const PropertyPanel: React.FC<PropertyPanelProps> = ({
React.useEffect(() => {
if (selectedField) {
form.setFieldsValue(selectedField);
// 确保 apiDataSource 被正确设置到表单
const formValues = {
...selectedField,
apiDataSource: selectedField.apiDataSource || {
url: '',
method: 'GET',
dataPath: '',
labelField: '',
valueField: '',
},
};
form.setFieldsValue(formValues);
}
}, [selectedField, form]);
const handleFieldUpdate = (changedValues: any) => {
if (selectedField) {
onFieldChange({ ...selectedField, ...changedValues });
// 深度合并,特别处理 apiDataSource 嵌套对象
let updatedField = { ...selectedField };
// 如果改变的是数据源类型
if ('dataSourceType' in changedValues) {
updatedField.dataSourceType = changedValues.dataSourceType;
// 切换到 API 数据源时,初始化 apiDataSource 对象
if (changedValues.dataSourceType === 'api' && !updatedField.apiDataSource) {
updatedField.apiDataSource = {
url: '',
method: 'GET',
dataPath: '',
labelField: '',
valueField: '',
};
console.log('🔄 切换到 API 数据源,已初始化配置对象');
}
}
// 如果改变的是 apiDataSource 的子字段
if ('apiDataSource' in changedValues) {
updatedField.apiDataSource = {
...updatedField.apiDataSource,
...changedValues.apiDataSource,
};
console.log('💾 API 数据源配置已更新:', updatedField.apiDataSource);
}
// 合并其他字段
Object.keys(changedValues).forEach(key => {
if (key !== 'dataSourceType' && key !== 'apiDataSource') {
(updatedField as any)[key] = changedValues[key];
}
});
onFieldChange(updatedField);
}
};
@ -260,41 +308,104 @@ const PropertyPanel: React.FC<PropertyPanelProps> = ({
</>
)}
{hasOptions && selectedField.options && (
{hasOptions && (
<div>
<Divider style={{ margin: '16px 0' }} />
<div style={{ marginBottom: 8, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Text strong></Text>
<Button
type="dashed"
size="small"
icon={<PlusOutlined />}
onClick={addOption}
>
</Button>
</div>
<Space direction="vertical" style={{ width: '100%' }} size="small">
{selectedField.options.map((option, index) => (
<div key={index} style={{ display: 'flex', gap: 8 }}>
<Input
placeholder="标签"
value={option.label}
onChange={(e) => updateOption(index, 'label', e.target.value)}
/>
<Input
placeholder="值"
value={option.value}
onChange={(e) => updateOption(index, 'value', e.target.value)}
/>
{/* 数据源类型选择 */}
<Form.Item label="数据源类型" name="dataSourceType">
<Radio.Group buttonStyle="solid">
<Radio.Button value="static"></Radio.Button>
<Radio.Button value="api"></Radio.Button>
</Radio.Group>
</Form.Item>
{/* 静态数据配置 */}
{selectedField.dataSourceType === 'static' && selectedField.options && (
<div>
<div style={{ marginBottom: 8, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Text strong></Text>
<Button
danger
icon={<DeleteOutlined />}
onClick={() => deleteOption(index)}
/>
type="dashed"
size="small"
icon={<PlusOutlined />}
onClick={addOption}
>
</Button>
</div>
))}
</Space>
<Space direction="vertical" style={{ width: '100%' }} size="small">
{selectedField.options.map((option, index) => (
<div key={index} style={{ display: 'flex', gap: 8 }}>
<Input
placeholder="标签"
value={option.label}
onChange={(e) => updateOption(index, 'label', e.target.value)}
/>
<Input
placeholder="值"
value={option.value}
onChange={(e) => updateOption(index, 'value', e.target.value)}
/>
<Button
danger
icon={<DeleteOutlined />}
onClick={() => deleteOption(index)}
/>
</div>
))}
</Space>
</div>
)}
{/* API 数据源配置 */}
{selectedField.dataSourceType === 'api' && (
<div>
<Text strong></Text>
<div style={{ marginTop: 12 }}>
<Form.Item
label="接口地址"
name={['apiDataSource', 'url']}
rules={[{ required: true, message: '请输入接口地址' }]}
>
<Input placeholder="如:/api/v1/dict/status" />
</Form.Item>
<Form.Item label="请求方法" name={['apiDataSource', 'method']}>
<Select placeholder="选择请求方法" defaultValue="GET">
<Select.Option value="GET">GET</Select.Option>
<Select.Option value="POST">POST</Select.Option>
</Select>
</Form.Item>
<Form.Item
label="数据路径"
name={['apiDataSource', 'dataPath']}
tooltip="从接口响应中提取数组的路径。如响应为 {data: [...]},则填 'data';如为 {result: {list: [...]}},则填 'result.list'。留空表示直接返回数组"
>
<Input placeholder="如data 或 result.list可选" />
</Form.Item>
<Form.Item
label="标签字段"
name={['apiDataSource', 'labelField']}
tooltip="数组中每个对象用作显示文本的字段名"
rules={[{ required: true, message: '请输入标签字段' }]}
>
<Input placeholder="如name" />
</Form.Item>
<Form.Item
label="值字段"
name={['apiDataSource', 'valueField']}
tooltip="数组中每个对象用作实际值的字段名"
rules={[{ required: true, message: '请输入值字段' }]}
>
<Input placeholder="如id" />
</Form.Item>
</div>
</div>
)}
</div>
)}
</Form>

View File

@ -90,6 +90,7 @@ export const COMPONENT_LIST: ComponentMeta[] = [
category: '基础字段',
defaultConfig: {
placeholder: '请选择',
dataSourceType: 'static',
options: [
{ label: '选项1', value: '1' },
{ label: '选项2', value: '2' },
@ -103,6 +104,7 @@ export const COMPONENT_LIST: ComponentMeta[] = [
icon: CheckCircleOutlined,
category: '基础字段',
defaultConfig: {
dataSourceType: 'static',
options: [
{ label: '选项1', value: '1' },
{ label: '选项2', value: '2' },
@ -116,6 +118,7 @@ export const COMPONENT_LIST: ComponentMeta[] = [
icon: CheckSquareOutlined,
category: '基础字段',
defaultConfig: {
dataSourceType: 'static',
options: [
{ label: '选项1', value: '1' },
{ label: '选项2', value: '2' },
@ -201,6 +204,7 @@ export const COMPONENT_LIST: ComponentMeta[] = [
category: '高级字段',
defaultConfig: {
placeholder: '请选择',
dataSourceType: 'static',
options: [
{
label: '浙江',

View File

@ -0,0 +1,105 @@
/**
* Hook
* API数据
*/
import { useState, useEffect } from 'react';
import type { FieldConfig, FieldOption } from '../types';
import request from '../../../utils/request';
/**
*
* @param obj
* @param path 'data' 'result.list'
*/
const getValueByPath = (obj: any, path?: string): any => {
if (!path) return obj;
const keys = path.split('.');
let result = obj;
for (const key of keys) {
if (result && typeof result === 'object' && key in result) {
result = result[key];
} else {
return undefined;
}
}
return result;
};
export const useFieldOptions = (field: FieldConfig): FieldOption[] => {
const [options, setOptions] = useState<FieldOption[]>([]);
useEffect(() => {
const loadOptions = async () => {
// 静态数据源
if (field.dataSourceType === 'static' || !field.dataSourceType) {
setOptions(field.options || []);
return;
}
// API 数据源
if (field.dataSourceType === 'api' && field.apiDataSource) {
const { url, method = 'GET', dataPath, labelField, valueField } = field.apiDataSource;
// 验证必填字段(空字符串也视为缺失)
if (!url || !url.trim() || !labelField || !labelField.trim() || !valueField || !valueField.trim()) {
console.warn('⚠️ API 数据源配置不完整,缺少必填字段', {
fieldId: field.id,
fieldName: field.name,
配置信息: field.apiDataSource,
: {
url: !url || !url.trim() ? '缺少 URL' : '✓',
labelField: !labelField || !labelField.trim() ? '缺少 labelField' : '✓',
valueField: !valueField || !valueField.trim() ? '缺少 valueField' : '✓',
},
});
setOptions([]);
return;
}
try {
let response: any;
// 根据请求方法调用接口
if (method === 'POST') {
response = await request.post(url, field.apiDataSource.params || {});
} else {
response = await request.get(url, { params: field.apiDataSource.params || {} });
}
// 根据 dataPath 提取数据数组
let dataArray = dataPath ? getValueByPath(response, dataPath) : response;
// 转换数据格式
if (Array.isArray(dataArray)) {
const transformedOptions: FieldOption[] = dataArray.map((item: any) => ({
label: item[labelField],
value: item[valueField],
}));
setOptions(transformedOptions);
console.log('✅ API 数据加载成功', { url, count: transformedOptions.length });
} else {
console.error('❌ API 返回的数据格式不正确,期望数组', {
url,
dataPath,
actualData: dataArray,
fullResponse: response,
});
setOptions([]);
}
} catch (error) {
console.error('❌ 加载 API 数据失败', { url, error });
setOptions([]);
}
}
};
loadOptions();
}, [field.dataSourceType, field.options, field.apiDataSource]);
return options;
};

View File

@ -28,6 +28,19 @@ export interface FieldOption {
value: string | number;
}
// 数据源类型
export type DataSourceType = 'static' | 'api';
// API 数据源配置
export interface ApiDataSource {
url: string; // 接口地址
method?: 'GET' | 'POST'; // 请求方法,默认 GET
params?: Record<string, any>; // 请求参数
dataPath?: string; // 数据路径(如 'data', 'result.list'),用于从响应中提取数组
labelField: string; // 标签字段名(如 'name', 'label', 'text'
valueField: string; // 值字段名(如 'id', 'code', 'value'
}
// 字段配置
export interface FieldConfig {
id: string;
@ -38,7 +51,9 @@ export interface FieldConfig {
required?: boolean;
disabled?: boolean;
defaultValue?: any;
options?: FieldOption[];
options?: FieldOption[]; // 静态选项数据
dataSourceType?: DataSourceType; // 数据源类型static静态或 api接口
apiDataSource?: ApiDataSource; // API 数据源配置(当 dataSourceType 为 'api' 时使用)
min?: number;
max?: number;
rows?: number;