From 00e5bf6315af0f5e7dc9a4015c49061e3d566bbe Mon Sep 17 00:00:00 2001 From: dengqichen Date: Thu, 23 Oct 2025 23:17:48 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A1=A8=E5=8D=95=E8=AE=BE=E8=AE=A1=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dataSource/CascadeDataSourceRegistry.ts | 48 ++++ .../dataSource/CascadeDataSourceService.ts | 138 +++++++++ frontend/src/domain/dataSource/index.ts | 14 +- .../src/domain/dataSource/presets/cascade.ts | 96 +++++++ frontend/src/domain/dataSource/types.ts | 44 +++ .../components/CascadeOptionEditor.tsx | 262 ++++++++++++++++++ .../FormDesigner/components/FieldRenderer.tsx | 9 +- .../FormDesigner/components/PropertyPanel.tsx | 95 ++++++- frontend/src/pages/FormDesigner/config.ts | 16 +- .../FormDesigner/hooks/useCascaderOptions.ts | 110 ++++++++ frontend/src/pages/FormDesigner/types.ts | 17 +- 11 files changed, 838 insertions(+), 11 deletions(-) create mode 100644 frontend/src/domain/dataSource/CascadeDataSourceRegistry.ts create mode 100644 frontend/src/domain/dataSource/CascadeDataSourceService.ts create mode 100644 frontend/src/domain/dataSource/presets/cascade.ts create mode 100644 frontend/src/pages/FormDesigner/components/CascadeOptionEditor.tsx create mode 100644 frontend/src/pages/FormDesigner/hooks/useCascaderOptions.ts diff --git a/frontend/src/domain/dataSource/CascadeDataSourceRegistry.ts b/frontend/src/domain/dataSource/CascadeDataSourceRegistry.ts new file mode 100644 index 00000000..bfa1cf64 --- /dev/null +++ b/frontend/src/domain/dataSource/CascadeDataSourceRegistry.ts @@ -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[]; +}; + diff --git a/frontend/src/domain/dataSource/CascadeDataSourceService.ts b/frontend/src/domain/dataSource/CascadeDataSourceService.ts new file mode 100644 index 00000000..456484a2 --- /dev/null +++ b/frontend/src/domain/dataSource/CascadeDataSourceService.ts @@ -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 { + 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 { + 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 { + try { + // 构建请求参数 + const params: Record = { ...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); + diff --git a/frontend/src/domain/dataSource/index.ts b/frontend/src/domain/dataSource/index.ts index 106843a4..dd848a25 100644 --- a/frontend/src/domain/dataSource/index.ts +++ b/frontend/src/domain/dataSource/index.ts @@ -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'; diff --git a/frontend/src/domain/dataSource/presets/cascade.ts b/frontend/src/domain/dataSource/presets/cascade.ts new file mode 100644 index 00000000..7a5d21fc --- /dev/null +++ b/frontend/src/domain/dataSource/presets/cascade.ts @@ -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 + } + ] +}; + diff --git a/frontend/src/domain/dataSource/types.ts b/frontend/src/domain/dataSource/types.ts index 530eca62..d247235f 100644 --- a/frontend/src/domain/dataSource/types.ts +++ b/frontend/src/domain/dataSource/types.ts @@ -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; // 额外参数 + isLeaf?: (item: any) => boolean; // 判断是否为叶子节点 +} + +/** + * 级联数据源配置接口 + */ +export interface CascadeDataSourceConfig { + levels: CascadeLevelConfig[]; // 级联层级配置 + description?: string; // 描述 +} + /** * 数据源注册表类型 */ export type DataSourceRegistry = Record; +/** + * 级联数据源注册表类型 + */ +export type CascadeDataSourceRegistry = Record; + diff --git a/frontend/src/pages/FormDesigner/components/CascadeOptionEditor.tsx b/frontend/src/pages/FormDesigner/components/CascadeOptionEditor.tsx new file mode 100644 index 00000000..e51863e6 --- /dev/null +++ b/frontend/src/pages/FormDesigner/components/CascadeOptionEditor.tsx @@ -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 = ({ 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 ( +
+ {/* 当前选项 */} +
+ {editing ? ( + <> + setLabel(e.target.value)} + style={{ width: 120, marginRight: 8 }} + /> + setValue(e.target.value)} + style={{ width: 120, marginRight: 8 }} + /> + + + + ) : ( + <> +
+ {option.label} + + ({option.value}) + + {option.children && option.children.length > 0 && ( + + [{option.children.length} 个子项] + + )} +
+ + +
+ + {/* 子选项 */} + {option.children && option.children.length > 0 && ( +
+ {option.children.map((child, index) => ( + { + 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 }); + }} + /> + ))} +
+ )} +
+ ); +}; + +/** + * 级联选项编辑器主组件 + */ +const CascadeOptionEditor: React.FC = ({ 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 ( +
+
+ +
+ + {value.length === 0 ? ( +
+ 暂无选项,点击上方按钮添加 +
+ ) : ( +
+ {value.map((option, index) => ( + handleUpdateOption(index, updatedOption)} + onDelete={() => handleDeleteOption(index)} + onAddChild={() => handleAddChild(index)} + /> + ))} +
+ )} + +
+
+ 💡 使用提示: +
+
1. 点击"添加顶级选项"创建第一级选项
+
2. 点击 图标为选项添加子级
+
3. 支持多级嵌套,构建完整的级联结构
+
4. 值(value)留空时将自动使用标签(label)作为值
+
+
+ ); +}; + +export default CascadeOptionEditor; + diff --git a/frontend/src/pages/FormDesigner/components/FieldRenderer.tsx b/frontend/src/pages/FormDesigner/components/FieldRenderer.tsx index a847eb6e..3a313387 100644 --- a/frontend/src/pages/FormDesigner/components/FieldRenderer.tsx +++ b/frontend/src/pages/FormDesigner/components/FieldRenderer.tsx @@ -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 = ({ }) => { // 获取字段选项(支持静态和动态数据源) const options = useFieldOptions(field); + + // 获取级联选择器选项和懒加载函数 + const cascadeOptions = useCascaderOptions(field); + const loadData = useCascaderLoadData(field); // 布局组件特殊处理 if (field.type === 'divider') { @@ -229,9 +234,11 @@ const FieldRenderer: React.FC = ({ 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'} /> ); diff --git a/frontend/src/pages/FormDesigner/components/PropertyPanel.tsx b/frontend/src/pages/FormDesigner/components/PropertyPanel.tsx index 040d9d02..e98fc05a 100644 --- a/frontend/src/pages/FormDesigner/components/PropertyPanel.tsx +++ b/frontend/src/pages/FormDesigner/components/PropertyPanel.tsx @@ -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 = ({ React.useEffect(() => { if (selectedField) { // 确保嵌套对象被正确设置到表单 - const formValues = { + const formValues: any = { ...selectedField, apiDataSource: selectedField.apiDataSource || { url: '', @@ -54,7 +55,11 @@ const PropertyPanel: React.FC = ({ predefinedDataSource: selectedField.predefinedDataSource || { sourceType: '', }, + predefinedCascadeDataSource: selectedField.predefinedCascadeDataSource || { + sourceType: '', + }, }; + form.setFieldsValue(formValues); } }, [selectedField, form]); @@ -107,9 +112,21 @@ const PropertyPanel: React.FC = ({ 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 = ({ ); } - const hasOptions = ['select', 'radio', 'checkbox', 'cascader'].includes(selectedField.type); + const hasOptions = ['select', 'radio', 'checkbox'].includes(selectedField.type); + const isCascader = selectedField.type === 'cascader'; return (
= ({ )} )} + + {/* 级联选择器专用配置 */} + {isCascader && ( +
+ + + {/* 数据源类型选择(级联选择器只支持静态和预定义) */} + + + 静态数据 + 预定义 + + + + {/* 静态级联数据配置 */} + {selectedField.dataSourceType === 'static' && ( +
+ 级联选项配置 +
+ + + +
+
+ )} + + {/* 预定义级联数据源配置 */} + {selectedField.dataSourceType === 'predefined' && ( +
+ 预定义级联数据源 +
+ + + + +
+
💡 提示:
+
使用系统预定义的级联数据源,支持动态懒加载
+
例如:环境 → 项目、部门 → 用户等
+
+
+
+ )} +
+ )}
); }; diff --git a/frontend/src/pages/FormDesigner/config.ts b/frontend/src/pages/FormDesigner/config.ts index 70dad97e..682cf4e9 100644 --- a/frontend/src/pages/FormDesigner/config.ts +++ b/frontend/src/pages/FormDesigner/config.ts @@ -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' }, + ], + }, + ], }, }, ]; diff --git a/frontend/src/pages/FormDesigner/hooks/useCascaderOptions.ts b/frontend/src/pages/FormDesigner/hooks/useCascaderOptions.ts new file mode 100644 index 00000000..37bbafaf --- /dev/null +++ b/frontend/src/pages/FormDesigner/hooks/useCascaderOptions.ts @@ -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([]); + + 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; +}; + diff --git a/frontend/src/pages/FormDesigner/types.ts b/frontend/src/pages/FormDesigner/types.ts index 7143389b..adc52fe7 100644 --- a/frontend/src/pages/FormDesigner/types.ts +++ b/frontend/src/pages/FormDesigner/types.ts @@ -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;