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:
parent
f651f460d2
commit
05cc91c6fc
@ -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,
|
||||
}}
|
||||
|
@ -6,6 +6,7 @@ const customComponent = {
|
||||
'Upload.Attachment': 'MImageUploader',
|
||||
DatePicker: 'MDatePicker',
|
||||
Select: 'MSelect',
|
||||
Cascader: 'MCascader',
|
||||
};
|
||||
|
||||
export const canMobileField = (componentName: string) => customComponent[componentName];
|
||||
|
@ -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}
|
||||
</>
|
||||
);
|
||||
});
|
@ -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),
|
||||
);
|
@ -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>
|
||||
);
|
||||
};
|
@ -0,0 +1,5 @@
|
||||
export const defaultFieldNames = {
|
||||
label: 'label',
|
||||
value: 'value',
|
||||
children: 'children',
|
||||
};
|
@ -0,0 +1 @@
|
||||
export * from './Cadcader';
|
@ -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;
|
||||
}
|
||||
}
|
||||
`,
|
||||
}));
|
@ -226,7 +226,6 @@ export const AntdSelect = observer((props) => {
|
||||
if (paramsFilter[fieldNamesLabel]) delete paramsFilter[fieldNamesLabel];
|
||||
setSearchValue('');
|
||||
setFilter(paramsFilter);
|
||||
setSearchValue('');
|
||||
}}
|
||||
>
|
||||
确定
|
||||
|
@ -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: {},
|
||||
},
|
||||
};
|
@ -4,3 +4,4 @@ export * from './DatePicker';
|
||||
export * from './ImageUploader';
|
||||
export * from './Radio';
|
||||
export * from './Select';
|
||||
export * from './Cascader';
|
||||
|
@ -45,5 +45,6 @@
|
||||
"Add block": "Add block",
|
||||
"all": "all",
|
||||
"AllProducts": "AllProducts",
|
||||
"sort":"sort"
|
||||
"sort":"sort",
|
||||
"Add new":"Add new"
|
||||
}
|
||||
|
@ -48,5 +48,6 @@
|
||||
"sort": "排序",
|
||||
"Swiper":"轮播图",
|
||||
"TabSearch":"表格搜索",
|
||||
"ImageSearch":"图片搜索"
|
||||
"ImageSearch":"图片搜索",
|
||||
"Add new":"添加"
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user