: 移动端录单与筛选新设计 (#969)
Co-authored-by: sealday <zhanglin@daoyoucloud.com> Co-authored-by: hello@lv <2256334253@qq.com> Co-authored-by: bai.zixv <bai.zixv@foxmail.com> Reviewed-on: daoyoucloud/tachybase#951 Co-authored-by: wjh <wwwjh0710@163.com> Co-committed-by: wjh <wwwjh0710@163.com> Co-authored-by: wjh <wwwjh0710@163.com> Co-authored-by: bai.zixv <bai.zixv@foxmail.com> Reviewed-on: daoyoucloud/tachybase#969
This commit is contained in:
		
							parent
							
								
									0602743922
								
							
						
					
					
						commit
						d7b2695612
					
				| @ -35,6 +35,11 @@ import { | |||||||
|   usePropsRelatedImageSearchItemField, |   usePropsRelatedImageSearchItemField, | ||||||
|   useTabSearchFieldItemProps, |   useTabSearchFieldItemProps, | ||||||
|   useTabSearchFieldItemRelatedProps, |   useTabSearchFieldItemRelatedProps, | ||||||
|  |   MInput, | ||||||
|  |   MCheckbox, | ||||||
|  |   MDatePicker, | ||||||
|  |   MRadio, | ||||||
|  |   MImageUploader, | ||||||
| } from './schema'; | } from './schema'; | ||||||
| import './bridge'; | import './bridge'; | ||||||
| import './assets/svg'; | import './assets/svg'; | ||||||
| @ -70,6 +75,11 @@ export const MobileCore: React.FC = (props) => { | |||||||
|         ImageSearchItemView: ImageSearchItemView, |         ImageSearchItemView: ImageSearchItemView, | ||||||
|         // NoticeBlock,
 |         // NoticeBlock,
 | ||||||
|         // NoticeBlockInitializer,
 |         // NoticeBlockInitializer,
 | ||||||
|  |         MInput, | ||||||
|  |         MCheckbox, | ||||||
|  |         MDatePicker, | ||||||
|  |         MRadio, | ||||||
|  |         MImageUploader, | ||||||
|       }} |       }} | ||||||
|       scope={{ |       scope={{ | ||||||
|         useGridCardBlockItemProps, |         useGridCardBlockItemProps, | ||||||
|  | |||||||
| @ -0,0 +1,69 @@ | |||||||
|  | import { useCollectionField } from '@tachybase/client'; | ||||||
|  | import { connect, isValid, mapProps, mapReadPretty, useField } from '@tachybase/schema'; | ||||||
|  | import { Checkbox, CheckboxGroupProps, CheckboxProps, Tag } from 'antd-mobile'; | ||||||
|  | import React, { useEffect } from 'react'; | ||||||
|  | type ComposedCheckBox = React.FC<CheckboxProps> & { | ||||||
|  |   Group?: any; | ||||||
|  | }; | ||||||
|  | export const MCheckbox: ComposedCheckBox = connect( | ||||||
|  |   Checkbox, | ||||||
|  |   mapProps((props) => { | ||||||
|  |     return { ...props }; | ||||||
|  |   }), | ||||||
|  | ); | ||||||
|  | MCheckbox.Group = connect( | ||||||
|  |   (props) => { | ||||||
|  |     const collectionField = useCollectionField(); | ||||||
|  |     const dataSource = collectionField?.uiSchema.enum || []; | ||||||
|  |     const check = props.value || []; | ||||||
|  |     return ( | ||||||
|  |       <Checkbox.Group value={check}> | ||||||
|  |         {dataSource.map((item, index) => { | ||||||
|  |           return ( | ||||||
|  |             <Checkbox | ||||||
|  |               value={item.value} | ||||||
|  |               key={index} | ||||||
|  |               style={{ marginRight: '10px' }} | ||||||
|  |               onChange={(status) => { | ||||||
|  |                 if (status) { | ||||||
|  |                   check.push(item.value); | ||||||
|  |                 } else { | ||||||
|  |                   const itemIndex = check.findIndex((cItem) => item.value?.toString() === cItem); | ||||||
|  |                   check.splice(itemIndex, 1); | ||||||
|  |                 } | ||||||
|  |                 props.onChange(check); | ||||||
|  |               }} | ||||||
|  |             > | ||||||
|  |               {item.label} | ||||||
|  |             </Checkbox> | ||||||
|  |           ); | ||||||
|  |         })} | ||||||
|  |       </Checkbox.Group> | ||||||
|  |     ); | ||||||
|  |   }, | ||||||
|  |   mapProps((props: any, field: any) => { | ||||||
|  |     return { ...props }; | ||||||
|  |   }), | ||||||
|  |   mapReadPretty((props) => { | ||||||
|  |     if (!isValid(props.value)) { | ||||||
|  |       return <div></div>; | ||||||
|  |     } | ||||||
|  |     const { value } = props; | ||||||
|  |     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={option.color}> | ||||||
|  |               {option.label} | ||||||
|  |             </Tag> | ||||||
|  |           ))} | ||||||
|  |       </div> | ||||||
|  |     ); | ||||||
|  |   }), | ||||||
|  | ); | ||||||
|  | 
 | ||||||
|  | export default MCheckbox; | ||||||
| @ -0,0 +1 @@ | |||||||
|  | export * from './Checkbox'; | ||||||
| @ -0,0 +1,37 @@ | |||||||
|  | import { dayjs } from '@tachybase/utils/client'; | ||||||
|  | import { connect, mapProps } from '@tachybase/schema'; | ||||||
|  | import { Button, DatePicker, Input, Space } from 'antd-mobile'; | ||||||
|  | import React, { useState } from 'react'; | ||||||
|  | 
 | ||||||
|  | export const MDatePicker = connect( | ||||||
|  |   (props) => { | ||||||
|  |     const [visible, setVisible] = useState(false); | ||||||
|  |     const nowDate = props.value || new Date(); | ||||||
|  |     return ( | ||||||
|  |       <> | ||||||
|  |         <Button | ||||||
|  |           onClick={() => { | ||||||
|  |             setVisible(true); | ||||||
|  |           }} | ||||||
|  |         > | ||||||
|  |           {dayjs(nowDate).format('YYYY-MM-DD')} | ||||||
|  |         </Button> | ||||||
|  | 
 | ||||||
|  |         <DatePicker | ||||||
|  |           visible={visible} | ||||||
|  |           onClose={() => { | ||||||
|  |             setVisible(false); | ||||||
|  |           }} | ||||||
|  |           onConfirm={(value) => { | ||||||
|  |             props.onChange(value); | ||||||
|  |             setVisible(false); | ||||||
|  |           }} | ||||||
|  |         /> | ||||||
|  |       </> | ||||||
|  |     ); | ||||||
|  |   }, | ||||||
|  |   mapProps((props) => { | ||||||
|  |     return { ...props }; | ||||||
|  |   }), | ||||||
|  | ); | ||||||
|  | export default MDatePicker; | ||||||
| @ -0,0 +1 @@ | |||||||
|  | export * from './DatePicker'; | ||||||
| @ -0,0 +1,65 @@ | |||||||
|  | import { useAPIClient } from '@tachybase/client'; | ||||||
|  | import { connect, mapProps, mapReadPretty, useForm } from '@tachybase/schema'; | ||||||
|  | import { ImageUploadItem, ImageUploader } from 'antd-mobile'; | ||||||
|  | import React, { useEffect, useState } from 'react'; | ||||||
|  | 
 | ||||||
|  | export const MImageUploader = connect( | ||||||
|  |   (props) => { | ||||||
|  |     const [fileList, setFileList] = useState<ImageUploadItem[]>([]); | ||||||
|  |     const [change, setChange] = useState(false); | ||||||
|  |     const field = props.value || []; | ||||||
|  |     useEffect(() => { | ||||||
|  |       if (!change && props.value) { | ||||||
|  |         const data = props.value.map((item) => { | ||||||
|  |           return { url: item.url, key: item.id, thumbnailUrl: item.url }; | ||||||
|  |         }); | ||||||
|  |         setFileList(data); | ||||||
|  |       } | ||||||
|  |     }, [props.value]); | ||||||
|  |     const api = useAPIClient(); | ||||||
|  |     return ( | ||||||
|  |       <ImageUploader | ||||||
|  |         value={fileList} | ||||||
|  |         upload={async (file) => { | ||||||
|  |           const { name } = file; | ||||||
|  |           const imageField = isImage(name); | ||||||
|  |           if (imageField) { | ||||||
|  |             const formData = new FormData(); | ||||||
|  |             formData.append('file', file); | ||||||
|  |             let result; | ||||||
|  |             await api | ||||||
|  |               .request({ url: props.action, method: 'post', data: formData }) | ||||||
|  |               .then((res) => { | ||||||
|  |                 result = res?.data?.data; | ||||||
|  |                 field.push(result); | ||||||
|  |               }) | ||||||
|  |               .catch(() => {}); | ||||||
|  |             return { | ||||||
|  |               url: result?.url, | ||||||
|  |               key: result?.id, | ||||||
|  |               thumbnailUrl: result?.url, | ||||||
|  |             }; | ||||||
|  |           } | ||||||
|  |         }} | ||||||
|  |         onChange={(file) => { | ||||||
|  |           setFileList(file); | ||||||
|  |           setChange(true); | ||||||
|  |           props.onChange(field); | ||||||
|  |         }} | ||||||
|  |       /> | ||||||
|  |     ); | ||||||
|  |   }, | ||||||
|  |   mapProps((props) => { | ||||||
|  |     return { ...props }; | ||||||
|  |   }), | ||||||
|  | ); | ||||||
|  | export default MImageUploader; | ||||||
|  | 
 | ||||||
|  | export const isImage = (extName: string) => { | ||||||
|  |   const reg = /\.(png|jpg|jpeg|gif|webp)$/i; | ||||||
|  |   return reg.test(extName); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export const isPdf = (extName: string) => { | ||||||
|  |   return extName.toLowerCase().endsWith('.pdf'); | ||||||
|  | }; | ||||||
| @ -0,0 +1 @@ | |||||||
|  | export * from './ImageUploader'; | ||||||
| @ -0,0 +1,23 @@ | |||||||
|  | import { connect, mapProps } from '@tachybase/schema'; | ||||||
|  | import { Input, InputProps, TextArea, TextAreaProps } from 'antd-mobile'; | ||||||
|  | 
 | ||||||
|  | type ComposedInput = React.FC<InputProps> & { | ||||||
|  |   TextArea?: React.FC<TextAreaProps>; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export const MInput: ComposedInput = connect( | ||||||
|  |   Input, | ||||||
|  |   mapProps((props) => { | ||||||
|  |     return { placeholder: '请输入内容', clearable: true, ...props }; | ||||||
|  |   }), | ||||||
|  | ); | ||||||
|  | 
 | ||||||
|  | const MTextArea = connect( | ||||||
|  |   TextArea, | ||||||
|  |   mapProps((props) => { | ||||||
|  |     return { placeholder: '请输入内容', ...props }; | ||||||
|  |   }), | ||||||
|  | ); | ||||||
|  | 
 | ||||||
|  | MInput.TextArea = MTextArea; | ||||||
|  | export default MInput; | ||||||
| @ -0,0 +1 @@ | |||||||
|  | export * from './Input'; | ||||||
| @ -0,0 +1,67 @@ | |||||||
|  | import { useCollectionField } from '@tachybase/client'; | ||||||
|  | import { connect, isValid, mapProps, mapReadPretty, useField } from '@tachybase/schema'; | ||||||
|  | import { Radio, RadioGroupProps, RadioProps, Tag } from 'antd-mobile'; | ||||||
|  | import React, { useEffect } from 'react'; | ||||||
|  | 
 | ||||||
|  | type ComposedRadio = React.FC<RadioProps> & { | ||||||
|  |   Group?: any; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export const MRadio: ComposedRadio = connect( | ||||||
|  |   Radio, | ||||||
|  |   mapProps({ | ||||||
|  |     value: 'checked', | ||||||
|  |     onInput: 'onChange', | ||||||
|  |   }), | ||||||
|  | ); | ||||||
|  | 
 | ||||||
|  | MRadio.Group = connect( | ||||||
|  |   (props) => { | ||||||
|  |     const collectionField = useCollectionField(); | ||||||
|  |     const dataSource = collectionField?.uiSchema.enum || []; | ||||||
|  |     return ( | ||||||
|  |       <Radio.Group {...props}> | ||||||
|  |         {dataSource.map((item, index) => { | ||||||
|  |           return ( | ||||||
|  |             <Radio value={item.value} key={index} style={{ marginRight: '10px' }}> | ||||||
|  |               {item.label} | ||||||
|  |             </Radio> | ||||||
|  |           ); | ||||||
|  |         })} | ||||||
|  |       </Radio.Group> | ||||||
|  |     ); | ||||||
|  |   }, | ||||||
|  |   mapProps((props: any, field: any) => { | ||||||
|  |     useEffect(() => { | ||||||
|  |       const defaultOption = field.dataSource?.find((option) => option.value == props.value); | ||||||
|  |       if (defaultOption) { | ||||||
|  |         field.setValue(defaultOption.value); | ||||||
|  |       } | ||||||
|  |     }, [props.value, field.dataSource]); | ||||||
|  |     return { | ||||||
|  |       ...props, | ||||||
|  |     }; | ||||||
|  |   }), | ||||||
|  |   mapReadPretty((props) => { | ||||||
|  |     if (!isValid(props.value)) { | ||||||
|  |       return <div></div>; | ||||||
|  |     } | ||||||
|  |     const { value } = props; | ||||||
|  |     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={option.color}> | ||||||
|  |               {option.label} | ||||||
|  |             </Tag> | ||||||
|  |           ))} | ||||||
|  |       </div> | ||||||
|  |     ); | ||||||
|  |   }), | ||||||
|  | ); | ||||||
|  | 
 | ||||||
|  | export default MRadio; | ||||||
| @ -0,0 +1 @@ | |||||||
|  | export * from './Radio'; | ||||||
| @ -0,0 +1,5 @@ | |||||||
|  | export * from './Input'; | ||||||
|  | export * from './Checkbox'; | ||||||
|  | export * from './DatePicker'; | ||||||
|  | export * from './ImageUploader'; | ||||||
|  | export * from './Radio'; | ||||||
| @ -4,6 +4,7 @@ import React, { useEffect } from 'react'; | |||||||
| import { Navigate, useLocation, useNavigate, useParams } from 'react-router-dom'; | import { Navigate, useLocation, useNavigate, useParams } from 'react-router-dom'; | ||||||
| import { ContainerDesigner } from './Container.Designer'; | import { ContainerDesigner } from './Container.Designer'; | ||||||
| import useStyles from './style'; | import useStyles from './style'; | ||||||
|  | import { MobileProvider } from '../../provider/MobileProvider'; | ||||||
| 
 | 
 | ||||||
| const findGrid = (schema, uid) => { | const findGrid = (schema, uid) => { | ||||||
|   return schema.reduceProperties((final, next) => { |   return schema.reduceProperties((final, next) => { | ||||||
| @ -52,6 +53,7 @@ const InternalContainer: React.FC = (props) => { | |||||||
|   }, [location.pathname, navigate, params.name, redirectToUid]); |   }, [location.pathname, navigate, params.name, redirectToUid]); | ||||||
| 
 | 
 | ||||||
|   return ( |   return ( | ||||||
|  |     <MobileProvider> | ||||||
|       <SortableItem eid="nb-mobile-scroll-wrapper" className={cx('nb-mobile-container', styles.mobileContainer)}> |       <SortableItem eid="nb-mobile-scroll-wrapper" className={cx('nb-mobile-container', styles.mobileContainer)}> | ||||||
|         <Designer></Designer> |         <Designer></Designer> | ||||||
|         <div |         <div | ||||||
| @ -83,6 +85,7 @@ const InternalContainer: React.FC = (props) => { | |||||||
|           </div> |           </div> | ||||||
|         )} |         )} | ||||||
|       </SortableItem> |       </SortableItem> | ||||||
|  |     </MobileProvider> | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -0,0 +1,10 @@ | |||||||
|  | const custonComponent = { | ||||||
|  |   Input: 'MInput', | ||||||
|  |   'Input.TextArea': 'MInput.TextArea', | ||||||
|  |   'Radio.Group': 'MRadio.Group', | ||||||
|  |   'Checkbox.Group': 'MCheckbox.Group', | ||||||
|  |   'Upload.Attachment': 'MImageUploader', | ||||||
|  |   DatePicker: 'MDatePicker', | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export const canMobileField = (componentName: string) => custonComponent[componentName]; | ||||||
| @ -0,0 +1,146 @@ | |||||||
|  | import { | ||||||
|  |   AssociatedFields, | ||||||
|  |   CompatibleSchemaInitializer, | ||||||
|  |   ParentCollectionFields, | ||||||
|  |   SchemaInitializerItemType, | ||||||
|  |   formItemInitializers_deprecated, | ||||||
|  |   gridRowColWrap, | ||||||
|  |   useActionContext, | ||||||
|  |   useCollectionManager_deprecated, | ||||||
|  |   useCollection_deprecated, | ||||||
|  | } from '@tachybase/client'; | ||||||
|  | import { Schema, useForm } from '@tachybase/schema'; | ||||||
|  | import { useIsMobile } from '../../hooks'; | ||||||
|  | import { canMobileField } from './CustomComponent'; | ||||||
|  | 
 | ||||||
|  | export const useFormItemInitializerFields = (options?: any) => { | ||||||
|  |   const { name, currentFields } = useCollection_deprecated(); | ||||||
|  |   const isMobile = useIsMobile(); | ||||||
|  |   const { getInterface, getCollection } = useCollectionManager_deprecated(); | ||||||
|  |   const form = useForm(); | ||||||
|  |   const { readPretty = form.readPretty, block = 'Form' } = options || {}; | ||||||
|  |   const { fieldSchema } = useActionContext(); | ||||||
|  |   const action = fieldSchema?.['x-action']; | ||||||
|  | 
 | ||||||
|  |   return currentFields | ||||||
|  |     ?.filter((field) => field?.interface && !field?.isForeignKey && !field?.treeChildren) | ||||||
|  |     ?.map((field) => { | ||||||
|  |       const interfaceConfig = getInterface(field.interface); | ||||||
|  |       const targetCollection = getCollection(field.target); | ||||||
|  |       const isFileCollection = field?.target && getCollection(field?.target)?.template === 'file'; | ||||||
|  |       const isAssociationField = targetCollection; | ||||||
|  |       const fieldNames = field?.uiSchema['x-component-props']?.['fieldNames']; | ||||||
|  |       const isMobileComponent = canMobileField(field.uiSchema['x-component']); | ||||||
|  |       const schema = { | ||||||
|  |         type: 'string', | ||||||
|  |         name: field.name, | ||||||
|  |         'x-toolbar': 'FormItemSchemaToolbar', | ||||||
|  |         'x-settings': 'fieldSettings:FormItem', | ||||||
|  |         'x-component': 'CollectionField', | ||||||
|  |         'x-decorator': 'FormItem', | ||||||
|  |         'x-collection-field': `${name}.${field.name}`, | ||||||
|  |         'x-component-props': isFileCollection | ||||||
|  |           ? { | ||||||
|  |               fieldNames: { | ||||||
|  |                 label: 'preview', | ||||||
|  |                 value: 'id', | ||||||
|  |               }, | ||||||
|  |             } | ||||||
|  |           : isAssociationField && fieldNames | ||||||
|  |             ? { | ||||||
|  |                 fieldNames: { ...fieldNames, label: targetCollection?.titleField || fieldNames.label }, | ||||||
|  |               } | ||||||
|  |             : {}, | ||||||
|  |         'x-read-pretty': field?.uiSchema?.['x-read-pretty'], | ||||||
|  |       }; | ||||||
|  |       if (isMobile && isMobileComponent) { | ||||||
|  |         schema['x-component-props']['component'] = isMobileComponent; | ||||||
|  |       } | ||||||
|  |       const resultItem = { | ||||||
|  |         type: 'item', | ||||||
|  |         name: field.name, | ||||||
|  |         title: field?.uiSchema?.title || field.name, | ||||||
|  |         Component: 'CollectionFieldInitializer', | ||||||
|  |         remove: removeGridFormItem, | ||||||
|  |         schemaInitialize: (s) => { | ||||||
|  |           interfaceConfig?.schemaInitialize?.(s, { | ||||||
|  |             field, | ||||||
|  |             block, | ||||||
|  |             readPretty, | ||||||
|  |             action, | ||||||
|  |             targetCollection, | ||||||
|  |           }); | ||||||
|  |         }, | ||||||
|  |         schema, | ||||||
|  |       } as SchemaInitializerItemType; | ||||||
|  |       if (block == 'Kanban') { | ||||||
|  |         resultItem['find'] = (schema: Schema, key: string, action: string) => { | ||||||
|  |           const s = findSchema(schema, 'x-component', block); | ||||||
|  |           return findSchema(s, key, action); | ||||||
|  |         }; | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       return resultItem; | ||||||
|  |     }); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export const MobileFormItemInitializers = new CompatibleSchemaInitializer( | ||||||
|  |   { | ||||||
|  |     name: 'form:configureFields', | ||||||
|  |     wrap: gridRowColWrap, | ||||||
|  |     icon: 'SettingOutlined', | ||||||
|  |     title: '{{t("Configure fields")}}', | ||||||
|  |     items: [ | ||||||
|  |       { | ||||||
|  |         type: 'itemGroup', | ||||||
|  |         name: 'displayFields', | ||||||
|  |         title: '{{t("Display fields")}}', | ||||||
|  |         useChildren: useFormItemInitializerFields, | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         name: 'parentCollectionFields', | ||||||
|  |         Component: ParentCollectionFields, | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         name: 'associationFields', | ||||||
|  |         Component: AssociatedFields, | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         name: 'divider', | ||||||
|  |         type: 'divider', | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         name: 'addText', | ||||||
|  |         title: '{{t("Add text")}}', | ||||||
|  |         Component: 'MarkdownFormItemInitializer', | ||||||
|  |       }, | ||||||
|  |     ], | ||||||
|  |   }, | ||||||
|  |   formItemInitializers_deprecated, | ||||||
|  | ); | ||||||
|  | 
 | ||||||
|  | export const removeGridFormItem = (schema, cb) => { | ||||||
|  |   cb(schema, { | ||||||
|  |     removeParentsIfNoChildren: true, | ||||||
|  |     breakRemoveOn: { | ||||||
|  |       'x-component': 'Grid', | ||||||
|  |     }, | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | const findSchema = (schema: Schema, key: string, action: string) => { | ||||||
|  |   if (!Schema.isSchemaInstance(schema)) return null; | ||||||
|  |   return schema.reduceProperties((buf, s) => { | ||||||
|  |     if (s[key] === action) { | ||||||
|  |       return s; | ||||||
|  |     } | ||||||
|  |     if (s['x-component'] !== 'Action.Container' && s['x-component'] !== 'AssociationField.Viewer') { | ||||||
|  |       const c = findSchema(s, key, action); | ||||||
|  |       if (c) { | ||||||
|  |         return c; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return buf; | ||||||
|  |   }); | ||||||
|  | }; | ||||||
| @ -0,0 +1 @@ | |||||||
|  | export * from './MobileFormItemInitializers'; | ||||||
| @ -1,7 +1,7 @@ | |||||||
| import { SchemaInitializer, useCollection, useCollectionManager } from '@tachybase/client'; | import { SchemaInitializer, useCollection, useCollectionManager } from '@tachybase/client'; | ||||||
| import { useIsMobile } from '../tab-search/components/field-item/hooks'; |  | ||||||
| import { canBeOptionalField, canBeRelatedField } from '../tab-search/utils'; | import { canBeOptionalField, canBeRelatedField } from '../tab-search/utils'; | ||||||
| import { createSchemaImageSearchItem } from './search-item/ImageSearchItem.schema'; | import { createSchemaImageSearchItem } from './search-item/ImageSearchItem.schema'; | ||||||
|  | import { useIsMobile } from '../../hooks'; | ||||||
| 
 | 
 | ||||||
| export const ImageSearchConfigureFields = new SchemaInitializer({ | export const ImageSearchConfigureFields = new SchemaInitializer({ | ||||||
|   name: 'ImageSearchView:configureFields', |   name: 'ImageSearchView:configureFields', | ||||||
|  | |||||||
| @ -8,3 +8,5 @@ export * from './image-search'; | |||||||
| export * from './swiper'; | export * from './swiper'; | ||||||
| export * from './tab-search'; | export * from './tab-search'; | ||||||
| export * from './notice'; | export * from './notice'; | ||||||
|  | export * from './form'; | ||||||
|  | export * from './antd-mobile'; | ||||||
|  | |||||||
| @ -1,10 +1,11 @@ | |||||||
| import React, { useState } from 'react'; | import { dayjs } from '@tachybase/utils/client'; | ||||||
| import { useTranslation } from '../../../../../../locale'; | import { ActionSheet, Button, Calendar, Divider, Grid, Input, Picker, Popup } from 'antd-mobile'; | ||||||
| import { Grid, Divider, Picker, Input, Space, ActionSheet, DatePicker, CalendarPicker } from 'antd-mobile'; |  | ||||||
| import { DownOutline } from 'antd-mobile-icons'; | import { DownOutline } from 'antd-mobile-icons'; | ||||||
| import type { Action } from 'antd-mobile/es/components/action-sheet'; | import type { Action } from 'antd-mobile/es/components/action-sheet'; | ||||||
| import { changFormat, convertFormat } from '../../utils'; | import React, { useRef, useState } from 'react'; | ||||||
| import { dayjs } from '@tachybase/utils/client'; | import { useTranslation } from '../../../../../../locale'; | ||||||
|  | import { convertFormat } from '../../utils'; | ||||||
|  | import { css } from '@tachybase/client'; | ||||||
| 
 | 
 | ||||||
| export const ISelect = (props) => { | export const ISelect = (props) => { | ||||||
|   const { options, onChange, customLabelKey } = props; |   const { options, onChange, customLabelKey } = props; | ||||||
| @ -59,13 +60,34 @@ export const IDatePicker = (props) => { | |||||||
|   const { options, value, onChange, onInputChange } = props; |   const { options, value, onChange, onInputChange } = props; | ||||||
|   const time = value.split('&'); |   const time = value.split('&'); | ||||||
|   const [visible, setVisible] = useState(false); |   const [visible, setVisible] = useState(false); | ||||||
|  |   const [clickCount, setClickCount] = useState(0); | ||||||
|  |   const minDate = dayjs().subtract(10, 'year').toDate(); | ||||||
|  |   const maxDate = dayjs().add(3, 'year').toDate(); | ||||||
|  | 
 | ||||||
|  |   const onClick = () => { | ||||||
|  |     setVisible(true); | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   const onChangeDate = ([start, end]) => { | ||||||
|  |     const startTime = dayjs(start).startOf('date').toISOString(); | ||||||
|  |     const endTime = dayjs(end).endOf('date').toISOString(); | ||||||
|  |     // TODO: 此处受上游影响,格式必须确定为这样, 时间有限,不再往上追查
 | ||||||
|  |     const timeString = `"${startTime}"&"${endTime}"`; | ||||||
|  |     onInputChange(timeString); | ||||||
|  |     onChange(timeString); | ||||||
|  | 
 | ||||||
|  |     // XXX: UI 组件库这个组件是实验性组件, 此处模拟实现自动关闭浮层功能
 | ||||||
|  |     if (clickCount > 0 && clickCount % 2 === 1) { | ||||||
|  |       setVisible(false); | ||||||
|  |       setClickCount(0); | ||||||
|  |     } else { | ||||||
|  |       setClickCount((preClickCount) => preClickCount + 1); | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|   return ( |   return ( | ||||||
|     <Grid.Item span={options.length > 1 ? 3 : 4}> |     <Grid.Item span={options.length > 1 ? 3 : 4}> | ||||||
|       <div |       <div onClick={onClick}> | ||||||
|         onClick={() => { |  | ||||||
|           setVisible(true); |  | ||||||
|         }} |  | ||||||
|       > |  | ||||||
|         <Grid columns={5}> |         <Grid columns={5}> | ||||||
|           <Grid.Item span={2} style={{ textAlign: 'end' }}> |           <Grid.Item span={2} style={{ textAlign: 'end' }}> | ||||||
|             {convertFormat(JSON.parse(time[0]))} |             {convertFormat(JSON.parse(time[0]))} | ||||||
| @ -78,22 +100,24 @@ export const IDatePicker = (props) => { | |||||||
|           </Grid.Item> |           </Grid.Item> | ||||||
|         </Grid> |         </Grid> | ||||||
|       </div> |       </div> | ||||||
|       <CalendarPicker |       <Popup | ||||||
|         visible={visible} |         visible={visible} | ||||||
|         selectionMode="range" |         destroyOnClose | ||||||
|  |         mask | ||||||
|  |         closeOnMaskClick | ||||||
|         onMaskClick={() => setVisible(false)} |         onMaskClick={() => setVisible(false)} | ||||||
|         onClose={() => setVisible(false)} |         onClose={() => setVisible(false)} | ||||||
|         onConfirm={([start, end]) => { |         // bodyStyle={{ height: '50vh' }}
 | ||||||
|           const startTime = dayjs(start).startOf('date').toISOString(); |       > | ||||||
|           const endTime = dayjs(end).endOf('date').toISOString(); |         <Calendar | ||||||
| 
 |           selectionMode="range" | ||||||
|           // TODO: 此处受上游影响,格式必须确定为这样, 时间有限,不再往上追查
 |           allowClear | ||||||
|           const timeString = `"${startTime}"&"${endTime}"`; |           min={minDate} | ||||||
| 
 |           max={maxDate} | ||||||
|           onInputChange(timeString); |           // defaultValue={defaultRange}
 | ||||||
|           onChange(timeString); |           onChange={onChangeDate} | ||||||
|         }} |  | ||||||
|         /> |         /> | ||||||
|  |       </Popup> | ||||||
|     </Grid.Item> |     </Grid.Item> | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
|  | |||||||
| @ -81,9 +81,3 @@ export const useTabSearchCollapsibleInputItem = () => { | |||||||
|     onSelected, |     onSelected, | ||||||
|   }; |   }; | ||||||
| }; | }; | ||||||
| 
 |  | ||||||
| export const useIsMobile = () => { |  | ||||||
|   const fieldSchema = useFieldSchema(); |  | ||||||
|   const isMobile = Object.values(fieldSchema.root.properties).some((value) => value['x-component'] === 'MContainer'); |  | ||||||
|   return isMobile; |  | ||||||
| }; |  | ||||||
|  | |||||||
| @ -14,7 +14,8 @@ import { | |||||||
| import React, { useCallback } from 'react'; | import React, { useCallback } from 'react'; | ||||||
| import { tval } from '../../../../../locale'; | import { tval } from '../../../../../locale'; | ||||||
| import { canBeDataField, canBeOptionalField, canBeRelatedField, canBeSearchField } from '../utils'; | import { canBeDataField, canBeOptionalField, canBeRelatedField, canBeSearchField } from '../utils'; | ||||||
| import { useIsMobile } from '../components/field-item/hooks'; | import { useIsMobile } from '../../../hooks'; | ||||||
|  | import { mapToMobile } from '../mapToMobile'; | ||||||
| 
 | 
 | ||||||
| interface ItemInterface { | interface ItemInterface { | ||||||
|   field: FieldOptions; |   field: FieldOptions; | ||||||
| @ -159,7 +160,7 @@ function getItemInput(params: ItemInterface, isMobile?: boolean) { | |||||||
|   // 然后排除其他类型的关联字段
 |   // 然后排除其他类型的关联字段
 | ||||||
|   if (isAssocField(field)) return null; |   if (isAssocField(field)) return null; | ||||||
| 
 | 
 | ||||||
|   if (!(canBeSearchField(_interface) && !canBeRelatedField(_interface) && !canBeDataField(_interface))) { |   if (!canBeSearchField(_interface) && !canBeRelatedField(_interface) && !canBeDataField(_interface)) { | ||||||
|     return null; |     return null; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
| @ -201,10 +202,7 @@ function getItemInput(params: ItemInterface, isMobile?: boolean) { | |||||||
|       'x-collection-field': `${collectionName}.${schemaName}`, |       'x-collection-field': `${collectionName}.${schemaName}`, | ||||||
|       // 'x-decorator': 'FormItem',
 |       // 'x-decorator': 'FormItem',
 | ||||||
|       // 'x-component': 'CollectionField',
 |       // 'x-component': 'CollectionField',
 | ||||||
|       'x-component': matchTruthValue({ |       'x-component': mapToMobile('TabSearchCollapsibleInputItem'), | ||||||
|         ['TabSearchCollapsibleInputMItem']: isMobile, |  | ||||||
|         ['TabSearchCollapsibleInputItem']: !isMobile, |  | ||||||
|       }), |  | ||||||
|       'x-component-props': { |       'x-component-props': { | ||||||
|         fieldNames: { |         fieldNames: { | ||||||
|           label, |           label, | ||||||
| @ -317,10 +315,7 @@ function getItemChoice(params: ItemInterface, isMobile?: boolean) { | |||||||
|       'x-collection-field': `${collectionName}.${schemaName}`, |       'x-collection-field': `${collectionName}.${schemaName}`, | ||||||
|       // 'x-decorator': 'FormItem',
 |       // 'x-decorator': 'FormItem',
 | ||||||
|       // 'x-component': 'CollectionField',
 |       // 'x-component': 'CollectionField',
 | ||||||
|       'x-component': matchTruthValue({ |       'x-component': mapToMobile('TabSearchFieldItem'), | ||||||
|         ['TabSearchFieldMItem']: isMobile, |  | ||||||
|         ['TabSearchFieldItem']: !isMobile, |  | ||||||
|       }), |  | ||||||
|       'x-component-props': { |       'x-component-props': { | ||||||
|         fieldNames: { |         fieldNames: { | ||||||
|           label, |           label, | ||||||
|  | |||||||
| @ -1,9 +1,9 @@ | |||||||
| import { SchemaInitializer, useCollection, useCollectionManager } from '@tachybase/client'; | import { SchemaInitializer, useCollection, useCollectionManager } from '@tachybase/client'; | ||||||
| import { tval } from '../../../../../locale'; | import { tval } from '../../../../../locale'; | ||||||
| import { useIsMobile } from '../components/field-item/hooks'; |  | ||||||
| import { createTabSearchItemSchema } from '../create/createTabSearchItemSchema'; | import { createTabSearchItemSchema } from '../create/createTabSearchItemSchema'; | ||||||
| import { canBeDataField, canBeOptionalField, canBeRelatedField, canBeSearchField } from '../utils'; | import { canBeDataField, canBeOptionalField, canBeRelatedField, canBeSearchField } from '../utils'; | ||||||
| import { TabSearchAssociatedFields } from './TabSearchAssociated.fields'; | import { TabSearchAssociatedFields } from './TabSearchAssociated.fields'; | ||||||
|  | import { useIsMobile } from '../../../hooks'; | ||||||
| 
 | 
 | ||||||
| export const TabSearchFieldSchemaInitializer = new SchemaInitializer({ | export const TabSearchFieldSchemaInitializer = new SchemaInitializer({ | ||||||
|   name: 'tabSearch:configureFields', |   name: 'tabSearch:configureFields', | ||||||
|  | |||||||
| @ -0,0 +1,9 @@ | |||||||
|  | const customComponent = { | ||||||
|  |   ['TabSearchCollapsibleInputItem']: 'TabSearchCollapsibleInputMItem', | ||||||
|  |   ['useTabSearchFieldItemProps']: 'useTabSearchFieldItemRelatedProps', | ||||||
|  |   ['TabSearchFieldItem']: 'TabSearchFieldMItem', | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export function mapToMobile(componentName: string) { | ||||||
|  |   return customComponent[componentName]; | ||||||
|  | } | ||||||
| @ -1 +1,2 @@ | |||||||
| export * from './useSchemaPatch'; | export * from './useSchemaPatch'; | ||||||
|  | export * from './useIsMobile'; | ||||||
|  | |||||||
| @ -0,0 +1,7 @@ | |||||||
|  | import { useContext } from 'react'; | ||||||
|  | import { MobileContext } from '../provider/MobileProvider'; | ||||||
|  | 
 | ||||||
|  | export const useIsMobile = () => { | ||||||
|  |   const ctx = useContext(MobileContext); | ||||||
|  |   return ctx.isMobile; | ||||||
|  | }; | ||||||
| @ -31,11 +31,11 @@ export const mBlockInitializers_deprecated = new CompatibleSchemaInitializer({ | |||||||
|           title: '{{t("Form")}}', |           title: '{{t("Form")}}', | ||||||
|           Component: 'FormBlockInitializer', |           Component: 'FormBlockInitializer', | ||||||
|         }, |         }, | ||||||
|         // {
 |         { | ||||||
|         //   name: 'details',
 |           name: 'details', | ||||||
|         //   title: '{{t("Details")}}',
 |           title: '{{t("Details")}}', | ||||||
|         //   Component: 'DetailsBlockInitializer',
 |           Component: 'DetailsBlockInitializer', | ||||||
|         // },
 |         }, | ||||||
|         { |         { | ||||||
|           name: 'calendar', |           name: 'calendar', | ||||||
|           title: '{{t("Calendar")}}', |           title: '{{t("Calendar")}}', | ||||||
| @ -96,11 +96,11 @@ export const mBlockInitializers = new CompatibleSchemaInitializer( | |||||||
|             title: '{{t("Table")}}', |             title: '{{t("Table")}}', | ||||||
|             Component: 'TableBlockInitializer', |             Component: 'TableBlockInitializer', | ||||||
|           }, |           }, | ||||||
|           // {
 |           { | ||||||
|           //   name: 'form',
 |             name: 'form', | ||||||
|           //   title: '{{t("Form")}}',
 |             title: '{{t("Form")}}', | ||||||
|           //   Component: 'FormBlockInitializer',
 |             Component: 'FormBlockInitializer', | ||||||
|           // },
 |           }, | ||||||
|           { |           { | ||||||
|             name: 'details', |             name: 'details', | ||||||
|             title: '{{t("Details")}}', |             title: '{{t("Details")}}', | ||||||
|  | |||||||
| @ -0,0 +1,7 @@ | |||||||
|  | import React, { createContext } from 'react'; | ||||||
|  | 
 | ||||||
|  | export const MobileContext = createContext<any>({}); | ||||||
|  | 
 | ||||||
|  | export const MobileProvider = (props) => { | ||||||
|  |   return <MobileContext.Provider value={{ isMobile: true }}> {props.children}</MobileContext.Provider>; | ||||||
|  | }; | ||||||
| @ -8,6 +8,7 @@ import { | |||||||
|   ImageSearchItemFieldSettings, |   ImageSearchItemFieldSettings, | ||||||
|   mBlockInitializers, |   mBlockInitializers, | ||||||
|   mBlockInitializers_deprecated, |   mBlockInitializers_deprecated, | ||||||
|  |   MobileFormItemInitializers, | ||||||
|   TabSearchFieldSchemaInitializer, |   TabSearchFieldSchemaInitializer, | ||||||
|   TabSearchItemFieldSettings, |   TabSearchItemFieldSettings, | ||||||
| } from './core/schema'; | } from './core/schema'; | ||||||
| @ -27,7 +28,8 @@ export class MobileClientPlugin extends Plugin { | |||||||
|     this.app.schemaInitializerManager.add(TabSearchFieldSchemaInitializer); |     this.app.schemaInitializerManager.add(TabSearchFieldSchemaInitializer); | ||||||
| 
 | 
 | ||||||
|     this.app.schemaSettingsManager.add(ImageSearchItemFieldSettings); |     this.app.schemaSettingsManager.add(ImageSearchItemFieldSettings); | ||||||
|     this.schemaSettingsManager.add(TabSearchItemFieldSettings); |     this.app.schemaSettingsManager.add(TabSearchItemFieldSettings); | ||||||
|  |     this.app.schemaInitializerManager.add(MobileFormItemInitializers); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   addSettings() { |   addSettings() { | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user