增加代码编辑器表单组件
This commit is contained in:
parent
670093e9ca
commit
3e40119f09
@ -35,6 +35,7 @@ import { useImperativeHandle, forwardRef } from 'react';
|
|||||||
import { Form, message } from 'antd';
|
import { Form, message } from 'antd';
|
||||||
import type { FieldConfig, FormConfig } from './types';
|
import type { FieldConfig, FormConfig } from './types';
|
||||||
import { useFormCore } from './hooks/useFormCore';
|
import { useFormCore } from './hooks/useFormCore';
|
||||||
|
import { computeFormLayout } from './utils/formLayoutHelper';
|
||||||
import FormFieldsRenderer from './components/FormFieldsRenderer';
|
import FormFieldsRenderer from './components/FormFieldsRenderer';
|
||||||
import './styles.css';
|
import './styles.css';
|
||||||
|
|
||||||
@ -96,20 +97,18 @@ const FormPreview = forwardRef<FormPreviewRef, FormPreviewProps>(({ fields, form
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const formLayout = formConfig.labelAlign === 'top' ? 'vertical' : 'horizontal';
|
const layoutConfig = computeFormLayout(formConfig);
|
||||||
const labelColSpan = formConfig.labelAlign === 'left' ? 6 : formConfig.labelAlign === 'right' ? 6 : undefined;
|
|
||||||
const wrapperColSpan = formConfig.labelAlign === 'left' || formConfig.labelAlign === 'right' ? 18 : undefined;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="form-container-padding form-preview-compact">
|
<div className="form-container-padding form-preview-compact">
|
||||||
<Form
|
<Form
|
||||||
form={form}
|
form={form}
|
||||||
layout={formLayout}
|
layout={layoutConfig.layout}
|
||||||
size={formConfig.size}
|
size={formConfig.size}
|
||||||
colon={true}
|
colon={true}
|
||||||
labelAlign={formConfig.labelAlign === 'top' ? undefined : formConfig.labelAlign}
|
labelAlign={layoutConfig.labelAlign}
|
||||||
labelCol={labelColSpan ? { span: labelColSpan } : undefined}
|
labelCol={layoutConfig.labelCol}
|
||||||
wrapperCol={wrapperColSpan ? { span: wrapperColSpan } : undefined}
|
wrapperCol={layoutConfig.wrapperCol}
|
||||||
onValuesChange={handleValuesChange}
|
onValuesChange={handleValuesChange}
|
||||||
>
|
>
|
||||||
{formConfig.title && (
|
{formConfig.title && (
|
||||||
|
|||||||
@ -50,6 +50,7 @@ import type { ComponentMeta } from './config';
|
|||||||
import { COMPONENT_LIST } from './config';
|
import { COMPONENT_LIST } from './config';
|
||||||
import { ComponentsContext } from './Designer';
|
import { ComponentsContext } from './Designer';
|
||||||
import { useFormCore } from './hooks/useFormCore';
|
import { useFormCore } from './hooks/useFormCore';
|
||||||
|
import { computeFormLayout } from './utils/formLayoutHelper';
|
||||||
import FormFieldsRenderer from './components/FormFieldsRenderer';
|
import FormFieldsRenderer from './components/FormFieldsRenderer';
|
||||||
import './styles.css';
|
import './styles.css';
|
||||||
|
|
||||||
@ -230,21 +231,19 @@ const FormRenderer = forwardRef<FormRendererRef, FormRendererProps>((props, ref)
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const formLayout = formConfig.labelAlign === 'top' ? 'vertical' : 'horizontal';
|
const layoutConfig = computeFormLayout(formConfig);
|
||||||
const labelColSpan = formConfig.labelAlign === 'left' ? 6 : formConfig.labelAlign === 'right' ? 6 : undefined;
|
|
||||||
const wrapperColSpan = formConfig.labelAlign === 'left' || formConfig.labelAlign === 'right' ? 18 : undefined;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ComponentsContext.Provider value={allComponents}>
|
<ComponentsContext.Provider value={allComponents}>
|
||||||
<div className="form-container-padding form-renderer-compact">
|
<div className="form-container-padding form-renderer-compact">
|
||||||
<Form
|
<Form
|
||||||
form={form}
|
form={form}
|
||||||
layout={formLayout}
|
layout={layoutConfig.layout}
|
||||||
size={formConfig.size}
|
size={formConfig.size}
|
||||||
colon={true}
|
colon={true}
|
||||||
labelAlign={formConfig.labelAlign === 'top' ? undefined : formConfig.labelAlign}
|
labelAlign={layoutConfig.labelAlign}
|
||||||
labelCol={labelColSpan ? { span: labelColSpan } : undefined}
|
labelCol={layoutConfig.labelCol}
|
||||||
wrapperCol={wrapperColSpan ? { span: wrapperColSpan } : undefined}
|
wrapperCol={layoutConfig.wrapperCol}
|
||||||
onValuesChange={handleValuesChange}
|
onValuesChange={handleValuesChange}
|
||||||
>
|
>
|
||||||
{formConfig.title && (
|
{formConfig.title && (
|
||||||
@ -260,7 +259,10 @@ const FormRenderer = forwardRef<FormRendererRef, FormRendererProps>((props, ref)
|
|||||||
{(showSubmit || showCancel) && (
|
{(showSubmit || showCancel) && (
|
||||||
<Form.Item
|
<Form.Item
|
||||||
style={{ marginTop: 32, marginBottom: 0 }}
|
style={{ marginTop: 32, marginBottom: 0 }}
|
||||||
wrapperCol={{ offset: labelColSpan || 0, span: wrapperColSpan || 24 }}
|
wrapperCol={{
|
||||||
|
offset: layoutConfig.labelCol?.span || 0,
|
||||||
|
span: layoutConfig.wrapperCol?.span || 24
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ display: 'flex', gap: 8 }}>
|
<div style={{ display: 'flex', gap: 8 }}>
|
||||||
{showSubmit && (
|
{showSubmit && (
|
||||||
|
|||||||
@ -8,32 +8,19 @@ import { ComponentsContext } from '../Designer';
|
|||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
Input,
|
Input,
|
||||||
InputNumber,
|
|
||||||
Select,
|
|
||||||
Radio,
|
|
||||||
Checkbox,
|
|
||||||
DatePicker,
|
|
||||||
TimePicker,
|
|
||||||
Switch,
|
|
||||||
Slider,
|
|
||||||
Rate,
|
|
||||||
Upload,
|
|
||||||
Cascader,
|
|
||||||
Divider,
|
Divider,
|
||||||
Button,
|
|
||||||
Typography,
|
Typography,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
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 { useCascaderOptions, useCascaderLoadData } from '../hooks/useCascaderOptions';
|
||||||
|
import { fieldRenderStrategyRegistry } from '../renderers/FieldRenderStrategyRegistry';
|
||||||
|
import type { FieldRenderContext } from '../renderers/IFieldRenderStrategy';
|
||||||
import '../styles.css';
|
import '../styles.css';
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
|
||||||
const { TextArea } = Input;
|
|
||||||
|
|
||||||
interface FieldRendererProps {
|
interface FieldRendererProps {
|
||||||
field: FieldConfig;
|
field: FieldConfig;
|
||||||
value?: any;
|
value?: any;
|
||||||
@ -63,40 +50,27 @@ const FieldRenderer: React.FC<FieldRendererProps> = ({
|
|||||||
hasClipboard = false,
|
hasClipboard = false,
|
||||||
duplicateFieldIds = new Set(),
|
duplicateFieldIds = new Set(),
|
||||||
}) => {
|
}) => {
|
||||||
// 获取字段选项(支持静态和动态数据源)
|
// 获取字段选项和级联选项(支持静态和动态数据源)
|
||||||
const options = useFieldOptions(field);
|
const options = useFieldOptions(field);
|
||||||
|
|
||||||
// 获取级联选择器选项和懒加载函数
|
|
||||||
const cascadeOptions = useCascaderOptions(field);
|
const cascadeOptions = useCascaderOptions(field);
|
||||||
const loadData = useCascaderLoadData(field);
|
const loadData = useCascaderLoadData(field);
|
||||||
|
|
||||||
// 🔌 检查是否有自定义渲染组件(插件式扩展)
|
// 🔌 优先检查自定义渲染组件(插件式扩展)
|
||||||
const allComponents = useContext(ComponentsContext);
|
const allComponents = useContext(ComponentsContext);
|
||||||
const componentMeta = allComponents.find(c => c.type === field.type);
|
const componentMeta = allComponents.find(c => c.type === field.type);
|
||||||
|
|
||||||
if (componentMeta?.FieldRendererComponent) {
|
if (componentMeta?.FieldRendererComponent) {
|
||||||
const CustomRenderer = componentMeta.FieldRendererComponent;
|
const CustomRenderer = componentMeta.FieldRendererComponent;
|
||||||
console.log('✅ FieldRenderer: 使用自定义渲染组件', {
|
|
||||||
fieldType: field.type,
|
|
||||||
isPreview,
|
|
||||||
hasRenderer: !!componentMeta.FieldRendererComponent
|
|
||||||
});
|
|
||||||
return <CustomRenderer field={field} value={value} onChange={onChange} isPreview={isPreview} />;
|
return <CustomRenderer field={field} value={value} onChange={onChange} isPreview={isPreview} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 布局组件特殊处理
|
// 布局组件特殊处理(这些组件不通过策略模式)
|
||||||
if (field.type === 'divider') {
|
if (field.type === 'divider') {
|
||||||
const showText = field.showText !== false; // 默认显示文字
|
const showText = field.showText !== false;
|
||||||
const dividerText = field.dividerText || field.label || '分割线';
|
const dividerText = field.dividerText || field.label || '分割线';
|
||||||
const dividerColor = field.dividerColor || 'rgba(5, 5, 5, 0.06)';
|
const dividerColor = field.dividerColor || 'rgba(5, 5, 5, 0.06)';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Divider
|
<Divider style={{ borderColor: dividerColor, color: dividerColor }}>
|
||||||
style={{
|
|
||||||
borderColor: dividerColor,
|
|
||||||
color: dividerColor,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{showText ? dividerText : null}
|
{showText ? dividerText : null}
|
||||||
</Divider>
|
</Divider>
|
||||||
);
|
);
|
||||||
@ -104,10 +78,7 @@ const FieldRenderer: React.FC<FieldRendererProps> = ({
|
|||||||
|
|
||||||
if (field.type === 'text') {
|
if (field.type === 'text') {
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div style={{ padding: '8px 0', textAlign: field.textAlign || 'left' }}>
|
||||||
padding: '8px 0',
|
|
||||||
textAlign: field.textAlign || 'left',
|
|
||||||
}}>
|
|
||||||
<Text style={{
|
<Text style={{
|
||||||
fontSize: field.fontSize ? `${field.fontSize}px` : '14px',
|
fontSize: field.fontSize ? `${field.fontSize}px` : '14px',
|
||||||
color: field.textColor || '#000000',
|
color: field.textColor || '#000000',
|
||||||
@ -135,171 +106,44 @@ const FieldRenderer: React.FC<FieldRendererProps> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 使用策略模式渲染字段(工厂模式获取策略)
|
||||||
|
const strategy = fieldRenderStrategyRegistry.get(field.type);
|
||||||
|
|
||||||
|
// 计算只读样式
|
||||||
|
const isReadonly = field.readonly || false;
|
||||||
|
const readonlyStyle = isReadonly ? {
|
||||||
|
backgroundColor: '#f5f5f5',
|
||||||
|
cursor: 'not-allowed',
|
||||||
|
color: '#00000040',
|
||||||
|
} : undefined;
|
||||||
|
|
||||||
|
// 构造渲染上下文
|
||||||
|
const renderContext: FieldRenderContext = {
|
||||||
|
field,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
isDisabled: field.disabled || false,
|
||||||
|
isReadonly,
|
||||||
|
readonlyStyle,
|
||||||
|
options,
|
||||||
|
cascadeOptions,
|
||||||
|
loadData,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 渲染字段组件
|
||||||
const renderField = () => {
|
const renderField = () => {
|
||||||
switch (field.type) {
|
if (strategy) {
|
||||||
case 'input':
|
return strategy.render(renderContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 兜底策略:未注册的字段类型使用默认 Input
|
||||||
return (
|
return (
|
||||||
<Input
|
<Input
|
||||||
placeholder={field.placeholder}
|
placeholder={field.placeholder}
|
||||||
disabled={field.disabled}
|
readOnly={isReadonly}
|
||||||
value={value}
|
disabled={field.disabled && !isReadonly}
|
||||||
onChange={(e) => onChange?.(e.target.value)}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
case 'textarea':
|
|
||||||
return (
|
|
||||||
<TextArea
|
|
||||||
rows={field.rows || 4}
|
|
||||||
placeholder={field.placeholder}
|
|
||||||
disabled={field.disabled}
|
|
||||||
value={value}
|
|
||||||
onChange={(e) => onChange?.(e.target.value)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 'number':
|
|
||||||
return (
|
|
||||||
<InputNumber
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
placeholder={field.placeholder}
|
|
||||||
disabled={field.disabled}
|
|
||||||
min={field.min}
|
|
||||||
max={field.max}
|
|
||||||
value={value}
|
|
||||||
onChange={onChange}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 'select':
|
|
||||||
return (
|
|
||||||
<Select
|
|
||||||
placeholder={field.placeholder}
|
|
||||||
disabled={field.disabled}
|
|
||||||
options={options}
|
|
||||||
value={value}
|
|
||||||
onChange={onChange}
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 'radio':
|
|
||||||
return (
|
|
||||||
<Radio.Group
|
|
||||||
options={options}
|
|
||||||
disabled={field.disabled}
|
|
||||||
value={value}
|
|
||||||
onChange={(e) => onChange?.(e.target.value)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 'checkbox':
|
|
||||||
return (
|
|
||||||
<Checkbox.Group
|
|
||||||
options={options}
|
|
||||||
disabled={field.disabled}
|
|
||||||
value={value}
|
|
||||||
onChange={onChange}
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: field.checkboxLayout === 'vertical' ? 'column' : 'row',
|
|
||||||
gap: field.checkboxLayout === 'vertical' ? '8px' : '16px',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 'date':
|
|
||||||
return (
|
|
||||||
<DatePicker
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
placeholder={field.placeholder}
|
|
||||||
disabled={field.disabled}
|
|
||||||
value={value}
|
|
||||||
onChange={onChange}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 'datetime':
|
|
||||||
return (
|
|
||||||
<DatePicker
|
|
||||||
showTime
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
placeholder={field.placeholder}
|
|
||||||
disabled={field.disabled}
|
|
||||||
value={value}
|
|
||||||
onChange={onChange}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 'time':
|
|
||||||
return (
|
|
||||||
<TimePicker
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
placeholder={field.placeholder}
|
|
||||||
disabled={field.disabled}
|
|
||||||
value={value}
|
|
||||||
onChange={onChange}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 'switch':
|
|
||||||
return (
|
|
||||||
<Switch
|
|
||||||
disabled={field.disabled}
|
|
||||||
checked={value}
|
|
||||||
onChange={onChange}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 'slider':
|
|
||||||
return (
|
|
||||||
<Slider
|
|
||||||
min={field.min}
|
|
||||||
max={field.max}
|
|
||||||
disabled={field.disabled}
|
|
||||||
value={value}
|
|
||||||
onChange={onChange}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 'rate':
|
|
||||||
return (
|
|
||||||
<Rate
|
|
||||||
disabled={field.disabled}
|
|
||||||
value={value}
|
|
||||||
onChange={onChange}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 'upload':
|
|
||||||
return (
|
|
||||||
<Upload
|
|
||||||
maxCount={field.maxCount}
|
|
||||||
disabled={field.disabled}
|
|
||||||
>
|
|
||||||
<Button icon={<UploadOutlined />}>上传文件</Button>
|
|
||||||
</Upload>
|
|
||||||
);
|
|
||||||
|
|
||||||
// approver-selector 等扩展字段已通过插件机制处理,不在此硬编码
|
|
||||||
|
|
||||||
case 'cascader':
|
|
||||||
return (
|
|
||||||
<Cascader
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
placeholder={field.placeholder}
|
|
||||||
disabled={field.disabled}
|
|
||||||
options={cascadeOptions as any}
|
|
||||||
value={value}
|
|
||||||
onChange={onChange}
|
|
||||||
loadData={loadData as any}
|
|
||||||
changeOnSelect={field.dataSourceType === 'predefined'}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
default:
|
|
||||||
return <Input placeholder={field.placeholder} />;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 预览模式下,外部已经用 Form.Item 包裹了,这里直接返回控件
|
// 预览模式下,外部已经用 Form.Item 包裹了,这里直接返回控件
|
||||||
|
|||||||
@ -8,9 +8,22 @@ import { Form } from 'antd';
|
|||||||
import type { FieldConfig, FormConfig } from '../types';
|
import type { FieldConfig, FormConfig } from '../types';
|
||||||
import type { FieldState } from '../utils/linkageHelper';
|
import type { FieldState } from '../utils/linkageHelper';
|
||||||
import { mergeValidationRules } from '../utils/validationHelper';
|
import { mergeValidationRules } from '../utils/validationHelper';
|
||||||
|
import { computeFieldState, FieldClassifier } from '../utils/fieldStateHelper';
|
||||||
import FieldRenderer from './FieldRenderer';
|
import FieldRenderer from './FieldRenderer';
|
||||||
import GridFieldPreview from './GridFieldPreview';
|
import GridFieldPreview from './GridFieldPreview';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 隐藏字段组件(内部使用)
|
||||||
|
* 用于渲染隐藏字段(不显示任何 UI,但参与表单提交)
|
||||||
|
*/
|
||||||
|
const HiddenField: React.FC<{ fieldName: string }> = ({ fieldName }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item name={fieldName} noStyle>
|
||||||
|
<input type="hidden" />
|
||||||
|
</Form.Item>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export interface FormFieldsRendererProps {
|
export interface FormFieldsRendererProps {
|
||||||
/** 字段列表 */
|
/** 字段列表 */
|
||||||
fields: FieldConfig[];
|
fields: FieldConfig[];
|
||||||
@ -37,8 +50,11 @@ const FormFieldsRenderer: React.FC<FormFieldsRendererProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const renderFields = (fieldList: FieldConfig[]): React.ReactNode => {
|
const renderFields = (fieldList: FieldConfig[]): React.ReactNode => {
|
||||||
return fieldList.map((field) => {
|
return fieldList.map((field) => {
|
||||||
|
// 使用访问者模式:根据字段类型分类处理
|
||||||
|
const fieldType = FieldClassifier.classify(field);
|
||||||
|
|
||||||
// 布局组件:文本、分割线
|
// 布局组件:文本、分割线
|
||||||
if (field.type === 'text' || field.type === 'divider') {
|
if (fieldType === 'layout') {
|
||||||
return (
|
return (
|
||||||
<div key={field.id} style={{ marginBottom: 8 }}>
|
<div key={field.id} style={{ marginBottom: 8 }}>
|
||||||
<FieldRenderer field={field} isPreview={true} />
|
<FieldRenderer field={field} isPreview={true} />
|
||||||
@ -47,7 +63,7 @@ const FormFieldsRenderer: React.FC<FormFieldsRendererProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 栅格布局:使用专门的预览组件
|
// 栅格布局:使用专门的预览组件
|
||||||
if (field.type === 'grid') {
|
if (fieldType === 'grid') {
|
||||||
return (
|
return (
|
||||||
<GridFieldPreview
|
<GridFieldPreview
|
||||||
key={field.id}
|
key={field.id}
|
||||||
@ -57,19 +73,22 @@ const FormFieldsRenderer: React.FC<FormFieldsRendererProps> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取字段状态(联动规则影响)
|
// 计算字段最终状态(统一处理 hidden, readonly, disabled, visible, required)
|
||||||
const fieldState = fieldStates[field.name] || {};
|
const fieldState = fieldStates[field.name] || {};
|
||||||
const isVisible = fieldState.visible !== false; // 默认显示
|
const computedState = computeFieldState(field, fieldState);
|
||||||
const isDisabled = fieldState.disabled || field.disabled || false;
|
|
||||||
const isRequired = fieldState.required !== undefined ? fieldState.required : field.required;
|
|
||||||
|
|
||||||
// 如果字段被隐藏,不渲染
|
// 如果字段被隐藏,使用隐藏字段组件
|
||||||
if (!isVisible) {
|
if (!computedState.isVisible) {
|
||||||
return null;
|
return (
|
||||||
|
<HiddenField
|
||||||
|
key={field.id}
|
||||||
|
fieldName={field.name}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 合并验证规则
|
// 合并验证规则
|
||||||
const allRules = mergeValidationRules(field, isRequired);
|
const allRules = mergeValidationRules(field, computedState.isRequired);
|
||||||
|
|
||||||
// 普通表单组件使用 Form.Item 包裹
|
// 普通表单组件使用 Form.Item 包裹
|
||||||
return (
|
return (
|
||||||
@ -81,7 +100,11 @@ const FormFieldsRenderer: React.FC<FormFieldsRendererProps> = ({
|
|||||||
rules={allRules}
|
rules={allRules}
|
||||||
>
|
>
|
||||||
<FieldRenderer
|
<FieldRenderer
|
||||||
field={{ ...field, disabled: isDisabled }}
|
field={{
|
||||||
|
...field,
|
||||||
|
disabled: computedState.isDisabled,
|
||||||
|
readonly: computedState.isReadonly
|
||||||
|
}}
|
||||||
isPreview={true}
|
isPreview={true}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import { useDroppable } from '@dnd-kit/core';
|
|||||||
import { Row, Col, Button, Space, Tooltip } from 'antd';
|
import { Row, Col, Button, Space, Tooltip } from 'antd';
|
||||||
import { DeleteOutlined, CopyOutlined, PlusOutlined, ExclamationCircleOutlined } from '@ant-design/icons';
|
import { DeleteOutlined, CopyOutlined, PlusOutlined, ExclamationCircleOutlined } from '@ant-design/icons';
|
||||||
import type { FieldConfig } from '../types';
|
import type { FieldConfig } from '../types';
|
||||||
|
import { computeGridColSpan } from '../utils/formLayoutHelper';
|
||||||
import FieldRenderer from './FieldRenderer';
|
import FieldRenderer from './FieldRenderer';
|
||||||
|
|
||||||
interface GridFieldProps {
|
interface GridFieldProps {
|
||||||
@ -38,20 +39,12 @@ const GridField: React.FC<GridFieldProps> = ({
|
|||||||
const columns = field.columns || 2;
|
const columns = field.columns || 2;
|
||||||
const children = field.children || Array(columns).fill([]);
|
const children = field.children || Array(columns).fill([]);
|
||||||
|
|
||||||
// 使用自定义列宽度或平均分配
|
|
||||||
const getColSpan = (colIndex: number) => {
|
|
||||||
if (field.columnSpans && field.columnSpans.length > colIndex) {
|
|
||||||
return field.columnSpans[colIndex];
|
|
||||||
}
|
|
||||||
return 24 / columns; // 平均分配
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ padding: '8px 0', width: '100%' }}>
|
<div style={{ padding: '8px 0', width: '100%' }}>
|
||||||
<Row gutter={field.gutter || 16}>
|
<Row gutter={field.gutter || 16}>
|
||||||
{children.map((columnFields, colIndex) => {
|
{children.map((columnFields, colIndex) => {
|
||||||
const dropId = `grid-${field.id}-col-${colIndex}`;
|
const dropId = `grid-${field.id}-col-${colIndex}`;
|
||||||
const colSpan = getColSpan(colIndex);
|
const colSpan = computeGridColSpan(field.columnSpans, columns, colIndex);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GridColumn
|
<GridColumn
|
||||||
|
|||||||
@ -5,10 +5,23 @@
|
|||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Row, Col, Form } from 'antd';
|
import { Row, Col, Form } from 'antd';
|
||||||
import type { Rule } from 'antd/es/form';
|
import type { FieldConfig, FormConfig } from '../types';
|
||||||
import type { FieldConfig, FormConfig, ValidationRule } from '../types';
|
import { mergeValidationRules } from '../utils/validationHelper';
|
||||||
|
import { computeFieldState, FieldClassifier } from '../utils/fieldStateHelper';
|
||||||
|
import { computeGridColSpan } from '../utils/formLayoutHelper';
|
||||||
import FieldRenderer from './FieldRenderer';
|
import FieldRenderer from './FieldRenderer';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 隐藏字段组件(内部使用)
|
||||||
|
*/
|
||||||
|
const HiddenField: React.FC<{ fieldName: string }> = ({ fieldName }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item name={fieldName} noStyle>
|
||||||
|
<input type="hidden" />
|
||||||
|
</Form.Item>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
interface GridFieldPreviewProps {
|
interface GridFieldPreviewProps {
|
||||||
field: FieldConfig;
|
field: FieldConfig;
|
||||||
formConfig?: FormConfig;
|
formConfig?: FormConfig;
|
||||||
@ -21,62 +34,24 @@ const GridFieldPreview: React.FC<GridFieldPreviewProps> = ({
|
|||||||
const columns = field.columns || 2;
|
const columns = field.columns || 2;
|
||||||
const children = field.children || Array(columns).fill([]);
|
const children = field.children || Array(columns).fill([]);
|
||||||
|
|
||||||
// 使用自定义列宽度或平均分配
|
|
||||||
const getColSpan = (colIndex: number) => {
|
|
||||||
if (field.columnSpans && field.columnSpans.length > colIndex) {
|
|
||||||
return field.columnSpans[colIndex];
|
|
||||||
}
|
|
||||||
return 24 / columns; // 平均分配
|
|
||||||
};
|
|
||||||
|
|
||||||
// 将验证规则转换为Ant Design的Rule数组(和FormPreview保持一致)
|
|
||||||
const convertValidationRules = (validationRules?: ValidationRule[]): Rule[] => {
|
|
||||||
if (!validationRules || validationRules.length === 0) return [];
|
|
||||||
|
|
||||||
return validationRules.map(rule => {
|
|
||||||
const antdRule: Rule = {
|
|
||||||
type: rule.type === 'email' ? 'email' : rule.type === 'url' ? 'url' : undefined,
|
|
||||||
message: rule.message || `请输入正确的${rule.type}`,
|
|
||||||
};
|
|
||||||
|
|
||||||
switch (rule.type) {
|
|
||||||
case 'required':
|
|
||||||
antdRule.required = true;
|
|
||||||
break;
|
|
||||||
case 'pattern':
|
|
||||||
if (rule.value) {
|
|
||||||
antdRule.pattern = new RegExp(rule.value);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'min':
|
|
||||||
antdRule.type = 'number';
|
|
||||||
antdRule.min = rule.value;
|
|
||||||
break;
|
|
||||||
case 'max':
|
|
||||||
antdRule.type = 'number';
|
|
||||||
antdRule.max = rule.value;
|
|
||||||
break;
|
|
||||||
case 'minLength':
|
|
||||||
antdRule.min = rule.value;
|
|
||||||
break;
|
|
||||||
case 'maxLength':
|
|
||||||
antdRule.max = rule.value;
|
|
||||||
break;
|
|
||||||
case 'phone':
|
|
||||||
antdRule.pattern = /^1[3-9]\d{9}$/;
|
|
||||||
break;
|
|
||||||
case 'idCard':
|
|
||||||
antdRule.pattern = /^[1-9]\d{5}(18|19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dX]$/;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return antdRule;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderFieldItem = (childField: FieldConfig) => {
|
const renderFieldItem = (childField: FieldConfig) => {
|
||||||
// 布局组件直接渲染,不需要 Form.Item
|
// 计算字段最终状态(这里联动规则状态需要从外层传入,暂时只使用字段自身属性)
|
||||||
if (['text', 'divider', 'grid'].includes(childField.type)) {
|
// TODO: 如果需要支持栅格内的联动规则,需要将 fieldStates 作为 props 传入
|
||||||
|
const computedState = computeFieldState(childField);
|
||||||
|
|
||||||
|
// 如果字段被隐藏,使用隐藏字段组件
|
||||||
|
if (!computedState.isVisible) {
|
||||||
|
return (
|
||||||
|
<HiddenField
|
||||||
|
key={childField.id}
|
||||||
|
fieldName={childField.name}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用统一的字段分类器判断布局组件
|
||||||
|
const fieldType = FieldClassifier.classify(childField);
|
||||||
|
if (fieldType === 'layout' || fieldType === 'grid') {
|
||||||
return (
|
return (
|
||||||
<div key={childField.id} style={{ marginBottom: 8 }}>
|
<div key={childField.id} style={{ marginBottom: 8 }}>
|
||||||
<FieldRenderer field={childField} isPreview={true} />
|
<FieldRenderer field={childField} isPreview={true} />
|
||||||
@ -87,25 +62,10 @@ const GridFieldPreview: React.FC<GridFieldPreviewProps> = ({
|
|||||||
// 根据表单配置决定布局方式
|
// 根据表单配置决定布局方式
|
||||||
const isVertical = formConfig?.labelAlign === 'top';
|
const isVertical = formConfig?.labelAlign === 'top';
|
||||||
|
|
||||||
// 合并基础验证规则和自定义验证规则
|
// 合并验证规则(使用统一的工具函数)
|
||||||
const customRules = convertValidationRules(childField.validationRules);
|
const allRules = mergeValidationRules(childField, computedState.isRequired);
|
||||||
|
|
||||||
// 检查自定义验证规则中是否已经包含必填验证
|
|
||||||
const hasRequiredRule = childField.validationRules?.some(rule => rule.type === 'required');
|
|
||||||
|
|
||||||
// 基础规则:只有在没有自定义必填验证时,才使用字段属性中的"是否必填"
|
|
||||||
const baseRules: Rule[] = [];
|
|
||||||
if (childField.required && !hasRequiredRule) {
|
|
||||||
baseRules.push({
|
|
||||||
required: true,
|
|
||||||
message: `请输入${childField.label}`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const allRules = [...baseRules, ...customRules];
|
|
||||||
|
|
||||||
// 普通表单字段用 Form.Item 包裹
|
// 普通表单字段用 Form.Item 包裹
|
||||||
// Form.Item 会自动管理 value 和 onChange,不需要手动传递
|
|
||||||
return (
|
return (
|
||||||
<Form.Item
|
<Form.Item
|
||||||
key={childField.id}
|
key={childField.id}
|
||||||
@ -117,7 +77,11 @@ const GridFieldPreview: React.FC<GridFieldPreviewProps> = ({
|
|||||||
rules={allRules}
|
rules={allRules}
|
||||||
>
|
>
|
||||||
<FieldRenderer
|
<FieldRenderer
|
||||||
field={childField}
|
field={{
|
||||||
|
...childField,
|
||||||
|
disabled: computedState.isDisabled,
|
||||||
|
readonly: computedState.isReadonly,
|
||||||
|
}}
|
||||||
isPreview={true}
|
isPreview={true}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
@ -128,7 +92,7 @@ const GridFieldPreview: React.FC<GridFieldPreviewProps> = ({
|
|||||||
<div style={{ marginBottom: 8 }}>
|
<div style={{ marginBottom: 8 }}>
|
||||||
<Row gutter={field.gutter || 16}>
|
<Row gutter={field.gutter || 16}>
|
||||||
{children.map((columnFields, colIndex) => {
|
{children.map((columnFields, colIndex) => {
|
||||||
const colSpan = getColSpan(colIndex);
|
const colSpan = computeGridColSpan(field.columnSpans, columns, colIndex);
|
||||||
return (
|
return (
|
||||||
<Col key={colIndex} span={colSpan}>
|
<Col key={colIndex} span={colSpan}>
|
||||||
{columnFields.map((childField: FieldConfig) => renderFieldItem(childField))}
|
{columnFields.map((childField: FieldConfig) => renderFieldItem(childField))}
|
||||||
|
|||||||
@ -278,6 +278,14 @@ const PropertyPanel: React.FC<PropertyPanelProps> = ({
|
|||||||
<Switch />
|
<Switch />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item label="是否隐藏" name="hidden" valuePropName="checked">
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item label="是否只读" name="readonly" valuePropName="checked">
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item label="默认值" name="defaultValue">
|
<Form.Item label="默认值" name="defaultValue">
|
||||||
{selectedField.type === 'number' ? (
|
{selectedField.type === 'number' ? (
|
||||||
<InputNumber style={{ width: '100%' }} placeholder="请输入默认值" />
|
<InputNumber style={{ width: '100%' }} placeholder="请输入默认值" />
|
||||||
|
|||||||
@ -5,9 +5,40 @@
|
|||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import type { FormInstance } from 'antd';
|
import type { FormInstance } from 'antd';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
import type { FieldConfig } from '../types';
|
import type { FieldConfig } from '../types';
|
||||||
import { calculateLinkageStates, evaluateCondition, flattenFields, type FieldState } from '../utils/linkageHelper';
|
import { calculateLinkageStates, evaluateCondition, flattenFields, type FieldState } from '../utils/linkageHelper';
|
||||||
import { collectDefaultValues } from '../utils/defaultValueHelper';
|
|
||||||
|
/**
|
||||||
|
* 收集字段的默认值(内部使用)
|
||||||
|
*/
|
||||||
|
const collectDefaultValues = (fieldList: FieldConfig[]): Record<string, any> => {
|
||||||
|
const defaultValues: Record<string, any> = {};
|
||||||
|
|
||||||
|
const collect = (fields: FieldConfig[]) => {
|
||||||
|
fields.forEach(field => {
|
||||||
|
// 处理有默认值的字段
|
||||||
|
if (field.defaultValue !== undefined && field.name) {
|
||||||
|
// 日期字段需要转换为 dayjs 对象
|
||||||
|
if (field.type === 'date' && typeof field.defaultValue === 'string') {
|
||||||
|
defaultValues[field.name] = dayjs(field.defaultValue);
|
||||||
|
} else {
|
||||||
|
defaultValues[field.name] = field.defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 递归处理栅格布局内的字段
|
||||||
|
if (field.type === 'grid' && field.children) {
|
||||||
|
field.children.forEach(columnFields => {
|
||||||
|
collect(columnFields);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
collect(fieldList);
|
||||||
|
return defaultValues;
|
||||||
|
};
|
||||||
|
|
||||||
export interface UseFormCoreOptions {
|
export interface UseFormCoreOptions {
|
||||||
/** 字段列表 */
|
/** 字段列表 */
|
||||||
|
|||||||
@ -0,0 +1,71 @@
|
|||||||
|
/**
|
||||||
|
* 字段渲染策略注册表
|
||||||
|
* 工厂模式:统一管理所有字段类型的渲染策略
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { FieldType } from '../types';
|
||||||
|
import type { IFieldRenderStrategy } from './IFieldRenderStrategy';
|
||||||
|
import { InputStrategy } from './strategies/InputStrategy';
|
||||||
|
import { TextAreaStrategy } from './strategies/TextAreaStrategy';
|
||||||
|
import { NumberStrategy } from './strategies/NumberStrategy';
|
||||||
|
import { SelectStrategy } from './strategies/SelectStrategy';
|
||||||
|
import { RadioStrategy } from './strategies/RadioStrategy';
|
||||||
|
import { CheckboxStrategy } from './strategies/CheckboxStrategy';
|
||||||
|
import { DateStrategy } from './strategies/DateStrategy';
|
||||||
|
import { DateTimeStrategy } from './strategies/DateTimeStrategy';
|
||||||
|
import { TimeStrategy } from './strategies/TimeStrategy';
|
||||||
|
import { SwitchStrategy } from './strategies/SwitchStrategy';
|
||||||
|
import { SliderStrategy } from './strategies/SliderStrategy';
|
||||||
|
import { RateStrategy } from './strategies/RateStrategy';
|
||||||
|
import { UploadStrategy } from './strategies/UploadStrategy';
|
||||||
|
import { CascaderStrategy } from './strategies/CascaderStrategy';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 策略注册表
|
||||||
|
*/
|
||||||
|
class FieldRenderStrategyRegistry {
|
||||||
|
private strategies = new Map<FieldType, IFieldRenderStrategy>();
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
// 注册所有内置策略
|
||||||
|
this.register('input', new InputStrategy());
|
||||||
|
this.register('textarea', new TextAreaStrategy());
|
||||||
|
this.register('number', new NumberStrategy());
|
||||||
|
this.register('select', new SelectStrategy());
|
||||||
|
this.register('radio', new RadioStrategy());
|
||||||
|
this.register('checkbox', new CheckboxStrategy());
|
||||||
|
this.register('date', new DateStrategy());
|
||||||
|
this.register('datetime', new DateTimeStrategy());
|
||||||
|
this.register('time', new TimeStrategy());
|
||||||
|
this.register('switch', new SwitchStrategy());
|
||||||
|
this.register('slider', new SliderStrategy());
|
||||||
|
this.register('rate', new RateStrategy());
|
||||||
|
this.register('upload', new UploadStrategy());
|
||||||
|
this.register('cascader', new CascaderStrategy());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册策略
|
||||||
|
*/
|
||||||
|
register(fieldType: FieldType, strategy: IFieldRenderStrategy): void {
|
||||||
|
this.strategies.set(fieldType, strategy);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取策略(工厂方法)
|
||||||
|
*/
|
||||||
|
get(fieldType: FieldType): IFieldRenderStrategy | undefined {
|
||||||
|
return this.strategies.get(fieldType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否已注册
|
||||||
|
*/
|
||||||
|
has(fieldType: FieldType): boolean {
|
||||||
|
return this.strategies.has(fieldType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 单例模式:全局唯一的注册表实例
|
||||||
|
export const fieldRenderStrategyRegistry = new FieldRenderStrategyRegistry();
|
||||||
|
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
/**
|
||||||
|
* 字段渲染策略接口
|
||||||
|
* 策略模式:每种字段类型有独立的渲染策略
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import type { FieldConfig } from '../types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 字段渲染上下文
|
||||||
|
*/
|
||||||
|
export interface FieldRenderContext {
|
||||||
|
field: FieldConfig;
|
||||||
|
value?: any;
|
||||||
|
onChange?: (value: any) => void;
|
||||||
|
isDisabled: boolean;
|
||||||
|
isReadonly: boolean;
|
||||||
|
readonlyStyle?: React.CSSProperties;
|
||||||
|
options?: any[];
|
||||||
|
cascadeOptions?: any[];
|
||||||
|
loadData?: (selectedOptions: any[]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 字段渲染策略接口
|
||||||
|
*/
|
||||||
|
export interface IFieldRenderStrategy {
|
||||||
|
/** 渲染字段组件 */
|
||||||
|
render(context: FieldRenderContext): React.ReactNode;
|
||||||
|
|
||||||
|
/** 是否支持只读属性(如果不支持,将使用 disabled) */
|
||||||
|
supportsReadonly?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,31 @@
|
|||||||
|
/**
|
||||||
|
* 基础字段策略基类
|
||||||
|
* 提供通用的只读和禁用处理逻辑
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import type { IFieldRenderStrategy, FieldRenderContext } from '../IFieldRenderStrategy';
|
||||||
|
|
||||||
|
export abstract class BaseFieldStrategy implements IFieldRenderStrategy {
|
||||||
|
supportsReadonly = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取组件的禁用/只读状态
|
||||||
|
*/
|
||||||
|
protected getDisabledProps(context: FieldRenderContext): { disabled?: boolean; readOnly?: boolean } {
|
||||||
|
const { isDisabled, isReadonly } = context;
|
||||||
|
const supportsReadonly = this.supportsReadonly ?? false;
|
||||||
|
|
||||||
|
if (supportsReadonly && isReadonly) {
|
||||||
|
return { readOnly: true, disabled: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { disabled: isDisabled || isReadonly };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 抽象方法:子类实现具体的渲染逻辑
|
||||||
|
*/
|
||||||
|
abstract render(context: FieldRenderContext): React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,31 @@
|
|||||||
|
/**
|
||||||
|
* Cascader 字段渲染策略
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { Cascader } from 'antd';
|
||||||
|
import { BaseFieldStrategy } from './BaseFieldStrategy';
|
||||||
|
import type { FieldRenderContext } from '../IFieldRenderStrategy';
|
||||||
|
|
||||||
|
export class CascaderStrategy extends BaseFieldStrategy {
|
||||||
|
supportsReadonly = false;
|
||||||
|
|
||||||
|
render(context: FieldRenderContext): React.ReactNode {
|
||||||
|
const { field, value, onChange, cascadeOptions, loadData } = context;
|
||||||
|
const disabledProps = this.getDisabledProps(context);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Cascader
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
placeholder={field.placeholder}
|
||||||
|
{...disabledProps}
|
||||||
|
options={cascadeOptions as any}
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
loadData={loadData as any}
|
||||||
|
changeOnSelect={field.dataSourceType === 'predefined'}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
/**
|
||||||
|
* Checkbox 字段渲染策略
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { Checkbox } from 'antd';
|
||||||
|
import { BaseFieldStrategy } from './BaseFieldStrategy';
|
||||||
|
import type { FieldRenderContext } from '../IFieldRenderStrategy';
|
||||||
|
|
||||||
|
export class CheckboxStrategy extends BaseFieldStrategy {
|
||||||
|
supportsReadonly = false;
|
||||||
|
|
||||||
|
render(context: FieldRenderContext): React.ReactNode {
|
||||||
|
const { field, value, onChange, options } = context;
|
||||||
|
const disabledProps = this.getDisabledProps(context);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Checkbox.Group
|
||||||
|
{...disabledProps}
|
||||||
|
options={options}
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: field.checkboxLayout === 'vertical' ? 'column' : 'row',
|
||||||
|
gap: field.checkboxLayout === 'vertical' ? '8px' : '16px',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
/**
|
||||||
|
* Date 字段渲染策略
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { DatePicker } from 'antd';
|
||||||
|
import { BaseFieldStrategy } from './BaseFieldStrategy';
|
||||||
|
import type { FieldRenderContext } from '../IFieldRenderStrategy';
|
||||||
|
|
||||||
|
export class DateStrategy extends BaseFieldStrategy {
|
||||||
|
supportsReadonly = false;
|
||||||
|
|
||||||
|
render(context: FieldRenderContext): React.ReactNode {
|
||||||
|
const { field, value, onChange } = context;
|
||||||
|
const disabledProps = this.getDisabledProps(context);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DatePicker
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
placeholder={field.placeholder}
|
||||||
|
{...disabledProps}
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
/**
|
||||||
|
* DateTime 字段渲染策略
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { DatePicker } from 'antd';
|
||||||
|
import { BaseFieldStrategy } from './BaseFieldStrategy';
|
||||||
|
import type { FieldRenderContext } from '../IFieldRenderStrategy';
|
||||||
|
|
||||||
|
export class DateTimeStrategy extends BaseFieldStrategy {
|
||||||
|
supportsReadonly = false;
|
||||||
|
|
||||||
|
render(context: FieldRenderContext): React.ReactNode {
|
||||||
|
const { field, value, onChange } = context;
|
||||||
|
const disabledProps = this.getDisabledProps(context);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DatePicker
|
||||||
|
showTime
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
placeholder={field.placeholder}
|
||||||
|
{...disabledProps}
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
/**
|
||||||
|
* Input 字段渲染策略
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { Input } from 'antd';
|
||||||
|
import { BaseFieldStrategy } from './BaseFieldStrategy';
|
||||||
|
import type { FieldRenderContext } from '../IFieldRenderStrategy';
|
||||||
|
|
||||||
|
export class InputStrategy extends BaseFieldStrategy {
|
||||||
|
supportsReadonly = true;
|
||||||
|
|
||||||
|
render(context: FieldRenderContext): React.ReactNode {
|
||||||
|
const { field, value, onChange, isReadonly, readonlyStyle } = context;
|
||||||
|
const disabledProps = this.getDisabledProps(context);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Input
|
||||||
|
placeholder={field.placeholder}
|
||||||
|
{...disabledProps}
|
||||||
|
style={isReadonly ? readonlyStyle : undefined}
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onChange?.(e.target.value)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
/**
|
||||||
|
* Number 字段渲染策略
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { InputNumber } from 'antd';
|
||||||
|
import { BaseFieldStrategy } from './BaseFieldStrategy';
|
||||||
|
import type { FieldRenderContext } from '../IFieldRenderStrategy';
|
||||||
|
|
||||||
|
export class NumberStrategy extends BaseFieldStrategy {
|
||||||
|
supportsReadonly = false; // InputNumber 不支持 readOnly
|
||||||
|
|
||||||
|
render(context: FieldRenderContext): React.ReactNode {
|
||||||
|
const { field, value, onChange } = context;
|
||||||
|
const disabledProps = this.getDisabledProps(context);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<InputNumber
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
placeholder={field.placeholder}
|
||||||
|
{...disabledProps}
|
||||||
|
min={field.min}
|
||||||
|
max={field.max}
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
/**
|
||||||
|
* Radio 字段渲染策略
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { Radio } from 'antd';
|
||||||
|
import { BaseFieldStrategy } from './BaseFieldStrategy';
|
||||||
|
import type { FieldRenderContext } from '../IFieldRenderStrategy';
|
||||||
|
|
||||||
|
export class RadioStrategy extends BaseFieldStrategy {
|
||||||
|
supportsReadonly = false;
|
||||||
|
|
||||||
|
render(context: FieldRenderContext): React.ReactNode {
|
||||||
|
const { value, onChange, options } = context;
|
||||||
|
const disabledProps = this.getDisabledProps(context);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Radio.Group
|
||||||
|
{...disabledProps}
|
||||||
|
options={options}
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onChange?.(e.target.value)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
/**
|
||||||
|
* Rate 字段渲染策略
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { Rate } from 'antd';
|
||||||
|
import { BaseFieldStrategy } from './BaseFieldStrategy';
|
||||||
|
import type { FieldRenderContext } from '../IFieldRenderStrategy';
|
||||||
|
|
||||||
|
export class RateStrategy extends BaseFieldStrategy {
|
||||||
|
supportsReadonly = false;
|
||||||
|
|
||||||
|
render(context: FieldRenderContext): React.ReactNode {
|
||||||
|
const { value, onChange } = context;
|
||||||
|
const disabledProps = this.getDisabledProps(context);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Rate
|
||||||
|
{...disabledProps}
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
/**
|
||||||
|
* Select 字段渲染策略
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { Select } from 'antd';
|
||||||
|
import { BaseFieldStrategy } from './BaseFieldStrategy';
|
||||||
|
import type { FieldRenderContext } from '../IFieldRenderStrategy';
|
||||||
|
|
||||||
|
export class SelectStrategy extends BaseFieldStrategy {
|
||||||
|
supportsReadonly = false;
|
||||||
|
|
||||||
|
render(context: FieldRenderContext): React.ReactNode {
|
||||||
|
const { field, value, onChange, options } = context;
|
||||||
|
const disabledProps = this.getDisabledProps(context);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
placeholder={field.placeholder}
|
||||||
|
{...disabledProps}
|
||||||
|
options={options}
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
/**
|
||||||
|
* Slider 字段渲染策略
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { Slider } from 'antd';
|
||||||
|
import { BaseFieldStrategy } from './BaseFieldStrategy';
|
||||||
|
import type { FieldRenderContext } from '../IFieldRenderStrategy';
|
||||||
|
|
||||||
|
export class SliderStrategy extends BaseFieldStrategy {
|
||||||
|
supportsReadonly = false;
|
||||||
|
|
||||||
|
render(context: FieldRenderContext): React.ReactNode {
|
||||||
|
const { field, value, onChange } = context;
|
||||||
|
const disabledProps = this.getDisabledProps(context);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Slider
|
||||||
|
{...disabledProps}
|
||||||
|
min={field.min}
|
||||||
|
max={field.max}
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
/**
|
||||||
|
* Switch 字段渲染策略
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { Switch } from 'antd';
|
||||||
|
import { BaseFieldStrategy } from './BaseFieldStrategy';
|
||||||
|
import type { FieldRenderContext } from '../IFieldRenderStrategy';
|
||||||
|
|
||||||
|
export class SwitchStrategy extends BaseFieldStrategy {
|
||||||
|
supportsReadonly = false;
|
||||||
|
|
||||||
|
render(context: FieldRenderContext): React.ReactNode {
|
||||||
|
const { value, onChange } = context;
|
||||||
|
const disabledProps = this.getDisabledProps(context);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Switch
|
||||||
|
{...disabledProps}
|
||||||
|
checked={value}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,31 @@
|
|||||||
|
/**
|
||||||
|
* TextArea 字段渲染策略
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { Input } from 'antd';
|
||||||
|
import { BaseFieldStrategy } from './BaseFieldStrategy';
|
||||||
|
import type { FieldRenderContext } from '../IFieldRenderStrategy';
|
||||||
|
|
||||||
|
const { TextArea } = Input;
|
||||||
|
|
||||||
|
export class TextAreaStrategy extends BaseFieldStrategy {
|
||||||
|
supportsReadonly = true;
|
||||||
|
|
||||||
|
render(context: FieldRenderContext): React.ReactNode {
|
||||||
|
const { field, value, onChange, isReadonly, readonlyStyle } = context;
|
||||||
|
const disabledProps = this.getDisabledProps(context);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TextArea
|
||||||
|
rows={field.rows || 4}
|
||||||
|
placeholder={field.placeholder}
|
||||||
|
{...disabledProps}
|
||||||
|
style={isReadonly ? readonlyStyle : undefined}
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onChange?.(e.target.value)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
/**
|
||||||
|
* Time 字段渲染策略
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { TimePicker } from 'antd';
|
||||||
|
import { BaseFieldStrategy } from './BaseFieldStrategy';
|
||||||
|
import type { FieldRenderContext } from '../IFieldRenderStrategy';
|
||||||
|
|
||||||
|
export class TimeStrategy extends BaseFieldStrategy {
|
||||||
|
supportsReadonly = false;
|
||||||
|
|
||||||
|
render(context: FieldRenderContext): React.ReactNode {
|
||||||
|
const { field, value, onChange } = context;
|
||||||
|
const disabledProps = this.getDisabledProps(context);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TimePicker
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
placeholder={field.placeholder}
|
||||||
|
{...disabledProps}
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
/**
|
||||||
|
* Upload 字段渲染策略
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { Upload, Button } from 'antd';
|
||||||
|
import { UploadOutlined } from '@ant-design/icons';
|
||||||
|
import { BaseFieldStrategy } from './BaseFieldStrategy';
|
||||||
|
import type { FieldRenderContext } from '../IFieldRenderStrategy';
|
||||||
|
|
||||||
|
export class UploadStrategy extends BaseFieldStrategy {
|
||||||
|
supportsReadonly = false;
|
||||||
|
|
||||||
|
render(context: FieldRenderContext): React.ReactNode {
|
||||||
|
const { field } = context;
|
||||||
|
const disabledProps = this.getDisabledProps(context);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Upload
|
||||||
|
{...disabledProps}
|
||||||
|
maxCount={field.maxCount}
|
||||||
|
>
|
||||||
|
<Button icon={<UploadOutlined />}>上传文件</Button>
|
||||||
|
</Upload>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -363,3 +363,24 @@
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 只读字段样式:灰色背景,不可编辑 */
|
||||||
|
.ant-input[readonly],
|
||||||
|
.ant-input:read-only {
|
||||||
|
background-color: #f5f5f5 !important;
|
||||||
|
cursor: not-allowed !important;
|
||||||
|
color: #00000040 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-input-number-disabled {
|
||||||
|
background-color: #f5f5f5 !important;
|
||||||
|
cursor: not-allowed !important;
|
||||||
|
color: #00000040 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea.ant-input[readonly],
|
||||||
|
textarea.ant-input:read-only {
|
||||||
|
background-color: #f5f5f5 !important;
|
||||||
|
cursor: not-allowed !important;
|
||||||
|
color: #00000040 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@ -115,6 +115,8 @@ export interface FieldConfig {
|
|||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
hidden?: boolean; // 是否隐藏字段(隐藏字段不显示,但仍参与表单提交)
|
||||||
|
readonly?: boolean; // 是否只读(只读字段显示但不可编辑)
|
||||||
defaultValue?: any;
|
defaultValue?: any;
|
||||||
options?: FieldOption[]; // 静态选项数据(select, radio, checkbox 使用)
|
options?: FieldOption[]; // 静态选项数据(select, radio, checkbox 使用)
|
||||||
cascadeOptions?: CascadeFieldOption[]; // 静态级联选项数据(cascader 静态模式使用)
|
cascadeOptions?: CascadeFieldOption[]; // 静态级联选项数据(cascader 静态模式使用)
|
||||||
|
|||||||
@ -1,41 +0,0 @@
|
|||||||
/**
|
|
||||||
* 表单默认值处理工具
|
|
||||||
* 处理字段的默认值,包括日期类型的特殊处理
|
|
||||||
*/
|
|
||||||
|
|
||||||
import dayjs from 'dayjs';
|
|
||||||
import type { FieldConfig } from '../types';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 收集字段的默认值
|
|
||||||
* @param fieldList 字段列表
|
|
||||||
* @returns 默认值对象
|
|
||||||
*/
|
|
||||||
export const collectDefaultValues = (fieldList: FieldConfig[]): Record<string, any> => {
|
|
||||||
const defaultValues: Record<string, any> = {};
|
|
||||||
|
|
||||||
const collect = (fields: FieldConfig[]) => {
|
|
||||||
fields.forEach(field => {
|
|
||||||
// 处理有默认值的字段
|
|
||||||
if (field.defaultValue !== undefined && field.name) {
|
|
||||||
// 日期字段需要转换为 dayjs 对象
|
|
||||||
if (field.type === 'date' && typeof field.defaultValue === 'string') {
|
|
||||||
defaultValues[field.name] = dayjs(field.defaultValue);
|
|
||||||
} else {
|
|
||||||
defaultValues[field.name] = field.defaultValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 递归处理栅格布局内的字段
|
|
||||||
if (field.type === 'grid' && field.children) {
|
|
||||||
field.children.forEach(columnFields => {
|
|
||||||
collect(columnFields);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
collect(fieldList);
|
|
||||||
return defaultValues;
|
|
||||||
};
|
|
||||||
|
|
||||||
@ -0,0 +1,73 @@
|
|||||||
|
/**
|
||||||
|
* 字段状态工具
|
||||||
|
* 统一计算和处理字段的显示状态、禁用状态、只读状态等
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { FieldConfig } from '../types';
|
||||||
|
import type { FieldState } from './linkageHelper';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算字段的最终状态
|
||||||
|
* @param field 字段配置
|
||||||
|
* @param fieldState 联动规则计算的字段状态
|
||||||
|
* @returns 最终字段状态
|
||||||
|
*/
|
||||||
|
export interface ComputedFieldState {
|
||||||
|
isVisible: boolean; // 是否可见(考虑 hidden 和联动规则)
|
||||||
|
isDisabled: boolean; // 是否禁用(考虑 disabled 和联动规则)
|
||||||
|
isReadonly: boolean; // 是否只读
|
||||||
|
isRequired: boolean; // 是否必填(考虑 required 和联动规则)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const computeFieldState = (
|
||||||
|
field: FieldConfig,
|
||||||
|
fieldState: FieldState = {}
|
||||||
|
): ComputedFieldState => {
|
||||||
|
// 可见性:联动规则的 visible 优先级高于字段的 hidden
|
||||||
|
// 如果联动规则设置为 visible=false,则隐藏;否则检查字段的 hidden 属性
|
||||||
|
const isVisible = fieldState.visible !== false && !field.hidden;
|
||||||
|
|
||||||
|
// 禁用状态:联动规则的 disabled 或字段的 disabled
|
||||||
|
const isDisabled = fieldState.disabled || field.disabled || false;
|
||||||
|
|
||||||
|
// 只读状态:字段的 readonly(不受联动规则影响)
|
||||||
|
const isReadonly = field.readonly || false;
|
||||||
|
|
||||||
|
// 必填状态:联动规则的 required 优先,否则使用字段的 required
|
||||||
|
const isRequired = fieldState.required !== undefined
|
||||||
|
? fieldState.required
|
||||||
|
: field.required || false;
|
||||||
|
|
||||||
|
return {
|
||||||
|
isVisible,
|
||||||
|
isDisabled,
|
||||||
|
isReadonly,
|
||||||
|
isRequired,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 字段分类器:根据字段类型判断应该使用哪个渲染方式
|
||||||
|
*/
|
||||||
|
export class FieldClassifier {
|
||||||
|
/**
|
||||||
|
* 判断字段类型
|
||||||
|
* @returns 'layout' | 'grid' | 'field'
|
||||||
|
* 注意:hidden 字段的判断在 FormFieldsRenderer 中通过 computeFieldState 完成
|
||||||
|
*/
|
||||||
|
static classify(field: FieldConfig): 'layout' | 'grid' | 'field' {
|
||||||
|
// 布局字段:文本、分割线
|
||||||
|
if (field.type === 'text' || field.type === 'divider') {
|
||||||
|
return 'layout';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 栅格布局
|
||||||
|
if (field.type === 'grid') {
|
||||||
|
return 'grid';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 普通表单字段
|
||||||
|
return 'field';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,60 @@
|
|||||||
|
/**
|
||||||
|
* 表单布局工具
|
||||||
|
* 统一处理表单布局相关的计算逻辑
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { FormConfig } from '../types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算表单布局配置
|
||||||
|
* @param formConfig 表单配置
|
||||||
|
* @returns 表单布局相关配置
|
||||||
|
*/
|
||||||
|
export interface FormLayoutConfig {
|
||||||
|
/** Form layout 属性 */
|
||||||
|
layout: 'vertical' | 'horizontal';
|
||||||
|
/** labelCol 配置 */
|
||||||
|
labelCol?: { span: number };
|
||||||
|
/** wrapperCol 配置 */
|
||||||
|
wrapperCol?: { span: number };
|
||||||
|
/** labelAlign 属性 */
|
||||||
|
labelAlign?: 'left' | 'right';
|
||||||
|
}
|
||||||
|
|
||||||
|
export const computeFormLayout = (formConfig: FormConfig): FormLayoutConfig => {
|
||||||
|
const layout = formConfig.labelAlign === 'top' ? 'vertical' : 'horizontal';
|
||||||
|
|
||||||
|
let labelCol: { span: number } | undefined;
|
||||||
|
let wrapperCol: { span: number } | undefined;
|
||||||
|
|
||||||
|
if (formConfig.labelAlign === 'left' || formConfig.labelAlign === 'right') {
|
||||||
|
labelCol = { span: 6 };
|
||||||
|
wrapperCol = { span: 18 };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
layout,
|
||||||
|
labelCol,
|
||||||
|
wrapperCol,
|
||||||
|
labelAlign: formConfig.labelAlign === 'top' ? undefined : formConfig.labelAlign,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算栅格列的宽度
|
||||||
|
* @param columnSpans 自定义列宽度数组
|
||||||
|
* @param columns 列数
|
||||||
|
* @param colIndex 当前列索引
|
||||||
|
* @returns 列宽度(0-24)
|
||||||
|
*/
|
||||||
|
export const computeGridColSpan = (
|
||||||
|
columnSpans: number[] | undefined,
|
||||||
|
columns: number,
|
||||||
|
colIndex: number
|
||||||
|
): number => {
|
||||||
|
if (columnSpans && columnSpans.length > colIndex) {
|
||||||
|
return columnSpans[colIndex];
|
||||||
|
}
|
||||||
|
return Math.floor(24 / columns);
|
||||||
|
};
|
||||||
|
|
||||||
@ -2,7 +2,8 @@
|
|||||||
* 级联数据源注册表
|
* 级联数据源注册表
|
||||||
* 集中管理所有预设级联数据源配置
|
* 集中管理所有预设级联数据源配置
|
||||||
*/
|
*/
|
||||||
import { CascadeDataSourceType, type CascadeDataSourceRegistry } from './types';
|
import { CascadeDataSourceType, type CascadeDataSourceConfig, type CascadeDataSourceRegistry } from './types';
|
||||||
|
import { BaseRegistry } from './core/BaseRegistry';
|
||||||
import {
|
import {
|
||||||
environmentProjectsConfig,
|
environmentProjectsConfig,
|
||||||
jenkinsServerViewsJobsConfig,
|
jenkinsServerViewsJobsConfig,
|
||||||
@ -12,39 +13,46 @@ import {
|
|||||||
} from './presets/cascade';
|
} from './presets/cascade';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 级联数据源配置注册表
|
* 级联数据源注册表实例
|
||||||
*/
|
*/
|
||||||
export const CASCADE_DATA_SOURCE_REGISTRY: CascadeDataSourceRegistry = {
|
class CascadeDataSourceRegistryImpl extends BaseRegistry<CascadeDataSourceType, CascadeDataSourceConfig> {
|
||||||
[CascadeDataSourceType.ENVIRONMENT_PROJECTS]: environmentProjectsConfig,
|
constructor() {
|
||||||
[CascadeDataSourceType.JENKINS_SERVER_VIEWS_JOBS]: jenkinsServerViewsJobsConfig,
|
super();
|
||||||
[CascadeDataSourceType.DEPARTMENT_USERS]: departmentUsersConfig,
|
// 注册所有预设配置
|
||||||
[CascadeDataSourceType.DEPARTMENT_TREE]: departmentTreeConfig,
|
this.register(CascadeDataSourceType.ENVIRONMENT_PROJECTS, environmentProjectsConfig);
|
||||||
[CascadeDataSourceType.PROJECT_GROUP_APPS]: projectGroupAppsConfig
|
this.register(CascadeDataSourceType.JENKINS_SERVER_VIEWS_JOBS, jenkinsServerViewsJobsConfig);
|
||||||
};
|
this.register(CascadeDataSourceType.DEPARTMENT_USERS, departmentUsersConfig);
|
||||||
|
this.register(CascadeDataSourceType.DEPARTMENT_TREE, departmentTreeConfig);
|
||||||
|
this.register(CascadeDataSourceType.PROJECT_GROUP_APPS, projectGroupAppsConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取注册表对象(向后兼容)
|
||||||
|
*/
|
||||||
|
toObject(): CascadeDataSourceRegistry {
|
||||||
|
return this['registry'] as CascadeDataSourceRegistry;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const registryInstance = new CascadeDataSourceRegistryImpl();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 级联数据源配置注册表(向后兼容)
|
||||||
|
*/
|
||||||
|
export const CASCADE_DATA_SOURCE_REGISTRY: CascadeDataSourceRegistry = registryInstance.toObject();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取级联数据源配置
|
* 获取级联数据源配置
|
||||||
* @param type 级联数据源类型
|
|
||||||
* @returns 级联数据源配置,如果不存在则返回 undefined
|
|
||||||
*/
|
*/
|
||||||
export const getCascadeDataSourceConfig = (type: CascadeDataSourceType) => {
|
export const getCascadeDataSourceConfig = (type: CascadeDataSourceType) => registryInstance.get(type);
|
||||||
return CASCADE_DATA_SOURCE_REGISTRY[type];
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查级联数据源类型是否已注册
|
* 检查级联数据源类型是否已注册
|
||||||
* @param type 级联数据源类型
|
|
||||||
* @returns 是否已注册
|
|
||||||
*/
|
*/
|
||||||
export const hasCascadeDataSource = (type: CascadeDataSourceType): boolean => {
|
export const hasCascadeDataSource = (type: CascadeDataSourceType): boolean => registryInstance.has(type);
|
||||||
return type in CASCADE_DATA_SOURCE_REGISTRY;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取所有已注册的级联数据源类型
|
* 获取所有已注册的级联数据源类型
|
||||||
* @returns 级联数据源类型列表
|
|
||||||
*/
|
*/
|
||||||
export const getAllCascadeDataSourceTypes = (): CascadeDataSourceType[] => {
|
export const getAllCascadeDataSourceTypes = (): CascadeDataSourceType[] => registryInstance.getAllKeys();
|
||||||
return Object.keys(CASCADE_DATA_SOURCE_REGISTRY) as CascadeDataSourceType[];
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|||||||
@ -2,9 +2,10 @@
|
|||||||
* 级联数据源服务
|
* 级联数据源服务
|
||||||
* 提供懒加载级联数据功能
|
* 提供懒加载级联数据功能
|
||||||
*/
|
*/
|
||||||
import request from '@/utils/request';
|
|
||||||
import {CascadeDataSourceType, type CascadeOption, type CascadeLevelConfig} from './types';
|
import {CascadeDataSourceType, type CascadeOption, type CascadeLevelConfig} from './types';
|
||||||
import {getCascadeDataSourceConfig} from './CascadeDataSourceRegistry';
|
import {getCascadeDataSourceConfig} from './CascadeDataSourceRegistry';
|
||||||
|
import { loadData } from './core/requestHelper';
|
||||||
|
import { warn } from './core/errorHandler';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 级联数据源服务类
|
* 级联数据源服务类
|
||||||
@ -26,7 +27,7 @@ class CascadeDataSourceService {
|
|||||||
const config = getCascadeDataSourceConfig(type);
|
const config = getCascadeDataSourceConfig(type);
|
||||||
|
|
||||||
if (!config) {
|
if (!config) {
|
||||||
console.error(`❌ 级联数据源类型 ${type} 未配置`);
|
warn(`级联数据源类型 ${type} 未配置`);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -37,7 +38,7 @@ class CascadeDataSourceService {
|
|||||||
|
|
||||||
// 固定层级模式
|
// 固定层级模式
|
||||||
if (!config.levels || config.levels.length === 0) {
|
if (!config.levels || config.levels.length === 0) {
|
||||||
console.error(`❌ 级联数据源类型 ${type} 未配置层级`);
|
warn(`级联数据源类型 ${type} 未配置层级`);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -57,7 +58,7 @@ class CascadeDataSourceService {
|
|||||||
const config = getCascadeDataSourceConfig(type);
|
const config = getCascadeDataSourceConfig(type);
|
||||||
|
|
||||||
if (!config) {
|
if (!config) {
|
||||||
console.error(`❌ 级联数据源类型 ${type} 未配置`);
|
warn(`级联数据源类型 ${type} 未配置`);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,7 +77,7 @@ class CascadeDataSourceService {
|
|||||||
const levelIndex = selectedOptions.length;
|
const levelIndex = selectedOptions.length;
|
||||||
|
|
||||||
if (levelIndex >= config.levels.length) {
|
if (levelIndex >= config.levels.length) {
|
||||||
console.warn(`⚠️ 级联层级 ${levelIndex} 超出配置范围`);
|
warn(`级联层级 ${levelIndex} 超出配置范围`);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -97,7 +98,6 @@ class CascadeDataSourceService {
|
|||||||
recursiveConfig: any,
|
recursiveConfig: any,
|
||||||
parentValue: any
|
parentValue: any
|
||||||
): Promise<CascadeOption[]> {
|
): Promise<CascadeOption[]> {
|
||||||
try {
|
|
||||||
// 构建请求参数
|
// 构建请求参数
|
||||||
const params: Record<string, any> = {...recursiveConfig.params};
|
const params: Record<string, any> = {...recursiveConfig.params};
|
||||||
|
|
||||||
@ -106,9 +106,12 @@ class CascadeDataSourceService {
|
|||||||
params[recursiveConfig.parentParam] = parentValue;
|
params[recursiveConfig.parentParam] = parentValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 发起请求
|
// 使用统一的请求工具加载数据
|
||||||
const response = await request.get(recursiveConfig.url, {params});
|
const data = await loadData<any>(
|
||||||
const data = response || [];
|
recursiveConfig.url,
|
||||||
|
params,
|
||||||
|
`递归级联数据 (${recursiveConfig.url})`
|
||||||
|
);
|
||||||
|
|
||||||
// 转换为级联选项格式
|
// 转换为级联选项格式
|
||||||
return data.map((item: any) => {
|
return data.map((item: any) => {
|
||||||
@ -134,10 +137,6 @@ class CascadeDataSourceService {
|
|||||||
|
|
||||||
return option;
|
return option;
|
||||||
});
|
});
|
||||||
} catch (error) {
|
|
||||||
console.error(`❌ 加载递归级联数据失败:`, error);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -152,7 +151,6 @@ class CascadeDataSourceService {
|
|||||||
parentValue: any,
|
parentValue: any,
|
||||||
hasNextLevel: boolean
|
hasNextLevel: boolean
|
||||||
): Promise<CascadeOption[]> {
|
): Promise<CascadeOption[]> {
|
||||||
try {
|
|
||||||
// 构建请求参数
|
// 构建请求参数
|
||||||
const params: Record<string, any> = {...levelConfig.params};
|
const params: Record<string, any> = {...levelConfig.params};
|
||||||
|
|
||||||
@ -161,9 +159,12 @@ class CascadeDataSourceService {
|
|||||||
params[levelConfig.parentParam] = parentValue;
|
params[levelConfig.parentParam] = parentValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 发起请求
|
// 使用统一的请求工具加载数据
|
||||||
const response = await request.get(levelConfig.url, {params});
|
const data = await loadData<any>(
|
||||||
const data = response || [];
|
levelConfig.url,
|
||||||
|
params,
|
||||||
|
`级联数据 (${levelConfig.url})`
|
||||||
|
);
|
||||||
|
|
||||||
// 转换为级联选项格式
|
// 转换为级联选项格式
|
||||||
return data.map((item: any) => {
|
return data.map((item: any) => {
|
||||||
@ -182,10 +183,6 @@ class CascadeDataSourceService {
|
|||||||
|
|
||||||
return option;
|
return option;
|
||||||
});
|
});
|
||||||
} catch (error) {
|
|
||||||
console.error(`❌ 加载级联数据失败:`, error);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -2,56 +2,71 @@
|
|||||||
* 数据源注册表
|
* 数据源注册表
|
||||||
* 集中管理所有预设数据源配置
|
* 集中管理所有预设数据源配置
|
||||||
*/
|
*/
|
||||||
import { DataSourceType, type DataSourceRegistry } from './types';
|
import { DataSourceType, type DataSourceConfig, type DataSourceRegistry } from './types';
|
||||||
import { jenkinsServersConfig } from './presets/jenkins';
|
import { BaseRegistry } from './core/BaseRegistry';
|
||||||
import { k8sClustersConfig } from './presets/k8s';
|
import {
|
||||||
import { gitRepositoriesConfig } from './presets/git';
|
jenkinsServersConfig,
|
||||||
import { dockerRegistriesConfig } from './presets/docker';
|
k8sClustersConfig,
|
||||||
import { notificationChannelTypesConfig, notificationChannelsConfig } from './presets/notification';
|
gitRepositoriesConfig,
|
||||||
import { usersConfig, rolesConfig, departmentsConfig } from './presets/user';
|
dockerRegistriesConfig,
|
||||||
import { environmentsConfig, projectGroupsConfig, applicationsConfig } from './presets/deploy';
|
environmentsConfig,
|
||||||
|
projectGroupsConfig,
|
||||||
|
applicationsConfig,
|
||||||
|
notificationChannelTypesConfig,
|
||||||
|
notificationChannelsConfig,
|
||||||
|
usersConfig,
|
||||||
|
rolesConfig,
|
||||||
|
departmentsConfig
|
||||||
|
} from './presets/infrastructure';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 数据源配置注册表
|
* 数据源注册表实例
|
||||||
*/
|
*/
|
||||||
export const DATA_SOURCE_REGISTRY: DataSourceRegistry = {
|
class DataSourceRegistryImpl extends BaseRegistry<DataSourceType, DataSourceConfig> {
|
||||||
[DataSourceType.JENKINS_SERVERS]: jenkinsServersConfig,
|
constructor() {
|
||||||
[DataSourceType.K8S_CLUSTERS]: k8sClustersConfig,
|
super();
|
||||||
[DataSourceType.GIT_REPOSITORIES]: gitRepositoriesConfig,
|
// 注册所有预设配置
|
||||||
[DataSourceType.DOCKER_REGISTRIES]: dockerRegistriesConfig,
|
this.register(DataSourceType.JENKINS_SERVERS, jenkinsServersConfig);
|
||||||
[DataSourceType.NOTIFICATION_CHANNEL_TYPES]: notificationChannelTypesConfig,
|
this.register(DataSourceType.K8S_CLUSTERS, k8sClustersConfig);
|
||||||
[DataSourceType.NOTIFICATION_CHANNELS]: notificationChannelsConfig,
|
this.register(DataSourceType.GIT_REPOSITORIES, gitRepositoriesConfig);
|
||||||
[DataSourceType.USERS]: usersConfig,
|
this.register(DataSourceType.DOCKER_REGISTRIES, dockerRegistriesConfig);
|
||||||
[DataSourceType.ROLES]: rolesConfig,
|
this.register(DataSourceType.NOTIFICATION_CHANNEL_TYPES, notificationChannelTypesConfig);
|
||||||
[DataSourceType.DEPARTMENTS]: departmentsConfig,
|
this.register(DataSourceType.NOTIFICATION_CHANNELS, notificationChannelsConfig);
|
||||||
[DataSourceType.ENVIRONMENTS]: environmentsConfig,
|
this.register(DataSourceType.USERS, usersConfig);
|
||||||
[DataSourceType.PROJECT_GROUPS]: projectGroupsConfig,
|
this.register(DataSourceType.ROLES, rolesConfig);
|
||||||
[DataSourceType.APPLICATIONS]: applicationsConfig
|
this.register(DataSourceType.DEPARTMENTS, departmentsConfig);
|
||||||
};
|
this.register(DataSourceType.ENVIRONMENTS, environmentsConfig);
|
||||||
|
this.register(DataSourceType.PROJECT_GROUPS, projectGroupsConfig);
|
||||||
|
this.register(DataSourceType.APPLICATIONS, applicationsConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取注册表对象(向后兼容)
|
||||||
|
*/
|
||||||
|
toObject(): DataSourceRegistry {
|
||||||
|
return this['registry'] as DataSourceRegistry;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const registryInstance = new DataSourceRegistryImpl();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据源配置注册表(向后兼容)
|
||||||
|
*/
|
||||||
|
export const DATA_SOURCE_REGISTRY: DataSourceRegistry = registryInstance.toObject();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取数据源配置
|
* 获取数据源配置
|
||||||
* @param type 数据源类型
|
|
||||||
* @returns 数据源配置,如果不存在则返回 undefined
|
|
||||||
*/
|
*/
|
||||||
export const getDataSourceConfig = (type: DataSourceType) => {
|
export const getDataSourceConfig = (type: DataSourceType) => registryInstance.get(type);
|
||||||
return DATA_SOURCE_REGISTRY[type];
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查数据源类型是否已注册
|
* 检查数据源类型是否已注册
|
||||||
* @param type 数据源类型
|
|
||||||
* @returns 是否已注册
|
|
||||||
*/
|
*/
|
||||||
export const hasDataSource = (type: DataSourceType): boolean => {
|
export const hasDataSource = (type: DataSourceType): boolean => registryInstance.has(type);
|
||||||
return type in DATA_SOURCE_REGISTRY;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取所有已注册的数据源类型
|
* 获取所有已注册的数据源类型
|
||||||
* @returns 数据源类型列表
|
|
||||||
*/
|
*/
|
||||||
export const getAllDataSourceTypes = (): DataSourceType[] => {
|
export const getAllDataSourceTypes = (): DataSourceType[] => registryInstance.getAllKeys();
|
||||||
return Object.keys(DATA_SOURCE_REGISTRY) as DataSourceType[];
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|||||||
@ -2,9 +2,10 @@
|
|||||||
* 数据源服务
|
* 数据源服务
|
||||||
* 提供数据源加载和管理功能
|
* 提供数据源加载和管理功能
|
||||||
*/
|
*/
|
||||||
import request from '@/utils/request';
|
|
||||||
import {DataSourceType, type DataSourceOption} from './types';
|
import {DataSourceType, type DataSourceOption} from './types';
|
||||||
import {getDataSourceConfig, getAllDataSourceTypes} from './DataSourceRegistry';
|
import {getDataSourceConfig, getAllDataSourceTypes} from './DataSourceRegistry';
|
||||||
|
import { loadData } from './core/requestHelper';
|
||||||
|
import { warn } from './core/errorHandler';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 数据源服务类
|
* 数据源服务类
|
||||||
@ -20,7 +21,7 @@ class DataSourceService {
|
|||||||
|
|
||||||
if (!config) {
|
if (!config) {
|
||||||
const registeredTypes = getAllDataSourceTypes();
|
const registeredTypes = getAllDataSourceTypes();
|
||||||
console.warn(`⚠️ 数据源类型 "${type}" 未配置,请检查:
|
warn(`数据源类型 "${type}" 未配置,请检查:
|
||||||
|
|
||||||
📋 可能的原因:
|
📋 可能的原因:
|
||||||
1. 节点定义中使用了字符串而非 DataSourceType 枚举(如 "jenkins-servers" 应改为 DataSourceType.JENKINS_SERVERS)
|
1. 节点定义中使用了字符串而非 DataSourceType 枚举(如 "jenkins-servers" 应改为 DataSourceType.JENKINS_SERVERS)
|
||||||
@ -34,14 +35,9 @@ ${registeredTypes.map(t => ` - ${t}`).join('\n')}
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
// 使用统一的请求工具
|
||||||
const response = await request.get(config.url, {params: config.params});
|
const data = await loadData<any>(config.url, config.params, `数据源 ${type}`);
|
||||||
// request 拦截器已经提取了 data 字段,response 直接是数组
|
return config.transform(data);
|
||||||
return config.transform(response || []);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`❌ 加载数据源 ${type} 失败:`, error);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
47
frontend/src/domain/dataSource/core/BaseRegistry.ts
Normal file
47
frontend/src/domain/dataSource/core/BaseRegistry.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
/**
|
||||||
|
* 注册表基类
|
||||||
|
* 统一管理注册表的通用操作
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通用注册表基类
|
||||||
|
*/
|
||||||
|
export class BaseRegistry<TKey extends string | number | symbol, TValue> {
|
||||||
|
protected registry: Record<TKey, TValue> = {} as Record<TKey, TValue>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册配置
|
||||||
|
*/
|
||||||
|
register(key: TKey, value: TValue): void {
|
||||||
|
this.registry[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取配置
|
||||||
|
*/
|
||||||
|
get(key: TKey): TValue | undefined {
|
||||||
|
return this.registry[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否已注册
|
||||||
|
*/
|
||||||
|
has(key: TKey): boolean {
|
||||||
|
return key in this.registry;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有已注册的键
|
||||||
|
*/
|
||||||
|
getAllKeys(): TKey[] {
|
||||||
|
return Object.keys(this.registry) as TKey[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取注册表大小
|
||||||
|
*/
|
||||||
|
size(): number {
|
||||||
|
return Object.keys(this.registry).length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
37
frontend/src/domain/dataSource/core/errorHandler.ts
Normal file
37
frontend/src/domain/dataSource/core/errorHandler.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
/**
|
||||||
|
* 统一错误处理工具
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 错误处理选项
|
||||||
|
*/
|
||||||
|
export interface ErrorHandlerOptions {
|
||||||
|
context: string;
|
||||||
|
fallbackValue?: any;
|
||||||
|
silent?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统一错误处理函数
|
||||||
|
*/
|
||||||
|
export async function handleDataSourceError<T>(
|
||||||
|
operation: () => Promise<T>,
|
||||||
|
options: ErrorHandlerOptions
|
||||||
|
): Promise<T> {
|
||||||
|
try {
|
||||||
|
return await operation();
|
||||||
|
} catch (error) {
|
||||||
|
if (!options.silent) {
|
||||||
|
console.error(`❌ ${options.context}失败:`, error);
|
||||||
|
}
|
||||||
|
return options.fallbackValue ?? ([] as T);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 警告处理函数
|
||||||
|
*/
|
||||||
|
export function warn(message: string): void {
|
||||||
|
console.warn(`⚠️ ${message}`);
|
||||||
|
}
|
||||||
|
|
||||||
33
frontend/src/domain/dataSource/core/requestHelper.ts
Normal file
33
frontend/src/domain/dataSource/core/requestHelper.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
/**
|
||||||
|
* 请求工具函数
|
||||||
|
* 统一处理数据源请求逻辑
|
||||||
|
*/
|
||||||
|
|
||||||
|
import request from '@/utils/request';
|
||||||
|
import { handleDataSourceError } from './errorHandler';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通用请求加载函数
|
||||||
|
* @param url 请求地址
|
||||||
|
* @param params 请求参数
|
||||||
|
* @param context 错误上下文(用于错误日志)
|
||||||
|
* @returns 数据数组
|
||||||
|
*/
|
||||||
|
export async function loadData<T>(
|
||||||
|
url: string,
|
||||||
|
params?: Record<string, any>,
|
||||||
|
context?: string
|
||||||
|
): Promise<T[]> {
|
||||||
|
return handleDataSourceError(
|
||||||
|
async () => {
|
||||||
|
const response = await request.get(url, { params });
|
||||||
|
// request 拦截器已经提取了 data 字段,response 直接是数组
|
||||||
|
return (response || []) as T[];
|
||||||
|
},
|
||||||
|
{
|
||||||
|
context: context || `加载数据 (${url})`,
|
||||||
|
fallbackValue: []
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@ -1,55 +0,0 @@
|
|||||||
/**
|
|
||||||
* 部署相关数据源
|
|
||||||
*/
|
|
||||||
import type { DataSourceConfig } from '../types';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 环境列表数据源
|
|
||||||
*/
|
|
||||||
export const environmentsConfig: DataSourceConfig = {
|
|
||||||
url: '/api/v1/environments/list',
|
|
||||||
params: { enabled: true },
|
|
||||||
transform: (data: any[]) => {
|
|
||||||
return data.map((item: any) => ({
|
|
||||||
label: item.name,
|
|
||||||
value: item.id,
|
|
||||||
code: item.code,
|
|
||||||
type: item.type,
|
|
||||||
description: item.description
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 项目组列表数据源
|
|
||||||
*/
|
|
||||||
export const projectGroupsConfig: DataSourceConfig = {
|
|
||||||
url: '/api/v1/project-groups/list',
|
|
||||||
params: { enabled: true },
|
|
||||||
transform: (data: any[]) => {
|
|
||||||
return data.map((item: any) => ({
|
|
||||||
label: item.name,
|
|
||||||
value: item.id,
|
|
||||||
code: item.code,
|
|
||||||
description: item.description
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 应用列表数据源
|
|
||||||
*/
|
|
||||||
export const applicationsConfig: DataSourceConfig = {
|
|
||||||
url: '/api/v1/applications/list',
|
|
||||||
params: { enabled: true },
|
|
||||||
transform: (data: any[]) => {
|
|
||||||
return data.map((item: any) => ({
|
|
||||||
label: item.appName, // ✅ 修正:使用 appName
|
|
||||||
value: item.appCode,
|
|
||||||
code: item.appCode, // ✅ 修正:使用 appCode
|
|
||||||
projectGroupId: item.projectGroupId,
|
|
||||||
description: item.appDesc || item.description // ✅ 修正:使用 appDesc
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
/**
|
|
||||||
* Docker 镜像仓库数据源
|
|
||||||
*/
|
|
||||||
import type { DataSourceConfig } from '../types';
|
|
||||||
|
|
||||||
export const dockerRegistriesConfig: DataSourceConfig = {
|
|
||||||
url: '/api/v1/docker-registry/list',
|
|
||||||
params: { enabled: true },
|
|
||||||
transform: (data: any[]) => {
|
|
||||||
return data.map((item: any) => ({
|
|
||||||
label: item.name,
|
|
||||||
value: item.id,
|
|
||||||
url: item.url
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
/**
|
|
||||||
* Git 仓库数据源
|
|
||||||
*/
|
|
||||||
import type { DataSourceConfig } from '../types';
|
|
||||||
|
|
||||||
export const gitRepositoriesConfig: DataSourceConfig = {
|
|
||||||
url: '/api/v1/git-repo/list',
|
|
||||||
params: { enabled: true },
|
|
||||||
transform: (data: any[]) => {
|
|
||||||
return data.map((item: any) => ({
|
|
||||||
label: `${item.name} (${item.url})`,
|
|
||||||
value: item.id,
|
|
||||||
url: item.url
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
190
frontend/src/domain/dataSource/presets/infrastructure.ts
Normal file
190
frontend/src/domain/dataSource/presets/infrastructure.ts
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
/**
|
||||||
|
* 基础设施、部署和用户相关数据源
|
||||||
|
* 包含:基础设施(Docker、Git、K8s、Jenkins)、部署相关(环境、项目组、应用)、通知渠道、用户相关(用户、角色、部门)
|
||||||
|
*/
|
||||||
|
import type { DataSourceConfig } from '../types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Docker 镜像仓库数据源
|
||||||
|
*/
|
||||||
|
export const dockerRegistriesConfig: DataSourceConfig = {
|
||||||
|
url: '/api/v1/docker-registry/list',
|
||||||
|
params: { enabled: true },
|
||||||
|
transform: (data: any[]) => {
|
||||||
|
return data.map((item: any) => ({
|
||||||
|
label: item.name,
|
||||||
|
value: item.id,
|
||||||
|
url: item.url
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Git 仓库数据源
|
||||||
|
*/
|
||||||
|
export const gitRepositoriesConfig: DataSourceConfig = {
|
||||||
|
url: '/api/v1/git-repo/list',
|
||||||
|
params: { enabled: true },
|
||||||
|
transform: (data: any[]) => {
|
||||||
|
return data.map((item: any) => ({
|
||||||
|
label: `${item.name} (${item.url})`,
|
||||||
|
value: item.id,
|
||||||
|
url: item.url
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kubernetes 集群数据源
|
||||||
|
*/
|
||||||
|
export const k8sClustersConfig: DataSourceConfig = {
|
||||||
|
url: '/api/v1/k8s-cluster/list',
|
||||||
|
params: { enabled: true },
|
||||||
|
transform: (data: any[]) => {
|
||||||
|
return data.map((item: any) => ({
|
||||||
|
label: item.name,
|
||||||
|
value: item.id,
|
||||||
|
apiServer: item.apiServer
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Jenkins 服务器数据源
|
||||||
|
*/
|
||||||
|
export const jenkinsServersConfig: DataSourceConfig = {
|
||||||
|
url: '/api/v1/external-system/list',
|
||||||
|
params: { type: 'JENKINS', enabled: true },
|
||||||
|
transform: (data: any[]) => {
|
||||||
|
return data.map((item: any) => ({
|
||||||
|
label: `${item.name} (${item.url})`,
|
||||||
|
value: item.id,
|
||||||
|
url: item.url,
|
||||||
|
name: item.name
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 环境列表数据源
|
||||||
|
*/
|
||||||
|
export const environmentsConfig: DataSourceConfig = {
|
||||||
|
url: '/api/v1/environments/list',
|
||||||
|
params: { enabled: true },
|
||||||
|
transform: (data: any[]) => {
|
||||||
|
return data.map((item: any) => ({
|
||||||
|
label: item.name,
|
||||||
|
value: item.id,
|
||||||
|
code: item.code,
|
||||||
|
type: item.type,
|
||||||
|
description: item.description
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 项目组列表数据源
|
||||||
|
*/
|
||||||
|
export const projectGroupsConfig: DataSourceConfig = {
|
||||||
|
url: '/api/v1/project-groups/list',
|
||||||
|
params: { enabled: true },
|
||||||
|
transform: (data: any[]) => {
|
||||||
|
return data.map((item: any) => ({
|
||||||
|
label: item.name,
|
||||||
|
value: item.id,
|
||||||
|
code: item.code,
|
||||||
|
description: item.description
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用列表数据源
|
||||||
|
*/
|
||||||
|
export const applicationsConfig: DataSourceConfig = {
|
||||||
|
url: '/api/v1/applications/list',
|
||||||
|
params: { enabled: true },
|
||||||
|
transform: (data: any[]) => {
|
||||||
|
return data.map((item: any) => ({
|
||||||
|
label: item.appName, // ✅ 修正:使用 appName
|
||||||
|
value: item.appCode,
|
||||||
|
code: item.appCode, // ✅ 修正:使用 appCode
|
||||||
|
projectGroupId: item.projectGroupId,
|
||||||
|
description: item.appDesc || item.description // ✅ 修正:使用 appDesc
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通知渠道类型数据源
|
||||||
|
*/
|
||||||
|
export const notificationChannelTypesConfig: DataSourceConfig = {
|
||||||
|
url: '/api/v1/notification-channel/types',
|
||||||
|
transform: (data: any[]) => {
|
||||||
|
return data.map((item: any) => ({
|
||||||
|
label: `${item.label}`,
|
||||||
|
value: item.code
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通知渠道数据源
|
||||||
|
*/
|
||||||
|
export const notificationChannelsConfig: DataSourceConfig = {
|
||||||
|
url: '/api/v1/notification-channel/list',
|
||||||
|
transform: (data: any[]) => {
|
||||||
|
return data.map((item: any) => ({
|
||||||
|
label: `(${item.channelType})-${item.name}`,
|
||||||
|
value: item.id
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户数据源
|
||||||
|
*/
|
||||||
|
export const usersConfig: DataSourceConfig = {
|
||||||
|
url: '/api/v1/user/list',
|
||||||
|
transform: (data: any[]) => {
|
||||||
|
return data.map((item: any) => ({
|
||||||
|
label: `${item.nickname} (${item.username})`,
|
||||||
|
value: item.username, // 后台使用 username 进行审批
|
||||||
|
id: item.id,
|
||||||
|
email: item.email,
|
||||||
|
departmentName: item.departmentName
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 角色数据源
|
||||||
|
*/
|
||||||
|
export const rolesConfig: DataSourceConfig = {
|
||||||
|
url: '/api/v1/role/list',
|
||||||
|
transform: (data: any[]) => {
|
||||||
|
return data.map((item: any) => ({
|
||||||
|
label: `${item.name} (${item.code})`,
|
||||||
|
value: item.code, // 使用 code 作为值
|
||||||
|
id: item.id,
|
||||||
|
description: item.description
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 部门数据源
|
||||||
|
*/
|
||||||
|
export const departmentsConfig: DataSourceConfig = {
|
||||||
|
url: '/api/v1/department/list',
|
||||||
|
transform: (data: any[]) => {
|
||||||
|
return data.map((item: any) => ({
|
||||||
|
label: `${item.name} (${item.code})`,
|
||||||
|
value: item.code, // 使用 code 作为值
|
||||||
|
id: item.id,
|
||||||
|
description: item.description,
|
||||||
|
parentId: item.parentId
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
/**
|
|
||||||
* Jenkins 服务器数据源
|
|
||||||
*/
|
|
||||||
import type {DataSourceConfig} from '../types';
|
|
||||||
|
|
||||||
export const jenkinsServersConfig: DataSourceConfig = {
|
|
||||||
url: '/api/v1/external-system/list',
|
|
||||||
params: {type: 'JENKINS', enabled: true},
|
|
||||||
transform: (data: any[]) => {
|
|
||||||
return data.map((item: any) => ({
|
|
||||||
label: `${item.name} (${item.url})`,
|
|
||||||
value: item.id,
|
|
||||||
url: item.url,
|
|
||||||
name: item.name
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
/**
|
|
||||||
* Kubernetes 集群数据源
|
|
||||||
*/
|
|
||||||
import type { DataSourceConfig } from '../types';
|
|
||||||
|
|
||||||
export const k8sClustersConfig: DataSourceConfig = {
|
|
||||||
url: '/api/v1/k8s-cluster/list',
|
|
||||||
params: { enabled: true },
|
|
||||||
transform: (data: any[]) => {
|
|
||||||
return data.map((item: any) => ({
|
|
||||||
label: item.name,
|
|
||||||
value: item.id,
|
|
||||||
apiServer: item.apiServer
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
@ -1,25 +0,0 @@
|
|||||||
/**
|
|
||||||
* 通知渠道数据源
|
|
||||||
*/
|
|
||||||
import type { DataSourceConfig } from '../types';
|
|
||||||
|
|
||||||
export const notificationChannelTypesConfig: DataSourceConfig = {
|
|
||||||
url: '/api/v1/notification-channel/types',
|
|
||||||
transform: (data: any[]) => {
|
|
||||||
return data.map((item: any) => ({
|
|
||||||
label: `${item.label}`,
|
|
||||||
value: item.code
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const notificationChannelsConfig: DataSourceConfig = {
|
|
||||||
url: '/api/v1/notification-channel/list',
|
|
||||||
transform: (data: any[]) => {
|
|
||||||
return data.map((item: any) => ({
|
|
||||||
label: `(${item.channelType})-${item.name}`,
|
|
||||||
value: item.id
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
@ -1,43 +0,0 @@
|
|||||||
/**
|
|
||||||
* 用户、角色、部门数据源
|
|
||||||
*/
|
|
||||||
import type { DataSourceConfig } from '../types';
|
|
||||||
|
|
||||||
export const usersConfig: DataSourceConfig = {
|
|
||||||
url: '/api/v1/user/list',
|
|
||||||
transform: (data: any[]) => {
|
|
||||||
return data.map((item: any) => ({
|
|
||||||
label: `${item.nickname} (${item.username})`,
|
|
||||||
value: item.username, // 后台使用 username 进行审批
|
|
||||||
id: item.id,
|
|
||||||
email: item.email,
|
|
||||||
departmentName: item.departmentName
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const rolesConfig: DataSourceConfig = {
|
|
||||||
url: '/api/v1/role/list',
|
|
||||||
transform: (data: any[]) => {
|
|
||||||
return data.map((item: any) => ({
|
|
||||||
label: `${item.name} (${item.code})`,
|
|
||||||
value: item.code, // 使用 code 作为值
|
|
||||||
id: item.id,
|
|
||||||
description: item.description
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const departmentsConfig: DataSourceConfig = {
|
|
||||||
url: '/api/v1/department/list',
|
|
||||||
transform: (data: any[]) => {
|
|
||||||
return data.map((item: any) => ({
|
|
||||||
label: `${item.name} (${item.code})`,
|
|
||||||
value: item.code, // 使用 code 作为值
|
|
||||||
id: item.id,
|
|
||||||
description: item.description,
|
|
||||||
parentId: item.parentId
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Loading…
Reference in New Issue
Block a user