表单设计器
This commit is contained in:
parent
93adb8e71e
commit
90d0792f0d
@ -25,6 +25,7 @@ import {
|
|||||||
import { UploadOutlined } from '@ant-design/icons';
|
import { UploadOutlined } from '@ant-design/icons';
|
||||||
import type { FieldConfig } from '../types';
|
import type { FieldConfig } from '../types';
|
||||||
import GridField from './GridField';
|
import GridField from './GridField';
|
||||||
|
import { useFieldOptions } from '../hooks/useFieldOptions';
|
||||||
import '../styles.css';
|
import '../styles.css';
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
@ -52,6 +53,9 @@ const FieldRenderer: React.FC<FieldRendererProps> = ({
|
|||||||
onDeleteField,
|
onDeleteField,
|
||||||
labelAlign = 'right',
|
labelAlign = 'right',
|
||||||
}) => {
|
}) => {
|
||||||
|
// 获取字段选项(支持静态和动态数据源)
|
||||||
|
const options = useFieldOptions(field);
|
||||||
|
|
||||||
// 布局组件特殊处理
|
// 布局组件特殊处理
|
||||||
if (field.type === 'divider') {
|
if (field.type === 'divider') {
|
||||||
return <Divider>{field.label}</Divider>;
|
return <Divider>{field.label}</Divider>;
|
||||||
@ -119,7 +123,7 @@ const FieldRenderer: React.FC<FieldRendererProps> = ({
|
|||||||
<Select
|
<Select
|
||||||
placeholder={field.placeholder}
|
placeholder={field.placeholder}
|
||||||
disabled={field.disabled}
|
disabled={field.disabled}
|
||||||
options={field.options}
|
options={options}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
@ -129,7 +133,7 @@ const FieldRenderer: React.FC<FieldRendererProps> = ({
|
|||||||
case 'radio':
|
case 'radio':
|
||||||
return (
|
return (
|
||||||
<Radio.Group
|
<Radio.Group
|
||||||
options={field.options}
|
options={options}
|
||||||
disabled={field.disabled}
|
disabled={field.disabled}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(e) => onChange?.(e.target.value)}
|
onChange={(e) => onChange?.(e.target.value)}
|
||||||
@ -139,7 +143,7 @@ const FieldRenderer: React.FC<FieldRendererProps> = ({
|
|||||||
case 'checkbox':
|
case 'checkbox':
|
||||||
return (
|
return (
|
||||||
<Checkbox.Group
|
<Checkbox.Group
|
||||||
options={field.options}
|
options={options}
|
||||||
disabled={field.disabled}
|
disabled={field.disabled}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
@ -225,7 +229,7 @@ const FieldRenderer: React.FC<FieldRendererProps> = ({
|
|||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
placeholder={field.placeholder}
|
placeholder={field.placeholder}
|
||||||
disabled={field.disabled}
|
disabled={field.disabled}
|
||||||
options={field.options as any}
|
options={options as any}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import {
|
|||||||
Tabs,
|
Tabs,
|
||||||
Typography,
|
Typography,
|
||||||
Divider,
|
Divider,
|
||||||
|
Select,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import { PlusOutlined, DeleteOutlined } from '@ant-design/icons';
|
import { PlusOutlined, DeleteOutlined } from '@ant-design/icons';
|
||||||
import type { FieldConfig, FormConfig } from '../types';
|
import type { FieldConfig, FormConfig } from '../types';
|
||||||
@ -39,13 +40,60 @@ const PropertyPanel: React.FC<PropertyPanelProps> = ({
|
|||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (selectedField) {
|
if (selectedField) {
|
||||||
form.setFieldsValue(selectedField);
|
// 确保 apiDataSource 被正确设置到表单
|
||||||
|
const formValues = {
|
||||||
|
...selectedField,
|
||||||
|
apiDataSource: selectedField.apiDataSource || {
|
||||||
|
url: '',
|
||||||
|
method: 'GET',
|
||||||
|
dataPath: '',
|
||||||
|
labelField: '',
|
||||||
|
valueField: '',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
form.setFieldsValue(formValues);
|
||||||
}
|
}
|
||||||
}, [selectedField, form]);
|
}, [selectedField, form]);
|
||||||
|
|
||||||
const handleFieldUpdate = (changedValues: any) => {
|
const handleFieldUpdate = (changedValues: any) => {
|
||||||
if (selectedField) {
|
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>
|
<div>
|
||||||
<Divider style={{ margin: '16px 0' }} />
|
<Divider style={{ margin: '16px 0' }} />
|
||||||
<div style={{ marginBottom: 8, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
||||||
<Text strong>选项配置</Text>
|
{/* 数据源类型选择 */}
|
||||||
<Button
|
<Form.Item label="数据源类型" name="dataSourceType">
|
||||||
type="dashed"
|
<Radio.Group buttonStyle="solid">
|
||||||
size="small"
|
<Radio.Button value="static">静态数据</Radio.Button>
|
||||||
icon={<PlusOutlined />}
|
<Radio.Button value="api">接口数据</Radio.Button>
|
||||||
onClick={addOption}
|
</Radio.Group>
|
||||||
>
|
</Form.Item>
|
||||||
添加选项
|
|
||||||
</Button>
|
{/* 静态数据配置 */}
|
||||||
</div>
|
{selectedField.dataSourceType === 'static' && selectedField.options && (
|
||||||
<Space direction="vertical" style={{ width: '100%' }} size="small">
|
<div>
|
||||||
{selectedField.options.map((option, index) => (
|
<div style={{ marginBottom: 8, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
<div key={index} style={{ display: 'flex', gap: 8 }}>
|
<Text strong>选项配置</Text>
|
||||||
<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
|
<Button
|
||||||
danger
|
type="dashed"
|
||||||
icon={<DeleteOutlined />}
|
size="small"
|
||||||
onClick={() => deleteOption(index)}
|
icon={<PlusOutlined />}
|
||||||
/>
|
onClick={addOption}
|
||||||
|
>
|
||||||
|
添加选项
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
))}
|
<Space direction="vertical" style={{ width: '100%' }} size="small">
|
||||||
</Space>
|
{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>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Form>
|
</Form>
|
||||||
|
|||||||
@ -90,6 +90,7 @@ export const COMPONENT_LIST: ComponentMeta[] = [
|
|||||||
category: '基础字段',
|
category: '基础字段',
|
||||||
defaultConfig: {
|
defaultConfig: {
|
||||||
placeholder: '请选择',
|
placeholder: '请选择',
|
||||||
|
dataSourceType: 'static',
|
||||||
options: [
|
options: [
|
||||||
{ label: '选项1', value: '1' },
|
{ label: '选项1', value: '1' },
|
||||||
{ label: '选项2', value: '2' },
|
{ label: '选项2', value: '2' },
|
||||||
@ -103,6 +104,7 @@ export const COMPONENT_LIST: ComponentMeta[] = [
|
|||||||
icon: CheckCircleOutlined,
|
icon: CheckCircleOutlined,
|
||||||
category: '基础字段',
|
category: '基础字段',
|
||||||
defaultConfig: {
|
defaultConfig: {
|
||||||
|
dataSourceType: 'static',
|
||||||
options: [
|
options: [
|
||||||
{ label: '选项1', value: '1' },
|
{ label: '选项1', value: '1' },
|
||||||
{ label: '选项2', value: '2' },
|
{ label: '选项2', value: '2' },
|
||||||
@ -116,6 +118,7 @@ export const COMPONENT_LIST: ComponentMeta[] = [
|
|||||||
icon: CheckSquareOutlined,
|
icon: CheckSquareOutlined,
|
||||||
category: '基础字段',
|
category: '基础字段',
|
||||||
defaultConfig: {
|
defaultConfig: {
|
||||||
|
dataSourceType: 'static',
|
||||||
options: [
|
options: [
|
||||||
{ label: '选项1', value: '1' },
|
{ label: '选项1', value: '1' },
|
||||||
{ label: '选项2', value: '2' },
|
{ label: '选项2', value: '2' },
|
||||||
@ -201,6 +204,7 @@ export const COMPONENT_LIST: ComponentMeta[] = [
|
|||||||
category: '高级字段',
|
category: '高级字段',
|
||||||
defaultConfig: {
|
defaultConfig: {
|
||||||
placeholder: '请选择',
|
placeholder: '请选择',
|
||||||
|
dataSourceType: 'static',
|
||||||
options: [
|
options: [
|
||||||
{
|
{
|
||||||
label: '浙江',
|
label: '浙江',
|
||||||
|
|||||||
105
frontend/src/pages/FormDesigner/hooks/useFieldOptions.ts
Normal file
105
frontend/src/pages/FormDesigner/hooks/useFieldOptions.ts
Normal 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;
|
||||||
|
};
|
||||||
|
|
||||||
@ -28,6 +28,19 @@ export interface FieldOption {
|
|||||||
value: string | number;
|
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 {
|
export interface FieldConfig {
|
||||||
id: string;
|
id: string;
|
||||||
@ -38,7 +51,9 @@ export interface FieldConfig {
|
|||||||
required?: boolean;
|
required?: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
defaultValue?: any;
|
defaultValue?: any;
|
||||||
options?: FieldOption[];
|
options?: FieldOption[]; // 静态选项数据
|
||||||
|
dataSourceType?: DataSourceType; // 数据源类型:static(静态)或 api(接口)
|
||||||
|
apiDataSource?: ApiDataSource; // API 数据源配置(当 dataSourceType 为 'api' 时使用)
|
||||||
min?: number;
|
min?: number;
|
||||||
max?: number;
|
max?: number;
|
||||||
rows?: number;
|
rows?: number;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user