一、backdrop-filter:blur(20px);
毛玻璃效果,在安卓机上有兼容问题,添加兼容前缀也无效;
解决方案:让设计师调整渐变,不要使用该属性!
复制代码
background: radial-gradient(33% 33% at 100% 5%, #e9e5e5 0%, rgba(254, 251, 243, 0) 100%),
radial-gradient(
54% 54% at -5% 6%,
#d2d1cf 0%,
#ebe9e2 30%,
#fdfaf0 42%,
rgba(255, 255, 255, 0) 100%
),
linear-gradient(160deg, #edeae3 0%, #fffdfe 31%, #ffeff7 94%),
linear-gradient(180deg, rgba(255, 255, 255, 0.6) 0%, #ffffff 32%);
二、如何实现倒计时效果
首先我们需要一个时间差,并且这个差值根据现在的差值一直在递减
其次我们需要维护一个时间状态,每隔一秒钟就需要更新这个状态从而触发页面的更新
JavaScript
复制代码
useEffect(()=>{
let timer = null;
// 注意月份是从0开始的,所以4月是3
const targetTime = new Date(2024, 3, 20, 20, 0, 0).getTime();
timer = setInterval(() => {
const currentTime = new Date().getTime();
if (new Date(2024, 3, 20, 0, 0, 1).getTime() >= new Date(2024, 3, 20, 0, 0, 0).getTime()) {
setShowCount(true);
} else {
return;
}
const remainingTime = targetTime - currentTime;
if (remainingTime >= 0) {
const total = remainingTime / 1000; // 剩余秒数
const hh = parseInt(total / (60 * 60)); // 剩余小时
const day = parseInt(hh / 24); // 剩余天数
const h = hh - parseInt(day * 24);
const mm = parseInt((total - hh * 60 * 60) / 60); // 剩余分钟
const ss = parseInt(total % 60); // 剩余秒
setTime({
day,
h,
m: mm,
s: ss,
});
} else {
clearInterval(timer);
}
}, 1000);
return () => {
if (timer) clearInterval(timer);
};
},[]);
<View className="header">
距离直播开始{time.day}天{time.h}小时{time.m.toString().padStart(2, '0')}分钟
{time.s.toString().padStart(2, '0')}秒钟
</View>
三、如何实现两个动画丝滑的切换
动画的实现使用的是web-lottie
大概的思路基本上是只针对url的切换,但是只是这样局部更新url 并不会引起容器里动画的更新,需要停止上一个动画并清除容器
const initLottieRef = useRef();// 展示动画的容器
const [url, setUrl] = useState(
'https://g.alicdn.com/ani-assets/1bb995ac4ad001ab8f091e412dc5e7c0/0.0.1/lottie.json',
);
const curlt = useRef();// 用来接第一个动画实例
useEffect(() => {
curlt.current = lottie.loadAnimation({
container: initLottieRef.current,
renderer: 'svg',
loop: true,
autoplay: true,
path: url, // 替换为你的第一个动画的 JSON 文件路径
});
}, [url]);
const onClick = () => {
// 停止第一个动画并移除其容器
curlt.current.stop();
// 清空容器以移除第一个动画
initLottieRef.current.innerHTML = '';
// 初始化并显示第二个动画
setUrl('https://g.alicdn.com/ani-assets/93cbb9eeb82ff32a1fd746bc7a284e04/0.0.1/lottie.json');
};
四、弹窗组件如何阻止点击滚动穿透
蒙层级别提高,在容器点击阻止冒泡和默认行为
onClick={(e) => e.stopPropagation()}
onTouchMove={(e) => e.preventDefault()}设置了这个可能会影响本身的滚动
const onAppear = () => {
// 防止蒙层滚动穿透
document.body.style.overflow = 'hidden';
};
const onClose = () => {
setIsShow(false);
document.body.style.overflow = 'auto';
};
五、实现淡入淡出
用原生js 动态添加类名的方式试了好久,还是不行
reatc 组件有自己的更新机制,即使添加上了类名但没有遵守react 更新规则并不会触发更新
<div className={`${classlist.join(' ')}`} ></div>
const [classlist, setClasslist] = useState(['container', 'show-pop']);
const onClose = () => {
setClasslist(['container', 'hide-pop']);
setTimeout(() => {
setIsShow(false);
document.body.style.overflow = 'auto';
setClasslist(['container', 'show-pop']);
}, 300);
};
.show-pop {
animation: show-pop 0.3s ease-in-out forwards;
}
.hide-pop {
animation: hide-pop 0.3s ease-in-out forwards;
}
@keyframes show-pop {
0% {
transform: translateY(100%); /* 初始位置:屏幕底部之外 */
opacity: 0; /* 初始透明度为0,实现淡入效果 */
}
100% {
transform: translateY(0); /* 结束位置:屏幕顶部 */
opacity: 1; /* 结束透明度为1,完全显示 */
}
}
@keyframes hide-pop {
0% {
transform: translateY(0); /* 结束位置:屏幕顶部 */
opacity: 1; /* 结束透明度为1,完全显示 */
}
100% {
transform: translateY(100%); /* 初始位置:屏幕底部之外 */
opacity: 0; /* 初始透明度为0,实现淡入效果 */
}
}
六、实现骨架屏
先上关键代码
background: linear-gradient(
90deg,
rgba(190, 190, 190, 0.2) 25%,
rgba(129, 129, 129, 0.24) 37%,
rgba(190, 190, 190, 0.2) 63%
);
background-size: 400% 100%;
animation: skeleton 1.4s ease infinite;
@keyframes skeleton {
0% {
background-position: 0% 0%;
}
100% {
background-position: 100% 0%;
}
}
为什么这样能实现?
以下是这段代码的工作原理:
背景渐变: background: linear-gradient(90deg, …); 定义了一个从左到右的线性渐变。渐变中有三个颜色停止点:
rgba(190, 190, 190, 0.2) 25%: 在25%的位置,颜色为浅灰色,透明度为20%。
rgba(129, 129, 129, 0.24) 37%: 在37%的位置,颜色为深灰色,透明度为24%。
rgba(190, 190, 190, 0.2) 63%: 在63%的位置,再次变为浅灰色,透明度为20%。 这样的渐变在视觉上产生了一种类似条纹的效果,模拟数据加载时的动态感。
背景尺寸: background-size: 400% 100%; 设置背景的尺寸为元素宽度的400%。这使得背景图像是元素宽度的4倍,为动画提供了足够的空间。
动画: animation: skeleton 1.4s ease infinite; 应用名为skeleton的动画,持续时间为1.4秒,缓动函数为ease(意味着动画开始和结束时速度较慢,中间速度快),并且无限循环。
@keyframes定义了一个动画,从0%到100%的动画过程中,背景的位置从0%平移到100%,即从左到右移动一个完整背景的长度。由于背景尺寸是元素宽度的400%,所以在动画过程中,这个条纹效果会来回移动,模拟数据加载的进度。
这个简单动画组合起来就形成了一个骨架屏效果,给人一种数据正在加载的感觉。当然,真正的骨架屏通常会包含更复杂的形状和元素来匹配页面的实际布局。
七、隐藏滚动条
<style>{` #scroll-list::-webkit-scrollbar { display: none }`}</style>
<div className="root" id="scroll-list">
八、实现吸顶效果
使用粘性定位来实现,他的参考点是距离它最近的一个拥有滚动机制的祖先元素,不设置默认就是当前窗口
position: -webkit-sticky; /* Safari */
position: sticky;
top: 30px;
九、实现回到顶部,按钮带动画效果
动画效果:按钮实现从右向左出现,从左向右消失
import React, { useState, useEffect } from 'react';
import './styles.css'; // 引入样式
const ScrollToTopButton = () => {
const [isVisible, setIsVisible] = useState(false);
const [isAnimating, setIsAnimating] = useState(false);
const scrollToTop = () => {
window.scrollTo({ top: 0, behavior: 'smooth' });
};
useEffect(() => {
const toggleVisibility = () => {
if (window.pageYOffset > 300) {
setIsVisible(true);
setIsAnimating(true);
} else {
setIsAnimating(false);
setTimeout(() => {
setIsVisible(false);
}, 1000); // 动画持续时间加一点延迟,确保动画完成后再重置
}
};
window.addEventListener('scroll', toggleVisibility);
// 清理函数,移除滚动事件监听器
return () => window.removeEventListener('scroll', toggleVisibility);
}, [])
return (
{isVisible && <button className={`back-top ${isAnimating ? 'fade-in-right' : 'fade-out-left'}`} onClick={scrollToTop}>top</button>}
);
};
export default ScrollToTopButton;
.fade-in-right {
animation: fade-in-right 1s ease-in-out forwards;
}
.fade-out-left {
animation: fade-out-left 1s ease-in-out forwards;
}
@keyframes fade-in-right {
0% {
transform: translateX(100%);
opacity: 0;
}
100% {
transform: translateX(0);
opacity: 1;
}
}
@keyframes fade-out-left {
0% {
transform: translateX(0);
opacity: 1;
}
100% {
transform: translateX(100%);
opacity: 0;
}
}
十、轮播
滚动的方式:
1 transform
2 scrollto
3 绝对定位移动left
动态更新索引,并移动
困惑 :滚完就反向弹回了第一个,怎么让他继续滚?
要解决视觉上回弹的问题只需要在滚到最初位置的时候取消过渡效果
import React, { useEffect, useRef } from 'react';
import './index.css'; // 假设你有一个外部CSS文件来定义样式
const Slider = () => {
const images = [
'1', '2', '3', '4'
]
const currentIndex = useRef(0);
const sliderRef = useRef(null);
// 自动切换到下一张的逻辑
useEffect(() => {
const timer = setInterval(() => {
const nextIndex = currentIndex.current === images.length - 1 ? 0 : currentIndex.current + 1;
const translateX = nextIndex * -100 + '%'; // 假设每张图片占据100%的宽度。
sliderRef.current.style.transform = `translateX(${translateX})`;
if (translateX === '0%') {
sliderRef.current.style.transition = '';//取消过渡视觉上不会出现反向滚动
} else {
sliderRef.current.style.transition = 'transform 0.5s ease-in-out';
}
currentIndex.current = nextIndex;
}, 2000);
// 清理函数,防止内存泄漏
return () => {
clearInterval(timer);
};
}, []);
return (
<div ref={sliderRef} >
<div style={{ display: 'flex' }}>
{images.map((image, index) => (
<div style={{ width: '100%', height: 100, border: '1px solid red', flexShrink: 0, boxSizing: 'border-box' }}>slide {index + 1}</div>
))}
</div>
</div>
);
};
export default function App() {
return (
<div className="App">
<Slider />
</div>
);
}
十一、下拉刷新
import React, { useState, useEffect, useRef } from 'react';
import './index.css';
let startY;
// 提取防抖函数到组件外
const debounce = (func, wait) => {
let timeout;
return function (...args) {
clearTimeout(timeout);
timeout = setTimeout(() => {
try {
func.apply(this, args);
} catch (error) {
console.error('Error in debounced function:', error);
}
}, wait);
};
};
const MyComponent = () => {
const scrollRef = useRef(null);
const [loading, setLoading] = useState(false);
const [refresh, setRefresh] = useState(false);
const fetachData = () => new Promise(resolve => setTimeout(() => {
console.log('数据刷新完成');
setLoading(false);
resolve();
}, 1000));
const fetchNewData = debounce(async () => {
await fetachData();
}, 500); // 防抖延迟时间
const onTouchStart = (e) => {
startY = e.touches[0].clientY;
}
const onTouchmove = (e) => {
requestAnimationFrame(() => { // 使用 requestAnimationFrame 优化
const moveY = e.touches[0].clientY;
const diffY = moveY - startY;
scrollRef.current.style.transform = `translateY(${diffY > 100 ? 100 : diffY}px)`;
if (diffY > 90) {
setRefresh(true);
// 发起请求
if (!loading) {
setLoading(true);
fetchNewData();
}
}
});
};
const onTouchend = () => {
requestAnimationFrame(() => { // 使用 requestAnimationFrame 优化
scrollRef.current.style.transform = `translateY(0)`;
scrollRef.current.style.transition = 'transform 0.3s ease';
setRefresh(false);
setLoading(false);
});
}
useEffect(() => {
const element = scrollRef.current;
element.addEventListener('touchstart', onTouchStart);
element.addEventListener('touchmove', onTouchmove);
element.addEventListener('touchend', onTouchend);
return () => {
element.removeEventListener('touchstart', onTouchStart);
element.removeEventListener('touchmove', onTouchmove);
element.addEventListener('touchend', onTouchend);
};
}, []);
useEffect(() => {
if (refresh && !loading) {
setTimeout(() => {
scrollRef.current.style.transform = `translateY(0)`;
scrollRef.current.style.transition = 'transform 0.3s ease';
setRefresh(false);
}, 500)
}
}, [loading]);
return (
<div ref={scrollRef} >
{refresh && <div className='top-container'>{loading ? '数据刷新中...' : '刷新成功了'}</div>}
<div className='tabbar'>我是导航栏</div>
<div className='content'>
{
Array.from({ length: 10 }, (v, i) => {
return <div key={i} className='content-item'>我是内容{i}</div>
})
}
</div>
</div >
);
};
export default MyComponent;
十二、获取路由参数
const searchPramas = new URLSearchParams(window.location.search);
const all = Object.fromEntries(searchPramas.entries());
const hasRefresh = searchPramas.has('refresh');
const refreshParam = searchPramas.get('refresh');
console.log(refreshParam, 'refreshParam', all, 'all', hasRefresh);
十三、编码的url 中解出参数
首先,我们需要解码整个URL中的%编码,这将解码出所有基本的URL编码字符。
然后,对于query参数的值,我们需要再次解码,因为它内部还包含着URL编码的token和signature。
URLSearchParams 是JavaScript中的一个内置对象,它提供了处理URL查询字符串的方法。当创建一个 URLSearchParams 实例时,可以传入一个查询字符串,该对象会自动解析字符串中的键值对,并将它们存储为可迭代的键值对集合。
// 解码整个URL
let decodedUrl = decodeURIComponent("alipays://platformapi/startapp?appId=xxx&page=/pages/index/index&query=token%3D%252BsoqT7nSP55V2hFvz4c17VRwZORC16i8UlBVUPEJoBGslu5Xg%252B%252FVVXdJMy%252BnZcOnRgVTxzcZb0y2O7NGj8U7XecLkhIfF7%252FBG%252F%252BJgmBy779VZ7En1c6CktgXRIWcL9cPhUakJsujjk8l2%252BRTQe0%252BZ72jDBQa2f0uKnF8shOL5jQd7xlrjpNczKVj3OwWD0Tj%26signature%3DN551LAnjY4d0kMFnT6EUi9V4%252Br8FdCw6oR1TUiXfqJ5z5junPL7wGQGDzm0vQxlpZlbZcU4kcDJ9Jdj4Bt%252BvVpe2w75yTd8bPXtlKipUU2osFqJbX0ymc2T6qFI2Oqd3eW2MchEzcQ2hYTMAkY63KF6Q%252FsOFaQtHZ%252F4apr3GUpWGgMkKzYzBNyDBPTIT4RfPrDmallj3IOAE1%252Fn83uv0arpmw23MehoX3LH%252BYluKqS5V0d8ylZOGU3e%252BaLIchjAREzyYXYICFNsXQiZ7LiPh4aqISo8PS5IwjO7YQH%252BPFx568qxPfO2ocRL3gg1mcOmgQECoJlJYrVpgjuNGjO7SBA%253D%253D");
// 解码query参数
let query = new URLSearchParams(decodedUrl.split('?')[1]);
decodedUrl.split('?')[1]结果:appId=2018120462484211&page=/pages/index/index&query=token=%2BsoqT7nSP55V2hFvz4c17VRwZORC16i8UlBVUPEJoBGslu5Xg%2B%2FVVXdJMy%2BnZcOnRgVTxzcZb0y2O7NGj8U7XecLkhIfF7%2FBG%2F%2BJgmBy779VZ7En1c6CktgXRIWcL9cPhUakJsujjk8l2%2BRTQe0%2BZ72jDBQa2f0uKnF8shOL5jQd7xlrjpNczKVj3OwWD0Tj&signature=N551LAnjY4d0kMFnT6EUi9V4%2Br8FdCw6oR1TUiXfqJ5z5junPL7wGQGDzm0vQxlpZlbZcU4kcDJ9Jdj4Bt%2BvVpe2w75yTd8bPXtlKipUU2osFqJbX0ymc2T6qFI2Oqd3eW2MchEzcQ2hYTMAkY63KF6Q%2FsOFaQtHZ%2F4apr3GUpWGgMkKzYzBNyDBPTIT4RfPrDmallj3IOAE1%2Fn83uv0arpmw23MehoX3LH%2BYluKqS5V0d8ylZOGU3e%2BaLIchjAREzyYXYICFNsXQiZ7LiPh4aqISo8PS5IwjO7YQH%2BPFx568qxPfO2ocRL3gg1mcOmgQECoJlJYrVpgjuNGjO7SBA%3D%3D decodedUrl.split
// 解码query中的token和signature
let decodedToken = decodeURIComponent(query.get('query').split('=')[1]);
let decodedSignature = decodeURIComponent(query.get('signature'));
console.log('Decoded Token:', decodedToken);
console.log('Decoded Signature:', decodedSignature);
十四、前端控制用户操作频率为一天一次
function getNextDayZeroHour() {
const now = new Date();
const tomorrow = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1);
return tomorrow;
}
当用户首次点击按钮时,记录这次点击,并将下次可点击的时间(即第二天0点的时间)存储到LocalStorage或Cookie中。同时,你可以设置一个标志表示按钮已被点击。
function onClickHandler() {
const clickTimeKey = 'clickTime';
const nextDayZeroHour = getNextDayZeroHour().getTime(); // 获取毫秒时间戳
// 检查是否有今天的点击记录,如果没有则允许点击
if (!localStorage.getItem(clickTimeKey) || localStorage.getItem(clickTimeKey) < Date.now()) {
// 存储下次可点击的时间
localStorage.setItem(clickTimeKey, nextDayZeroHour);
// 执行点击后的操作
console.log('Button clicked. Next available click at:', new Date(nextDayZeroHour));
// 实际操作,如发送请求、状态更新等
} else {
// 如果还没到可点击的时间,则阻止操作并提示用户
const nextClickTime = new Date(localStorage.getItem(clickTimeKey));
console.log('Please wait until', nextClickTime, 'to click again.');
}
}
十五、实现弹性可轮播可左右滑动
import React, { useEffect, useRef, useState } from 'react';
import './index.css'; // 假设你有一个外部CSS文件来定义样式
const Slider = () => {
const eleWidth = 150;
const images = [
'1', '2', '3', '4', '5', '6', '1'
];
let timer = null;
const sliderRef = useRef(null);
const [currentIndex, setCurrentIndex] = useState(0)
const [isUsrTouch, setIsUsrTouch] = useState(false);
const [startX, setStartX] = useState(0);
const [diffY, setDiffY] = useState(0);
// 自动切换到下一张的逻辑
useEffect(() => {
if (isUsrTouch) {
clearTimeout(timer);
return
}// 手动模式下,清除自动切换的定时器
timer = setTimeout(() => {
const nextIndex = currentIndex === images.length - 1 ? 0 : currentIndex + 1;
const translateX = nextIndex * -eleWidth + 'px'; // 假设每张图片占据100%的宽度。
sliderRef.current.style.transform = `translateX(${translateX})`;
if (nextIndex === 0) {
sliderRef.current.style.transition = '';//取消过渡视觉上不会出现反向滚动
} else {
sliderRef.current.style.transition = 'transform 0.5s ease-in-out';
}
setCurrentIndex(nextIndex)
}, 1000);
// 清理函数,防止内存泄漏
return () => {
clearTimeout(timer);
};
}, [currentIndex, isUsrTouch]);
const onTouchStar = (e) => {
setIsUsrTouch(true);
setStartX(e.touches[0].clientX)
clearTimeout(timer);
// 启动手动模式,计算滑动的距离,同时滑动元素,滑动的距离除元素宽度等于滑动的元素个数,再这基础上加上原来的索引,更新当前索引
}
const onTouchMove = (e) => {
const diff = e.touches[0].clientX - startX;
setDiffY(diff);
}
const onTouchEnd = (e) => {
let newIndex = currentIndex;
let move = eleWidth;
// 根据滑动方向计算新的索引
if (diffY > 0) {
// 向右滑动
newIndex -= 1;
} else if (diffY < 0) {
// 向左滑动
newIndex += 1;
move = -eleWidth;
}
// 确保索引值在有效范围内
newIndex = Math.min(Math.max(newIndex, 0), images.length - 1);
console.log(diffY, 'DIFF', newIndex);
// 如果目标元素是第一个和最后一个不应该继续滚动
if (newIndex !== 0 && newIndex !== images.length - 1) {
sliderRef.current.style.transition = 'transform 0.5s ease-in-out';
sliderRef.current.style.transform = `translateX(${currentIndex * -eleWidth + move}px)`
}
setCurrentIndex(newIndex);
setIsUsrTouch(false);
}
return (
<div style={{ width: '100%', backgroundColor: 'pink', overflow: 'hidden' }}>
<div style={{ display: 'flex' }} ref={sliderRef} onTouchStart={onTouchStar} onTouchMove={onTouchMove} onTouchEnd={onTouchEnd}>
{images.map((image, index) => (
<div key={index} className={index === currentIndex ? 'item active' : 'item'}>slide {image}</div>
))}
</div>
</div>
);
};
export default function App() {
return (
<div className="App">
<Slider />
</div>
);
}