表单设计器

This commit is contained in:
dengqichen 2025-10-23 23:17:48 +08:00
parent da38d2386b
commit 00e5bf6315
11 changed files with 838 additions and 11 deletions

View File

@ -0,0 +1,48 @@
/**
*
*
*/
import { CascadeDataSourceType, type CascadeDataSourceRegistry } from './types';
import {
environmentProjectsConfig,
jenkinsServerViewsJobsConfig,
departmentUsersConfig,
projectGroupAppsConfig
} from './presets/cascade';
/**
*
*/
export const CASCADE_DATA_SOURCE_REGISTRY: CascadeDataSourceRegistry = {
[CascadeDataSourceType.ENVIRONMENT_PROJECTS]: environmentProjectsConfig,
[CascadeDataSourceType.JENKINS_SERVER_VIEWS_JOBS]: jenkinsServerViewsJobsConfig,
[CascadeDataSourceType.DEPARTMENT_USERS]: departmentUsersConfig,
[CascadeDataSourceType.PROJECT_GROUP_APPS]: projectGroupAppsConfig
};
/**
*
* @param type
* @returns undefined
*/
export const getCascadeDataSourceConfig = (type: CascadeDataSourceType) => {
return CASCADE_DATA_SOURCE_REGISTRY[type];
};
/**
*
* @param type
* @returns
*/
export const hasCascadeDataSource = (type: CascadeDataSourceType): boolean => {
return type in CASCADE_DATA_SOURCE_REGISTRY;
};
/**
*
* @returns
*/
export const getAllCascadeDataSourceTypes = (): CascadeDataSourceType[] => {
return Object.keys(CASCADE_DATA_SOURCE_REGISTRY) as CascadeDataSourceType[];
};

View File

@ -0,0 +1,138 @@
/**
*
*
*/
import request from '@/utils/request';
import { CascadeDataSourceType, type CascadeOption, type CascadeLevelConfig } from './types';
import { getCascadeDataSourceConfig } from './CascadeDataSourceRegistry';
/**
*
*/
class CascadeDataSourceService {
/**
*
* @param type
* @returns
*/
async loadFirstLevel(type: CascadeDataSourceType): Promise<CascadeOption[]> {
const config = getCascadeDataSourceConfig(type);
if (!config || config.levels.length === 0) {
console.error(`❌ 级联数据源类型 ${type} 未配置或配置为空`);
return [];
}
return this.loadLevel(config.levels[0], null, config.levels.length > 1);
}
/**
*
* @param type
* @param selectedOptions [env1, project1]
* @returns
*/
async loadChildren(
type: CascadeDataSourceType,
selectedOptions: any[]
): Promise<CascadeOption[]> {
const config = getCascadeDataSourceConfig(type);
if (!config) {
console.error(`❌ 级联数据源类型 ${type} 未配置`);
return [];
}
const levelIndex = selectedOptions.length;
if (levelIndex >= config.levels.length) {
console.warn(`⚠️ 级联层级 ${levelIndex} 超出配置范围`);
return [];
}
const levelConfig = config.levels[levelIndex];
const parentValue = selectedOptions[levelIndex - 1];
const hasNextLevel = levelIndex + 1 < config.levels.length;
return this.loadLevel(levelConfig, parentValue, hasNextLevel);
}
/**
*
* @param levelConfig
* @param parentValue
* @param hasNextLevel
* @returns
*/
private async loadLevel(
levelConfig: CascadeLevelConfig,
parentValue: any,
hasNextLevel: boolean
): Promise<CascadeOption[]> {
try {
// 构建请求参数
const params: Record<string, any> = { ...levelConfig.params };
// 如果有父级值且配置了父级参数名,添加到请求参数中
if (parentValue !== null && levelConfig.parentParam) {
params[levelConfig.parentParam] = parentValue;
}
// 发起请求
const response = await request.get(levelConfig.url, { params });
const data = response || [];
// 转换为级联选项格式
return data.map((item: any) => {
const option: CascadeOption = {
label: item[levelConfig.labelField],
value: item[levelConfig.valueField]
};
// 判断是否为叶子节点
if (levelConfig.isLeaf) {
option.isLeaf = levelConfig.isLeaf(item);
} else {
// 如果没有下一级配置,则为叶子节点
option.isLeaf = !hasNextLevel;
}
return option;
});
} catch (error) {
console.error(`❌ 加载级联数据失败:`, error);
return [];
}
}
/**
*
* @param type
* @returns
*/
getLevelCount(type: CascadeDataSourceType): number {
const config = getCascadeDataSourceConfig(type);
return config?.levels.length || 0;
}
/**
*
* @param type
* @returns
*/
getDescription(type: CascadeDataSourceType): string {
const config = getCascadeDataSourceConfig(type);
return config?.description || '';
}
}
// 导出单例
export const cascadeDataSourceService = new CascadeDataSourceService();
// 向后兼容导出函数式API
export const loadCascadeFirstLevel = (type: CascadeDataSourceType) =>
cascadeDataSourceService.loadFirstLevel(type);
export const loadCascadeChildren = (type: CascadeDataSourceType, selectedOptions: any[]) =>
cascadeDataSourceService.loadChildren(type, selectedOptions);

View File

@ -4,12 +4,22 @@
*/ */
// 类型定义 // 类型定义
export { DataSourceType } from './types'; export { DataSourceType, CascadeDataSourceType } from './types';
export type { DataSourceOption, DataSourceConfig, DataSourceRegistry } from './types'; export type {
DataSourceOption,
DataSourceConfig,
DataSourceRegistry,
CascadeOption,
CascadeDataSourceConfig,
CascadeDataSourceRegistry,
CascadeLevelConfig
} from './types';
// 注册表 // 注册表
export { DATA_SOURCE_REGISTRY, getDataSourceConfig, hasDataSource, getAllDataSourceTypes } from './DataSourceRegistry'; export { DATA_SOURCE_REGISTRY, getDataSourceConfig, hasDataSource, getAllDataSourceTypes } from './DataSourceRegistry';
export { CASCADE_DATA_SOURCE_REGISTRY, getCascadeDataSourceConfig, hasCascadeDataSource, getAllCascadeDataSourceTypes } from './CascadeDataSourceRegistry';
// 服务 // 服务
export { dataSourceService, loadDataSource, loadMultipleDataSources } from './DataSourceService'; export { dataSourceService, loadDataSource, loadMultipleDataSources } from './DataSourceService';
export { cascadeDataSourceService, loadCascadeFirstLevel, loadCascadeChildren } from './CascadeDataSourceService';

View File

@ -0,0 +1,96 @@
/**
*
*/
import type { CascadeDataSourceConfig } from '../types';
/**
*
*/
export const environmentProjectsConfig: CascadeDataSourceConfig = {
description: '环境 → 项目',
levels: [
{
url: '/api/v1/environment/list',
labelField: 'name',
valueField: 'id'
},
{
url: '/api/v1/deployment-config/list',
parentParam: 'environmentId',
labelField: 'applicationName',
valueField: 'id',
isLeaf: () => true // 第二级是叶子节点
}
]
};
/**
* Jenkins View Job
*/
export const jenkinsServerViewsJobsConfig: CascadeDataSourceConfig = {
description: 'Jenkins 服务器 → View → Job',
levels: [
{
url: '/api/v1/external-system/list',
params: { type: 'JENKINS' },
labelField: 'name',
valueField: 'id'
},
{
url: '/api/v1/jenkins/views',
parentParam: 'externalSystemId',
labelField: 'viewName',
valueField: 'id'
},
{
url: '/api/v1/jenkins/jobs',
parentParam: 'viewId',
labelField: 'jobName',
valueField: 'id',
isLeaf: () => true
}
]
};
/**
*
*/
export const departmentUsersConfig: CascadeDataSourceConfig = {
description: '部门 → 用户',
levels: [
{
url: '/api/v1/department/list',
labelField: 'name',
valueField: 'code'
},
{
url: '/api/v1/user/list',
parentParam: 'departmentCode',
labelField: 'nickname',
valueField: 'username',
isLeaf: () => true
}
]
};
/**
*
*/
export const projectGroupAppsConfig: CascadeDataSourceConfig = {
description: '项目组 → 应用',
levels: [
{
url: '/api/v1/project-group/list',
labelField: 'name',
valueField: 'id'
},
{
url: '/api/v1/application/list',
parentParam: 'projectGroupId',
labelField: 'appName',
valueField: 'id',
isLeaf: () => true
}
]
};

View File

@ -17,6 +17,16 @@ export enum DataSourceType {
DEPARTMENTS = 'DEPARTMENTS' DEPARTMENTS = 'DEPARTMENTS'
} }
/**
*
*/
export enum CascadeDataSourceType {
ENVIRONMENT_PROJECTS = 'ENVIRONMENT_PROJECTS', // 环境 → 项目
JENKINS_SERVER_VIEWS_JOBS = 'JENKINS_SERVER_VIEWS_JOBS', // Jenkins → View → Job
DEPARTMENT_USERS = 'DEPARTMENT_USERS', // 部门 → 用户
PROJECT_GROUP_APPS = 'PROJECT_GROUP_APPS' // 项目组 → 应用
}
/** /**
* *
*/ */
@ -26,6 +36,15 @@ export interface DataSourceOption {
[key: string]: any; [key: string]: any;
} }
/**
* children
*/
export interface CascadeOption extends DataSourceOption {
children?: CascadeOption[];
isLeaf?: boolean; // 是否为叶子节点
loading?: boolean; // 加载状态
}
/** /**
* *
*/ */
@ -35,8 +54,33 @@ export interface DataSourceConfig {
transform: (data: any) => DataSourceOption[]; transform: (data: any) => DataSourceOption[];
} }
/**
*
*/
export interface CascadeLevelConfig {
url: string; // 接口地址
labelField: string; // 标签字段
valueField: string; // 值字段
parentParam?: string; // 父级参数名(如 'environmentId'
params?: Record<string, any>; // 额外参数
isLeaf?: (item: any) => boolean; // 判断是否为叶子节点
}
/**
*
*/
export interface CascadeDataSourceConfig {
levels: CascadeLevelConfig[]; // 级联层级配置
description?: string; // 描述
}
/** /**
* *
*/ */
export type DataSourceRegistry = Record<DataSourceType, DataSourceConfig>; export type DataSourceRegistry = Record<DataSourceType, DataSourceConfig>;
/**
*
*/
export type CascadeDataSourceRegistry = Record<CascadeDataSourceType, CascadeDataSourceConfig>;

View File

@ -0,0 +1,262 @@
/**
*
*
*/
import React, { useState } from 'react';
import { Button, Input, Space, Popconfirm, Typography, Tooltip } from 'antd';
import { PlusOutlined, DeleteOutlined, EditOutlined, PlusCircleOutlined } from '@ant-design/icons';
import type { CascadeFieldOption } from '../types';
const { Text } = Typography;
interface CascadeOptionEditorProps {
value?: CascadeFieldOption[];
onChange?: (value: CascadeFieldOption[]) => void;
}
interface OptionItemProps {
option: CascadeFieldOption;
level: number;
onUpdate: (option: CascadeFieldOption) => void;
onDelete: () => void;
onAddChild: () => void;
}
/**
*
*/
const OptionItem: React.FC<OptionItemProps> = ({ option, level, onUpdate, onDelete, onAddChild }) => {
const [editing, setEditing] = useState(false);
const [label, setLabel] = useState(option.label);
const [value, setValue] = useState(option.value);
const handleSave = () => {
onUpdate({ ...option, label, value: value || label });
setEditing(false);
};
const handleCancel = () => {
setLabel(option.label);
setValue(option.value);
setEditing(false);
};
const paddingLeft = level * 24;
return (
<div style={{ marginBottom: 8 }}>
{/* 当前选项 */}
<div
style={{
display: 'flex',
alignItems: 'center',
padding: '8px 12px',
backgroundColor: level % 2 === 0 ? '#fafafa' : '#f5f5f5',
borderRadius: 4,
paddingLeft: paddingLeft + 12,
border: '1px solid #e8e8e8',
}}
>
{editing ? (
<>
<Input
size="small"
placeholder="标签"
value={label}
onChange={(e) => setLabel(e.target.value)}
style={{ width: 120, marginRight: 8 }}
/>
<Input
size="small"
placeholder="值(可选)"
value={value}
onChange={(e) => setValue(e.target.value)}
style={{ width: 120, marginRight: 8 }}
/>
<Button size="small" type="primary" onClick={handleSave} style={{ marginRight: 4 }}>
</Button>
<Button size="small" onClick={handleCancel}>
</Button>
</>
) : (
<>
<div style={{ flex: 1, display: 'flex', alignItems: 'center', gap: 8 }}>
<Text strong>{option.label}</Text>
<Text type="secondary" style={{ fontSize: 12 }}>
({option.value})
</Text>
{option.children && option.children.length > 0 && (
<Text type="secondary" style={{ fontSize: 12 }}>
[{option.children.length} ]
</Text>
)}
</div>
<Space size="small">
<Tooltip title="添加子选项">
<Button
size="small"
type="text"
icon={<PlusCircleOutlined />}
onClick={onAddChild}
/>
</Tooltip>
<Tooltip title="编辑">
<Button
size="small"
type="text"
icon={<EditOutlined />}
onClick={() => setEditing(true)}
/>
</Tooltip>
<Popconfirm
title="确定删除此选项吗?"
description={option.children?.length ? '将同时删除所有子选项' : undefined}
onConfirm={onDelete}
okText="确定"
cancelText="取消"
>
<Tooltip title="删除">
<Button size="small" type="text" danger icon={<DeleteOutlined />} />
</Tooltip>
</Popconfirm>
</Space>
</>
)}
</div>
{/* 子选项 */}
{option.children && option.children.length > 0 && (
<div style={{ marginTop: 4 }}>
{option.children.map((child, index) => (
<OptionItem
key={index}
option={child}
level={level + 1}
onUpdate={(updatedChild) => {
const newChildren = [...(option.children || [])];
newChildren[index] = updatedChild;
onUpdate({ ...option, children: newChildren });
}}
onDelete={() => {
const newChildren = option.children?.filter((_, i) => i !== index) || [];
onUpdate({ ...option, children: newChildren.length > 0 ? newChildren : undefined });
}}
onAddChild={() => {
const newChild: CascadeFieldOption = {
label: '新选项',
value: `option_${Date.now()}`,
};
const newChildren = [...(child.children || []), newChild];
const updatedChild = { ...child, children: newChildren };
const newChildren2 = [...(option.children || [])];
newChildren2[index] = updatedChild;
onUpdate({ ...option, children: newChildren2 });
}}
/>
))}
</div>
)}
</div>
);
};
/**
*
*/
const CascadeOptionEditor: React.FC<CascadeOptionEditorProps> = ({ value = [], onChange }) => {
const handleAddOption = () => {
const newOption: CascadeFieldOption = {
label: '新选项',
value: `option_${Date.now()}`,
};
onChange?.([...value, newOption]);
};
const handleUpdateOption = (index: number, updatedOption: CascadeFieldOption) => {
const newOptions = [...value];
newOptions[index] = updatedOption;
onChange?.(newOptions);
};
const handleDeleteOption = (index: number) => {
const newOptions = value.filter((_, i) => i !== index);
onChange?.(newOptions);
};
const handleAddChild = (index: number) => {
const newChild: CascadeFieldOption = {
label: '新子选项',
value: `child_${Date.now()}`,
};
const newOptions = [...value];
const currentOption = newOptions[index];
newOptions[index] = {
...currentOption,
children: [...(currentOption.children || []), newChild],
};
onChange?.(newOptions);
};
return (
<div>
<div style={{ marginBottom: 12 }}>
<Button type="dashed" icon={<PlusOutlined />} onClick={handleAddOption} block>
</Button>
</div>
{value.length === 0 ? (
<div
style={{
padding: 24,
textAlign: 'center',
color: '#8c8c8c',
backgroundColor: '#fafafa',
borderRadius: 4,
border: '1px dashed #d9d9d9',
}}
>
</div>
) : (
<div>
{value.map((option, index) => (
<OptionItem
key={index}
option={option}
level={0}
onUpdate={(updatedOption) => handleUpdateOption(index, updatedOption)}
onDelete={() => handleDeleteOption(index)}
onAddChild={() => handleAddChild(index)}
/>
))}
</div>
)}
<div
style={{
marginTop: 12,
padding: 12,
background: '#f5f5f5',
borderRadius: 4,
fontSize: 12,
color: '#666',
}}
>
<div>
<strong>💡 使</strong>
</div>
<div style={{ marginTop: 4 }}>1. "添加顶级选项"</div>
<div>2. <PlusCircleOutlined /> </div>
<div>3. </div>
<div>4. value使label</div>
</div>
</div>
);
};
export default CascadeOptionEditor;

View File

@ -26,6 +26,7 @@ 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 { useFieldOptions } from '../hooks/useFieldOptions';
import { useCascaderOptions, useCascaderLoadData } from '../hooks/useCascaderOptions';
import '../styles.css'; import '../styles.css';
const { Text } = Typography; const { Text } = Typography;
@ -56,6 +57,10 @@ const FieldRenderer: React.FC<FieldRendererProps> = ({
// 获取字段选项(支持静态和动态数据源) // 获取字段选项(支持静态和动态数据源)
const options = useFieldOptions(field); const options = useFieldOptions(field);
// 获取级联选择器选项和懒加载函数
const cascadeOptions = useCascaderOptions(field);
const loadData = useCascaderLoadData(field);
// 布局组件特殊处理 // 布局组件特殊处理
if (field.type === 'divider') { if (field.type === 'divider') {
return <Divider>{field.label}</Divider>; return <Divider>{field.label}</Divider>;
@ -229,9 +234,11 @@ const FieldRenderer: React.FC<FieldRendererProps> = ({
style={{ width: '100%' }} style={{ width: '100%' }}
placeholder={field.placeholder} placeholder={field.placeholder}
disabled={field.disabled} disabled={field.disabled}
options={options as any} options={cascadeOptions as any}
value={value} value={value}
onChange={onChange} onChange={onChange}
loadData={loadData as any}
changeOnSelect={field.dataSourceType === 'predefined'}
/> />
); );

View File

@ -19,7 +19,8 @@ import {
} 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';
import { DataSourceType } from '@/domain/dataSource'; import { DataSourceType, CascadeDataSourceType } from '@/domain/dataSource';
import CascadeOptionEditor from './CascadeOptionEditor';
const { Text } = Typography; const { Text } = Typography;
const { TabPane } = Tabs; const { TabPane } = Tabs;
@ -42,7 +43,7 @@ const PropertyPanel: React.FC<PropertyPanelProps> = ({
React.useEffect(() => { React.useEffect(() => {
if (selectedField) { if (selectedField) {
// 确保嵌套对象被正确设置到表单 // 确保嵌套对象被正确设置到表单
const formValues = { const formValues: any = {
...selectedField, ...selectedField,
apiDataSource: selectedField.apiDataSource || { apiDataSource: selectedField.apiDataSource || {
url: '', url: '',
@ -54,7 +55,11 @@ const PropertyPanel: React.FC<PropertyPanelProps> = ({
predefinedDataSource: selectedField.predefinedDataSource || { predefinedDataSource: selectedField.predefinedDataSource || {
sourceType: '', sourceType: '',
}, },
predefinedCascadeDataSource: selectedField.predefinedCascadeDataSource || {
sourceType: '',
},
}; };
form.setFieldsValue(formValues); form.setFieldsValue(formValues);
} }
}, [selectedField, form]); }, [selectedField, form]);
@ -107,9 +112,21 @@ const PropertyPanel: React.FC<PropertyPanelProps> = ({
console.log('💾 预定义数据源配置已更新:', updatedField.predefinedDataSource); console.log('💾 预定义数据源配置已更新:', updatedField.predefinedDataSource);
} }
// 合并其他字段 // 如果改变的是 predefinedCascadeDataSource 的子字段
if ('predefinedCascadeDataSource' in changedValues) {
updatedField.predefinedCascadeDataSource = {
...updatedField.predefinedCascadeDataSource,
...changedValues.predefinedCascadeDataSource,
};
console.log('💾 预定义级联数据源配置已更新:', updatedField.predefinedCascadeDataSource);
}
// 合并其他字段cascadeOptions 现在直接作为对象处理,不需要特殊处理)
Object.keys(changedValues).forEach(key => { Object.keys(changedValues).forEach(key => {
if (key !== 'dataSourceType' && key !== 'apiDataSource' && key !== 'predefinedDataSource') { if (key !== 'dataSourceType' &&
key !== 'apiDataSource' &&
key !== 'predefinedDataSource' &&
key !== 'predefinedCascadeDataSource') {
(updatedField as any)[key] = changedValues[key]; (updatedField as any)[key] = changedValues[key];
} }
}); });
@ -155,7 +172,8 @@ const PropertyPanel: React.FC<PropertyPanelProps> = ({
); );
} }
const hasOptions = ['select', 'radio', 'checkbox', 'cascader'].includes(selectedField.type); const hasOptions = ['select', 'radio', 'checkbox'].includes(selectedField.type);
const isCascader = selectedField.type === 'cascader';
return ( return (
<Form <Form
@ -466,6 +484,73 @@ const PropertyPanel: React.FC<PropertyPanelProps> = ({
)} )}
</div> </div>
)} )}
{/* 级联选择器专用配置 */}
{isCascader && (
<div>
<Divider style={{ margin: '16px 0' }} />
{/* 数据源类型选择(级联选择器只支持静态和预定义) */}
<Form.Item label="数据源类型" name="dataSourceType">
<Radio.Group buttonStyle="solid" style={{ display: 'flex', flexWrap: 'nowrap' }}>
<Radio.Button value="static" style={{ flex: 1, textAlign: 'center' }}></Radio.Button>
<Radio.Button value="predefined" style={{ flex: 1, textAlign: 'center' }}></Radio.Button>
</Radio.Group>
</Form.Item>
{/* 静态级联数据配置 */}
{selectedField.dataSourceType === 'static' && (
<div>
<Text strong></Text>
<div style={{ marginTop: 12 }}>
<Form.Item
name="cascadeOptions"
tooltip="可视化配置树形结构的级联选项"
>
<CascadeOptionEditor />
</Form.Item>
</div>
</div>
)}
{/* 预定义级联数据源配置 */}
{selectedField.dataSourceType === 'predefined' && (
<div>
<Text strong></Text>
<div style={{ marginTop: 12 }}>
<Form.Item
label="数据源"
name={['predefinedCascadeDataSource', 'sourceType']}
rules={[{ required: true, message: '请选择级联数据源' }]}
>
<Select placeholder="选择预定义级联数据源">
{Object.keys(CascadeDataSourceType).map((key) => {
const sourceType = CascadeDataSourceType[key as keyof typeof CascadeDataSourceType];
return (
<Select.Option key={sourceType} value={sourceType}>
{key.replace(/_/g, ' → ')}
</Select.Option>
);
})}
</Select>
</Form.Item>
<div style={{
padding: 12,
background: '#f5f5f5',
borderRadius: 4,
fontSize: 12,
color: '#666'
}}>
<div><strong>💡 </strong></div>
<div style={{ marginTop: 4 }}>使</div>
<div style={{ marginTop: 4 }}> </div>
</div>
</div>
</div>
)}
</div>
)}
</Form> </Form>
); );
}; };

View File

@ -205,12 +205,24 @@ export const COMPONENT_LIST: ComponentMeta[] = [
defaultConfig: { defaultConfig: {
placeholder: '请选择', placeholder: '请选择',
dataSourceType: 'static', dataSourceType: 'static',
options: [ cascadeOptions: [
{ {
label: '浙江', label: '浙江',
value: 'zhejiang', value: 'zhejiang',
children: [
{ label: '杭州', value: 'hangzhou' },
{ label: '宁波', value: 'ningbo' },
],
}, },
] as any, {
label: '江苏',
value: 'jiangsu',
children: [
{ label: '南京', value: 'nanjing' },
{ label: '苏州', value: 'suzhou' },
],
},
],
}, },
}, },
]; ];

View File

@ -0,0 +1,110 @@
/**
* Hook
*
*/
import { useState, useEffect, useCallback } from 'react';
import type { FieldConfig, CascadeFieldOption } from '../types';
import {
cascadeDataSourceService,
CascadeDataSourceType,
type CascadeOption
} from '@/domain/dataSource';
/**
* CascadeOption CascadeFieldOption
*/
const convertToCascadeFieldOption = (options: CascadeOption[]): CascadeFieldOption[] => {
return options.map(opt => ({
label: opt.label,
value: opt.value,
isLeaf: opt.isLeaf,
children: opt.children ? convertToCascadeFieldOption(opt.children) : undefined
}));
};
/**
* 使
* @param field
* @returns
*/
export const useCascaderOptions = (field: FieldConfig): CascadeFieldOption[] => {
const [options, setOptions] = useState<CascadeFieldOption[]>([]);
useEffect(() => {
// 如果是静态数据源,直接使用配置的选项
if (field.dataSourceType === 'static' && field.cascadeOptions) {
console.log('📊 使用静态级联数据:', field.cascadeOptions);
setOptions(field.cascadeOptions);
return;
}
// 如果是预定义级联数据源
if (field.dataSourceType === 'predefined' && field.predefinedCascadeDataSource?.sourceType) {
const sourceType = field.predefinedCascadeDataSource.sourceType as CascadeDataSourceType;
console.log(`📡 加载预定义级联数据源: ${sourceType}`);
// 加载第一级数据
cascadeDataSourceService
.loadFirstLevel(sourceType)
.then(data => {
console.log(`✅ 预定义级联数据源 ${sourceType} 第一级加载成功:`, data);
setOptions(convertToCascadeFieldOption(data));
})
.catch(error => {
console.error(`❌ 加载预定义级联数据源 ${sourceType} 失败:`, error);
setOptions([]);
});
}
}, [field.dataSourceType, field.cascadeOptions, field.predefinedCascadeDataSource]);
return options;
};
/**
*
*
*/
export const useCascaderLoadData = (field: FieldConfig) => {
// 获取数据源类型(如果不是预定义类型,则为 null
const sourceType = field.dataSourceType === 'predefined' && field.predefinedCascadeDataSource?.sourceType
? (field.predefinedCascadeDataSource.sourceType as CascadeDataSourceType)
: null;
// 始终调用 useCallback但在内部处理条件逻辑
const loadData = useCallback(
async (selectedOptions: any[]) => {
// 如果不是预定义数据源,直接返回
if (!sourceType) {
return;
}
console.log(`🔄 懒加载级联数据: ${sourceType}, 已选择:`, selectedOptions);
// 提取已选择的值
const selectedValues = selectedOptions.map(opt => opt.value);
try {
// 加载子级数据
const childrenData = await cascadeDataSourceService.loadChildren(sourceType, selectedValues);
console.log(`✅ 懒加载成功,子级数据:`, childrenData);
// 更新最后一个选项的 children
const targetOption = selectedOptions[selectedOptions.length - 1];
targetOption.loading = false;
targetOption.children = convertToCascadeFieldOption(childrenData);
} catch (error) {
console.error(`❌ 懒加载失败:`, error);
const targetOption = selectedOptions[selectedOptions.length - 1];
targetOption.loading = false;
targetOption.children = [];
}
},
[sourceType]
);
// 只有预定义级联数据源才返回懒加载函数,否则返回 undefined
return sourceType ? loadData : undefined;
};

View File

@ -28,6 +28,14 @@ export interface FieldOption {
value: string | number; value: string | number;
} }
// 级联选项类型(用于 cascader支持 children
export interface CascadeFieldOption {
label: string;
value: string | number;
children?: CascadeFieldOption[];
isLeaf?: boolean;
}
// 数据源类型 // 数据源类型
export type DataSourceType = 'static' | 'api' | 'predefined'; export type DataSourceType = 'static' | 'api' | 'predefined';
@ -46,6 +54,11 @@ export interface PredefinedDataSource {
sourceType: string; // 数据源类型,对应 DataSourceType 枚举(如 'JENKINS_SERVERS' sourceType: string; // 数据源类型,对应 DataSourceType 枚举(如 'JENKINS_SERVERS'
} }
// 预定义级联数据源配置
export interface PredefinedCascadeDataSource {
sourceType: string; // 级联数据源类型,对应 CascadeDataSourceType 枚举(如 'ENVIRONMENT_PROJECTS'
}
// 字段配置 // 字段配置
export interface FieldConfig { export interface FieldConfig {
id: string; id: string;
@ -56,10 +69,12 @@ export interface FieldConfig {
required?: boolean; required?: boolean;
disabled?: boolean; disabled?: boolean;
defaultValue?: any; defaultValue?: any;
options?: FieldOption[]; // 静态选项数据 options?: FieldOption[]; // 静态选项数据select, radio, checkbox 使用)
cascadeOptions?: CascadeFieldOption[]; // 静态级联选项数据cascader 静态模式使用)
dataSourceType?: DataSourceType; // 数据源类型static静态、api接口或 predefined预定义 dataSourceType?: DataSourceType; // 数据源类型static静态、api接口或 predefined预定义
apiDataSource?: ApiDataSource; // API 数据源配置(当 dataSourceType 为 'api' 时使用) apiDataSource?: ApiDataSource; // API 数据源配置(当 dataSourceType 为 'api' 时使用)
predefinedDataSource?: PredefinedDataSource; // 预定义数据源配置(当 dataSourceType 为 'predefined' 时使用) predefinedDataSource?: PredefinedDataSource; // 预定义数据源配置(当 dataSourceType 为 'predefined' 时使用)
predefinedCascadeDataSource?: PredefinedCascadeDataSource; // 预定义级联数据源配置cascader 使用)
min?: number; min?: number;
max?: number; max?: number;
rows?: number; rows?: number;