feat: cloud options

This commit is contained in:
jialin
2025-09-16 11:26:17 +08:00
parent 632f206228
commit 6e5a7035c8
29 changed files with 788 additions and 276 deletions
@@ -0,0 +1,19 @@
import ComponentsMap from '@/components/seal-form/config/components';
import { SealFormItemProps } from '@/components/seal-form/types';
import { Form } from 'antd';
import React from 'react';
interface FieldItemProps extends SealFormItemProps {
widget: keyof typeof ComponentsMap;
name: string;
}
const FieldItem: React.FC<FieldItemProps> = (props) => {
const { name, widget, required = [], ...rest } = props;
const Component = ComponentsMap[widget];
return <Form.Item name={name}></Form.Item>;
};
export default FieldItem;
@@ -0,0 +1,45 @@
import ComponentsMap from '@/components/seal-form/config/components';
import { FormWidgetProps } from '../config/types';
const FormWidget: React.FC<
FormWidgetProps & {
onChange?: (data: any) => void;
}
> = ({
widget,
title: label,
required,
placeholder,
options,
description,
enum: enumValues,
style,
value,
min,
max,
checked,
onChange
}) => {
const Component = ComponentsMap[widget];
const optionList = enumValues?.map((item: string | number) => ({
label: item,
value: item
}));
return Component ? (
<Component
{...{ label, required, placeholder, description, min, max }}
options={options || optionList}
value={value}
checked={checked}
style={{
width: '100%',
...style
}}
onChange={onChange}
/>
) : null;
};
export default FormWidget;
@@ -0,0 +1,136 @@
import Wrapper from '@/components/label-selector/wrapper';
import { MinusOutlined } from '@ant-design/icons';
import { Button } from 'antd';
import React, { useEffect, useMemo } from 'react';
import styled from 'styled-components';
import FormWidget from './form-widget';
interface ListMapProps {
dataList: any[];
label?: React.ReactNode;
btnText?: string;
properties: Record<string, any>;
onChange?: (data: any) => void;
}
interface ListItemProps {
schemaList: any[];
data: Record<string, any>;
onChange?: (data: any) => void;
}
const RowWrapper = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
`;
const WidgetBox = styled.div`
display: flex;
align-items: center;
gap: 8px;
width: 100%;
`;
const ListItem: React.FC<ListItemProps> = ({ schemaList, data, onChange }) => {
const handleValueChange = (name: string, target: any) => {
if (target?.target?.type === 'checkbox') {
const checked = target.target?.checked;
onChange?.({ [name]: checked });
} else {
const value = target?.target ? target.target.value : target;
onChange?.({ [name]: value });
}
};
return (
<>
{schemaList.map((schema: any) => (
<FormWidget
widget={schema.type}
{...schema}
key={schema.name}
value={data?.[schema.name]}
checked={data?.[schema.name]}
onChange={(target) => handleValueChange(schema.name, target)}
/>
))}
</>
);
};
const ListMap: React.FC<ListMapProps> = ({
dataList = [],
label,
btnText,
properties = {},
onChange
}) => {
const [items, setItems] = React.useState(dataList || []);
const schemaList = useMemo(() => {
const list = Object.entries(properties).map(([key, value]) => ({
...value,
name: key
}));
return list;
}, [properties]);
const handleOnAdd = () => {
const keys = Object.keys(properties);
setItems([
...items,
{ ...keys.reduce((acc, key) => ({ ...acc, [key]: '' }), {}) }
]);
};
const handleDelete = (index: number) => {
const newItems = items.filter((_, i) => i !== index);
setItems(newItems);
onChange?.(newItems);
};
const handleItemChange = (index: number, data: { [key: string]: any }) => {
const newItems = [...items];
newItems[index] = { ...newItems[index], ...data };
setItems(newItems);
onChange?.(newItems);
};
useEffect(() => {
if (!dataList.length) {
handleOnAdd();
}
}, []);
return (
<Wrapper label={label} btnText={btnText} onAdd={handleOnAdd}>
{items.map((item, index) => (
<RowWrapper key={index}>
<WidgetBox>
<ListItem
schemaList={schemaList}
data={item}
onChange={(value) => handleItemChange(index, value)}
/>
</WidgetBox>
<Button
size="small"
type="default"
shape="circle"
style={{
width: 24,
marginLeft: 10,
flex: 'none'
}}
icon={<MinusOutlined />}
onClick={() => handleDelete(index)}
/>
</RowWrapper>
))}
</Wrapper>
);
};
export default ListMap;
@@ -0,0 +1,34 @@
import React from 'react';
// refer to json schema
export interface FieldSchema {
type: 'string' | 'number' | 'boolean' | 'object' | 'array';
title?: string;
name: string;
description?: string;
properties?: Record<string, FieldSchema>;
default?: any;
enum?: string[];
minItems?: number;
maxItems?: number;
items?: FieldSchema[];
widget?: string;
min?: number;
style?: React.CSSProperties;
}
export interface FormWidgetProps {
widget: 'Input' | 'Select' | 'Checkbox' | 'InputNumber';
name: string;
title?: string;
required?: boolean;
placeholder?: string;
options?: { label: string; value: string | number }[];
description?: string;
enum?: (string | number)[];
style?: React.CSSProperties;
value?: any;
checked?: boolean;
min?: number;
max?: number;
}
@@ -0,0 +1,33 @@
import { useMemo } from 'react';
import { FieldSchema } from '../config/types';
interface ParsedField {
name: (string | number)[];
schema: FieldSchema;
}
const parseSchema = (
schema: Record<string, FieldSchema>,
parentName: (string | number)[] = []
): ParsedField[] => {
const fields: ParsedField[] = [];
Object.entries(schema).forEach(([key, fieldSchema]) => {
const currentName = [...parentName, key];
if (fieldSchema.type === 'object' && fieldSchema.properties) {
fields.push(...parseSchema(fieldSchema.properties, currentName));
} else if (fieldSchema.type === 'array' && fieldSchema.items) {
fields.push({ name: currentName, schema: fieldSchema });
} else {
fields.push({ name: currentName, schema: fieldSchema });
}
});
return fields;
};
const useParsedFields = (schema: Record<string, FieldSchema>) => {
return useMemo(() => parseSchema(schema), [schema]);
};
export default useParsedFields;
+26
View File
@@ -0,0 +1,26 @@
import { Form } from 'antd';
import React from 'react';
import { FieldSchema } from './config/types';
interface DynamicFormProps {
schema: FieldSchema;
onSubmit: (values: any) => void;
}
const DynamicForm: React.FC<DynamicFormProps> = ({ schema, onSubmit }) => {
const form = Form.useFormInstance();
const handleFinish = (values: any) => {
onSubmit(values);
};
return (
<>
<Form form={form} onFinish={handleFinish}>
{/* Render form fields based on schema */}
</Form>
</>
);
};
export default DynamicForm;
+8 -24
View File
@@ -1,8 +1,6 @@
import { PlusOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import { Button } from 'antd';
import _ from 'lodash';
import React, { useRef } from 'react';
import React from 'react';
import LabelItem from './label-item';
import Wrapper from './wrapper';
interface LabelSelectorProps {
@@ -31,8 +29,6 @@ const Inner: React.FC<LabelSelectorProps> = ({
description
}) => {
const intl = useIntl();
const buttonRef = useRef<HTMLButtonElement>(null);
const boxRef = useRef<HTMLDivElement>(null);
const updateLabels = (list: { key: string; value: string }[]) => {
const newLabels = _.reduce(
@@ -75,7 +71,12 @@ const Inner: React.FC<LabelSelectorProps> = ({
};
return (
<Wrapper label={label} description={description}>
<Wrapper
label={label}
description={description}
onAdd={handleAddLabel}
btnText={btnText}
>
<>
{labelList?.map((item: any, index: number) => {
return (
@@ -91,26 +92,9 @@ const Inner: React.FC<LabelSelectorProps> = ({
/>
);
})}
<div className="flex justify-center">
<Button
ref={buttonRef}
type="text"
block
style={{
marginTop: 16,
backgroundColor: 'var(--ant-color-fill-secondary)'
}}
onClick={handleAddLabel}
>
<PlusOutlined className="font-size-14" />{' '}
{intl.formatMessage({
id: btnText || 'common.button.addSelector'
})}
</Button>
</div>
</>
</Wrapper>
);
};
export default React.memo(Inner);
export default Inner;
@@ -1,20 +0,0 @@
.wrapper {
position: relative;
padding: 14px;
padding-top: 34px;
border: 1px solid var(--ant-color-border);
border-radius: var(--border-radius-base);
display: flex;
width: 100%;
flex-direction: column;
:global {
.label {
position: absolute;
left: 16px;
line-height: 1;
top: 12px;
color: var(--ant-color-text-tertiary);
}
}
}
+56 -8
View File
@@ -1,15 +1,54 @@
import LabelInfo from '@/components/seal-form/components/label-info';
import { PlusOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import { Button } from 'antd';
import React from 'react';
import styles from './styles/wrapper.less';
import styled from 'styled-components';
const Wrapper: React.FC<{
interface WrapperProps {
label?: React.ReactNode;
description?: React.ReactNode;
labelExtra?: React.ReactNode;
children: React.ReactNode;
}> = ({ children, label, description, labelExtra, ...rest }) => {
btnText?: string;
onAdd?: () => void;
button?: React.ReactNode;
}
const Container = styled.div`
position: relative;
padding: 14px;
padding-top: 34px;
border: 1px solid var(--ant-color-border);
border-radius: var(--border-radius-base);
display: flex;
width: 100%;
flex-direction: column;
.label {
position: absolute;
left: 16px;
line-height: 1;
top: 12px;
color: var(--ant-color-text-tertiary);
}
`;
const ButtonWrapper = styled.div`
margin-top: 16px;
`;
const Wrapper: React.FC<WrapperProps> = ({
children,
label,
description,
labelExtra,
onAdd,
btnText,
button
}) => {
const intl = useIntl();
return (
<div className={styles['wrapper']}>
<Container>
{label && (
<span className="label">
<LabelInfo
@@ -19,10 +58,19 @@ const Wrapper: React.FC<{
></LabelInfo>
</span>
)}
{React.isValidElement(children)
? React.cloneElement(children, { ...rest })
: children}
</div>
{children}
<ButtonWrapper>
{button || (
<Button variant="filled" color="default" block onClick={onAdd}>
<PlusOutlined className="font-size-14" />
{btnText ||
intl.formatMessage({
id: 'common.button.addSelector'
})}
</Button>
)}
</ButtonWrapper>
</Container>
);
};
+8 -25
View File
@@ -1,6 +1,4 @@
import { PlusOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import { Button } from 'antd';
import _ from 'lodash';
import React from 'react';
import Wrapper from '../label-selector/wrapper';
@@ -34,7 +32,6 @@ const ListInput: React.FC<ListInputProps> = (props) => {
} = props;
const [list, setList] = React.useState<{ value: string; uid: number }[]>([]);
const countRef = React.useRef(0);
const buttonRef = React.useRef<HTMLButtonElement>(null);
const updateCountRef = () => {
countRef.current = countRef.current + 1;
@@ -65,9 +62,6 @@ const ListInput: React.FC<ListInputProps> = (props) => {
uid: countRef.current
});
setList(values);
// setTimeout(() => {
// buttonRef.current?.scrollIntoView?.({ behavior: 'smooth' });
// }, 100);
};
React.useEffect(() => {
@@ -85,7 +79,13 @@ const ListInput: React.FC<ListInputProps> = (props) => {
}, [dataList]);
return (
<Wrapper label={label} description={description} labelExtra={labelExtra}>
<Wrapper
label={label}
description={description}
labelExtra={labelExtra}
onAdd={handleOnAdd}
btnText={btnText}
>
<>
{_.map(list, (item: any, index: number) => {
return (
@@ -100,26 +100,9 @@ const ListInput: React.FC<ListInputProps> = (props) => {
/>
);
})}
<div className="flex justify-center">
<Button
ref={buttonRef}
variant="filled"
color="default"
block
style={{
marginTop: 16
}}
onClick={handleOnAdd}
>
<PlusOutlined className="font-size-14" />{' '}
{intl.formatMessage({
id: btnText
})}
</Button>
</div>
</>
</Wrapper>
);
};
export default React.memo(ListInput);
export default ListInput;
+1 -1
View File
@@ -85,7 +85,7 @@ export const FilterBar: React.FC<FilterBarProps> = (props) => {
handleDeleteByBatch,
handleClickPrimary,
rowSelection,
actionItems,
actionItems = [],
selectOptions,
showSelect,
buttonText,
+2 -1
View File
@@ -22,6 +22,7 @@ const SealInput: React.FC<InputProps & SealFormItemProps> = (props) => {
trim = true,
loading,
labelExtra,
style,
...rest
} = props;
const [isFocus, setIsFocus] = useState(false);
@@ -73,7 +74,7 @@ const SealInput: React.FC<InputProps & SealFormItemProps> = (props) => {
};
return (
<InputWrapper>
<InputWrapper style={style}>
<Wrapper
status={checkStatus || status}
label={label}