feat: 添加mobile的级联组件 (#1221)

Reviewed-on: daoyoucloud/tachybase#1221
Reviewed-by: sealday <zhanglin@daoyoucloud.com>
Co-authored-by: wjh <wwwjh0710@163.com>
Co-committed-by: wjh <wwwjh0710@163.com>
This commit is contained in:
wjh 2024-06-20 18:57:35 +08:00 committed by sealday
parent f651f460d2
commit 05cc91c6fc
13 changed files with 369 additions and 134 deletions

View File

@ -9,6 +9,7 @@ import {
ImageSearchItemView,
ImageSearchProvider,
ImageSearchView,
MCascader,
MCheckbox,
MContainer,
MDatePicker,
@ -87,6 +88,7 @@ export const MobileCore: React.FC<PropsWithChildren> = (props) => {
MDatePicker,
MRadio,
MImageUploader,
MCascader,
CollectionField: CollectionField,
MSelect,
}}

View File

@ -6,6 +6,7 @@ const customComponent = {
'Upload.Attachment': 'MImageUploader',
DatePicker: 'MDatePicker',
Select: 'MSelect',
Cascader: 'MCascader',
};
export const canMobileField = (componentName: string) => customComponent[componentName];

View File

@ -0,0 +1,282 @@
import React, { useEffect, useState } from 'react';
import {
BlockItem,
CollectionProvider,
useCollectionManager,
useFieldServiceFilter,
useRequest,
} from '@tachybase/client';
import { observer, useFieldSchema, useForm } from '@tachybase/schema';
import { isArray } from '@tachybase/utils/client';
import { Button, CascaderView, Divider, Popup, SearchBar, Space } from 'antd-mobile';
import { CloseOutline } from 'antd-mobile-icons';
import { useTranslation } from '../../../../../locale';
import { MobileProvider } from '../../../provider';
import { MInput } from '../Input';
import { useStyles } from './style';
export const AntdCascader = observer((props) => {
const { service, fieldNames, disabled, multiple, onChange, value, formCascaderValues, index } = props as any;
const fieldSchema = useFieldSchema();
const cm = useCollectionManager();
const { styles } = useStyles();
const collectionField = cm.getCollectionField(fieldSchema['x-collection-field']);
const changOnSelect = fieldSchema['x-component-props']?.changOnSelect || false;
const collection = cm.getCollection(fieldSchema['x-collection-field']);
const isChinaRegion = collectionField.interface === 'chinaRegion';
const [options, setOptions] = useState([]);
const [popupVisible, setPopupVisible] = useState(false);
const [fieldServiceFilter] = useFieldServiceFilter(service?.params);
const [filter, setFilter] = useState(fieldServiceFilter);
const [searchValue, setSearchValue] = useState('');
const [cascaderValue, setCascaderValue] = useState(value);
const [valueText, setValueText] = useState('');
const [flatOption, setFlatOption] = useState([]);
const { run } = useRequest(
{
resource: collection?.name,
action: 'list',
params: {
filter,
tree: true,
},
},
{
manual: true,
onSuccess({ data }) {
if (data) {
setOptions(cascadeOption(data));
const valueOption = flatOptions(data);
setFlatOption(valueOption);
if (value) {
setValueText(getValueText(valueOption, value, '', fieldNames['label']));
}
}
},
},
);
const cascadeOption = (option) => {
option.forEach((item) => {
item['value'] = item[fieldNames.value];
item['label'] = item[fieldNames.label];
if (item.children) {
item.children = cascadeOption(item.children);
}
});
return option;
};
useEffect(() => {
if (!isChinaRegion && collection?.name) {
checkedPopup();
}
}, [filter]);
useEffect(() => {
if (options.length) {
cascadeOption(options);
}
}, [fieldSchema['x-component-props']?.['fieldNames']]);
const handleSelect = async (option) => {
if (option) {
setCascaderValue(option[option.length - 1]);
} else {
setCascaderValue({});
}
};
const checkedPopup = () => {
if (collection && collection.name) {
run();
}
};
return (
<CollectionProvider name={collectionField.collectionName}>
<div
onClick={() => {
if (disabled) return;
setPopupVisible(true);
checkedPopup();
}}
style={{ display: 'flex', alignItems: 'center' }}
>
{value && Object.keys(value).length ? (
<MInput style={{ '--color': '#1e8bf1' }} value={valueText} disabled={disabled} clearable={false} />
) : (
<MInput value={valueText ? '' : '请选择'} style={{ '--color': '#c5c5c5' }} />
)}
{(value && Object.keys(value).length) || (formCascaderValues && index !== 0) ? (
<CloseOutline
style={{ margin: '5px 10px 5px 20px' }}
onClick={() => {
if (index) {
const formValue = [...formCascaderValues];
if (formCascaderValues.length === 1 && index === 0) {
formValue[index] = {};
} else {
formValue.splice(index, 1);
}
onChange(formValue);
} else {
onChange({});
}
}}
/>
) : null}
</div>
<Popup
visible={popupVisible}
className={`${styles['PopupStyle']}`}
onMaskClick={() => {
setPopupVisible(false);
}}
>
<MobileProvider>
<SearchBar
placeholder="请输入内容"
value={searchValue}
onChange={(value) => {
const paramsFilter = { ...filter };
paramsFilter[fieldNames['label']] = { $includes: value };
setFilter(paramsFilter);
setSearchValue(value);
}}
/>
<Divider />
<CascaderView options={options} onChange={(value, extend) => handleSelect(extend['items'])} />
<Space justify="evenly" align="center">
<Button
color="primary"
onClick={() => {
setPopupVisible(false);
const paramsFilter = { ...filter };
if (paramsFilter[fieldNames['label']]) delete paramsFilter[fieldNames['label']];
setSearchValue('');
setFilter(paramsFilter);
}}
>
</Button>
<Button
color="primary"
onClick={() => {
const changevalue = ['m2m', 'o2m'].includes(collectionField.interface)
? getChangValue(formCascaderValues, cascaderValue, index)
: cascaderValue;
onChange(changevalue);
setValueText(getValueText(flatOption, cascaderValue, '', fieldNames['label']));
setPopupVisible(false);
const paramsFilter = { ...filter };
if (paramsFilter[fieldNames['label']]) delete paramsFilter[fieldNames['label']];
setSearchValue('');
setFilter(paramsFilter);
}}
disabled={
!changOnSelect
? cascaderValue && cascaderValue !== '{}' && !cascaderValue?.children
? false
: true
: cascaderValue && cascaderValue !== '{}'
? false
: true
}
>
</Button>
</Space>
</MobileProvider>
</Popup>
</CollectionProvider>
);
});
const getValueText = (option, item, value, label) => {
const text = `${item?.[label] || ''}${value}`;
if (item?.parentId) {
const filterItem = option.find((optionsItem) => optionsItem.id === item.parentId);
const itemText = `/${text}`;
return getValueText(option, filterItem, itemText, label);
}
return text;
};
const flatOptions = (options) => {
const flatChildren = (item, option) => {
option.push(item);
if (item?.children) {
item.children.forEach((childrenItem) => {
return flatChildren(childrenItem, option);
});
}
return option;
};
const optionflat = [];
options.forEach((value) => {
const flatValue = flatChildren(value, []);
optionflat.push(...flatValue);
});
return optionflat;
};
const getChangValue = (formCascaderValues, cascaderValue, index) => {
const value = [];
if (formCascaderValues && (index || index === 0)) {
formCascaderValues[index] = cascaderValue;
value.push(...formCascaderValues);
} else {
value.push(cascaderValue);
}
return value;
};
export const InternalCascader = observer((props) => {
const { value, onChange } = props as any;
const fieldSchema = useFieldSchema();
const cm = useCollectionManager();
const collectionField = cm.getCollectionField(fieldSchema['x-collection-field']);
const form = useForm();
const { t } = useTranslation();
return (
<>
{isArray(value) ? (
<>
{value.map((item, index) => {
const filterProps = {
...props,
value: item,
formCascaderValues: value || [],
index,
};
return <AntdCascader {...filterProps} key={index} />;
})}
</>
) : (
<AntdCascader {...props} />
)}
{['m2m', 'o2m'].includes(collectionField.interface) ? (
<>
<Button
style={{
textAlign: 'center',
width: '100%',
border: '1px dashed #c5c5c5',
color: '#c5c5c5',
fontSize: '12px',
height: '5vh',
lineHeight: '2vh',
}}
onClick={() => {
const pushValue = value ? [...value, {}] : [{}, {}];
onChange(pushValue);
}}
>
+ {t('Add new')}
</Button>
</>
) : null}
</>
);
});

View File

@ -0,0 +1,14 @@
import React from 'react';
import { connect, mapProps, mapReadPretty, useFieldSchema } from '@tachybase/schema';
import { isArray } from '@tachybase/utils/client';
import { InternalCascader } from './AntdCadcader';
import { ReadPretty } from './ReadPretty';
export const MCascader = connect(
InternalCascader,
mapProps((props) => {
return { ...props };
}),
mapReadPretty(ReadPretty),
);

View File

@ -0,0 +1,34 @@
import React from 'react';
import { ArrayField, toArr, useField } from '@tachybase/schema';
import { defaultFieldNames } from './defaultFieldNames';
export const ReadPretty: React.FC<unknown> = (props: any) => {
const { fieldNames = defaultFieldNames } = props;
const values = toArr(props.value);
const len = values.length;
const field = useField<ArrayField>();
let dataSource = field.dataSource;
const data = [];
for (const item of values) {
if (typeof item === 'object') {
data.push(item);
} else {
const curr = dataSource?.find((v) => v[fieldNames.value] === item);
dataSource = curr?.[fieldNames.children] || [];
data.push(curr || { label: item, value: item });
}
}
return (
<div>
{data.map((item, index) => {
return (
<span key={index}>
{typeof item === 'object' ? item[fieldNames.label] : item}
{len > index + 1 && ' / '}
</span>
);
})}
</div>
);
};

View File

@ -0,0 +1,5 @@
export const defaultFieldNames = {
label: 'label',
value: 'value',
children: 'children',
};

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

@ -226,7 +226,6 @@ export const AntdSelect = observer((props) => {
if (paramsFilter[fieldNamesLabel]) delete paramsFilter[fieldNamesLabel];
setSearchValue('');
setFilter(paramsFilter);
setSearchValue('');
}}
>

View File

@ -1,131 +0,0 @@
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

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

View File

@ -45,5 +45,6 @@
"Add block": "Add block",
"all": "all",
"AllProducts": "AllProducts",
"sort":"sort"
"sort":"sort",
"Add new":"Add new"
}

View File

@ -48,5 +48,6 @@
"sort": "排序",
"Swiper":"轮播图",
"TabSearch":"表格搜索",
"ImageSearch":"图片搜索"
"ImageSearch":"图片搜索",
"Add new":"添加"
}