常用小功能代码片段

193 阅读5分钟

常用小功能代码片段

list 列表

useFetchList

const useFetchList = (fetchList, commonParams = null, initParams = null, handleResult) => {
  const [result, updateResult] = useAsync({});
  const update = useCallback((params) => updateResult({res: fetchList({...params, ...commonParams})}, handleResult), []);
  useEffect(() => {
    update({...initParams});
  }, []);
  const {res} = result;
  const isPending = !res || res.pending;

  return [{isPending, data: res?.result}, update];
};

使用:

const [list, updateList] = useFetchList(getRouter, {projectId: defProject._id});

可通过 updateList 更新列表。

useHandleList - 带分页、搜索的列表

list.png

参数示例:

  • fetchList:请求函数
  • commonParams:公共参数
  • initParams:默认参数
  • handleResult:返回结果处理(格式化等)。
const useHandleList = (fetchList, commonParams = null, initParams = null, handleResult) => {
  const {current, size, ...rest} = initParams || {};
  const search = useRef(rest || {});
  const page = useRef({current: current || 1, size: size || 10});
  const [result, update] = useFetchList(fetchList, commonParams, {...page.current, ...search.current}, handleResult);

  const pageChange = (current, size) => {
    page.current = {current, size};
    update({
      ...page.current,
      ...search.current,
    });
  };
  const searchList = (values) => {
    search.current = values;
    page.current = {...page.current, current: 1};
    update({...page.current, ...search.current});
  };

  return [result, (params) => update({...page.current, ...search.current, ...params}), pageChange, searchList];
};

使用:

const [result, update, pageChange, searchList] = useHandleList(allUser, null, {current: pageParams?.current, size: pageParams?.size});

const pagination = {
  onShowSizeChange: (current, size) => pageChange(current, size),
  onChange: (current, size) => pageChange(current, size),
  showSizeChanger: true,
  showQuickJumper: true,
  total: total || 1,
  current: current || 1,
  pageSize: size || 10,
  pageSizeOptions: ['10', '20', '30', '40'],
};

<SearchForm submit={searchList} loading={isPending} searchFormText={searchFormText} />;

这两个列表查询 hooks 都是基于 useAsync 封装的,便于列表查询。

useSearch

使用:

const [filterTree, setFilterTree] = useSearch(null);

const searchTree = (value) => setFilterTree(tree, value, 'name', 'path');

<Search placeholder="search..." allowClear enterButton style={{maxWidth: '240px', marginBottom: '12px'}} onSearch={searchTree} />;

useDebounce

防止频繁操作引起的性能问题。

如 search:

const [filterTree, setFilterTree] = useSearch(null);

const searchChange = useDebounce((e, data) => {
  const {value} = e.target;
  setFilterTree(data, value, 'name', 'path');
}, 500);

<Search placeholder="search..." allowClear enterButton style={{maxWidth: '240px', marginBottom: '12px'}} onChange={(e) => searchChange(e, data)} />;

使用:useDebounce(fn,delay),第二个参数 delay 设置时间间隔。

hooks

useStore

状态管理工具。

const [list,update,subscribe]=useStore('userList',{});

参数示例:

  • key:userList
  • 默认值:{}
  • list:value 值
  • update:更新函数
  • subscribe:监听函数
const Page1 = (props) => {
  const [list, update] = useStore('userList', []);
  const deleteUse = async (id) => {
    await fetchDel({id});
    update();
  };
};

const Page2 = (props) => {
  const [, , subscribe] = useStore('userList', []);
  useEffect(() => {
    subscribe((result) => {
      console.log(result);
    });
  }, []);
};

useRoute

将路由数据解耦出来。

const {path,name,router,store,...} = useRoute();

可在此监听路由跳转。

useEleResize

监听元素大小变化。

const {width,height} = useEleResize(ref, delay);

useEleResize有 2 个参数:

  • 监听元素:ref
  • 监听时间间隔(防抖):delay
const {width} = useEleResize(skeletonRef, 300);

<Panel title="skeleton" ref={skeletonRef}>
  panelWidth: {width}
</Panel>;

useClickAway

元素外面点击事件监听。

useClickAway(ref, fn);

  • 监听元素:ref
  • 事件操作:fn
const liRef = useRef();
useClickAway(liRef, (e) => li.open && itemClick(e, li));

<li ref={liRef}>...</li>;

useTime

当前时间。

const useTime = () => {
  const timeRef = useRef();
  const [time, setTime] = useState('');
  useEffect(() => {
    const getFormatTime = () => {
      timeRef.current = setInterval(() => setTime(formatTime()), 1000);
    };
    getFormatTime();
    return () => clearInterval(timeRef);
  }, []);

  return [time];
};

Mock 数据

fakeFetch

export const fakeFetch = async (params) => {
  await sleep();
  return {
    code: 200,
    message: 'success!',
    result: {},
  };
};

schema

const userSchema = {
  name: 'demo',
  email: 'demo@gmail.com',
  password: '123456',
  role: 2,
  token: uuidv4(),
  projectName: 'demo',
  projectId: 'demo-1',
  description: 'demo',
  active: 1,
  createtime: +new Date(),
  updatetime: +new Date(),
  creator: 'huy',
  updater: 'huy',
  avatar: 'https://pic2.zhimg.com/a2e68681a006bd3e60fd5b22d51cb629_im.jpg',
  github: '',
};

fakeList

export const fakeUsers = (schema, num = 10) =>
  [...Array(num)].map((item, index) => ({...schema, _id: uuidv4(), name: `${schema.name}-${index + 1}`, email: `${schema.name}${index + 1}@gmail.com`, role: randNum(5), active: randTrue()}));

export const users = fakeUsers(userSchema, 36);

数据持久化 - 增删改查

export const addUser = async (data) => {
  const res = await fakeFetch();
  const item = {...data, _id: uuidv4()};
  const users = store.getState('users');
  users.push(item);
  store.setState({users});
  return {
    ...res,
    result: item,
  };
};

export const allUser = async ({type, current, size, name, role}) => {
  const res = await fakeFetch();
  const users = store.getState('users');
  const index = size * (current - 1);
  let temp = [];
  if (type) {
    temp = users.filter((item) => item.type === type);
  } else {
    temp = [...users];
  }
  let list = [...temp];
  if (role) {
    list = list.filter((item) => item.role === role);
  }
  if (name) {
    const reg = new RegExp(name, 'gi');
    list = list.filter((item) => item.name.toString().match(reg));
  }
  list = list.slice(index, index + size);
  return {
    ...res,
    result: {
      current,
      size,
      total: temp.length,
      list,
    },
  };
};

通过 store.getState()store.setState() 来获取和修改数据。

路由

路由钩子 - beforeRender

在路由渲染前做一些处理。

合法性校验

if (validPath === initPath) {
  return next({path: '/'});
}

鉴权

if (validPath === initPath) {
  return next({path: '/'});
}

页面离开时提示

if (path !== prevPath && demoBackReg.test(prevPath)) {
  return confirmDesignPage(next);
}

记录页面停留时间

routerListenFn(path, prevPath);

页面刷新保留参数

// list.jsx

router.push({
  path: `./auth/${item._id}`,
  state: {item, backState: {path: props.path, params: {current, size}}},
});
  • path:路由地址
  • state:参数,可刷新保存

获取:

// add.jsx

const {getState} = props.history;

const {item, backState} = getState() || {};

页面刷新后,参数保存。

返回时保存跳转前的状态

如:列表页码、搜索条件、树节点选中等状态。

const {getState} = props.history;
const {item, backState} = getState() || {};

const back = () => {
  backState ? props.router.push(backState) : props.history.back();
};

获取状态:

const pageParams = props.params;
const [result, update, pageChange, searchList] = useHandleList(allUser, null, {current: pageParams?.current, size: pageParams?.size});

goBack 组件

back.png

const Index = ({back, actions = []}) => {
  const {store} = useRoute();
  const i18ns = store.getState('i18ns');
  const i18nCfg = i18ns?.main.components ?? {};
  return (
    <Panel>
      <Button onClick={(e) => (typeof back === 'function' ? back() : history.back())} type="link" size="small" icon={fixIcons('LeftOutlined')}>
        {i18nCfg.back}
      </Button>
      {actions.map(({text, icon, ...rest}) => (
        <Button key={text} size="small" {...rest} icon={fixIcons(icon)}>
          {text}
        </Button>
      ))}
    </Panel>
  );
};

components

components.png

Spinner

<Spinner global />

HandleError

<HandleError>
  <ErrorComp state={demoError} name="eb" />
</HandleError>

Ellipsis

当文本超过宽度时出现省略号并有 tooltips。

<div style={{width: 200}}>
  <Ellipsis>12233345657688967i8ijhfgrtrrfgthtgryhhyt</Ellipsis>
</div>

portal

const Index = ({children, mountNode = document.body}) => createPortal(children, mountNode);

mask

const Mask = ({open, close, delay = 300, children, mountNode}) => {
  const [delayOpen] = useDelayDestroy(open, delay);
  return (
    <Portal mountNode={mountNode}>
      <div>
        {delayOpen ? (
          <div style={wrapper}>
            <div style={mask} onClick={close} />
            <div style={container}>{children}</div>
          </div>
        ) : null}
      </div>
    </Portal>
  );
};

modal

modal.png

const Modal = ({open, cancel, submit, title = 'Modal 弹窗', className, children, delay = 0}) => {
  const cls = ['modal-wrap', open ? 'open' : '', className].filter(Boolean).join(' ');
  return (
    <Mask open={open} close={cancel} delay={delay}>
      <div className={cls}>
        <div className="modal-container">
          <div className="modal-header">{title}</div>
          <div className="modal-content">{children}</div>
          <div className="modal-footer">
            <div className="btn left" onClick={(e) => cancel?.()}>
              取消
            </div>
            <div className="btn right" onClick={(e) => submit?.()}>
              确定
            </div>
          </div>
        </div>
      </div>
    </Mask>
  );
};

fullScreen - 全屏

const Index = ({panel, fullIcon = defaultIcon, exitIcon = defaultIcon}) => {
  const container = panel?.current ?? panel;
  const [isFull, setIsFull] = useState();
  useEffect(() => {
    const destroy = watchScreen(() => {
      setIsFull((prev) => !prev);
    });
    return () => destroy();
  }, []);
  const Icon = isFull ? exitIcon : fullIcon;
  return <Icon onClick={(e) => fullScreen(container)} />;
};

maxSize - 最大化

最大化,可将目标元素最大化到指定节点。

const Index = ({panel, target, fullIcon = defaultIcon, exitIcon = defaultIcon}) => {
  const [isMax, setIsMax] = useState();
  const panelRef = useRef();
  const targetRef = useRef();
  useEffect(() => {
    const getTarget = typeof target === 'function' ? target : () => document.getElementsByClassName(target)[0];
    targetRef.current = getStyles(getPosition(getTarget()));
    panelRef.current = {
      ...maxStyle,
      ...getStyles(getPosition(panel.current)),
    };
  }, []);
  const handle = useCallback((isMax) => {
    if (isMax) {
      setStyles(panel.current, panelRef.current);
      setTimeout(() => {
        setStyles(panel.current, targetRef.current);
      }, 0);
    } else {
      resetStyles(panel.current, panelRef.current);
    }
    setIsMax(isMax);
  }, []);
  const Icon = isMax ? exitIcon : fullIcon;
  return <Icon onClick={(e) => handle(!isMax)} />;
};

utils

utils.png

日期格式化

export const getTime = (day = new Date()) => {
  const date = new Date(day);
  const y = date.getFullYear();
  const w = date.getDay();
  const m = date.getMonth() + 1;
  const d = date.getDate();
  const h = date.getHours();
  const M = date.getMinutes();
  const s = date.getSeconds();
  return [y, m, d, h, M, s, w];
};

时间间隔

export const getMonthDays = (day = new Date()) => {
  const date = getTime(day);
  return new Date(date[0], date[1], 0).getDate();
};

export const timeBase = (date) => [12, getMonthDays(date), 24, 60, 60];

export const timeUnit = ['年', '月', '日', '时', '分', '秒'];

export const minus = (start, end, base) => {
  let carry = false;
  const gap = [];
  end.map((v, i) => {
    const endValue = carry ? v - 1 : v;
    const diff = endValue - start[i];
    if (diff < 0) {
      gap[i] = diff + (base[i] || 10);
      carry = true;
    } else {
      gap[i] = diff;
      carry = false;
    }
  });
  return gap.reverse();
};

export const timeInterval = (start, end = new Date()) => {
  if (new Date(start) - new Date(end) > 0) {
    start = [end, (end = start)][0];
  }
  const base = timeBase(end).reverse();
  const sDate = getTime(start).slice(0, -1).reverse();
  const eDate = getTime(end).slice(0, -1).reverse();
  return minus(sDate, eDate, base);
};

export const timeGap = (start, end = new Date()) => {
  const gap = timeInterval(start, end);
  const index = gap.findIndex((v) => v > 0);
  const unitTime = gap.map((v, i) => `${v || 0}${timeUnit[i]}`);
  return unitTime.slice(index).join('');
};

进制转换

export const base2Ten = (num, base = 2) => parseInt(String(num), base);

export const ten2Base = (num, base = 2) => Number(num).toString(base);

色值转换

export const rgba2hex = (r = 0, g = 0, b = 0, a = 1) => {
  const hex = `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`;
  if (a === 1) {
    return hex;
  }
  a = Math.round(a * 255).toString(16);
  a = a.length === 1 ? `0${a}` : a.length === 3 ? 'ff' : a;
  return `${hex}${a}`;
};

export const hex2rgba = (hex) => {
  hex = hex.replace('#', '');
  const len = hex.length;
  if (len === 3) {
    hex = `${hex[0]}${hex[0]}${hex[1]}${hex[1]}${hex[2]}${hex[2]}`;
  } else if (len === 4) {
    hex = `${hex[0]}${hex[0]}${hex[1]}${hex[1]}${hex[2]}${hex[2]}${hex[3]}${hex[3]}`;
  }
  const r = parseInt(hex.slice(0, 2), 16) || 0;
  const g = parseInt(hex.slice(2, 4), 16) || 0;
  const b = parseInt(hex.slice(4, 6), 16) || 0;
  if (hex.length === 6) {
    return `rgb(${r},${g},${b})`;
  }
  const a = parseInt(hex.slice(6, 8), 16) / 255 || 0;
  return `rgba(${r},${g},${b},${a})`;
};

scroolTo - 滚动

export const scrollToTop = (top = 0) => window.scrollTo({top, behavior: 'smooth'});

export const scrollToAnchor = (ref = document.body) => ref?.scrollIntoView?.({behavior: 'smooth', block: 'center'});

export const scrollTop = () => document.documentElement.scrollTop || window.pageYOffset || document.body.scrollTop;

arr2Tree

export const arr2TreeById = (data, idKey = 'id', childKey = 'children', treeRoot = -1) =>
  arr2Tree((item) => {
    const id = item[idKey];
    const parentArr = id.match(/\S+(?=-\S+)/);
    return parentArr?.[0] || treeRoot;
  })(data, idKey, childKey, treeRoot);

export const arr2TreeByPath = (data, idKey = 'path', childKey = 'children', treeRoot = null) =>
  arr2Tree((item) => {
    const id = item[idKey];
    const hasSub = id.match(/.*\/[^:/]+\/:[^/]+/);
    if (hasSub) {
      return hasSub[0].match(/(.*)\/:+/)?.[1] ?? treeRoot;
    } else {
      return id.match(/(.*)\/+/)?.[1] ?? treeRoot;
    }
  })(data, idKey, childKey, treeRoot);

record

记录数据,可回退、重做、跳转等。

const {record, undo, redo, clean} = cacheData();

useEffect(() => {
  setCurrentItem(record(''));
  return () => clean();
}, []);

const prev = () => {
  const item = undo();
  setCurrentItem(item);
  setColorValue(item.data);
};
const next = () => {
  const item = redo();
  setCurrentItem(item);
  setColorValue(item.data);
};

copyToClipboard

拷贝到粘贴板。

const copyToClipboard = (text) => {
  const copyText = document.createElement('textarea');
  copyText.value = text;
  copyText.style.position = 'fixed';
  copyText.style.left = '-9999px';
  document.body.appendChild(copyText);
  copyText.select();
  copyText.setSelectionRange(0, -1);
  try {
    document.execCommand('copy');
  } catch (err) {
    console.error(err);
  }
  document.body.removeChild(copyText);
};

watermark

watermark.png

添加水印。

const watermark = ({
  container = document.body,
  width = '220px',
  height = '200px',
  textAlign = 'center',
  textBaseline = 'middle',
  font = '20px microsoft yahei',
  fillStyle = 'rgba(202,202,202,0.4)',
  content = '请勿外传',
  rotate = '-30',
  zIndex = 1000,
} = {}) => {
  container = container?.current ?? container;
  const oldCanvas = container.firstChild;
  if (oldCanvas?.className === 'watermark-canvas') {
    container.removeChild(oldCanvas);
  }
  const canvas = document.createElement('canvas');
  canvas.setAttribute('width', width);
  canvas.setAttribute('height', height);
  const ctx = canvas.getContext('2d');

  ctx.textAlign = textAlign;
  ctx.textBaseline = textBaseline;
  ctx.font = font;
  ctx.fillStyle = fillStyle;
  ctx.rotate((Math.PI / 180) * rotate);
  ctx.fillText(content, parseFloat(width) / 2, parseFloat(height) / 2);

  const base64Url = canvas.toDataURL();
  const watermarkDiv = document.createElement('div');
  watermarkDiv.setAttribute(
    'style',
    `
    position:absolute;
    top:0;
    left:0;
    width:100%;
    height:100%;
    z-index:${zIndex};
    pointer-events:none;
    background-repeat:repeat;
    background-image:url('${base64Url}')`,
  );

  container.style.position = 'relative';
  container.insertBefore(watermarkDiv, container.firstChild);
};

md2html

md2html.png

文件读取

const listFiles = async () =>
  await require
    .context('@app/doc', true, /^\.\/(.+)\.md$/)
    .keys()
    .map((name) => ({name: name.replace(/^\.\/(.+)\.md$/, '$1')}));

const getFileMenu = (list) => {
  const newArr = [];
  list.map((item) => {
    const fileList = item.name.split('/');
    const folder = fileList[0];
    const name = fileList[1];
    const hasFolder = newArr.find((item) => item.name === folder);
    if (!hasFolder) {
      newArr.push({
        name: folder,
        children: [{name, folder}],
      });
    } else {
      hasFolder.children.push({name, folder});
    }
  });
  return newArr;
};

export default async () => getFileMenu(await listFiles());

高亮渲染

const Index = ({item}) => {
  const [context, setContext] = useState('');
  useEffect(() => {
    const getMd = async () => {
      try {
        const context = await getContext({...item, type: '.md'});
        const newContext = await replacePath(context, item);
        setContext(marked(newContext));
      } catch (err) {
        setContext(err?.message);
      }
    };
    getMd();
  }, []);

  return (
    <div className="content">
      {str2React(context)}
      {!context && <Spinner global />}
    </div>
  );
};

锚点跟随

const Anchor = ({curName, itemList}) => {
  const timer = useRef(0);
  const isScrolling = useRef(false);
  const currentName = useRef('');
  const [name, setName] = useState(curName);
  useEffect(() => {
    itemList.current = validObj(itemList.current);
    if (!curName) {
      const items = Object.keys(itemList.current).map((key) => ({name: key, offsetTop: itemList.current[key]?.offsetTop ?? 0}));
      setName(items[0]?.name);
    }
    if (currentName.current !== curName) {
      currentName.current = curName;
      const offsetTop = itemList.current[curName]?.offsetTop ?? 0;
      isScrolling.current = true;
      scrollToTop(offsetTop);
      timer.current = setTimeout(() => (isScrolling.current = false), 500);
    }
    return () => {
      clearTimeout(timer.current);
    };
  }, [curName]);
  useEffect(() => {
    const scrollToAnchor = throttle(() => {
      if (!isScrolling.current) {
        const offsetTops = sort(
          Object.keys(itemList.current).map((key) => ({name: key, offsetTop: itemList.current[key]?.offsetTop ?? 0})),
          'offsetTop',
          true,
        );
        const name = offsetTops.find((item) => item.offsetTop < scrollTop())?.name;
        if (currentName.current !== name) {
          currentName.current = name;
          setName(name);
        }
      }
    });
    window.addEventListener('scroll', scrollToAnchor, false);
    return () => window.removeEventListener('scroll', scrollToAnchor, false);
  }, []);

  return [name];
};

styles

dropdown

@keyframes animate-top-in {
  0% {
    opacity: 0;
    transform: translate3d(0, -30px, 0);
  }
  100% {
    opacity: 0.98;
    transform: translate3d(0, 2px, 0);
  }
}

.demo-styles {
  display: flex;
  align-items: center;
  justify-content: center;
  > li {
    position: relative;
    > a {
    }
    > ul {
      display: none;
    }
    &.open {
      ul {
        display: block;
        animation: animate-top-in 0.2s forwards;
      }
    }
  }
}

动效 icon

动效 icon

icons

icons.png

[class^='ico-'],
[class*=' ico-'] {
  font-size: 1.8rem;
  speak: none;
  font-style: normal;
  font-weight: normal;
  font-variant: normal;
  text-transform: none;
  line-height: 1;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}
.ico-plus:before {
  content: '\002B';
}
.ico-plus-circle:before {
  content: '\2A01';
}

tooltils

.demo-tooltip { .tooltip-lb(); }

<a className="demo-tooltip" tooltips="{item.name}"> {item.name} </a>

arrow

.demo-arrow-lt {
  .arrow-lt(var(--navBgColor),var(--borderColor),8px,16px);
  right: auto;
  left: 0;
}
.demo-arrow-rt {
  .arrow-rt(var(--navBgColor),var(--borderColor),8px,16px);
  right: 0;
  left: auto;
}

angle

.demo-angle-top {
  .angle-top(5px,1px);
}
.demo-angle-bt {
  .angle-bt(5px,1px);
}

follow

.demo-follow {
  .follow(3px,currentColor,10px);
}

bagua.png

完整代码见github