: 移动端录单与筛选新设计 (#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,
 | 
			
		||||
  useTabSearchFieldItemProps,
 | 
			
		||||
  useTabSearchFieldItemRelatedProps,
 | 
			
		||||
  MInput,
 | 
			
		||||
  MCheckbox,
 | 
			
		||||
  MDatePicker,
 | 
			
		||||
  MRadio,
 | 
			
		||||
  MImageUploader,
 | 
			
		||||
} from './schema';
 | 
			
		||||
import './bridge';
 | 
			
		||||
import './assets/svg';
 | 
			
		||||
@ -70,6 +75,11 @@ export const MobileCore: React.FC = (props) => {
 | 
			
		||||
        ImageSearchItemView: ImageSearchItemView,
 | 
			
		||||
        // NoticeBlock,
 | 
			
		||||
        // NoticeBlockInitializer,
 | 
			
		||||
        MInput,
 | 
			
		||||
        MCheckbox,
 | 
			
		||||
        MDatePicker,
 | 
			
		||||
        MRadio,
 | 
			
		||||
        MImageUploader,
 | 
			
		||||
      }}
 | 
			
		||||
      scope={{
 | 
			
		||||
        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 { ContainerDesigner } from './Container.Designer';
 | 
			
		||||
import useStyles from './style';
 | 
			
		||||
import { MobileProvider } from '../../provider/MobileProvider';
 | 
			
		||||
 | 
			
		||||
const findGrid = (schema, uid) => {
 | 
			
		||||
  return schema.reduceProperties((final, next) => {
 | 
			
		||||
@ -52,37 +53,39 @@ const InternalContainer: React.FC = (props) => {
 | 
			
		||||
  }, [location.pathname, navigate, params.name, redirectToUid]);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <SortableItem eid="nb-mobile-scroll-wrapper" className={cx('nb-mobile-container', styles.mobileContainer)}>
 | 
			
		||||
      <Designer></Designer>
 | 
			
		||||
      <div
 | 
			
		||||
        style={{
 | 
			
		||||
          paddingBottom: redirectToUid ? token.paddingLG * 2 : 0,
 | 
			
		||||
        }}
 | 
			
		||||
        className="nb-mobile-container-content"
 | 
			
		||||
      >
 | 
			
		||||
        {redirectToUid ? (
 | 
			
		||||
          <TabContentComponent />
 | 
			
		||||
        ) : (
 | 
			
		||||
          <SchemaComponent
 | 
			
		||||
            filterProperties={(schema) => {
 | 
			
		||||
              return schema['x-component'] !== 'MTabBar';
 | 
			
		||||
            }}
 | 
			
		||||
            schema={fieldSchema}
 | 
			
		||||
          />
 | 
			
		||||
        )}
 | 
			
		||||
      </div>
 | 
			
		||||
      {isTabBarEnabled && (
 | 
			
		||||
        <div className={cx('nb-mobile-container-tab-bar', styles.tabBar)}>
 | 
			
		||||
          <SchemaComponent
 | 
			
		||||
            onlyRenderProperties
 | 
			
		||||
            filterProperties={(schema) => {
 | 
			
		||||
              return schema['x-component'] === 'MTabBar';
 | 
			
		||||
            }}
 | 
			
		||||
            schema={fieldSchema}
 | 
			
		||||
          ></SchemaComponent>
 | 
			
		||||
    <MobileProvider>
 | 
			
		||||
      <SortableItem eid="nb-mobile-scroll-wrapper" className={cx('nb-mobile-container', styles.mobileContainer)}>
 | 
			
		||||
        <Designer></Designer>
 | 
			
		||||
        <div
 | 
			
		||||
          style={{
 | 
			
		||||
            paddingBottom: redirectToUid ? token.paddingLG * 2 : 0,
 | 
			
		||||
          }}
 | 
			
		||||
          className="nb-mobile-container-content"
 | 
			
		||||
        >
 | 
			
		||||
          {redirectToUid ? (
 | 
			
		||||
            <TabContentComponent />
 | 
			
		||||
          ) : (
 | 
			
		||||
            <SchemaComponent
 | 
			
		||||
              filterProperties={(schema) => {
 | 
			
		||||
                return schema['x-component'] !== 'MTabBar';
 | 
			
		||||
              }}
 | 
			
		||||
              schema={fieldSchema}
 | 
			
		||||
            />
 | 
			
		||||
          )}
 | 
			
		||||
        </div>
 | 
			
		||||
      )}
 | 
			
		||||
    </SortableItem>
 | 
			
		||||
        {isTabBarEnabled && (
 | 
			
		||||
          <div className={cx('nb-mobile-container-tab-bar', styles.tabBar)}>
 | 
			
		||||
            <SchemaComponent
 | 
			
		||||
              onlyRenderProperties
 | 
			
		||||
              filterProperties={(schema) => {
 | 
			
		||||
                return schema['x-component'] === 'MTabBar';
 | 
			
		||||
              }}
 | 
			
		||||
              schema={fieldSchema}
 | 
			
		||||
            ></SchemaComponent>
 | 
			
		||||
          </div>
 | 
			
		||||
        )}
 | 
			
		||||
      </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 { useIsMobile } from '../tab-search/components/field-item/hooks';
 | 
			
		||||
import { canBeOptionalField, canBeRelatedField } from '../tab-search/utils';
 | 
			
		||||
import { createSchemaImageSearchItem } from './search-item/ImageSearchItem.schema';
 | 
			
		||||
import { useIsMobile } from '../../hooks';
 | 
			
		||||
 | 
			
		||||
export const ImageSearchConfigureFields = new SchemaInitializer({
 | 
			
		||||
  name: 'ImageSearchView:configureFields',
 | 
			
		||||
 | 
			
		||||
@ -8,3 +8,5 @@ export * from './image-search';
 | 
			
		||||
export * from './swiper';
 | 
			
		||||
export * from './tab-search';
 | 
			
		||||
export * from './notice';
 | 
			
		||||
export * from './form';
 | 
			
		||||
export * from './antd-mobile';
 | 
			
		||||
 | 
			
		||||
@ -1,10 +1,11 @@
 | 
			
		||||
import React, { useState } from 'react';
 | 
			
		||||
import { useTranslation } from '../../../../../../locale';
 | 
			
		||||
import { Grid, Divider, Picker, Input, Space, ActionSheet, DatePicker, CalendarPicker } from 'antd-mobile';
 | 
			
		||||
import { dayjs } from '@tachybase/utils/client';
 | 
			
		||||
import { ActionSheet, Button, Calendar, Divider, Grid, Input, Picker, Popup } from 'antd-mobile';
 | 
			
		||||
import { DownOutline } from 'antd-mobile-icons';
 | 
			
		||||
import type { Action } from 'antd-mobile/es/components/action-sheet';
 | 
			
		||||
import { changFormat, convertFormat } from '../../utils';
 | 
			
		||||
import { dayjs } from '@tachybase/utils/client';
 | 
			
		||||
import React, { useRef, useState } from 'react';
 | 
			
		||||
import { useTranslation } from '../../../../../../locale';
 | 
			
		||||
import { convertFormat } from '../../utils';
 | 
			
		||||
import { css } from '@tachybase/client';
 | 
			
		||||
 | 
			
		||||
export const ISelect = (props) => {
 | 
			
		||||
  const { options, onChange, customLabelKey } = props;
 | 
			
		||||
@ -59,13 +60,34 @@ export const IDatePicker = (props) => {
 | 
			
		||||
  const { options, value, onChange, onInputChange } = props;
 | 
			
		||||
  const time = value.split('&');
 | 
			
		||||
  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 (
 | 
			
		||||
    <Grid.Item span={options.length > 1 ? 3 : 4}>
 | 
			
		||||
      <div
 | 
			
		||||
        onClick={() => {
 | 
			
		||||
          setVisible(true);
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
      <div onClick={onClick}>
 | 
			
		||||
        <Grid columns={5}>
 | 
			
		||||
          <Grid.Item span={2} style={{ textAlign: 'end' }}>
 | 
			
		||||
            {convertFormat(JSON.parse(time[0]))}
 | 
			
		||||
@ -78,22 +100,24 @@ export const IDatePicker = (props) => {
 | 
			
		||||
          </Grid.Item>
 | 
			
		||||
        </Grid>
 | 
			
		||||
      </div>
 | 
			
		||||
      <CalendarPicker
 | 
			
		||||
      <Popup
 | 
			
		||||
        visible={visible}
 | 
			
		||||
        selectionMode="range"
 | 
			
		||||
        destroyOnClose
 | 
			
		||||
        mask
 | 
			
		||||
        closeOnMaskClick
 | 
			
		||||
        onMaskClick={() => setVisible(false)}
 | 
			
		||||
        onClose={() => setVisible(false)}
 | 
			
		||||
        onConfirm={([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);
 | 
			
		||||
        }}
 | 
			
		||||
      />
 | 
			
		||||
        // bodyStyle={{ height: '50vh' }}
 | 
			
		||||
      >
 | 
			
		||||
        <Calendar
 | 
			
		||||
          selectionMode="range"
 | 
			
		||||
          allowClear
 | 
			
		||||
          min={minDate}
 | 
			
		||||
          max={maxDate}
 | 
			
		||||
          // defaultValue={defaultRange}
 | 
			
		||||
          onChange={onChangeDate}
 | 
			
		||||
        />
 | 
			
		||||
      </Popup>
 | 
			
		||||
    </Grid.Item>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -81,9 +81,3 @@ export const useTabSearchCollapsibleInputItem = () => {
 | 
			
		||||
    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 { tval } from '../../../../../locale';
 | 
			
		||||
import { canBeDataField, canBeOptionalField, canBeRelatedField, canBeSearchField } from '../utils';
 | 
			
		||||
import { useIsMobile } from '../components/field-item/hooks';
 | 
			
		||||
import { useIsMobile } from '../../../hooks';
 | 
			
		||||
import { mapToMobile } from '../mapToMobile';
 | 
			
		||||
 | 
			
		||||
interface ItemInterface {
 | 
			
		||||
  field: FieldOptions;
 | 
			
		||||
@ -159,7 +160,7 @@ function getItemInput(params: ItemInterface, isMobile?: boolean) {
 | 
			
		||||
  // 然后排除其他类型的关联字段
 | 
			
		||||
  if (isAssocField(field)) return null;
 | 
			
		||||
 | 
			
		||||
  if (!(canBeSearchField(_interface) && !canBeRelatedField(_interface) && !canBeDataField(_interface))) {
 | 
			
		||||
  if (!canBeSearchField(_interface) && !canBeRelatedField(_interface) && !canBeDataField(_interface)) {
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -201,10 +202,7 @@ function getItemInput(params: ItemInterface, isMobile?: boolean) {
 | 
			
		||||
      'x-collection-field': `${collectionName}.${schemaName}`,
 | 
			
		||||
      // 'x-decorator': 'FormItem',
 | 
			
		||||
      // 'x-component': 'CollectionField',
 | 
			
		||||
      'x-component': matchTruthValue({
 | 
			
		||||
        ['TabSearchCollapsibleInputMItem']: isMobile,
 | 
			
		||||
        ['TabSearchCollapsibleInputItem']: !isMobile,
 | 
			
		||||
      }),
 | 
			
		||||
      'x-component': mapToMobile('TabSearchCollapsibleInputItem'),
 | 
			
		||||
      'x-component-props': {
 | 
			
		||||
        fieldNames: {
 | 
			
		||||
          label,
 | 
			
		||||
@ -317,10 +315,7 @@ function getItemChoice(params: ItemInterface, isMobile?: boolean) {
 | 
			
		||||
      'x-collection-field': `${collectionName}.${schemaName}`,
 | 
			
		||||
      // 'x-decorator': 'FormItem',
 | 
			
		||||
      // 'x-component': 'CollectionField',
 | 
			
		||||
      'x-component': matchTruthValue({
 | 
			
		||||
        ['TabSearchFieldMItem']: isMobile,
 | 
			
		||||
        ['TabSearchFieldItem']: !isMobile,
 | 
			
		||||
      }),
 | 
			
		||||
      'x-component': mapToMobile('TabSearchFieldItem'),
 | 
			
		||||
      'x-component-props': {
 | 
			
		||||
        fieldNames: {
 | 
			
		||||
          label,
 | 
			
		||||
 | 
			
		||||
@ -1,9 +1,9 @@
 | 
			
		||||
import { SchemaInitializer, useCollection, useCollectionManager } from '@tachybase/client';
 | 
			
		||||
import { tval } from '../../../../../locale';
 | 
			
		||||
import { useIsMobile } from '../components/field-item/hooks';
 | 
			
		||||
import { createTabSearchItemSchema } from '../create/createTabSearchItemSchema';
 | 
			
		||||
import { canBeDataField, canBeOptionalField, canBeRelatedField, canBeSearchField } from '../utils';
 | 
			
		||||
import { TabSearchAssociatedFields } from './TabSearchAssociated.fields';
 | 
			
		||||
import { useIsMobile } from '../../../hooks';
 | 
			
		||||
 | 
			
		||||
export const TabSearchFieldSchemaInitializer = new SchemaInitializer({
 | 
			
		||||
  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 './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")}}',
 | 
			
		||||
          Component: 'FormBlockInitializer',
 | 
			
		||||
        },
 | 
			
		||||
        // {
 | 
			
		||||
        //   name: 'details',
 | 
			
		||||
        //   title: '{{t("Details")}}',
 | 
			
		||||
        //   Component: 'DetailsBlockInitializer',
 | 
			
		||||
        // },
 | 
			
		||||
        {
 | 
			
		||||
          name: 'details',
 | 
			
		||||
          title: '{{t("Details")}}',
 | 
			
		||||
          Component: 'DetailsBlockInitializer',
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          name: 'calendar',
 | 
			
		||||
          title: '{{t("Calendar")}}',
 | 
			
		||||
@ -96,11 +96,11 @@ export const mBlockInitializers = new CompatibleSchemaInitializer(
 | 
			
		||||
            title: '{{t("Table")}}',
 | 
			
		||||
            Component: 'TableBlockInitializer',
 | 
			
		||||
          },
 | 
			
		||||
          // {
 | 
			
		||||
          //   name: 'form',
 | 
			
		||||
          //   title: '{{t("Form")}}',
 | 
			
		||||
          //   Component: 'FormBlockInitializer',
 | 
			
		||||
          // },
 | 
			
		||||
          {
 | 
			
		||||
            name: 'form',
 | 
			
		||||
            title: '{{t("Form")}}',
 | 
			
		||||
            Component: 'FormBlockInitializer',
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            name: '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,
 | 
			
		||||
  mBlockInitializers,
 | 
			
		||||
  mBlockInitializers_deprecated,
 | 
			
		||||
  MobileFormItemInitializers,
 | 
			
		||||
  TabSearchFieldSchemaInitializer,
 | 
			
		||||
  TabSearchItemFieldSettings,
 | 
			
		||||
} from './core/schema';
 | 
			
		||||
@ -27,7 +28,8 @@ export class MobileClientPlugin extends Plugin {
 | 
			
		||||
    this.app.schemaInitializerManager.add(TabSearchFieldSchemaInitializer);
 | 
			
		||||
 | 
			
		||||
    this.app.schemaSettingsManager.add(ImageSearchItemFieldSettings);
 | 
			
		||||
    this.schemaSettingsManager.add(TabSearchItemFieldSettings);
 | 
			
		||||
    this.app.schemaSettingsManager.add(TabSearchItemFieldSettings);
 | 
			
		||||
    this.app.schemaInitializerManager.add(MobileFormItemInitializers);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  addSettings() {
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user