鼠标是否在元素中
typescript
/*
* getBoundingClientRect
*
* getBoundingClientRect()方法用来获取页面中某个元素的左、上、右、下分别相对浏览器视窗(例如:元素
*
* 左边距离浏览器最左边的距离的位置,
*
* 返回的是一个矩形对象,包括四个属性,分别是left 、top、right、bottom。
*
* 分别表示元素各边与页面上边和左边的距离。
*/
// 组件中使用
/*
* 使用场景:
*
* 现在有这样一个需求,
*
* 鼠标在 是否在 antd switch 内, 决定了在接下来的 失焦事件中,是否调用 改变switch 开关的接口。
*
* 在内调用,不在内不调用,组件卸载,移除键盘监听事件。
*
* 修改完input的值,鼠标单击页面任意位置都会触发失焦事件,
*
* 区别在于,是否位于一个switch 元素内。
*
*
*/
typescript
/*
* 外壳组件
*/
import React, {VFC, useCallback, useState, useEffect} from 'react';
import {Form, InputNumber, Row, Col} from 'antd';
import {getAllInfo, updateNum, updateById} from '@/service/search';
import Personalized from '@/components/Personalized';
import styles from './index.less';
interface KeywordsProps {
content: string;
id: number;
position: number;
status: number;
}
const {log} = console;
const layout = {
labelCol: {span: 10},
wrapperCol: {span: 14},
};
const Relevant: VFC = () => {
const [keywordNum, setKeywordNum] = useState<number>(1);
const [keywords, setKeywords] = useState<KeywordsProps[]>([]);
const [form] = Form.useForm();
const getAllInfoLocal = useCallback(async () => {
const res = await getAllInfo();
const {totalNum, searchDiscoveryList} = res.data;
setKeywordNum(totalNum);
setKeywords(searchDiscoveryList);
form.setFieldsValue({searchNum: totalNum});
}, [form]);
const updateNumLocal = useCallback(
async (num: number) => {
await updateNum({content: String(num)});
await getAllInfoLocal();
},
[getAllInfoLocal]
);
useEffect(() => {
getAllInfoLocal();
}, [getAllInfoLocal]);
const onFinish = useCallback((values: any) => {
log('Success:', values);
}, []);
const numChange = useCallback(
e => {
updateNumLocal(e);
},
[updateNumLocal]
);
return (
<div className={styles.relevantWrap}>
<Form {...layout} form={form} name='num' onFinish={onFinish} initialValues={{searchNum: keywordNum}}>
<Form.Item
name='searchNum'
label='相关搜索数量设置'
extra='搜索数量1~10个'
rules={[{pattern: /^([1-9]|10)$/, message: '请输入1~10的数字'}]}
>
<InputNumber min={1} max={10} onStep={e => numChange(e)} key={keywordNum} />
</Form.Item>
<Row style={{marginBottom: 20}}>
<Col offset={5} span={7}>
搜索发现位
</Col>
<Col span={12}>个性化推荐</Col>
</Row>
{keywords.map((item: KeywordsProps, index) => (
<Personalized form={form} index={index + 1} key={item.id} data={item} updateById={updateById} />
))}
</Form>
</div>
);
};
export default Relevant;
typescript
/*
* input switch 组合组件
*
* 这样循环出来的组件有多个,他们不会相互影响,即便是键盘监听事件也是如此。
*
* 而对于是否在内的判断,也只是针对对当前组件的当前 switch 相对于窗口上下左右位置的判断。
*
* 每一个这样的组件都会注册键盘监听事件,
*
* 在一定程度上来说,这的确重复了,
*
* 但在他们会跟着父级组件在组件卸载的时候一起销毁。
*
* 确保不会在其他页面触发键盘事件,影响性能。
*
*/
import React, {FC, useEffect, useCallback, useState, useRef, MutableRefObject} from 'react';
import {Col, Form, FormInstance, Input, Row, Switch} from 'antd';
import _ from 'lodash';
import useSyncCallback from '@/hooks/useSyncCallback';
const layout = {
labelCol: {span: 8},
wrapperCol: {span: 16},
};
interface PersonalizedProps {
form: FormInstance;
index: number;
data: {
content: string;
id: number;
position: number;
status: number;
};
updateById: (params: any) => void;
}
const Personalized: FC<PersonalizedProps> = props => {
const {form, index, data, updateById} = props;
const [isDis, setIsDis] = useState<boolean>(false);
const [isOpen, setOpen] = useState(false);
const switchRef: MutableRefObject<any> = useRef(null);
// 双!是什么意思?
useEffect(() => {
setIsDis(!!data.status);
}, [data.status]);
const openIt = useCallback(
async (e: any, index: number) => {
if (form.getFieldsValue()[`words${index}`]) {
setIsDis(e);
await updateById({...data, status: e ? 1 : 0, content: form.getFieldValue(`words${index}`)});
}
setOpen(false);
},
[data, form, updateById]
);
const hasViewRange = (view: any, event: MouseEvent) => {
if (view) {
const wx = event.clientX;
const wy = event.clientY;
const {left, top, bottom, right} = view.getBoundingClientRect();
// 鼠标位于元素内,真空区域!
if (wx >= left && wx <= right && wy >= top && wy <= bottom) return true;
else return false;
}
return false;
};
const getSyncOpen = useSyncCallback(() => {
isOpen && openIt(!isDis, index);
});
const onInputBlur = async () => {
!!form.getFieldValue(`words${index}`) &&
form.isFieldTouched(`words${index}`) &&
(await updateById({...data, status: isDis ? 1 : 0, content: form.getFieldValue(`words${index}`)}));
getSyncOpen(); // 使用同步函数,确保 state 最新值
};
const moveFunc = _.debounce((e: MouseEvent) => {
// 若此刻鼠标位于 switch 区域内,hasViewRange 函数返回true
console.warn('检测元素', hasViewRange(switchRef.current, e));
setOpen(() => {
if (hasViewRange(switchRef.current, e)) {
return true;
}
return false;
});
}, 100); // 因为时间为 500 反应太慢,改成100
// input 值变化,注册鼠标移动事件
const handelChange = _.debounce(() => {
addEventListener('mousemove', moveFunc);
}, 500);
useEffect(() => {
return () => {
removeEventListener('mousemove', moveFunc);
};
}, []);
return (
<div>
<Row align='middle'>
<Col span={12}>
<Form.Item
{...layout}
name={`words${index}`}
label={`位置${index}`}
rules={[{required: true, message: '请输入'}]}
initialValue={data.content}
>
<Input
disabled={isDis}
placeholder='请输入'
onBlur={() => onInputBlur()}
onChange={() => handelChange()}
/>
</Form.Item>
</Col>
<Col span={11} offset={1}>
<Form.Item>
<Switch
ref={switchRef}
checkedChildren='开'
unCheckedChildren='关'
checked={isDis}
onChange={e => openIt(e, index)}
/>
</Form.Item>
</Col>
</Row>
</div>
);
};
export default Personalized;
tex
组件图片:
https://s3.bmp.ovh/imgs/2022/05/17/029b64903a16cb65.png
https://s3.bmp.ovh/imgs/2022/05/17/87dcded1c0920657.png