index.tsx 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. import React, { useLayoutEffect, useRef, useState } from 'react';
  2. import { isVoidField, Field } from '@formily/core';
  3. import { useField, observer } from '@formily/react';
  4. import { Popover } from 'antd';
  5. import { EditOutlined, CloseOutlined, MessageOutlined } from '@ant-design/icons';
  6. import { BaseItem, IFormItemProps } from '@formily/antd/lib/form-item';
  7. import { PopoverProps } from 'antd/lib/popover';
  8. import { useClickAway, usePrefixCls } from '@formily/antd/lib/__builtins__';
  9. import cls from 'classnames';
  10. import { get } from 'lodash';
  11. import { Ellipsis } from '@/components';
  12. /**
  13. * 默认Inline展示
  14. */
  15. type IPopoverProps = PopoverProps;
  16. type ComposedEditable = React.FC<React.PropsWithChildren<IFormItemProps>> & {
  17. Popover?: React.FC<React.PropsWithChildren<IPopoverProps & { title?: React.ReactNode }>>;
  18. };
  19. const useParentPattern = () => {
  20. const field = useField<Field>();
  21. return field?.parent?.pattern || field?.form?.pattern;
  22. };
  23. const useEditable = (): [boolean, (payload: boolean) => void] => {
  24. const pattern = useParentPattern();
  25. const field = useField<Field>();
  26. useLayoutEffect(() => {
  27. if (pattern === 'editable') {
  28. return field.setPattern('readPretty');
  29. }
  30. }, [pattern]);
  31. return [
  32. field.pattern === 'editable',
  33. (payload: boolean) => {
  34. if (pattern !== 'editable') return;
  35. field.setPattern(payload ? 'editable' : 'readPretty');
  36. },
  37. ];
  38. };
  39. const useFormItemProps = (): IFormItemProps => {
  40. const field = useField();
  41. if (isVoidField(field)) return {};
  42. if (!field) return {};
  43. const takeMessage = () => {
  44. if (field.selfErrors.length) return field.selfErrors;
  45. if (field.selfWarnings.length) return field.selfWarnings;
  46. if (field.selfSuccesses.length) return field.selfSuccesses;
  47. };
  48. return {
  49. feedbackStatus: field.validateStatus === 'validating' ? 'pending' : field.validateStatus,
  50. feedbackText: takeMessage(),
  51. extra: field.description,
  52. };
  53. };
  54. export const Editable: ComposedEditable = observer((props) => {
  55. const [editable, setEditable] = useEditable();
  56. const pattern = useParentPattern();
  57. const itemProps = useFormItemProps();
  58. const field = useField<Field>();
  59. const basePrefixCls = usePrefixCls();
  60. const prefixCls = usePrefixCls('formily-editable');
  61. const ref = useRef<boolean>();
  62. const innerRef = useRef<HTMLDivElement | any>();
  63. const recover = () => {
  64. if (ref.current && !field?.errors?.length) {
  65. setEditable(false);
  66. }
  67. };
  68. const renderEditHelper = () => {
  69. if (editable) return;
  70. return (
  71. <BaseItem {...props} {...itemProps}>
  72. {pattern === 'editable' && <EditOutlined className={`${prefixCls}-edit-btn`} />}
  73. {pattern !== 'editable' && <MessageOutlined className={`${prefixCls}-edit-btn`} />}
  74. </BaseItem>
  75. );
  76. };
  77. const renderCloseHelper = () => {
  78. if (!editable) return;
  79. return (
  80. <BaseItem {...props}>
  81. <CloseOutlined className={`${prefixCls}-close-btn`} />
  82. </BaseItem>
  83. );
  84. };
  85. useClickAway((e) => {
  86. const target = e.target as HTMLElement;
  87. if (target?.closest(`.${basePrefixCls}-select-dropdown`)) return;
  88. if (target?.closest(`.${basePrefixCls}-picker-dropdown`)) return;
  89. if (target?.closest(`.${basePrefixCls}-cascader-menus`)) return;
  90. recover();
  91. }, innerRef);
  92. const onClick = (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
  93. const target = e.target as HTMLElement;
  94. const close = innerRef.current?.querySelector(`.${prefixCls}-close-btn`);
  95. if (target?.contains(close) || close?.contains(target)) {
  96. recover();
  97. } else if (!ref.current) {
  98. setTimeout(() => {
  99. setEditable(true);
  100. setTimeout(() => {
  101. innerRef.current?.querySelector('input')?.focus();
  102. });
  103. });
  104. }
  105. };
  106. ref.current = editable;
  107. return (
  108. <div className={prefixCls} ref={innerRef} onClick={onClick}>
  109. <div className={`${prefixCls}-content`}>
  110. <BaseItem {...props} {...itemProps}>
  111. {props.children}
  112. </BaseItem>
  113. {renderEditHelper()}
  114. {renderCloseHelper()}
  115. </div>
  116. </div>
  117. );
  118. });
  119. Editable.Popover = observer((props) => {
  120. const field = useField<Field>();
  121. // console.log(field.path.segments)
  122. // console.log(field.form.query(field.path).pattern.segments)
  123. const pattern = useParentPattern();
  124. let title = props.title || field.title;
  125. const [visible, setVisible] = useState(false);
  126. const prefixCls = usePrefixCls('formily-editable');
  127. const closePopover = async () => {
  128. try {
  129. await field.form.validate(`${field.address}.*`);
  130. } finally {
  131. const errors = field.form.queryFeedbacks({
  132. type: 'error',
  133. address: `${field.address}.*`,
  134. });
  135. if (errors?.length) return;
  136. setVisible(false);
  137. }
  138. };
  139. const openPopover = () => {
  140. setVisible(true);
  141. };
  142. if ((field.title === '配置参数' || field.title === '指标数据') && !props.title) {
  143. const filterKeys = ['config', 'edit'];
  144. const path = field.path.segments.filter((key: any) => !filterKeys.includes(key));
  145. // console.log('EditTable', path, field.form.values);
  146. const value = get(field.form.values, path)?.name;
  147. title = value || '配置参数';
  148. }
  149. const headTitle = () => {
  150. return (
  151. <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
  152. <div style={{ width: 150 }}>
  153. <Ellipsis title={props.title || field.title} />
  154. </div>
  155. <CloseOutlined
  156. onClick={() => {
  157. setVisible(false);
  158. }}
  159. />
  160. </div>
  161. );
  162. };
  163. return (
  164. <Popover
  165. {...props}
  166. title={headTitle()}
  167. visible={visible}
  168. className={cls(prefixCls, props.className)}
  169. content={props.children}
  170. trigger="click"
  171. destroyTooltipOnHide
  172. onVisibleChange={(param) => {
  173. if (param) {
  174. openPopover();
  175. } else {
  176. closePopover();
  177. }
  178. }}
  179. >
  180. <div>
  181. <BaseItem className={`${prefixCls}-trigger`}>
  182. <div className={`${prefixCls}-content`}>
  183. <span className={`${prefixCls}-preview`}>{title}</span>
  184. {pattern === 'editable' && <EditOutlined className={`${prefixCls}-edit-btn`} />}
  185. {pattern !== 'editable' && <MessageOutlined className={`${prefixCls}-edit-btn`} />}
  186. </div>
  187. </BaseItem>
  188. </div>
  189. </Popover>
  190. );
  191. });
  192. export default Editable;