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 指向问题
- 箭头函数
这种写法,每次 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 指向当前组件的实例对象
- 组件方法
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>;
}
}
- 属性初始化语法(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 种主流的通信方式:
- props 和 callback 方式
这种常用父子间通讯 - ref 方式。
- React-redux 或 React-mobx 状态管理方式。
- context 上下文方式。
- 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 高阶组件
按照 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&fm=26&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
-
在 react 的 event handler 内部同步的多次 setState 会被 batch 为一次更新
使用promise
、setTimeout
可以打破这种更新 -
在一个异步的事件循环里面多次 setState,react 不会 batch
-
可以使用
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 关键字