表单设计器
This commit is contained in:
parent
da38d2386b
commit
00e5bf6315
48
frontend/src/domain/dataSource/CascadeDataSourceRegistry.ts
Normal file
48
frontend/src/domain/dataSource/CascadeDataSourceRegistry.ts
Normal 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[];
|
||||
};
|
||||
|
||||
138
frontend/src/domain/dataSource/CascadeDataSourceService.ts
Normal file
138
frontend/src/domain/dataSource/CascadeDataSourceService.ts
Normal 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);
|
||||
|
||||
@ -4,12 +4,22 @@
|
||||
*/
|
||||
|
||||
// 类型定义
|
||||
export { DataSourceType } from './types';
|
||||
export type { DataSourceOption, DataSourceConfig, DataSourceRegistry } from './types';
|
||||
export { DataSourceType, CascadeDataSourceType } from './types';
|
||||
export type {
|
||||
DataSourceOption,
|
||||
DataSourceConfig,
|
||||
DataSourceRegistry,
|
||||
CascadeOption,
|
||||
CascadeDataSourceConfig,
|
||||
CascadeDataSourceRegistry,
|
||||
CascadeLevelConfig
|
||||
} from './types';
|
||||
|
||||
// 注册表
|
||||
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 { cascadeDataSourceService, loadCascadeFirstLevel, loadCascadeChildren } from './CascadeDataSourceService';
|
||||
|
||||
|
||||
96
frontend/src/domain/dataSource/presets/cascade.ts
Normal file
96
frontend/src/domain/dataSource/presets/cascade.ts
Normal 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
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
@ -17,6 +17,16 @@ export enum DataSourceType {
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 级联选项接口(支持 children)
|
||||
*/
|
||||
export interface CascadeOption extends DataSourceOption {
|
||||
children?: CascadeOption[];
|
||||
isLeaf?: boolean; // 是否为叶子节点
|
||||
loading?: boolean; // 加载状态
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据源配置接口
|
||||
*/
|
||||
@ -35,8 +54,33 @@ export interface DataSourceConfig {
|
||||
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 CascadeDataSourceRegistry = Record<CascadeDataSourceType, CascadeDataSourceConfig>;
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -26,6 +26,7 @@ import { UploadOutlined } from '@ant-design/icons';
|
||||
import type { FieldConfig } from '../types';
|
||||
import GridField from './GridField';
|
||||
import { useFieldOptions } from '../hooks/useFieldOptions';
|
||||
import { useCascaderOptions, useCascaderLoadData } from '../hooks/useCascaderOptions';
|
||||
import '../styles.css';
|
||||
|
||||
const { Text } = Typography;
|
||||
@ -55,6 +56,10 @@ const FieldRenderer: React.FC<FieldRendererProps> = ({
|
||||
}) => {
|
||||
// 获取字段选项(支持静态和动态数据源)
|
||||
const options = useFieldOptions(field);
|
||||
|
||||
// 获取级联选择器选项和懒加载函数
|
||||
const cascadeOptions = useCascaderOptions(field);
|
||||
const loadData = useCascaderLoadData(field);
|
||||
|
||||
// 布局组件特殊处理
|
||||
if (field.type === 'divider') {
|
||||
@ -229,9 +234,11 @@ const FieldRenderer: React.FC<FieldRendererProps> = ({
|
||||
style={{ width: '100%' }}
|
||||
placeholder={field.placeholder}
|
||||
disabled={field.disabled}
|
||||
options={options as any}
|
||||
options={cascadeOptions as any}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
loadData={loadData as any}
|
||||
changeOnSelect={field.dataSourceType === 'predefined'}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
@ -19,7 +19,8 @@ import {
|
||||
} from 'antd';
|
||||
import { PlusOutlined, DeleteOutlined } from '@ant-design/icons';
|
||||
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 { TabPane } = Tabs;
|
||||
@ -42,7 +43,7 @@ const PropertyPanel: React.FC<PropertyPanelProps> = ({
|
||||
React.useEffect(() => {
|
||||
if (selectedField) {
|
||||
// 确保嵌套对象被正确设置到表单
|
||||
const formValues = {
|
||||
const formValues: any = {
|
||||
...selectedField,
|
||||
apiDataSource: selectedField.apiDataSource || {
|
||||
url: '',
|
||||
@ -54,7 +55,11 @@ const PropertyPanel: React.FC<PropertyPanelProps> = ({
|
||||
predefinedDataSource: selectedField.predefinedDataSource || {
|
||||
sourceType: '',
|
||||
},
|
||||
predefinedCascadeDataSource: selectedField.predefinedCascadeDataSource || {
|
||||
sourceType: '',
|
||||
},
|
||||
};
|
||||
|
||||
form.setFieldsValue(formValues);
|
||||
}
|
||||
}, [selectedField, form]);
|
||||
@ -107,9 +112,21 @@ const PropertyPanel: React.FC<PropertyPanelProps> = ({
|
||||
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 => {
|
||||
if (key !== 'dataSourceType' && key !== 'apiDataSource' && key !== 'predefinedDataSource') {
|
||||
if (key !== 'dataSourceType' &&
|
||||
key !== 'apiDataSource' &&
|
||||
key !== 'predefinedDataSource' &&
|
||||
key !== 'predefinedCascadeDataSource') {
|
||||
(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 (
|
||||
<Form
|
||||
@ -466,6 +484,73 @@ const PropertyPanel: React.FC<PropertyPanelProps> = ({
|
||||
)}
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -205,12 +205,24 @@ export const COMPONENT_LIST: ComponentMeta[] = [
|
||||
defaultConfig: {
|
||||
placeholder: '请选择',
|
||||
dataSourceType: 'static',
|
||||
options: [
|
||||
cascadeOptions: [
|
||||
{
|
||||
label: '浙江',
|
||||
value: 'zhejiang',
|
||||
children: [
|
||||
{ label: '杭州', value: 'hangzhou' },
|
||||
{ label: '宁波', value: 'ningbo' },
|
||||
],
|
||||
},
|
||||
] as any,
|
||||
{
|
||||
label: '江苏',
|
||||
value: 'jiangsu',
|
||||
children: [
|
||||
{ label: '南京', value: 'nanjing' },
|
||||
{ label: '苏州', value: 'suzhou' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
110
frontend/src/pages/FormDesigner/hooks/useCascaderOptions.ts
Normal file
110
frontend/src/pages/FormDesigner/hooks/useCascaderOptions.ts
Normal 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;
|
||||
};
|
||||
|
||||
@ -28,6 +28,14 @@ export interface FieldOption {
|
||||
value: string | number;
|
||||
}
|
||||
|
||||
// 级联选项类型(用于 cascader,支持 children)
|
||||
export interface CascadeFieldOption {
|
||||
label: string;
|
||||
value: string | number;
|
||||
children?: CascadeFieldOption[];
|
||||
isLeaf?: boolean;
|
||||
}
|
||||
|
||||
// 数据源类型
|
||||
export type DataSourceType = 'static' | 'api' | 'predefined';
|
||||
|
||||
@ -46,6 +54,11 @@ export interface PredefinedDataSource {
|
||||
sourceType: string; // 数据源类型,对应 DataSourceType 枚举(如 'JENKINS_SERVERS')
|
||||
}
|
||||
|
||||
// 预定义级联数据源配置
|
||||
export interface PredefinedCascadeDataSource {
|
||||
sourceType: string; // 级联数据源类型,对应 CascadeDataSourceType 枚举(如 'ENVIRONMENT_PROJECTS')
|
||||
}
|
||||
|
||||
// 字段配置
|
||||
export interface FieldConfig {
|
||||
id: string;
|
||||
@ -56,10 +69,12 @@ export interface FieldConfig {
|
||||
required?: boolean;
|
||||
disabled?: boolean;
|
||||
defaultValue?: any;
|
||||
options?: FieldOption[]; // 静态选项数据
|
||||
options?: FieldOption[]; // 静态选项数据(select, radio, checkbox 使用)
|
||||
cascadeOptions?: CascadeFieldOption[]; // 静态级联选项数据(cascader 静态模式使用)
|
||||
dataSourceType?: DataSourceType; // 数据源类型:static(静态)、api(接口)或 predefined(预定义)
|
||||
apiDataSource?: ApiDataSource; // API 数据源配置(当 dataSourceType 为 'api' 时使用)
|
||||
predefinedDataSource?: PredefinedDataSource; // 预定义数据源配置(当 dataSourceType 为 'predefined' 时使用)
|
||||
predefinedCascadeDataSource?: PredefinedCascadeDataSource; // 预定义级联数据源配置(cascader 使用)
|
||||
min?: number;
|
||||
max?: number;
|
||||
rows?: number;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user