- 控制多个倒计时同时进行;
- 使用动画框架mTween制作下架动画(缩放+抖动);
- 可修改到期时间;
思路:
1. 布局
2. 设置默认到期时间
3. 倒计时:页面一开始刷新和修改时间后都需要进行倒计时。
因为需要给多个li加定时器,不能只定义一个timer,可以使用li.timer给li加个timer属性的方法记录定时器编号;这样就不会和其他li的定时器编号重复
4. 修改到期时间
5. 到期之后,下架动画
- 1. 显示遮罩
- 2. 抖动:使用动画框架mTween()和shake()方法结合
- 3. 下落消失
- 4. 生成内容
HTML:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<link href="css/index.css" rel="stylesheet" />
</head>
<body>
<h1 id="logo">
<img src="img/miaov.png" />
</h1>
<div class="wrap">
<ul class="shopList">
<li>
<header>
<input type="datetime-local" class="datetime" />
<a class="btn">确定</a>
</header>
<p class="remainingTime">剩余<time><span>0</span><span>0</span>:<span>0</span><span>0</span>:<span>0</span><span>0</span></time></p>
<div class="shop-img">
<img src="img/shop0.jpg" />
</div>
<h2 class="shop-title">Apple iPhone 7 Plus 64g</h2>
<p class="shop-price">抢购价:<span>¥5799</span></p>
<div class="over"></div>
</li>
<li>
<header>
<input type="datetime-local" class="datetime" />
<a class="btn">确定</a>
</header>
<p class="remainingTime">剩余<time><span>0</span><span>0</span>:<span>0</span><span>0</span>:<span>0</span><span>0</span></time></p>
<div class="shop-img">
<img src="img/shop1.jpg" />
</div>
<h2 class="shop-title">27 英寸配备 Retina 5K显示屏</h2>
<p class="shop-price">抢购价:<span>¥15999</span></p>
<div class="over"></div>
</li>
<li>
<header>
<input type="datetime-local" class="datetime" />
<a class="btn">确定</a>
</header>
<p class="remainingTime">剩余<time><span>0</span><span>0</span>:<span>0</span><span>0</span>:<span>0</span><span>0</span></time></p>
<div class="shop-img">
<img src="img/shop2.jpg" />
</div>
<h2 class="shop-title">iPad mini 4</h2>
<p class="shop-price">抢购价:<span>¥1799</span></p>
<div class="over"></div>
</li>
<li>
<header>
<input type="datetime-local" class="datetime" />
<a class="btn">确定</a>
</header>
<p class="remainingTime">剩余<time><span>0</span><span>0</span>:<span>0</span><span>0</span>:<span>0</span><span>0</span></time></p>
<div class="shop-img">
<img src="img/shop3.jpg" />
</div>
<h2 class="shop-title">Apple Watch</h2>
<p class="shop-price">抢购价:<span>¥3799</span></p>
<div class="over"></div>
</li>
</ul>
<section class="overList">
<header>
<h3>商品名称</h3>
<h3>价格</h3>
</header>
<ul class="list">
<!-- <li>
<p>Apple iPhone 7 Plus 64g </p>
<p>¥5799</p>
<div class="shop-img">
<img src="img/shop0.jpg" />
</div>
</li>
-->
</ul>
</section>
</div>
<script src="js/mTween.js"></script>
<script>
/*
1. 布局
2. 设置默认到期时间
3. 倒计时:页面一开始刷新和修改时间后都需要进行倒计时。
因为需要给多个li加定时器,不能只定义一个timer,可以使用li.timer给li加个timer属性的方法记录定时器编号;这样就不会和其他li的定时器编号重复
4. 修改到期时间
5. 到期之后,下架动画
1. 显示遮罩
2. 抖动:使用动画框架mTween()和shake()方法结合
3. 下落消失
4. 生成内容
*/
{
let wrap = document.querySelector(".wrap");
let shopList = wrap.querySelector(".shopList");
let overList = wrap.querySelector(".overList");
let lis = shopList.querySelectorAll("li");
let list = wrap.querySelector(".list");
//时间补零操作
let fillZero = (date) =>{
return date<10?"0"+date:""+date;
}
//生成datetime-local的时间格式:2014-06-01T10:55
let formatToDatetime =(time)=>{
//获取默认时间的年月日
let date = new Date(time);//Tue Aug 06 2019 15:48:18 GMT+0800 (中国标准时间)
let year = date.getFullYear();
let mon = fillZero(date.getMonth() + 1);
let nowDate = fillZero(date.getDate());
let hour = fillZero(date.getHours());
let minute = fillZero(date.getMinutes());
//将时间设置到默认时间区域 2014-06-01T10:55
//注意:datetime-local的时间格式为:2017-06-01T08:30
return year+'-'+mon+'-'+nowDate+'T'+hour+':'+minute;
}
//商品下架
let fullDownGoods = (li)=>{
let over = li.querySelector(".over");
over.style.display = "none";
li.style.opacity = "0";
let shopPrice = li.querySelector(".shop-price span").innerHTML;
let shopTitle = li.querySelector(".shop-title").innerHTML;
list.innerHTML += `
<li>
<p>${shopTitle}</p>
<p>${shopPrice}</p>
<div class="shop-img">
<img src="img/shop0.jpg" />
</div>
</li>
`;
}
//设置商品下架抖动动画
let fullDownAnimation = (li)=>{
let over = li.querySelector(".over");
over.style.display = "block";
//要设置transform相关属性,必须先手动获取
css(over,"translateX",0);
css(over,"scale",1);
//注意mTween()和shake()方法返回的不是同一个Promise对象,所以不能通过异步执行实现
mTween({
el:over,
attr:{
scale:1.1
},
duration:10,
fx:'backIn',
cb:function(){
shake({
el:over,
attr:'translateX',
shakeLength:20,
//在所有动画执行成功后,下架动画
cb:function(){
// 下架:并添加到下架商品处
fullDownGoods(li);
}
});
}
});
}
//设置倒计时span
let countDown = (li,time)=>{
let overTime = time - Date.now();
let timeSpan = li.querySelector(".remainingTime time");
//把剩余的时间转成时分秒
let h = 60*60*1000;
let m = 60*1000;
let s = 1000;
//注意:取模是用总时间戳%对应时间的毫秒数,不足的需要补零
let hour = fillZero(parseInt(overTime/h)).split("");
let min = fillZero(parseInt(overTime%h/m)).split("");
let sec = fillZero(parseInt(overTime%m/s)).split("");
//时间的小时数可能不设置的很大,会超过4位,因此需要获取到各个时间,根据位数动态设置span个数
hourSpan = "<span>"+hour.join("</span><span>")+"</span>:";
minSpan = "<span>"+min.join("</span><span>")+"</span>:";
secSpan = "<span>"+sec.join("</span><span>")+"</span>";
timeSpan.innerHTML = hourSpan+minSpan+secSpan;
}
//设置剩余时间
let setOverTime = (li,time)=>{
//第一次先显示时间,如果已经有定时器先清空定时器
countDown(li,time);
clearInterval(li.timer);
//设置定时器:如果剩余时间小于等于0就直接下架商品,如果大于0继续倒计时
li.timer = setInterval(()=>{
let overTime = time - Date.now();//注意剩余时间需要在定时器里面计算,否则只会计算一次
if(overTime<=0){
clearInterval(li.timer);
//如果到时间了直接执行下架动画
fullDownAnimation(li);
}else{
countDown(li,time);
}
},1000);
}
//设置默认时间
let setDefaultTime =()=>{
//有多个不同的li所以需要循环设置
lis.forEach((item,index)=>{
//默认设置10分钟以内的随机事件
// Math.random()+1让倒计时至少有1分钟 再*就至少有5分钟
mins = Math.round(Math.random()*10) * 60 * 1000;
//计算出默认倒计时的时间毫秒数
let defaultTime = Date.now() + mins;
formatToDatetime(defaultTime);
//设置默认倒计时时间
let datetime = item.querySelector(".datetime");
datetime.value = formatToDatetime(defaultTime);
//页面刷新时就需要设置剩余时间
setOverTime(lis[index],defaultTime);
});
};
setDefaultTime();
//修改到期时间
let updateTime = () =>{
lis.forEach((item,index)=>{
let btn = item.querySelector(".btn");
let datetime = item.querySelector(".datetime");
btn.onclick = () =>{
//获取设置的时间的时间戳
let updateTime = new Date(datetime.value);//2019-08-06T16:42
//将设置的时间戳减去当前时间的时间戳,即为倒计时需要的时间。如果时间戳小于0就代表设置的时间在当前时间以前,就不能进行设置
overTime = updateTime.getTime() - Date.now();//90162
if(overTime<0){
alert("设置时间必须在现在以后");
return;
}
//设置过期时间,lis[index]传入的是具体的某个li
setOverTime(lis[index],updateTime.getTime());
};
});
}
updateTime();
}
</script>
</body>
</html>
CSS:
body {
margin: 0;
background:#e15671 url("../img/bg.png") center center no-repeat;
background-size: 100% 100%;
min-height: 100vh;
overflow: auto;
}
img {
vertical-align: top;
}
h2,
h3,
p {
margin: 0;
}
ul {
margin: 0;
padding: 0;
list-style: none;
}
input,
a {
outline: none;
}
#logo {
padding: 55px 0;
text-align: center;
}
.wrap {
margin: 0 auto;
width: 990px;
overflow: hidden;
}
.shopList {
margin: 0 -5px;
position: relative;
height: 400px;
}
.shopList li {
position: relative;
float: left;
margin: 0 5px;
width: 240px;
height: 400px;
background: #fff;
}
.shopList header {
padding: 8px;
height: 24px;
background: #191919;
}
.shopList .datetime {
float: left;
width: 170px;
height: 24px;
background: none;
border: none;
color: #e15671;
}
.btn {
float: right;
padding: 0 13px;
font: 12px/22px "宋体";
color: #e15671;
border: 1px solid #e15671;
border-radius: 3px;
}
.remainingTime {
margin-top: 30px;
font: 14px/28px "宋体";
text-align: center;
}
.remainingTime time {
display: inline-block;
padding-left: 10px;
font-size: 20px;
vertical-align: top;
color: #e15671;
}
.remainingTime span {
display: inline-block;
width: 20px;
margin: 0 2px;
color: #fff;
background: #e15671;
text-align: center;
perspective-origin: center center;
}
.shop-img {
display: flex;
height: 194px;
}
.shop-img img {
margin: auto;
max-width: 100%;
max-height: 100%;
}
.shop-title {
margin-top: 10px;
font: bold 16px/24px Arial,"微软雅黑";
color: #000;
text-align: center;
}
.shop-price {
font: 14px/46px "宋体";
text-align: center;
color: #8f8f8f;
}
.shop-price span {
font-size: 18px;
color: #e15671;
}
.shopList .over {
display: none;
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background:
rgba(0, 0, 0, .8)
url("../img/over.png")
center center no-repeat;
}
.overList {
margin-top: 10px;
}
.overList header {
height: 40px;
background: #191919;
}
.overList header h3 {
float: left;
font: 14px/40px "宋体";
color: #fff;
}
.overList .list li {
padding: 10px 0;
height: 60px;
background: #fff;
}
.overList .list li:nth-child(2n) {
background: #f6f5f6;
}
.overList .list p {
float: left;
font: bold 16px/60px "宋体";
}
.overList h3:nth-child(1),
.overList .list p:nth-child(1) {
width: 450px;
text-indent: 20px;
}
.overList h3:nth-child(2),
.overList .list p:nth-child(2) {
width: 100px;
text-align: center;
}
.overList .list p:nth-child(2) {
color: #e15671;
}
.overList .shop-img {
float: right;
margin-right: 60px;
width: 50px;
height: 50px;
padding: 4px;
border: 1px solid #bfbfbf;
background: #fff;
}
动画框架:mTween
var Tween = {
linear: function (t, b, c, d){ //匀速
return c*t/d + b;
},
easeIn: function(t, b, c, d){ //加速曲线
return c*(t/=d)*t + b;
},
easeOut: function(t, b, c, d){ //减速曲线
return -c *(t/=d)*(t-2) + b;
},
easeBoth: function(t, b, c, d){ //加速减速曲线
if ((t/=d/2) < 1) {
return c/2*t*t + b;
}
return -c/2 * ((--t)*(t-2) - 1) + b;
},
easeInStrong: function(t, b, c, d){ //加加速曲线
return c*(t/=d)*t*t*t + b;
},
easeOutStrong: function(t, b, c, d){ //减减速曲线
return -c * ((t=t/d-1)*t*t*t - 1) + b;
},
easeBothStrong: function(t, b, c, d){ //加加速减减速曲线
if ((t/=d/2) < 1) {
return c/2*t*t*t*t + b;
}
return -c/2 * ((t-=2)*t*t*t - 2) + b;
},
elasticIn: function(t, b, c, d, a, p){ //正弦衰减曲线(弹动渐入)
if (t === 0) {
return b;
}
if ( (t /= d) == 1 ) {
return b+c;
}
if (!p) {
p=d*0.3;
}
if (!a || a < Math.abs(c)) {
a = c;
var s = p/4;
} else {
var s = p/(2*Math.PI) * Math.asin (c/a);
}
return -(a*Math.pow(2,10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )) + b;
},
elasticOut: function(t, b, c, d, a, p){ //*正弦增强曲线(弹动渐出)
if (t === 0) {
return b;
}
if ( (t /= d) == 1 ) {
return b+c;
}
if (!p) {
p=d*0.3;
}
if (!a || a < Math.abs(c)) {
a = c;
var s = p / 4;
} else {
var s = p/(2*Math.PI) * Math.asin (c/a);
}
return a*Math.pow(2,-10*t) * Math.sin( (t*d-s)*(2*Math.PI)/p ) + c + b;
},
elasticBoth: function(t, b, c, d, a, p){
if (t === 0) {
return b;
}
if ( (t /= d/2) == 2 ) {
return b+c;
}
if (!p) {
p = d*(0.3*1.5);
}
if ( !a || a < Math.abs(c) ) {
a = c;
var s = p/4;
}
else {
var s = p/(2*Math.PI) * Math.asin (c/a);
}
if (t < 1) {
return - 0.5*(a*Math.pow(2,10*(t-=1)) *
Math.sin( (t*d-s)*(2*Math.PI)/p )) + b;
}
return a*Math.pow(2,-10*(t-=1)) *
Math.sin( (t*d-s)*(2*Math.PI)/p )*0.5 + c + b;
},
backIn: function(t, b, c, d, s){ //回退加速(回退渐入)
if (typeof s == 'undefined') {
s = 1.70158;
}
return c*(t/=d)*t*((s+1)*t - s) + b;
},
backOut: function(t, b, c, d, s){
if (typeof s == 'undefined') {
s = 1.70158; //回缩的距离
}
return c*((t=t/d-1)*t*((s+1)*t + s) + 1) + b;
},
backBoth: function(t, b, c, d, s){
if (typeof s == 'undefined') {
s = 1.70158;
}
if ((t /= d/2 ) < 1) {
return c/2*(t*t*(((s*=(1.525))+1)*t - s)) + b;
}
return c/2*((t-=2)*t*(((s*=(1.525))+1)*t + s) + 2) + b;
},
bounceIn: function(t, b, c, d){ //弹球减振(弹球渐出)
return c - Tween['bounceOut'](d-t, 0, c, d) + b;
},
bounceOut: function(t, b, c, d){//*
if ((t/=d) < (1/2.75)) {
return c*(7.5625*t*t) + b;
} else if (t < (2/2.75)) {
return c*(7.5625*(t-=(1.5/2.75))*t + 0.75) + b;
} else if (t < (2.5/2.75)) {
return c*(7.5625*(t-=(2.25/2.75))*t + 0.9375) + b;
}
return c*(7.5625*(t-=(2.625/2.75))*t + 0.984375) + b;
},
bounceBoth: function(t, b, c, d){
if (t < d/2) {
return Tween['bounceIn'](t*2, 0, c, d) * 0.5 + b;
}
return Tween['bounceOut'](t*2-d, 0, c, d) * 0.5 + c*0.5 + b;
}
};
(function(){
if(!window.requestAnimationFrame){
var lastTime = 0;
window.requestAnimationFrame = function(callback){
var nowTime = Date.now();
var dely = Math.max(0,16.7 - (nowTime - lastTime));
lastTime = nowTime;
return setTimeout(callback,dely);
};
window.cancelAnimationFrame = function(index){
clearTimeout(index);
};
}
})();
var transformAttr = [
"rotate",
"rotateX",
"rotateY",
"rotateZ",
"translateX",
"translateY",
"translateZ",
"scale",
"scaleX",
"scaleY",
"skewX",
"skewY"
];
var normalAttr = [
"width",
"height",
"left",
"top",
"right",
"bottom",
"marginBottom",
"marginleft",
"marginRight",
"marginTop",
"paddingLeft",
"paddingRight",
"paddingTop",
"paddingBottom"
];
function css(el,attr,val){
if(typeof attr == "object"){
for(var s in attr){
css(el,s,attr[s]);
}
return ;
}
if(transformAttr.indexOf(attr) >= 0){
return setTransform(el,attr,val);
}
if(val === undefined){
val = getComputedStyle(el)[attr];
return normalAttr.indexOf(attr)>=0||!isNaN(val)?parseFloat(val):val;
} else {
if(attr == "opacity"){
el.style[attr] = val;
el.style.filter = "alpha(opacity="+(val*100)+")";
} else if(normalAttr.indexOf(attr)>=0) {
el.style[attr] = val + "px";
} else if(attr == "zIndex") {
el.style[attr] = Math.round(val);
} else {
el.style[attr] = val;
}
}
}
function setTransform(el,attr,val){
el.transform = el.transform||{};
if(val === undefined){
return el.transform[attr];
}
el.transform[attr] = val;
var transformVal = "";
for(var s in el.transform){
switch(s){
case "rotate":
case "rotateX":
case "rotateY":
case "rotateZ":
case "skewX":
case "skewY":
transformVal += s+'('+ el.transform[s]+'deg) ';
break;
case "translateX":
case "translateY":
case "translateZ":
transformVal += s+'('+ el.transform[s]+'px) ';
break;
case "scale":
case "scaleX":
case "scaleY":
transformVal += s+'('+ el.transform[s]+') ';
break;
}
}
el.style.WebkitTransform = el.style.transform = transformVal.trim();
}
function mTween(op){
return new Promise((resolve,reject)=>{
var el = op.el,
attr = op.attr,
fx = op.fx||"easeOut",
duration = op.duration||400,
maxC = 0;
if(el.animationTimer){
return;
}
var t = 0;
var b = {};
var c = {};
for(var s in attr){
b[s] = css(el,s);
c[s] = attr[s] - b[s];
maxC = Math.max(maxC,Math.abs(c[s]));
}
if(typeof duration === "object"){
var durationOption = duration;
durationOption.multiple = durationOption.multiple||2;
duration = maxC * duration.multiple;
duration = durationOption.max?Math.min(duration,durationOption.max):duration;
duration = durationOption.min?Math.max(duration,durationOption.min):duration;
}
var d = Math.ceil(duration/(1000/60));
move();
function move(){
el.animationTimer = requestAnimationFrame(function(){
t++;
if(t > d){
el.animationTimer = null;
//虽然返回Promise对象,也让其可以执行回调,使得同时使用mTween()和shake()方法时,可回调
op.cb&&op.cb();
resolve();
} else {
for(var s in attr){
var val = Tween[fx](t,b[s],c[s],d);
css(el,s,val);
}
move();
}
});
}
});
}
mTween.stop = function(el){
cancelAnimationFrame(el.animationTimer);
el.animationTimer = null;
};
function shake(op){
return new Promise((resolve,reject)=>{
var el = op.el,
attr = op.attr,
shakeLength = op.shakeLength||15,
shakeArr = [];
el.shakeStart = {};
if(el.shake) {
return ;
}
if(typeof attr === "object" ){
for(var i = 0; i < attr.length; i++){
el.shakeStart[attr[i]] = css(el,attr[i]);
}
} else {
el.shakeStart[attr] = css(el,attr);
}
for(var i = shakeLength; i >= 0; i--){
shakeArr.push(i%2?i:-i);
}
move();
function move(){
el.shake = requestAnimationFrame(function(){
if(shakeArr.length <= 0){
el.shake = false;
op.cb&&op.cb();
resolve();
} else {
var nub = shakeArr.shift();
for(var s in el.shakeStart){
css(el,s, el.shakeStart[s] + nub);
}
move();
}
});
}
});
}
shake.stop = function(el){
cancelAnimationFrame(el.shake);
el.shake = false;
for(var s in el.shakeStart){
css(el,s, el.shakeStart[s]);
}
};