feature/nocobase next password (#159)
* feat: add Password Component into schema components * improve code Co-authored-by: chenos <chenlinxh@gmail.com>
This commit is contained in:
		
							parent
							
								
									637b3165ca
								
							
						
					
					
						commit
						206b37edc1
					
				@ -8,5 +8,6 @@ export * from './form';
 | 
			
		||||
export * from './form-item';
 | 
			
		||||
export * from './grid';
 | 
			
		||||
export * from './input';
 | 
			
		||||
export * from './password';
 | 
			
		||||
export * from './radio';
 | 
			
		||||
export * from './menu';
 | 
			
		||||
@ -0,0 +1,69 @@
 | 
			
		||||
import { connect, mapReadPretty } from '@formily/react';
 | 
			
		||||
import { isValid } from '@formily/shared';
 | 
			
		||||
import { Input } from 'antd';
 | 
			
		||||
import { PasswordProps } from 'antd/lib/input';
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import { PasswordStrength } from './PasswordStrength';
 | 
			
		||||
 | 
			
		||||
export interface IPasswordProps extends PasswordProps {
 | 
			
		||||
  checkStrength: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const Password = connect(
 | 
			
		||||
  (props: IPasswordProps) => {
 | 
			
		||||
    const { value, className, checkStrength, ...others } = props;
 | 
			
		||||
    const blockStyle: React.CSSProperties = {
 | 
			
		||||
      position: 'absolute',
 | 
			
		||||
      zIndex: 1,
 | 
			
		||||
      height: 8,
 | 
			
		||||
      top: 0,
 | 
			
		||||
      background: '#fff',
 | 
			
		||||
      width: 1,
 | 
			
		||||
      transform: 'translate(-50%, 0)',
 | 
			
		||||
    };
 | 
			
		||||
    return (
 | 
			
		||||
      <span className={className}>
 | 
			
		||||
        <Input.Password {...others} value={value} />
 | 
			
		||||
        {checkStrength && (
 | 
			
		||||
          <PasswordStrength value={value}>
 | 
			
		||||
            {(score) => {
 | 
			
		||||
              return (
 | 
			
		||||
                <div
 | 
			
		||||
                  style={{
 | 
			
		||||
                    background: '#e0e0e0',
 | 
			
		||||
                    marginBottom: 3,
 | 
			
		||||
                    position: 'relative',
 | 
			
		||||
                  }}
 | 
			
		||||
                >
 | 
			
		||||
                  <div style={{ ...blockStyle, left: '20%' }} />
 | 
			
		||||
                  <div style={{ ...blockStyle, left: '40%' }} />
 | 
			
		||||
                  <div style={{ ...blockStyle, left: '60%' }} />
 | 
			
		||||
                  <div style={{ ...blockStyle, left: '80%' }} />
 | 
			
		||||
                  <div
 | 
			
		||||
                    style={{
 | 
			
		||||
                      position: 'relative',
 | 
			
		||||
                      backgroundImage: '-webkit-linear-gradient(left, #ff5500, #ff9300)',
 | 
			
		||||
                      transition: 'all 0.35s ease-in-out',
 | 
			
		||||
                      height: 8,
 | 
			
		||||
                      width: '100%',
 | 
			
		||||
                      marginTop: 5,
 | 
			
		||||
                      clipPath: `polygon(0 0,${score}% 0,${score}% 100%,0 100%)`,
 | 
			
		||||
                    }}
 | 
			
		||||
                  />
 | 
			
		||||
                </div>
 | 
			
		||||
              );
 | 
			
		||||
            }}
 | 
			
		||||
          </PasswordStrength>
 | 
			
		||||
        )}
 | 
			
		||||
      </span>
 | 
			
		||||
    );
 | 
			
		||||
  },
 | 
			
		||||
  mapReadPretty((props) => {
 | 
			
		||||
    if (!props.value) {
 | 
			
		||||
      return <div></div>;
 | 
			
		||||
    }
 | 
			
		||||
    return <div>********</div>;
 | 
			
		||||
  }),
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
export default Password;
 | 
			
		||||
@ -0,0 +1,162 @@
 | 
			
		||||
import { isFn } from '@formily/shared';
 | 
			
		||||
import React, { Fragment } from 'react';
 | 
			
		||||
 | 
			
		||||
type ReactRenderPropsChildren<T = any> = React.ReactNode | ((props: T) => React.ReactElement);
 | 
			
		||||
 | 
			
		||||
interface IPasswordStrengthProps {
 | 
			
		||||
  value?: any;
 | 
			
		||||
  children?: ReactRenderPropsChildren<number>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const isNum = function (c) {
 | 
			
		||||
  return c >= 48 && c <= 57;
 | 
			
		||||
};
 | 
			
		||||
const isLower = function (c) {
 | 
			
		||||
  return c >= 97 && c <= 122;
 | 
			
		||||
};
 | 
			
		||||
const isUpper = function (c) {
 | 
			
		||||
  return c >= 65 && c <= 90;
 | 
			
		||||
};
 | 
			
		||||
const isSymbol = function (c) {
 | 
			
		||||
  return !(isLower(c) || isUpper(c) || isNum(c));
 | 
			
		||||
};
 | 
			
		||||
const isLetter = function (c) {
 | 
			
		||||
  return isLower(c) || isUpper(c);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const getStrength = (val) => {
 | 
			
		||||
  if (!val) return 0;
 | 
			
		||||
  let num = 0;
 | 
			
		||||
  let lower = 0;
 | 
			
		||||
  let upper = 0;
 | 
			
		||||
  let symbol = 0;
 | 
			
		||||
  let MNS = 0;
 | 
			
		||||
  let rep = 0;
 | 
			
		||||
  let repC = 0;
 | 
			
		||||
  let consecutive = 0;
 | 
			
		||||
  let sequential = 0;
 | 
			
		||||
  const len = () => num + lower + upper + symbol;
 | 
			
		||||
  const callMe = () => {
 | 
			
		||||
    let re = num > 0 ? 1 : 0;
 | 
			
		||||
    re += lower > 0 ? 1 : 0;
 | 
			
		||||
    re += upper > 0 ? 1 : 0;
 | 
			
		||||
    re += symbol > 0 ? 1 : 0;
 | 
			
		||||
    if (re > 2 && len() >= 8) {
 | 
			
		||||
      return re + 1;
 | 
			
		||||
    } else {
 | 
			
		||||
      return 0;
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
  for (let i = 0; i < val.length; i++) {
 | 
			
		||||
    const c = val.charCodeAt(i);
 | 
			
		||||
    if (isNum(c)) {
 | 
			
		||||
      num++;
 | 
			
		||||
      if (i !== 0 && i !== val.length - 1) {
 | 
			
		||||
        MNS++;
 | 
			
		||||
      }
 | 
			
		||||
      if (i > 0 && isNum(val.charCodeAt(i - 1))) {
 | 
			
		||||
        consecutive++;
 | 
			
		||||
      }
 | 
			
		||||
    } else if (isLower(c)) {
 | 
			
		||||
      lower++;
 | 
			
		||||
      if (i > 0 && isLower(val.charCodeAt(i - 1))) {
 | 
			
		||||
        consecutive++;
 | 
			
		||||
      }
 | 
			
		||||
    } else if (isUpper(c)) {
 | 
			
		||||
      upper++;
 | 
			
		||||
      if (i > 0 && isUpper(val.charCodeAt(i - 1))) {
 | 
			
		||||
        consecutive++;
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      symbol++;
 | 
			
		||||
      if (i !== 0 && i !== val.length - 1) {
 | 
			
		||||
        MNS++;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    let exists = false;
 | 
			
		||||
    for (let j = 0; j < val.length; j++) {
 | 
			
		||||
      if (val[i] === val[j] && i !== j) {
 | 
			
		||||
        exists = true;
 | 
			
		||||
        repC += Math.abs(val.length / (j - i));
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    if (exists) {
 | 
			
		||||
      rep++;
 | 
			
		||||
      const unique = val.length - rep;
 | 
			
		||||
      repC = unique ? Math.ceil(repC / unique) : Math.ceil(repC);
 | 
			
		||||
    }
 | 
			
		||||
    if (i > 1) {
 | 
			
		||||
      const last1 = val.charCodeAt(i - 1);
 | 
			
		||||
      const last2 = val.charCodeAt(i - 2);
 | 
			
		||||
      if (isLetter(c)) {
 | 
			
		||||
        if (isLetter(last1) && isLetter(last2)) {
 | 
			
		||||
          const v = val.toLowerCase();
 | 
			
		||||
          const vi = v.charCodeAt(i);
 | 
			
		||||
          const vi1 = v.charCodeAt(i - 1);
 | 
			
		||||
          const vi2 = v.charCodeAt(i - 2);
 | 
			
		||||
          if (vi - vi1 === vi1 - vi2 && Math.abs(vi - vi1) === 1) {
 | 
			
		||||
            sequential++;
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      } else if (isNum(c)) {
 | 
			
		||||
        if (isNum(last1) && isNum(last2)) {
 | 
			
		||||
          if (c - last1 === last1 - last2 && Math.abs(c - last1) === 1) {
 | 
			
		||||
            sequential++;
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      } else {
 | 
			
		||||
        if (isSymbol(last1) && isSymbol(last2)) {
 | 
			
		||||
          if (c - last1 === last1 - last2 && Math.abs(c - last1) === 1) {
 | 
			
		||||
            sequential++;
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  let sum = 0;
 | 
			
		||||
  const length = len();
 | 
			
		||||
  sum += 4 * length;
 | 
			
		||||
  if (lower > 0) {
 | 
			
		||||
    sum += 2 * (length - lower);
 | 
			
		||||
  }
 | 
			
		||||
  if (upper > 0) {
 | 
			
		||||
    sum += 2 * (length - upper);
 | 
			
		||||
  }
 | 
			
		||||
  if (num !== length) {
 | 
			
		||||
    sum += 4 * num;
 | 
			
		||||
  }
 | 
			
		||||
  sum += 6 * symbol;
 | 
			
		||||
  sum += 2 * MNS;
 | 
			
		||||
  sum += 2 * callMe();
 | 
			
		||||
  if (length === lower + upper) {
 | 
			
		||||
    sum -= length;
 | 
			
		||||
  }
 | 
			
		||||
  if (length === num) {
 | 
			
		||||
    sum -= num;
 | 
			
		||||
  }
 | 
			
		||||
  sum -= repC;
 | 
			
		||||
  sum -= 2 * consecutive;
 | 
			
		||||
  sum -= 3 * sequential;
 | 
			
		||||
  sum = sum < 0 ? 0 : sum;
 | 
			
		||||
  sum = sum > 100 ? 100 : sum;
 | 
			
		||||
 | 
			
		||||
  if (sum >= 80) {
 | 
			
		||||
    return 100;
 | 
			
		||||
  } else if (sum >= 60) {
 | 
			
		||||
    return 80;
 | 
			
		||||
  } else if (sum >= 40) {
 | 
			
		||||
    return 60;
 | 
			
		||||
  } else if (sum >= 20) {
 | 
			
		||||
    return 40;
 | 
			
		||||
  } else {
 | 
			
		||||
    return 20;
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const PasswordStrength: React.FC<IPasswordStrengthProps> = (props) => {
 | 
			
		||||
  if (isFn(props.children)) {
 | 
			
		||||
    return props.children(getStrength(String(props.value || '')));
 | 
			
		||||
  } else {
 | 
			
		||||
    return <Fragment>{props.children}</Fragment>;
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
@ -0,0 +1,41 @@
 | 
			
		||||
/**
 | 
			
		||||
 * title: Password
 | 
			
		||||
 */
 | 
			
		||||
import { FormItem } from '@formily/antd';
 | 
			
		||||
import { Password, SchemaComponent, SchemaComponentProvider } from '@nocobase/client';
 | 
			
		||||
import React from 'react';
 | 
			
		||||
 | 
			
		||||
const schema = {
 | 
			
		||||
  type: 'object',
 | 
			
		||||
  properties: {
 | 
			
		||||
    input: {
 | 
			
		||||
      type: 'boolean',
 | 
			
		||||
      title: `Editable`,
 | 
			
		||||
      'x-decorator': 'FormItem',
 | 
			
		||||
      'x-component': 'Password',
 | 
			
		||||
      'x-reactions': {
 | 
			
		||||
        target: 'read',
 | 
			
		||||
        fulfill: {
 | 
			
		||||
          state: {
 | 
			
		||||
            value: '{{$self.value}}',
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    read: {
 | 
			
		||||
      type: 'boolean',
 | 
			
		||||
      title: `Read pretty`,
 | 
			
		||||
      'x-read-pretty': true,
 | 
			
		||||
      'x-decorator': 'FormItem',
 | 
			
		||||
      'x-component': 'Password',
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default () => {
 | 
			
		||||
  return (
 | 
			
		||||
    <SchemaComponentProvider components={{ Password, FormItem }}>
 | 
			
		||||
      <SchemaComponent schema={schema} />
 | 
			
		||||
    </SchemaComponentProvider>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
@ -0,0 +1,45 @@
 | 
			
		||||
/**
 | 
			
		||||
 * title: Check strength
 | 
			
		||||
 */
 | 
			
		||||
 import { FormItem } from '@formily/antd';
 | 
			
		||||
 import { Password, SchemaComponent, SchemaComponentProvider } from '@nocobase/client';
 | 
			
		||||
 import React from 'react';
 | 
			
		||||
 
 | 
			
		||||
 const schema = {
 | 
			
		||||
   type: 'object',
 | 
			
		||||
   properties: {
 | 
			
		||||
     input: {
 | 
			
		||||
       type: 'boolean',
 | 
			
		||||
       title: `Editable`,
 | 
			
		||||
       'x-decorator': 'FormItem',
 | 
			
		||||
       'x-component': 'Password',
 | 
			
		||||
       'x-component-props': {
 | 
			
		||||
        checkStrength: true,
 | 
			
		||||
       },
 | 
			
		||||
       'x-reactions': {
 | 
			
		||||
         target: 'read',
 | 
			
		||||
         fulfill: {
 | 
			
		||||
           state: {
 | 
			
		||||
             value: '{{$self.value}}',
 | 
			
		||||
           },
 | 
			
		||||
         },
 | 
			
		||||
       },
 | 
			
		||||
     },
 | 
			
		||||
     read: {
 | 
			
		||||
       type: 'boolean',
 | 
			
		||||
       title: `Read pretty`,
 | 
			
		||||
       'x-read-pretty': true,
 | 
			
		||||
       'x-decorator': 'FormItem',
 | 
			
		||||
       'x-component': 'Password',
 | 
			
		||||
     },
 | 
			
		||||
   },
 | 
			
		||||
 };
 | 
			
		||||
 
 | 
			
		||||
 export default () => {
 | 
			
		||||
   return (
 | 
			
		||||
     <SchemaComponentProvider components={{ Password, FormItem }}>
 | 
			
		||||
       <SchemaComponent schema={schema} />
 | 
			
		||||
     </SchemaComponentProvider>
 | 
			
		||||
   );
 | 
			
		||||
 };
 | 
			
		||||
 
 | 
			
		||||
@ -6,3 +6,19 @@ group:
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
# Password
 | 
			
		||||
 | 
			
		||||
## Examples
 | 
			
		||||
 | 
			
		||||
### Password
 | 
			
		||||
 | 
			
		||||
<code src="./demos/demo1.tsx" />
 | 
			
		||||
 | 
			
		||||
### Check strength
 | 
			
		||||
 | 
			
		||||
<code src="./demos/demo2.tsx" />
 | 
			
		||||
 | 
			
		||||
## API
 | 
			
		||||
 | 
			
		||||
基于 antd 的 Password,新增的属性:
 | 
			
		||||
 | 
			
		||||
- `checkStrength` 检测密码强度
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1 @@
 | 
			
		||||
export * from './Password';
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user