ReactNeedAttention

类组件各个部分的功能

// 方法的箭头函数写法(1)
class Index extends React.Component {
  constructor(...arg) {
    /* 执行 react 底层 Component 函数 */
    super(...arg);
  }
  state = {}; /* state */
  static number = 1; /* 内置静态属性 */
  handleClick = () =>
    console.log(111); /* 方法: 箭头函数方法直接绑定在this实例上 */
  /* 生命周期 */
  componentDidMount() {
    console.log(Index.number, Index.number1); // 打印 1 , 2
  }
  /* 渲染函数 */
  render() {
    return (
      // 这里原文是单引号,复制过来md文档的js代码块直接没识别出来
      <div onClick={this.handleClick()} style={{ marginTop: "10px" }}>
        hello,React!
      </div>
    );
  }
}

Index.number1 = 2; /* 外置静态属性 */
/* 方法: 绑定在 Index 原型链的 方法*/
Index.prototype.handleClick = () => {
  console.log(222);
};
// 类组件方法的另一种写法 组件方法(2)
class Index extends React.Component {
  constructor(props) {
    /* 执行 react 底层 Component 函数 */
    super(props);
    this.state = {
      originValue: 1,
    };
    // 注意这里的函数需要手动绑定在构造器上
    this.handleClick = this.handleClick.bind(this);
  }
  static number = 1; /* 内置静态属性 */
  handleClick() {
    console.log(111);
  }
  /* 生命周期 */
  componentDidMount() {
    console.log(Index.number, Index.number1); // 打印 1 , 2
  }
  /* 渲染函数 */
  render() {
    return (
      // 这里原文是单引号,复制过来md文档的js代码块直接没识别出来
      <div onClick={this.handleClick()} style={{ marginTop: "10px" }}>
        hello,React!
      </div>
    );
  }
}

问:上述绑定了两个 handleClick ,那么点击 div 之后会打印什么呢?

答:结果是 111 。因为在 class 类内部,箭头函数是直接绑定在实例对象上的,而第二个 handleClick 是绑定在 prototype 原型链上的,它们的优先级是:
实例对象上方法属性 > 原型链对象上方法属性

生命周期详解

https://juejin.cn/post/7046218136177082398?searchId=2023091817010725CC4F0E8FE9A5056D74

只有类组件才具有生命周期方法,函数组件是没有的
挂载阶段,依次调用:

constructor(); // class 的构造方法,在其中 super(props) 接收参数
componentWillMount(); // 组件挂载前调用,实际比较少用到
render(); // 组件中定义的方法,返回一个 React 元素,并不负责实际的渲染工作
componentDidMount(); // 组件被挂载到 DOM 后调用,比如向后端请求一些数据,此时调用 setState 会引起组件的重新渲染

更新阶段,组件 props 或者 state 变化,依次调用:

componentWillReceiveProps(nextProps); // props 变化时调用,nextProps 是新参数
shouldComponentUpdate(nextProps, nextState); // 是否继续执行更新过程,返回一个布尔值
// 通过比较新旧值,如果新旧值相同,该方法会返回 false,后续的更新过程将不再继续,从而优化性能
componentWillUpdate(nextProps, nextState); // 更新之前,比较少用到
render();
componentDidUpdate(prevProps, prevState); // 组件更新之后调用,可以操作更新之后的 DOM 了

卸载阶段:

componentWillUnmount() { } // 组件被删除前调用,执行一些清理工作

类组件事件中的 this 指向问题

  1. 箭头函数
    这种写法,每次 render 调用时都会重新创建一个新的事件处理函数。
class MyComp extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      time: new Date(),
    };
  }
  handleClick() {
    console.log(this.state.time);
  }
  render() {
    return <button onClick={() => this.handleClick()}>按钮</button>;
  }
}
// this 指向当前组件的实例对象
  1. 组件方法
    render 调用时不会重新创建一个新的事件处理函数,但需要在构造函数中手动绑定 this。

还有一种选择是,我们可以在为元素事件属性赋值的同时绑定 this

class MyComp extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      time: new Date(),
    };
    this.handleClick = this.handleClick.bind(this);
    // 通过 bind 将这个方法绑定到当前组件实例
  }
  handleClick() {
    console.log(this.state.time);
  }
  render() {
    return <button onClick={this.handleClick}>按钮</button>;
  }
}
  1. 属性初始化语法(ES7)
    使用官方脚手架 Create React App 创建的项目默认是支持这个特性的,可以在项目中引入 babel 的 transform-class-properties 插件获取这个特性支持。
class MyComp extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      time: new Date(),
    };
  }
  handleClick = () => {
    console.log(this.state.time);
  }; // 也是箭头函数
  render() {
    return <button onClick={this.handleClick}>按钮</button>;
  }
}

函数组件

不要尝试给函数组件 prototype 绑定属性或方法,即使绑定了也没有任何作用,因为通过上面源码中 React 对函数组件的调用,是采用直接执行函数的方式,而不是通过 new 的方式。

函数组件和类组件本质的区别是什么呢?

对于类组件来说,底层只需要实例化一次,实例中保存了组件的 state 等状态。对于每一次更新只需要调用 render 方法以及对应的生命周期就可以了。但是在函数组件中,每一次更新都是一次新的函数执行,一次函数组件的更新,里面的变量会重新声明

为了能让函数组件可以保存一些状态,执行一些副作用钩子,React Hooks 应运而生,它可以帮助记录 React 中组件的状态,处理一些额外的副作用。

通信方式

React 一共有 5 种主流的通信方式:

  1. props 和 callback 方式
    这种常用父子间通讯
  2. ref 方式。
  3. React-redux 或 React-mobx 状态管理方式。
  4. context 上下文方式。
  5. event bus 事件总线。
    事件总线使用需要手动绑定与解绑,而且复杂工程不方便维护。由于事件总线使用的机会比较小,这里做个示例
import { BusService } from "./eventBus";
/* event Bus  */
function Son() {
  const [fatherSay, setFatherSay] = useState("");
  React.useEffect(() => {
    BusService.on("fatherSay", (value) => {
      /* 事件绑定 , 给父组件绑定事件 */
      setFatherSay(value);
    });
    return function () {
      BusService.off("fatherSay"); /* 解绑事件 */
    };
  }, []);

  const sonAction = (e) => {
    BusService.emit("childSay", e.target.value);
  };

  return (
    <div className="son">
      我是子组件
      <div> 父组件对我说:{fatherSay} </div>
      <input placeholder="我对父组件说" onChange={sonAction} />
    </div>
  );
}
/* 父组件 */
function Father() {
  const [childSay, setChildSay] = useState("");
  React.useEffect(() => {
    /* 事件绑定 , 给子组件绑定事件 */
    BusService.on("childSay", (value) => {
      setChildSay(value);
    });
    return function () {
      BusService.off("childSay"); /* 解绑事件 */
    };
  }, []);

  const fatherAction = (e) => BusService.emit("fatherSay", e.target.value);

  return (
    <div className="box father">
      我是父组件
      <div> 子组件对我说:{childSay} </div>
      <input placeholder="我对子组件说" onChange={fatherAction} />
      <Son />
    </div>
  );
}

类组件的继承

/* 人类 */
class Person extends React.Component {
  constructor(props) {
    super(props);
    console.log("hello , i am person");
  }
  componentDidMount() {
    console.log(1111);
  }
  eat() {
    /* 吃饭 */
  }
  sleep() {
    /* 睡觉 */
  }
  ddd() {
    console.log("打豆豆"); /* 打豆豆 */
  }
  render() {
    return <div>大家好,我是一个person</div>;
  }
}
/* 程序员 */
class Programmer extends Person {
  constructor(props) {
    super(props);
    console.log("hello , i am Programmer too");
  }
  componentDidMount() {
    console.log(this);
  }
  code() {
    /* 敲代码 */
  }
  render() {
    return (
      <div style={{ marginTop: "50px" }}>
        {super.render()} {/* 让 Person 中的 render 执行 */}
        我还是一个程序员! {/* 添加自己的内容 */}
      </div>
    );
  }
}
export default Programmer;

HOC 高阶组件

一文吃透 react 高阶组件

按照 hoc 主要功能,强化 props控制渲染赋能组件三个方向对 HOC 编写做了一个详细介绍,和应用场景的介绍,目的让大家在理解高阶组件的时候,更明白什么时候会用到?,怎么样去写?里面涵盖的知识点我总一个总结。

正向代理(不需要知道被代理的子组件业务是什么,直接混入需要的 state)
反向继承(与子组件强耦合,需要知晓子组件业务,且有副作用串联的隐患)

属性代理

强化 props & 抽离 state。
条件渲染,控制渲染,分片渲染,懒加载。
劫持事件和生命周期
ref 控制组件实例
添加事件监听器,日志

反向代理

劫持渲染,操纵渲染树
控制/替换生命周期,直接获取组件状态,绑定事件。

使用

项目中 nuomi 的逆天写法原来是装饰器的平面展开

对于有class声明的有状态组件,可以使用 ES6 的新语法装饰器。

@withStyles(styles)
@withRouter
@keepaliveLifeCycle
class Index extends React.Componen {
  /* ... */
}

注意一下包装顺序,越靠近 Index 组件的,就是越内层的 HOC,离组件 Index 也就越近。类似鸡蛋壳、鸡蛋清、鸡蛋黄的关系。

无状态组件(函数声明)

function Index() {
  /* .... */
}
export default withStyles(styles)(withRouter(keepaliveLifeCycle(Index)));

模型

对于不需要传递参数的 HOC,我们编写模型我们只需要嵌套一层

function withRouter() {
  return class wrapComponent extends React.Component {
    /* 编写逻辑 */
  };
}

对于需要参数的 HOC,我们需要一层代理,如下:

function connect(mapStateToProps) {
  /* 接受第一个参数 */
  return function connectAdvance(wrapCompoent) {
    /* 接受组件 */
    return class WrapComponent extends React.Component {};
  };
}

实例:跨层级捕获 ref

/**
 *
 * @param {*} Component 原始组件
 * @param {*} isRef  是否开启ref模式
 */
function HOC(Component, isRef) {
  class Wrap extends React.Component {
    render() {
      const { forwardedRef, ...otherprops } = this.props;
      return <Component ref={forwardedRef} {...otherprops} />;
    }
  }
  if (isRef) {
    return React.forwardRef((props, ref) => (
      <Wrap forwardedRef={ref} {...props} />
    ));
  }
  return Wrap;
}

class Index extends React.Component {
  componentDidMount() {
    console.log(666);
  }
  render() {
    return <div>hello,world</div>;
  }
}

const HocIndex = HOC(Index, true);

export default () => {
  const node = useRef(null);
  useEffect(() => {
    /* 就可以跨层级,捕获到 Index 组件的实例了 */
    console.log(node.current.componentDidMount);
  }, []);
  return (
    <div>
      <HocIndex ref={node} />
    </div>
  );
};

实例:分片渲染

const renderQueue = [];
let isFirstrender = false;

const tryRender = () => {
  const render = renderQueue.shift();
  if (!render) return;
  setTimeout(() => {
    render(); /* 执行下一段渲染 */
  }, 300);
};
/* HOC */
function renderHOC(WrapComponent) {
  return function Index(props) {
    const [isRender, setRender] = useState(false);
    useEffect(() => {
      renderQueue.push(() => {
        /* 放入待渲染队列中 */
        setRender(true);
      });
      if (!isFirstrender) {
        tryRender(); /**/
        isFirstrender = true;
      }
    }, []);
    return isRender ? (
      <WrapComponent tryRender={tryRender} {...props} />
    ) : (
      <div className="box">
        <div className="icon">
          <SyncOutlined spin />
        </div>
      </div>
    );
  };
}
/* 业务组件 */
class Index extends React.Component {
  componentDidMount() {
    const { name, tryRender } = this.props;
    /* 上一部分渲染完毕,进行下一部分渲染 */
    tryRender();
    console.log(name + "渲染");
  }
  render() {
    return (
      <div>
        <img src="https://ss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=294206908,2427609994&amp;fm=26&amp;gp=0.jpg" />
      </div>
    );
  }
}
/* 高阶组件包裹 */
const Item = renderHOC(Index);

export default () => {
  return (
    <React.Fragment>
      <Item name="组件一" />
      <Item name="组件二" />
      <Item name="组件三" />
    </React.Fragment>
  );
};

继承时的 constructor 写法特别

  constructor(...args) {
    super(...args);
    // 直接用super.props 会报错说找不到
    const {props} = args;
    this.props = {...props};
    // 将继承的父亲state合并过来
    this.state = {
        ...super.state
    }
    }

组件的构造函数签名是 constructor (props, context) {}。调用 super(props)的话,相当于给 context 传了 undefined。而在 react-redux 里面有这么一行,就报错了
this.store = props[storeKey] || context[storeKey]

另外,继承 connect 组件不调用 constructor 是正常现象。在 react-redux 中, connect 是 HOC 组件,子组件所调用的 super(props)构造函数,仅仅是进行了组件环境的初始化,事件订阅等等, WrappedComponent 在 connect 组件 render 的时候才会登场。

相比之下,taro 微信端的 redux ,实现 connect 组件的原理是继承,所以调用 constructor 的时候同样也会调用父组件的 constructor 。

https://github.com/NervJS/taro/issues/1365

关于 state 是异步还是同步

首先了解一下batchUpdate(批量更新)的概念
https://zhuanlan.zhihu.com/p/78516581

  1. 在 react 的 event handler 内部同步的多次 setState 会被 batch 为一次更新
    使用promisesetTimeout可以打破这种更新

  2. 在一个异步的事件循环里面多次 setState,react 不会 batch

  3. 可以使用 ReactDOM.unstable_batchedUpdates 来强制 batch
    使用该函数可以手动更新
    在实际工作中,unstable_batchedUpdates 可以用于 Ajax 数据交互之后,合并多次 setState,或者是多次 useState 。原因很简单,所有的数据交互都是在异步环境下,如果没有批量更新处理,一次数据交互多次改变 state 会促使视图多次渲染。

fn3 = () => {
  // 模拟一个异步操作,真实业务里面可能是网络请求等
  setTimeout(
    unstable_batchedUpdates(() => {
      this.setState({ a: Math.random() });
      this.setState({ a: Math.random() });
    }),
    0
  );
};

React 同一级别更新优先级关系是:

flushSync 中的 setState > 正常执行上下文中 setState > setTimeout ,Promise 中的 setState。

关于 react 的样式

内联

普通的 CSS className='box'

css modules className={styles.box}

这篇作者有详细的介绍,还有原子类 CSS 解决方案的探索(在下一篇)
https://juejin.cn/post/6844903633109139464#heading-11

使用 map 来改进模板代码

在有多种匹配 获取当前值的情况

// Map初始化 键-键值
const carryOverMapEnglish2Chinese = new Map([
  ["bussinessVoucher", "业务凭证"],
  ["computedOrCarryOverTax", "计提/结转税金"],
  ["otherCarryOver", "其他结转"],
  ["carryOverProfitAndLoss", "结转损益"],
]);

// 根据英文返回中文 localStorage中匹配键值对
export function getChineseFromMap(englishWord) {
  const isHave = carryOverMapEnglish2Chinese.has(englishWord);
  if (isHave) {
    return carryOverMapEnglish2Chinese.get(englishWord);
  }
  return "";
}

// 根据中文返回英文 注意这里是遍历查找
export function getEnglishFromMap(chineseWord) {
  let exactEnglishWord;
  // forEach 值在前 遍历寻找匹配的键名
  carryOverMapEnglish2Chinese.forEach((value, key) => {
    if (value) {
      const isSuit = value.includes(chineseWord);
      if (isSuit) {
        exactEnglishWord = key;
      }
    }
  });
  return exactEnglishWord;
}

export const localPrefix = "carryOverBtnState-";

// 根据当前传入的种类判断 是否已点击过 展开OR收起
export function getShowCollapseCards(target = "计提/结转税金") {
  const englishWord = getEnglishFromMap(target);

  // 策略容器
  const stratagy = {};

  // 为每种添加一个key,并赋值为一个计算对应状态的方法
  carryOverMapEnglish2Chinese.forEach((_, key) => {
    // 根据中文 转成 英文 并且存放在本地
    stratagy[key] = () => {
      // 初始本地无值 全部展开
      const curState = localStorage.getItem(`${localPrefix}${englishWord}`);
      // 0 展开 1 折叠 不存在的情况算作展开
      const isCollapsed = curState && +curState === 1;
      return isCollapsed;
    };
  });

  return stratagy[englishWord]();
}

React 父子通信

父组件

import { Button } from "antd";
import React, { useRef } from "react";
import ListenFatherChild from "./ListenFatherChild";

const FatherTalkToChildInReact = () => {
  const talkToChild = () => {
    ref.current.updateCount();
  };
  const ref = useRef({ count: 100 });

  return (
    <>
      <Button onClick={talkToChild}>点击调用子组件方法</Button>
      <ListenFatherChild ref={ref} />
    </>
  );
};

export default FatherTalkToChildInReact;

子组件

import { Button } from "antd";
import React from "react";
import { useState, forwardRef, useImperativeHandle } from "react";

const ListenFatherChild = (_, ref) => {
  // 初始化值为0 父组件对子组件说话后增加
  const [count, setCount] = useState(0);
  const updateCount = () => {
    setCount(count + 1);
  };

  useImperativeHandle(ref, () => ({ updateCount }));

  return (
    <span>
      我是子组件, count:{count}
      <br />
      <Button onClick={updateCount}>子组件中增加count</Button>
    </span>
  );
};

export default forwardRef(ListenFatherChild);

antd 列表操作 hover 行才显示

注意这里要是 i 标签

    .ant-table-row {
      i {
        visibility: hidden;
      }
      &:hover {
        i {
          visibility: visible;
        }
      }
    }
    ```

useLayoutEffect 与 useEffect

useEffect 执行顺序 组件更新挂载完成 -> 浏览器 dom 绘制完成 -> 执行 useEffect 回调 。

useLayoutEffect 执行顺序 组件更新挂载完成 -> 执行 useLayoutEffect 回调-> 浏览器 dom 绘制完成

注意这里useLayoutEffect的执行顺序是在渲染前,因此其内代码可能会阻塞浏览器的绘制。如果我们在useEffect 重新请求数据,渲染视图过程中,肯定会造成画面闪动的效果,而如果用useLayoutEffect ,回调函数的代码就会阻塞浏览器绘制,所以可定会引起画面卡顿等效果
react-hooks 如何使用

useEffect 如何模拟生命周期

第二个参数传递一个空数组, 模拟 componentDidMount
第二个参数传递依赖项,模拟 componentDidUpdate
第二个参数传递一个空数组,并且里面通过 return 的形式去调用一个方法,模拟 componentWillUnmount

useLayoutEffect 官方定义

其函数签名与  useEffect  相同,但它会在所有的 DOM 变更之后同步调用 effect。可以使用它来读取 DOM 布局并同步触发重渲染。在浏览器执行绘制之前,useLayoutEffect  内部的更新计划将被同步刷新
useLayoutEffect 与 componentDidMount 是同步执行的。在 commit 阶段之前执行,执行顺序优于 useEffect

作者:碎银几两 Fiber
链接:https://juejin.cn/post/7081103851884904484
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

年度时间选择器

YearTimePicker

import React, { useEffect, useState } from "react";
import { DatePicker } from "antd";

/**
 *
 * @param value 初始化进来的时间
 * @param onTimeChange 获取当前选择的时间
 * 参考 https://juejin.cn/post/6844903859987415047#heading-2
 */
const YearTimePicker = ({ value, onTimeChange }) => {
  const [isOpen, setIsOpen] = useState(false);
  // 因为datePicker接受的数值是对象
  const [time, setTime] = useState(value ?? null);

  // 当时间更新 同步更新
  useEffect(() => {
    setTime(value);
  }, [value]);

  const onOpenChange = (status) => {
    setIsOpen(status);
  };

  const onPanelChange = (v) => {
    setTime(v);
    onTimeChange(v);
    setIsOpen(false);
  };

  return (
    <DatePicker
      value={time}
      open={isOpen}
      mode="year"
      placeholder="请选择年份"
      format="YYYY"
      allowClear={false}
      onOpenChange={onOpenChange}
      onPanelChange={onPanelChange}
    />
  );
};

export default YearTimePicker;

分割的日期选择器

功能函数 utils

// 根据当前年月的字符串2023-08 生成可传入antd 年份选择器的时间
export const getMomentTimeByString = (stringTime) => {
  if (stringTime?.length) {
    const [year, month] = stringTime.split("-");
    return moment([year, month - 1]); // moment的月份范围为0-11
  }
  return moment(new Date());
};

// 根据当前年月的字符串2023-08-01 生成可传入antd 年份选择器的时间
export const getMomentTimeByStringWithLine = (stringTime) => {
  if (stringTime?.length) {
    const [year, month, date] = stringTime.split("-");
    return moment([year, month - 1, date]); // moment的月份范围为0-11
  }
  return moment(new Date());
};

// 根据单个月份2023-08 返回字符串时间20230801 20230831
export const getStartAndEndStringTimeByLineString = (stringTime) => {
  const momentTime = getMomentTimeByString(stringTime);
  const dateFrom = momentTime.startOf("month").format(`YYYYMMDD`);
  const dateTo = momentTime.endOf("month").format("YYYYMMDD");
  return { dateFrom, dateTo };
};

// 根据单个月份2023-08 返回字符串moment时间20230801 20230831
export const getStartAndEndMomentTimeByLineString = (stringTime) => {
  // 注意这里 获取moment月初月底对象 需要分开来返回 否则都是月底数值
  const momentTimeStart = getMomentTimeByString(stringTime);
  const momentTimeEnd = getMomentTimeByString(stringTime);

  const dateFrom = momentTimeStart.startOf("month");
  const dateTo = momentTimeEnd.endOf("month");
  return { dateFrom, dateTo };
};

// 根据当前年月的字符串20230831 生成可传入antd 年份选择器的时间
export const getMomentTimeByStringWithoutLine = (stringTime) => {
  if (stringTime?.length) {
    // 打散字符串
    const letterList = stringTime.split("");
    // 按照数组顺序 组合为年月日
    const year = [
      letterList[0],
      letterList[1],
      letterList[2],
      letterList[3],
    ].join("");
    const month = [letterList[4], letterList[5]].join("");
    // 注意日期为date 周为day
    const date = [letterList[6], letterList[7]].join("");
    return moment([year, month - 1, date]); // moment的月份范围为0-11
  }
  return moment(new Date());
};

// 根据moment时间 返回字符串时间
export const getStringTimeByMoment = (momentTime) => {
  const yearMonthDay = momentTime.format(`YYYYMMDD`);
  const yearMonth = momentTime.format("YYYYMM");
  return { yearMonthDay, yearMonth };
};

// 20230801 返回月初月底值
export const getStartAndEndMomentTimeWithoutLineString = (stringTime) => {
  // 打散字符串
  const letterList = stringTime.split("");
  // 按照数组顺序 组合为年月日
  const year = [
    letterList[0],
    letterList[1],
    letterList[2],
    letterList[3],
  ].join("");
  const month = [letterList[4], letterList[5]].join("");
  const momentStartTime = moment([year, month - 1]).startOf("month"); // moment的月份范围为0-11
  const momentEndTime = moment([year, month - 1]).endOf("month"); // moment的月份范围为0-11

  return { momentStartTime, momentEndTime };
};

// 判断传入的时间间隔是否>=6月
export const judgeTimeIntervalIsBeyondHalfYear = (momentStart, momentEnd) => {
  // 进来的时间,先都整成月初
  // 注意这里一定要用moment包裹一层 生成新的一个moment对象
  const timeStart = moment(momentStart);
  const timeEnd = moment(momentEnd);
  timeStart.startOf("month");
  timeEnd.startOf("month");

  const isBeyondHalfYear = timeEnd.diff(timeStart, "months") >= 6;

  return isBeyondHalfYear;
};

组件

/* eslint-disable react/prop-types */
import React, { useMemo } from "react";
import { DatePicker } from "antd";
import { connect } from "nuomi";
import {
  getMomentTimeByString,
  getMomentTimeByStringWithoutLine,
  getStringTimeByMoment,
  judgeTimeIntervalIsBeyondHalfYear,
} from "pages/voucher/journal/utils";

import styles from "./style.less";

// 分割型的日期选择器
const DatePickerCombination = ({
  periods,
  dateFrom: dateFromString,
  dateTo: dateToString,
  onChange,
}) => {
  //   两个不可超过的时间限制
  const [earliestTimeString, latestTimeString] = periods || [
    new Date(),
    new Date(),
  ];
  const earliestTime =
    getMomentTimeByString(earliestTimeString).startOf("month");
  const latestTime = getMomentTimeByString(latestTimeString).endOf("month");

  // 由于有个从结账页面跳转来的时间值,这里页面刷新必须重置为原始的
  const momentStart = useMemo(() => {
    const timeMoment = getMomentTimeByStringWithoutLine(dateFromString);
    return timeMoment;
  }, [dateFromString]);

  const momentEnd = useMemo(() => {
    const timeMoment = getMomentTimeByStringWithoutLine(dateToString);
    return timeMoment;
  }, [dateToString]);

  const onChangeStartMoment = (value) => {
    const { yearMonth, yearMonthDay } = getStringTimeByMoment(value);
    const { yearMonthDay: yearMonthDayEnd } = getStringTimeByMoment(momentEnd);

    const deliver = {
      accountPeriod: yearMonth,
      dateFrom: yearMonthDay,
      dateTo: yearMonthDayEnd,
    };
    onChange(deliver);
  };

  const onChangeEndMoment = (value) => {
    const { yearMonth, yearMonthDay } = getStringTimeByMoment(value);
    const { yearMonthDay: yearMonthDayStart } =
      getStringTimeByMoment(momentStart);

    const deliver = {
      accountPeriod: yearMonth,
      dateTo: yearMonthDay,
      dateFrom: yearMonthDayStart,
    };
    onChange(deliver);
  };

  //   时间区间不得在账期时间之外
  const disabledDate = (current) => {
    return current && (current < earliestTime || current > latestTime);
  };

  // 2024-1-9 禁选范围更新 后端根据月份 即1月只可查询到6月的,这里也要做相应处理
  //   开始时间不得大于结尾时间
  const disabledDateStart = (current) => {
    // 半年限制
    const isBeyondHalfYear = judgeTimeIntervalIsBeyondHalfYear(
      current,
      momentEnd
    );
    return disabledDate(current) || current > momentEnd || isBeyondHalfYear;
  };

  //   结尾时间不得小于开始时间
  const disabledDateEnd = (current) => {
    // 半年限制
    const isBeyondHalfYear = judgeTimeIntervalIsBeyondHalfYear(
      momentStart,
      current
    );
    return disabledDate(current) || current < momentStart || isBeyondHalfYear;
  };

  return (
    <div className={styles["journal-date-picker-combination"]}>
      <DatePicker
        allowClear={false}
        value={momentStart}
        onChange={onChangeStartMoment}
        disabledDate={disabledDateStart}
      />
      <span> - </span>
      <DatePicker
        allowClear={false}
        value={momentEnd}
        onChange={onChangeEndMoment}
        disabledDate={disabledDateEnd}
      />
    </div>
  );
};

export default connect(({ searchParams: { dateFrom, dateTo } }) => ({
  dateFrom,
  dateTo,
}))(DatePickerCombination);

范围选择器

这里需要注意的点是,范围选择器的时间,与传入的时间并不一致。有时范围选择器已选,但要用的时间还未选定,所以要分开。在确定范围选择器所选时间后,再更改传入的时间。同时这里还有对禁选时间的判断,也是用到了范围选择器所选时间作为进一步判断。

/* eslint-disable react/prop-types */
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { DatePicker } from "antd";
import { connect } from "nuomi";
import {
  getMomentTimeByString,
  getMomentTimeByStringWithoutLine,
  getStringTimeByMoment,
  judgeTimeIntervalIsBeyondHalfYear,
} from "pages/voucher/journal/utils";
import moment from "moment";

const { RangePicker } = DatePicker;

// 分割型的日期选择器
const TimePickerForJournal = ({
  periods,
  dateFrom: dateFromString,
  dateTo: dateToString,
  onChange,
}) => {
  // 两个不可超过的时间限制
  const [earliestTimeString, latestTimeString] = periods || [
    new Date(),
    new Date(),
  ];
  const earliestTime =
    getMomentTimeByString(earliestTimeString).startOf("month");
  const latestTime = getMomentTimeByString(latestTimeString).endOf("month");
  // 当前选中的日期组
  const [tagPeriods, setTagPeriods] = useState([]);

  // 由于有个从结账页面跳转来的时间值,这里页面刷新必须重置为原始的
  const momentStart = useMemo(() => {
    const timeMoment = getMomentTimeByStringWithoutLine(dateFromString);
    return timeMoment;
  }, [dateFromString]);

  const momentEnd = useMemo(() => {
    const timeMoment = getMomentTimeByStringWithoutLine(dateToString);
    return timeMoment;
  }, [dateToString]);

  // 如果标准时间改变,此时范围时间选择器需要同步
  useEffect(() => {
    setTagPeriods([momentStart, momentEnd]);
  }, [momentStart, momentEnd]);

  const onChangeRangePicker = (momentList) => {
    if (momentList?.length > 1) {
      const [moment1, moment2] = momentList;

      const { yearMonth, yearMonthDay } = getStringTimeByMoment(moment1);
      const { yearMonthDay: yearMonthDayEnd } = getStringTimeByMoment(moment2);

      const deliver = {
        accountPeriod: yearMonth,
        dateFrom: yearMonthDay,
        dateTo: yearMonthDayEnd,
      };
      onChange(deliver);
    }
  };

  // 时间区间不得在账期时间之外
  // 2024-1-9 禁选范围更新 后端根据月份 即1月只可查询到6月的,这里也要做相应处理
  const disabledDate = useCallback(
    (current) => {
      if (tagPeriods?.length) {
        const [start, end] = tagPeriods;
        // 判断当前的end是否为moment
        // 在end为空时,代表只选了起时间,这里根据起的时间,推算时间末
        if (!moment.isMoment(end)) {
          // 注意 范围选择器首先选中一个起始时间,再进行禁选范围判断
          // 这里只需要判断 选择第二个的是否符合半年内
          const isBeyondHalfYear = judgeTimeIntervalIsBeyondHalfYear(
            start,
            current
          );

          return (
            current &&
            (current < earliestTime || current > latestTime || isBeyondHalfYear)
          );
        }
      }
      return current && (current < earliestTime || current > latestTime);
    },
    [tagPeriods]
  );

  const onOpenChange = (status) => {
    // 如果关闭时还未选择完毕,则回显原来选中的
    if (!status) {
      const [start, end] = tagPeriods;
      if (!start || !end) {
        setTagPeriods([momentStart, momentEnd]);
      }
    }
  };

  // 选择开始时间/结束时间
  const onCalendarChange = (dates) => {
    setTagPeriods(dates);
  };

  return (
    <div>
      <RangePicker
        onOpenChange={onOpenChange}
        onCalendarChange={onCalendarChange}
        format="YYYY-MM-DD"
        style={{ width: 217 }}
        disabledDate={disabledDate}
        allowClear={false}
        onChange={onChangeRangePicker}
        value={tagPeriods}
      />
    </div>
  );
};

export default connect(({ searchParams: { dateFrom, dateTo } }) => ({
  dateFrom,
  dateTo,
}))(TimePickerForJournal);

iframe 窗口的 antd 弹窗,跟我写的剧情一样绝处逢生

设置新云代账弹窗的时候,遇到一个问题,Modal.confirm的弹窗脚一直没法设置上正确的样式,我换到云记账项目,完全没问题,疑惑得很。今天试的时候我直接把:global去掉,结果 antd 的弹窗样式居然就生效了!

探究一下:global到底对样式做了什么。

使用 global 声明的 class,都不会被编译成哈希字符串。
css(react)中 global

在 React 项目中使用 scss/less,如果想让样式仅作用在某个组件,而不影响全局,一般都会把样式文件进行模块化,即打包后每个 class 名都会被自动加上一串唯一的序列号。而修改全局样式的时候,可以无视这串序列号,直接修改 antd 样式。
修改 antd 组件默认样式的坑丨 :global 关键字

posted @ 2023-09-19 10:57  乐盘游  阅读(6)  评论(0编辑  收藏  举报