- 移动端安全高度问题
@constant: constant(safe-area-inset-top);
@env: env(safe-area-inset-top);
@supports ((height: constant(safe-area-inset-top)) or (height: env(safe-area-inset-top))) and (-webkit-overflow-scrolling: touch) {
.container {
:global(.navbar) {
padding-top: calc(~"@{constant} * 2");
padding-top: calc(~"@{env} * 2");
height: calc(~"@{constant} * 2 + 0.9rem");
height: calc(~"@{env} * 2 + 0.9rem");
}
}
}
- ios键盘弹起,出现底部空白问题
/*js监听ios手机键盘弹起和收起的事件*/
document.body.addEventListener('focusin',()=>{//软键盘弹起事件
console.log("键盘弹起");
});
document.body.addEventListener('focusout',()=>{//软键盘关闭事件
document.body.scrollTop=0;
document.documentElement.scrollTop=0;
});
- android键盘弹起可视区域高度变化做兼容
letoriginalHeight=document.documentElement.clientHeight||document.body.clientHeight;
window.onresize=function(){
//键盘弹起与隐藏都会引起窗口的高度发生变化
letresizeHeight=document.documentElement.clientHeight||document.body.clientHeight;
this.isFixedBottom=resizeHeight>=originalHeight;
}
- 图片分享
import html2canvas from "html2canvas"; // html2Canvas插件
function shareComponents(ref, bgLoaded, qrLoaded, shareFun){
// 图片加载完成截图
if (bgLoaded && qrLoaded) {
html2canvas(ref, {
scale: 2,
useCORS: true,
allowTaint: true,
width: ref.share.clientWidth,
height: ref.share.clientHeight,
dpi: window.devicePixelRatio * 4
}).then(canvas => {
setTimeout(() => {
const context = canvas.getContext('2d');
context.mozImageSmoothingEnabled = false;
context.webkitImageSmoothingEnabled = false;
context.msImageSmoothingEnabled = false;
context.imageSmoothingEnabled = false;
imgUrl = canvas.toDataURL();
// 分享options
const options = {
shareData: {
image: imgUrl
},
typeList: typeList,
callback: (res) => {
// 分享之后的callback
}
}
// 调起分享
shareFun(options)
}, 500)
}).catch(err => {
alert('分享图片生成失败,请稍后重试')
});
} else {
setTimeout(() => {
// 图片未加载完成,延迟执行截图
}, 500)
}
}
- 滚动一定高度
const $ = window.$;
componentDidUpdate() {
// 业务逻辑:一个滚动的回复列表,需要定位到当前回复所需位置,然后滚动
const { selectedId, selectedType, replyList } = this.props.relaxProps;
const scrollItem = replyList.find(item => item.get('replyId') == selectedId);
if (selectedType !== 0 && scrollItem) {
const dom = `#replay-item-${selectedId}`;
const parentDom = '#comment-replay';
setTimeout(() => {
const top = $(dom).offset().top;
if (top > 500) {
// 400取值因为当前卡片头部是400px;
$(parentDom).animate({ scrollTop: top - 400 }, 500);
}
});
}
}
- 解决ios滚动条卡顿问题:
body,html{
-webkit-overflow-scrolling: touch;
}
- 解决ios光标移动位置不准确问题
text-indent: -999px; // 直接隐藏光标
- 解决ios12.x(复现机型iphoneX)某些页面不能滚动问题
min-height: 100vh; // 给子元素一个min-height,帮助safari建立ScrollView。
- less中使用动画
// demo场景:让一张图片旋转
.batchImg {
// 定义动画样式
.animation(myDongHua, 1s, linear, infinite);
}
// 动画定义
.donghua(@DHname) {
@keyframes @DHname {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
};
// 调用动画
.donghua(myDongHua);
// 声明动画
.animation(@animation-name,@animation-duration,@animation-timing-function,@animation-iteration-count) {
animation: @arguments;
}
- antd组件批量上传的时候,需要对上传个数做拦截,本身的macCount不满足业务需求;
// 使用截流的方式,只让其出发一次,并返回false
const tips = throttle(function () {
message.error('一次最多上传20份简历')
}, 300)
const upLoadProps: UploadProps = {
name: 'file',
showUploadList: false,
multiple: true,
withCredentials: true,
action: `XXXXXXXX`,
beforeUpload: (file, fileList) => {
if (fileList.length > 20) {
tips();
return false;
}
...
},
...
}
- 在react中使用防抖方式
import _ from 'lodash'
const debounceTime = _.debounce(function (params, selectedList) {
// 需防抖的操作函数
}, 500,{leading: true})
function onConfirm(selectedList) {
...
// 触发时机调用
debounceTime(params, selectedList);
}
- 倒计时功能
(1)react版本:
const [count, setCount] = useState(59)
const [checkLoading, setCheckLoading] = useState<boolean>(false);
const countDown = () => {
const active = setInterval(() => {
setCount((preSecond) => {
if (preSecond <= 1) {
setCheckLoading(false)
clearInterval(active)
// 重置秒数
return 59
}
return preSecond - 1
})
}, 1000)
}
(2)js版本:
// 触发计时事件
$("#getCheckCode").click(function () {
Settime();
});
// 倒计时函数及dom文字处理
function Settime(obt) {
var countdown = 60;
settime('#getCheckCode');
function settime(obt) {
if (countdown === 0) {
$(obt).attr("disabled", false);
$(obt).text("获取验证码");
countdown = 60;
return;
}
if (countdown === 60) {
var phone = $('#checkPhone').val();
if (!phone) {
alert('请输入手机号')
return;
}
sendCode();
}
$(obt).attr("disabled", true);
$(obt).html("(" + countdown + ")s后重新获取");
countdown--;
setTimeout(function () {
settime(obt)
}, 1000)
}
};
/*发送验证码*/
function sendCode() {
var phone = $('#checkPhone').val();
$.ajax({
url: 'XXXXXX',
type: "get",
contentType: 'application/json',
data: {
phone: phone,
},
success: function (result) {
if (result.code === 200) {
singleAccept("验证码发送成功,请注意查收");
} else {
errorTip("验证码发送失败,请重新发送");
}
}
});
}
- 上传文件类型判断
beforeUpload: file => {
const splitList = file.name.split('.')
const type = splitList[splitList.length - 1].toLocaleLowerCase()
const typeList = ['doc', 'docx', 'pdf', 'jpg', 'png', 'html']
const isTrueFileType = typeList.some(item => item === type)
if (!isTrueFileType) {
message.error(`${file.name}文件类型错误`);
}
return isTrueFileType;
},
- 批量文件上传进度的计算
let counts = 0;
const [count, setCount] = useState(0); // 当前已上传数量,注意在处理完成之后需要清空
const [totalCount, setTotalCount] = useState(0); //需上传文件总数
// beforeUpload中获取上传文件总数
beforeUpload: (file, fileList) => {
setTotalCount(fileList.length)
...
},
//onChange中做出数量计算
onChange: (info) => {
const {status, name, response} = info.file;
if (status === 'done') {
counts += 1;
const diffCount = totalCount - counts;
if (diffCount > 0) {
setCount(counts)
} else {
//全部上传完成之后的操作
...
}
} else if (status === 'error') {
message.error(`${name} 上传失败,请重新上传.`);
}
},
- 循环调用接口,中间增加条件判断,适时停止
const [flag, setFlag] = useState(false)
useEffect(() => {
const timer = setInterval(() => {
if (flag) {
const params = {
pageSize: state.pageSize,
pageNumber: 1
}
FetchData(params).then((res: any) => {
if (!res) {
setFlag(false)
return message.error('获取数据失败')
}
// 无关业务代码
// ...
const newRes = res?.list?.length ? res?.list : []
// 判断是否继续进行调用,最好和服务端商量确定一个标志参数
const hasContinues = newRes.some(item => item.parseStatus === 'ing')
if(!hasContinues){
//置空某些状态参数
setCount(0)
counts=0;
}
// 设置是否继续调用
setFlag(hasContinues)
})
}
}, 2000);
return () => clearInterval(timer);
}, [flag])
- iframe动态设置src后,浏览器记录历史记录,在返回的时候造成路由错位。使用key做区分。
<iframe src={`${fileUrl}#toolbar=0`} width={'100%'} height={'100%'} key={fileUrl}/>
- 宽度固定,超出宽度…展示,并toopTips提示;
import React, { CSSProperties, PropsWithChildren, useState} from 'react'
import styles from './index.module.less'
import { Tooltip } from 'antd';
interface IProps{
className?: string
style?: CSSProperties
text?:any
}
export default function Ellipsis(props: PropsWithChildren<IProps>) {
const {text} = props;
const [tipVisible, setTipVisible] = useState(false)
const textOver = (e)=>{
if(e.target.scrollWidth>e.target.clientWidth){
setTipVisible(true)
}
}
const textLeave = ()=>{
setTipVisible(false)
}
return <>
<Tooltip visible={tipVisible} placement="top" title={<span dangerouslySetInnerHTML={{__html:text}}></span>}>
<div className={styles.ellipsisWrap} onMouseOver={textOver} onMouseLeave={textLeave} dangerouslySetInnerHTML={{__html:text}}>
</div>
</Tooltip>
</>
}
.ellipsisWrap{
width: 100%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
- 页面字体根据窗口大小进行缩放
使用zoom进行缩放
const setPageZoom = ()=>{
if (window.innerWidth < 1700) {
const zoomData = window.innerWidth/1700 > 0.7 ?window.innerWidth/1700:0.7
document.querySelector('body').style.cssText = `zoom:${zoomData}`
} else {
document.querySelector('body').style.cssText = 'zoom:1'
}
}
useEffect(()=>{
setPageZoom();
window.addEventListener('resize',setPageZoom);
return ()=>{
window.removeEventListener('resize',setPageZoom)
}
},[])
- antd-popover父级节点的位置偏差
使用id,挂在class
<div id={`popoverContainerWrap`}>
<Popover
getPopupContainer={()=>document.getElementById(`popoverContainerWrap`)}
...props
>
</div>
- 时间组件添加至今,并设置可选择时间
思路:
- 在antd-DatePicker组件基础上,使用renderExtraFooter提供的属性进行扩展。如下面第一段代码;
- 如果是时间段的选择:需要前后制约某个时间段是否可选;如下第二段代码:(在第一段代码上进行一层封装)
import React, {PropsWithChildren, useState, useEffect} from 'react'
import styles from './index.module.less'
import type {Moment} from 'moment';
import type {DatePickerProps} from 'antd/es/date-picker';
import moment from 'moment';
import {DatePicker} from 'antd';
import {useControllableValue} from "ahooks";
import {RangePickerProps} from "antd/es/date-picker";
type PropsType = DatePickerProps & {
showToNow?: boolean,// 是否显示至今
toNowText?: string,// 显示至今的文案
toNowKey?: string,// 显示至今的key
value?: any,// 显示至今的key
onChange?: Function, // 回调函数
controlDate?: unknown, // 禁止时间
typeTime?: number, // 类型 1为开始时间,2为结束时间,0为单个
}
// 适用年月日的业务组件 如有其他场景再添加
export default function DatePicker(props: PropsWithChildren<PropsType>) {
const {showToNow, toNowKey = 'toNow', toNowText = '至今', controlDate, picker, onChange, typeTime} = props;
const [pickerValue, setPickerValue] = useState<any>()
const [open, setOpen] = useState(false)
const [flag, setFlag] = useState(true)
const [value, setValue] = useControllableValue(props, {defaultValue: null})
const formatMap = {
year: 'YYYY',
month: 'YYYY-MM',
date: 'YYYY-MM-DD',
}
const formatPicker = formatMap[picker] || 'YYYY-MM-DD'
// 处理默认值
useEffect(() => {
if (!value || !flag) return;
// @ts-ignore
if (value === toNowKey || value === toNowText) {
setPickerValue(moment(moment(new Date()).format('YYYY-MM-DD HH')));
} else {
setPickerValue(moment(moment(value).format(formatPicker)));
}
}, [value])
// 选择日期回调
const handleChang = (date, dateString) => {
setFlag(false)
if (!date) {
setPickerValue(null)
setValue(null)
} else {
setPickerValue(moment(date.format(formatPicker)));
setValue(moment(date.format(formatPicker)))
}
// onChange && onChange(date, dateString)
setOpen(false)
}
// 点击至今回调
const handleNow = () => {
setFlag(false)
// onChange && onChange(moment(new Date()), moment(new Date()).format(formatPicker))
setPickerValue(moment(moment(new Date()).format('YYYY-MM-DD HH')));
setValue(moment(moment(new Date()).format('YYYY-MM-DD HH')))
setOpen(false)
}
// 显示过滤
const format = (value: Moment) => {
if (pickerValue && pickerValue._f?.includes('HH')) {
return toNowText;
} else {
return value.format(formatPicker);
}
};
const footer = () => {
return <>
{
showToNow ? <div onClick={handleNow} className={styles.datePickerFooter}>
{toNowText}
</div> : ""
}
</>
}
// eslint-disable-next-line arrow-body-style
const disabledDateFun: RangePickerProps['disabledDate'] = current => {
const currentDay = current && current >= moment().endOf('day')
if(typeTime === 1){
return current && current > moment(controlDate).endOf('day') || currentDay;
}else if(typeTime === 2){
return current && current < moment(controlDate).endOf('day') || currentDay;
}else{
return currentDay;
}
};
return <DatePicker
disabledDate={(current) => disabledDateFun(current)}
renderExtraFooter={footer}
{...props}
onBlur={() => setOpen(false)}
onClick={() => setOpen(true)}
open={open}
value={pickerValue}
onChange={handleChang}
format={format}
/>
}
备注: flag状态是为了解决一些场景中的问题而进行的useEffect的控制,如不要可忽略。
import React, {CSSProperties, PropsWithChildren, useEffect, useState} from 'react';
import JdDatePicker from "@/components/atoms/DatePicker";
import {useControllableValue} from "ahooks";
import {Form} from 'antd';
import {View} from "@jd/ea-project-components";
interface IProps {
style?: CSSProperties
width?: number
isDisable?: boolean
type?: string
fromKey?: any[]
isShowNow?: boolean
}
export default function FormRangePicker(props: PropsWithChildren<IProps>) {
const {
width,
isDisable = false,
type,
fromKey=[],
isShowNow=true
} = props
const [value = [null, null], setValue] = useControllableValue(props, {defaultValue: [null, null]})
const [startValue, setStartValue] = useState(null)
const [endValue, setEndValue] = useState(null)
useEffect(() => {
setStartValue(value[0])
setEndValue(value[1])
}, [])
useEffect(() => {
setValue([startValue, endValue])
}, [startValue,endValue])
return <View>
<Form.Item rules={[{required: true, message: '请选择开始时间'}]} name={fromKey[0]} fieldKey={fromKey[0]}>
<JdDatePicker
...
/>
</Form.Item>
<span style={{padding: 5}}>~</span>
<Form.Item rules={[{required: true, message: '请选择结束时间'}]} name={fromKey[1]} fieldKey={fromKey[1]}>
<JdDatePicker
key={'endTime'}
placeholder={`请选择结束时间`}
disabled={isDisable}
picker={type as any}
style={{width}}
showToNow={isShowNow}
toNowKey={'endTime'}
onChange={setEndValue}
controlDate={startValue}
typeTime={2}
/>
</Form.Item>
</View>
}
备注:主要使用controlDate参数进行disable时间的控制。
- antd-Popover组件: onVisibleChange使用
解决业务场景:使用 echarts图表信息需要在Popover的content中绘制出来,useEffect中无法监听dom渲染情况,导致没有时机对图表信息进行绘制。
// 控制图表渲染变量
const [renderChart, setChartRneder]=useState(false)
// 挂在图表的dom
const chartsRef = useRef(null);
// 渲染
useEffect(()=>{
chartsRef.current && getOptions()
},[renderChart])
// 图表信息
const getOptions = ()=>{
let myChart = echarts.init(chartsRef.current);
myChart?.clear()
let option = {
color: ['#4C7CFF'],
...//option参数
};
option && myChart?.setOption(option);
}
<Popover
content={content}
...// 业务场景props
onVisibleChange={(visible) => {
// 可在此事件中监听popover的展示,使用renderChart变量控制图表不会重复渲染
if(visible && !renderChart){
setChartRneder(true)
}
}}
>
前后端接口加解密实现:
const CryptoJS = require('crypto-js'); //引用AES源码js
const keyBytes = CryptoJS.enc.Utf8.parse('xxxxxxxx');
const ivBytes = CryptoJS.enc.Hex.parse('xxxxxxxxxxxxxxxxxxx');
function Encrypt(data) {
const encrypted = CryptoJS.AES.encrypt(data, keyBytes, {
iv: ivBytes,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
});
return encrypted.toString(); // 返回 Base64 编码的字符串
}
function Decrypt(encryptedData) {
const decrypted = CryptoJS.AES.decrypt(encryptedData, keyBytes, {
iv: ivBytes,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
});
const decryptedStr = decrypted.toString(CryptoJS.enc.Utf8);
try {
const tempJson = JSON.parse(decryptedStr);
return tempJson;
} catch (error) {
return decryptedStr;
}
}
export {
Decrypt ,
Encrypt
}