feat: 添加移动端选择框组件 (#1093)

Reviewed-on: daoyoucloud/tachybase#1093
Reviewed-by: sealday <zhanglin@daoyoucloud.com>
Co-authored-by: wangjiahui <wwwjh0710@163.com>
Co-committed-by: wangjiahui <wwwjh0710@163.com>
This commit is contained in:
wangjiahui 2024-05-31 19:04:40 +08:00 committed by sealday
parent ab49b68213
commit bc0f05ded2
19 changed files with 587 additions and 22 deletions

View File

@ -1,12 +1,14 @@
import { Field } from '@tachybase/schema';
import { useField, useFieldSchema } from '@tachybase/schema';
import _ from 'lodash';
import React from 'react';
import { Field, useField, useFieldSchema } from '@tachybase/schema';
import _ from 'lodash';
import { useTranslation } from 'react-i18next';
import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings';
import { useFormBlockContext } from '../../../../block-provider';
import { useCollectionManager_deprecated, useCollection_deprecated } from '../../../../collection-manager';
import { useCollection_deprecated, useCollectionManager_deprecated } from '../../../../collection-manager';
import { useFieldComponentName } from '../../../../common/useFieldComponentName';
import { useCollectionField } from '../../../../data-source';
import { useRecord } from '../../../../record-provider';
import { removeNullCondition, useDesignable, useFieldModeOptions, useIsAddNewForm } from '../../../../schema-component';
import { isSubMode } from '../../../../schema-component/antd/association-field/util';
@ -18,12 +20,11 @@ import {
useTitleFieldOptions,
} from '../../../../schema-component/antd/form-item/FormItem.Settings';
import { useColumnSchema } from '../../../../schema-component/antd/table-v2/Table.Column.Decorator';
import { VariableInput, getShouldChange } from '../../../../schema-settings';
import { getShouldChange, VariableInput } from '../../../../schema-settings';
import { useIsShowMultipleSwitch } from '../../../../schema-settings/hooks/useIsShowMultipleSwitch';
import { SchemaSettingsDataScope } from '../../../../schema-settings/SchemaSettingsDataScope';
import { SchemaSettingsSortingRule } from '../../../../schema-settings/SchemaSettingsSortingRule';
import { useIsShowMultipleSwitch } from '../../../../schema-settings/hooks/useIsShowMultipleSwitch';
import { useLocalVariables, useVariables } from '../../../../variables';
import { useCollectionField } from '../../../../data-source';
const enableLink = {
name: 'enableLink',

View File

@ -1,5 +1,6 @@
import { RecursionField, observer, useField, useFieldSchema } from '@tachybase/schema';
import React, { useState } from 'react';
import { observer, RecursionField, useField, useFieldSchema } from '@tachybase/schema';
import { CollectionProvider_deprecated, useCollectionManager_deprecated } from '../../../../collection-manager';
import { CreateAction } from '../../../../schema-initializer/components';
import { ActionContextProvider, useActionContext } from '../../action';

View File

@ -19,6 +19,7 @@ import {
MMenuBlockInitializer,
MPage,
MRadio,
MSelect,
MSettings,
MSettingsBlockInitializer,
MTabBar,
@ -87,6 +88,7 @@ export const MobileCore: React.FC<PropsWithChildren> = (props) => {
MRadio,
MImageUploader,
CollectionField: CollectionField,
MSelect,
}}
scope={{
useGridCardBlockItemProps,

View File

@ -1,10 +1,11 @@
const custonComponent = {
const customComponent = {
Input: 'MInput',
'Input.TextArea': 'MInput.TextArea',
'Radio.Group': 'MRadio.Group',
'Checkbox.Group': 'MCheckbox.Group',
'Upload.Attachment': 'MImageUploader',
DatePicker: 'MDatePicker',
Select: 'MSelect',
};
export const canMobileField = (componentName: string) => custonComponent[componentName];
export const canMobileField = (componentName: string) => customComponent[componentName];

View File

@ -1,5 +1,5 @@
import React, { useEffect } from 'react';
import { useCollectionField } from '@tachybase/client';
import { css, useCollectionField } from '@tachybase/client';
import { connect, isValid, mapProps, mapReadPretty, useField } from '@tachybase/schema';
import { Checkbox, CheckboxGroupProps, CheckboxProps, Tag } from 'antd-mobile';
@ -27,7 +27,7 @@ MCheckbox.Group = connect(
<Checkbox
value={item.value}
key={index}
style={{ marginRight: '10px' }}
style={{ '--icon-size': '12px', '--font-size': '12px' }}
onChange={(status) => {
if (status) {
check.push(item.value);
@ -37,6 +37,7 @@ MCheckbox.Group = connect(
}
props.onChange(check);
}}
{...props}
>
{item.label}
</Checkbox>

View File

@ -36,8 +36,8 @@ export const MDatePicker = connect(
}),
mapReadPretty((props) => {
const changeProps = { ...props };
changeProps.value = dayjs(props.value).format(props.dateFormat);
return <Input {...changeProps} readOnly />;
changeProps.value = props.value ? dayjs(props.value).format('YYYY-MM-DD') : '';
return <div>{changeProps.value}</div>;
}),
);
export default MDatePicker;

View File

@ -1,4 +1,5 @@
import React from 'react';
import { css } from '@tachybase/client';
import { connect, mapProps, mapReadPretty } from '@tachybase/schema';
import { Input, InputProps, TextArea, TextAreaProps } from 'antd-mobile';
@ -10,20 +11,40 @@ type ComposedInput = React.FC<InputProps> & {
export const MInput: ComposedInput = connect(
Input,
mapProps((props) => {
return { placeholder: '请输入内容', clearable: true, ...props, style: { fontSize: '12px' } };
return { placeholder: '请输入内容', clearable: true, ...props, style: { '--font-size': '12px' } };
}),
mapReadPretty((props) => {
return <Input {...props} readOnly style={{ fontSize: '12px' }} />;
return (
<Input
{...props}
readOnly
className={css`
.adm-text-area-element {
font-size: 12px;
}
`}
/>
);
}),
);
const MTextArea = connect(
TextArea,
mapProps((props) => {
return { ...props, placeholder: '请输入内容', clearable: true, style: { fontSize: '12px' } };
return { ...props, placeholder: '请输入内容', clearable: true, style: { '--font-size': '12px' } };
}),
mapReadPretty((props) => {
return <TextArea {...props} readOnly style={{ fontSize: '12px' }} />;
return (
<TextArea
{...props}
readOnly
className={css`
.adm-text-area-element {
font-size: 12px;
}
`}
/>
);
}),
);

View File

@ -26,7 +26,11 @@ MRadio.Group = connect(
<Radio.Group {...props}>
{dataSource.map((item, index) => {
return (
<Radio value={item.value} key={index} style={{ marginRight: '10px' }}>
<Radio
value={item.value}
key={index}
style={{ '--font-size': '12px', '--icon-size': '18px', marginLeft: '10px' }}
>
{item.label}
</Radio>
);

View File

@ -0,0 +1,7 @@
import { createContext, useContext } from 'react';
export const AntdPopupContext = createContext({});
export const useAntdPopupContext = () => {
return useContext(AntdPopupContext);
};

View File

@ -0,0 +1,237 @@
import React, { useEffect, useState } from 'react';
import { observer, RecursionField, useField, useFieldSchema, useForm } from '@tachybase/schema';
import { Button, CheckList, Divider, Modal, PickerView, Popup, SearchBar, Space, Tag, Toast } from 'antd-mobile';
import { MInput } from '../Input';
import './style';
import {
BlockItem,
CollectionProvider_deprecated,
useAPIClient,
useCollectionManager,
useDesignable,
useRequest,
} from '@tachybase/client';
import { isArray } from '@tachybase/utils/client';
import { CreateRecordAction } from './CreateRecordAction';
import { useInsertSchema } from './hook/hooks';
import schema from './schema';
import { useStyles } from './style';
export const AntdSelect = observer((props) => {
const { value, collectionName, service, fieldNames, addMode, onChange, multiple } = props as any;
const [popupVisible, setPopupVisible] = useState(false);
const [searchValue, setSearchValue] = useState('');
const { styles } = useStyles();
const [options, setOptions] = useState([]);
const fieldSchema = useFieldSchema();
const cm = useCollectionManager();
const [visibleValue, setVisibleValue] = useState({});
const [filter, setFilter] = useState(service?.params?.filter);
const api = useAPIClient();
const field = useField();
const { insertAfterBegin } = useDesignable();
const insertAddNewer = useInsertSchema('AddNewer', fieldSchema, insertAfterBegin);
const [selectValue, setSelectValue] = useState(value);
const { data, run } = useRequest(
{
resource: collectionName,
action: 'list',
params: {
filter,
},
},
{
manual: true,
},
);
useEffect(() => {
checkedPopup();
}, [filter]);
useEffect(() => {
if (data) {
const dataOption = data['data']?.map((value) => {
return {
...value,
label: value[fieldNames.label],
value: value[fieldNames.value],
};
});
setOptions(dataOption);
}
}, [data]);
const checkedPopup = () => {
if (collectionName) {
run();
} else {
const field = cm.getCollectionField(fieldSchema['x-collection-field']);
const data = field.uiSchema?.enum;
setVisibleValue(data.find((item) => item.value === value) || '');
setOptions(data);
}
};
const addData = () => {
api
.request({
url: collectionName + ':create',
method: 'post',
data,
})
.then((res) => {
if (res.status === 200) {
Toast.show({
icon: 'success',
content: '添加成功',
});
} else {
Toast.show({
icon: 'fail',
content: '失败成功',
});
}
const paramsFilter = { ...filter };
delete paramsFilter[fieldNames.label];
setFilter(paramsFilter);
setSearchValue('');
setPopupVisible(false);
})
.catch(() => {
console.error;
});
};
const quickAdd = () => {
const data = {};
data[fieldNames.label] = searchValue;
addData();
};
const modalAdd = () => {
const collectionField = cm.getCollectionField(fieldSchema['x-collection-field']);
// console.log('🚀 ~ modalAdd ~ collectionField:', collectionField);
const targetCollection = cm.getCollection(collectionField?.target);
// insertAddNewer(schema.AddNewer);
// const props = {
// fieldSchema,
// field,
// targetCollection,
// };
Modal.show({
content: (
<CollectionProvider_deprecated name={targetCollection?.name}>
<RecursionField
onlyRenderProperties
basePath={field.address}
schema={fieldSchema}
filterProperties={(s) => {
return s['x-component'] === 'AssociationField.AddNewer';
}}
/>
</CollectionProvider_deprecated>
),
showCloseButton: true,
});
};
return (
<BlockItem>
{collectionName ? (
<div
onClick={() => {
setPopupVisible(true);
checkedPopup();
}}
style={{ color: '#c5c5c5' }}
>
{isArray(value) ? (
value.map((item, index) => (
<Space key={index}>{`${item.label}${value.length - 1 === index ? '' : ','}`}</Space>
))
) : (
<Space>{value?.label || '请选择内容'}</Space>
)}
</div>
) : (
<Tag color={'default'}>{value?.['label']}</Tag>
)}
<Popup visible={popupVisible} className={`${styles['PopupStyle']}`} closeOnMaskClick>
<SearchBar
placeholder="请输入内容"
value={searchValue}
onChange={(value) => {
const paramsFilter = { ...filter };
paramsFilter[fieldNames.label] = { $includes: value };
setFilter(paramsFilter);
setSearchValue(value);
}}
/>
{searchValue && addMode === 'quickAdd' ? (
<Space justify="center" align="center" onClick={quickAdd}>
+ {searchValue}
</Space>
) : null}
<Divider />
{multiple ? (
<CheckList
multiple
className={`${styles['checkListStyle']}`}
onChange={(value) => {
if (!value.length) {
setSelectValue(null);
return;
}
const filterValue = options.filter((item) => value.includes(item.value));
setSelectValue(filterValue);
}}
>
{options.map((item, index) => {
return (
<CheckList.Item key={index} value={item.value}>
{item.label}
</CheckList.Item>
);
})}
</CheckList>
) : (
<PickerView
columns={[options]}
onChange={(value) => {
const changeValue = options.find((item) => item[fieldNames.value] === value[0]);
setSelectValue(changeValue);
}}
/>
)}
<Space justify="evenly" align="center">
{/* {addMode === 'modalAdd' ? (
<Button color="primary" fill="outline" onClick={modalAdd}>
</Button>
) : null} */}
<Button
color="primary"
onClick={() => {
setPopupVisible(false);
}}
>
</Button>
<Button
color="primary"
onClick={() => {
onChange(selectValue);
setPopupVisible(false);
}}
>
</Button>
</Space>
</Popup>
</BlockItem>
);
});

View File

@ -0,0 +1,7 @@
import { createContext, useContext } from 'react';
export const AntdSelectContext = createContext({});
export const useAntdSelectContext = () => {
return useContext(AntdSelectContext);
};

View File

@ -0,0 +1,29 @@
import React, { useState } from 'react';
import {
CollectionProvider,
CollectionProvider_deprecated,
useActionContext,
useCollectionManager,
useCollectionManager_deprecated,
} from '@tachybase/client';
import { RecursionField } from '@tachybase/schema';
import { useInsertSchema } from './hook/hooks';
import schema from './schema';
export const CreateRecordAction = ({ field, fieldSchema, targetCollection }) => {
const [currentCollection, setCurrentCollection] = useState(targetCollection?.name);
const [currentDataSource, setCurrentDataSource] = useState(targetCollection?.dataSource);
return (
<CollectionProvider_deprecated name={currentCollection} dataSource={currentDataSource}>
<RecursionField
onlyRenderProperties
basePath={field.address}
schema={fieldSchema}
filterProperties={(s) => {
return s['x-component'] === 'AssociationField.AddNewer';
}}
/>
</CollectionProvider_deprecated>
);
};

View File

@ -0,0 +1,56 @@
import React, { useEffect } from 'react';
import { css, useCollectionField, useCollectionManager } from '@tachybase/client';
import { connect, isValid, mapProps, mapReadPretty, useField, useFieldSchema } from '@tachybase/schema';
import { isArray } from '@tachybase/utils/client';
import { Input, Space, Tag } from 'antd-mobile';
import { getMobileColor } from '../../../CustomColor';
import { AntdSelect } from './AntdSelect';
export const MSelect = connect(
AntdSelect,
mapProps((props) => {
const filterProps = { ...props };
const fieldSchema = useFieldSchema();
const cm = useCollectionManager();
const collection = cm.getCollection(fieldSchema['x-collection-field']);
if (collection) {
filterProps['collectionName'] = collection.name;
}
return { ...filterProps };
}),
mapReadPretty((props) => {
const { value, fieldNames } = props;
const isCollectionField = typeof value === 'object' && fieldNames;
if (isCollectionField) {
const isArrayField = isArray(value);
return isArrayField ? (
<div>
{value.map((item, index) => (
<Space key={index}>{`${item?.[fieldNames.label] || ''}${value.length - 1 === index ? '' : ','}`}</Space>
))}
</div>
) : (
<Space>{value?.[fieldNames.label] || ''}</Space>
);
} else {
const field = useField<any>();
const collectionField = useCollectionField();
const dataSource = field.dataSource || collectionField?.uiSchema.enum || [];
return (
<div>
{dataSource
.filter((option) => option.value == value)
.map((option, key) => (
<Tag key={key} color={getMobileColor(option.color)}>
{option.label}
</Tag>
))}
</div>
);
}
}),
);
export default MSelect;

View File

@ -0,0 +1,23 @@
import { useCallback } from 'react';
import { useDesignable } from '@tachybase/client';
import { GeneralField, reaction, useField, useFieldSchema } from '@tachybase/schema';
import { flatten, getValuesByPath } from '@tachybase/utils/client';
import _, { isString } from 'lodash';
import cloneDeep from 'lodash/cloneDeep';
export const useInsertSchema = (component, fieldSchema, insertAfterBegin) => {
const insert = (ss) => {
const schema = fieldSchema.reduceProperties((buf, s) => {
if (s['x-component'] === 'AssociationField.' + component) {
return s;
}
return buf;
}, null);
if (!schema) {
insertAfterBegin(cloneDeep(ss));
}
};
return insert;
};

View File

@ -0,0 +1,131 @@
export default {
Nester: {
type: 'void',
'x-component': 'AssociationField.Nester',
properties: {
grid: {
type: 'void',
'x-component': 'Grid',
'x-initializer': 'form:configureFields',
},
},
},
AddNewer: {
type: 'void',
'x-component': 'AssociationField.AddNewer',
'x-action': 'create',
title: '{{ t("Add record") }}',
'x-component-props': {
className: 'nb-action-popup',
},
properties: {
tabs: {
type: 'void',
'x-component': 'Tabs',
'x-component-props': {},
'x-initializer': 'TabPaneInitializersForCreateFormBlock',
properties: {
tab1: {
type: 'void',
title: '{{t("Add new")}}',
'x-component': 'Tabs.TabPane',
'x-designer': 'Tabs.Designer',
'x-component-props': {},
properties: {
grid: {
type: 'void',
'x-component': 'Grid',
'x-initializer': 'popup:addNew:addBlock',
properties: {},
},
},
},
},
},
},
},
Selector: {
type: 'void',
'x-component': 'AssociationField.Selector',
title: '{{ t("Select record") }}',
'x-component-props': {
className: 'nb-record-picker-selector',
},
properties: {
grid: {
type: 'void',
'x-component': 'Grid',
'x-initializer': 'popup:tableSelector:addBlock',
properties: {},
},
footer: {
'x-component': 'Action.Container.Footer',
'x-component-props': {},
properties: {
actions: {
type: 'void',
'x-component': 'ActionBar',
'x-component-props': {},
properties: {
submit: {
title: '{{ t("Submit") }}',
'x-action': 'submit',
'x-component': 'Action',
'x-use-component-props': 'usePickActionProps',
// 'x-designer': 'Action.Designer',
'x-toolbar': 'ActionSchemaToolbar',
'x-settings': 'actionSettings:submit',
'x-component-props': {
type: 'primary',
htmlType: 'submit',
},
},
},
},
},
},
},
},
Viewer: {
type: 'void',
title: '{{ t("View record") }}',
'x-component': 'AssociationField.Viewer',
'x-component-props': {
className: 'nb-action-popup',
},
properties: {
tabs: {
type: 'void',
'x-component': 'Tabs',
'x-component-props': {},
'x-initializer': 'TabPaneInitializers',
properties: {
tab1: {
type: 'void',
title: '{{t("Details")}}',
'x-component': 'Tabs.TabPane',
'x-designer': 'Tabs.Designer',
'x-component-props': {},
properties: {
grid: {
type: 'void',
'x-component': 'Grid',
'x-initializer': 'popup:common:addBlock',
properties: {},
},
},
},
},
},
},
},
SubTable: {
type: 'void',
'x-component': 'AssociationField.SubTable',
'x-initializer': 'table:configureColumns',
'x-initializer-props': {
action: false,
},
properties: {},
},
};

View File

@ -0,0 +1,25 @@
import { createStyles } from '@tachybase/client';
export const useStyles = createStyles(({ css }) => ({
PopupStyle: css`
.adm-popup-body {
padding: 10px;
.adm-space {
width: 100%;
margin: 10px 0 10px 0;
color: #4592ff;
button {
width: 23vw;
}
}
.adm-divider {
margin: 0;
}
.adm-list-body {
height: 30vh;
overflow: auto;
text-align: center;
}
}
`,
}));

View File

@ -3,3 +3,4 @@ export * from './Checkbox';
export * from './DatePicker';
export * from './ImageUploader';
export * from './Radio';
export * from './Select';

View File

@ -2,6 +2,8 @@ import React, { useEffect, useMemo } from 'react';
import {
CollectionFieldProvider,
useCollectionField,
useCollectionManager,
useCollectionManager_deprecated,
useCompile,
useComponent,
useFormBlockContext,
@ -28,11 +30,15 @@ export const CollectionFieldInternalField: React.FC = (props: Props) => {
const compile = useCompile();
const field = useField<Field>();
const fieldSchema = useFieldSchema();
const { getCollectionJoinField, getCollection } = useCollectionManager_deprecated();
const { uiSchema: uiSchemaOrigin, defaultValue } = useCollectionField();
const { isAllowToSetDefaultValue } = useIsAllowToSetDefaultValue();
const uiSchema = useMemo(() => compile(uiSchemaOrigin), [JSON.stringify(uiSchemaOrigin)]);
const isMobile = useIsMobile();
const Component = useComponent(component || uiComponent(isMobile, uiSchema) || 'Input');
const currModel = getCurrModel({ getCollectionJoinField, getCollection, fieldSchema, uiSchema });
const Component = useComponent(component || uiComponent(isMobile, uiSchema, currModel) || 'Input');
const setFieldProps = (key, value) => {
field[key] = typeof field[key] === 'undefined' ? value : field[key];
};
@ -95,8 +101,8 @@ export const CollectionField = connect((props) => {
);
});
export const uiComponent = (isMobile, uiSchema) => {
const isMobileComponent = canMobileField(uiSchema['x-component']);
export const uiComponent = (isMobile, uiSchema, currMode?) => {
const isMobileComponent = canMobileField(currMode || uiSchema['x-component']);
if (isMobile && isMobileComponent) {
return isMobileComponent;
} else {
@ -104,4 +110,15 @@ export const uiComponent = (isMobile, uiSchema) => {
}
};
export const getCurrModel = ({ getCollectionJoinField, getCollection, fieldSchema, uiSchema }) => {
const collectionField = getCollectionJoinField(fieldSchema['x-collection-field']);
const isFileCollection = getCollection(collectionField?.target)?.template === 'file';
const isTreeCollection = getCollection(collectionField?.target)?.template === 'tree';
const currentMode =
fieldSchema['x-component-props']?.mode ||
(isFileCollection ? 'FileManager' : isTreeCollection ? 'Cascader' : 'Select');
return currentMode && uiSchema['x-component'] === 'AssociationField' ? currentMode : undefined;
};
CollectionField.displayName = 'CollectionField';