基于vue2实现的京东商城,现展示几个比较重要的模块。
tabbar功能,首页轮播图功能、首页倒计时秒杀功能,列表页等。
TabBar.vue
<template>
<div id="tab-bar">
<slot>slot>
div>
template>
<script>
export default {
name: 'TabBar'
}
script>
<style>
#tab-bar {
display: flex;
background-color: rgb(255, 255, 255);;
position: fixed;
left: 0;
right: 0;
bottom: 0;
box-shadow: 0 0 10px 0 hsl(0deg 6% 58% / 60%);
}
style>
TabBarItem
<template>
<div class="tab-bar-item" @click="itemClick">
<div v-if="!isActive">
<slot name="item-icon">slot>
div>
<div v-else>
<slot name="item-icon-active">slot>
div>
div>
template>
<script>
export default {
name: 'TabBarItem',
props: {
path: String,
},
computed: {
isActive() {
return this.$route.path.indexOf(this.path) !== -1
}
},
methods: {
itemClick() {
this.$router.replace(this.path)
}
}
}
script>
<style>
.tab-bar-item {
flex: 1;
text-align: center;
height: 49px;
font-size: 14px;
}
.tab-bar-item img {
width: 50%;
height: 50%;
margin-top: 3px;
vertical-align: middle;
margin-bottom: 2px;
}
style>
MainTabBar
<template>
<tab-bar>
<tab-bar-item path="/home">
<img slot="item-icon" src="~assets/img/tabbar/home.png" alt="">
<img slot="item-icon-active" src="~assets/img/tabbar/home_active.png" alt="">
tab-bar-item>
<tab-bar-item path="/category">
<img slot="item-icon" src="~assets/img/tabbar/category.png" alt="">
<img slot="item-icon-active" src="~assets/img/tabbar/category_active.png" alt="">
tab-bar-item>
<tab-bar-item path="/user">
<img slot="item-icon" src="~assets/img/tabbar/user.png" alt="">
<img slot="item-icon-active" src="~assets/img/tabbar/user_active.png" alt="">
tab-bar-item>
tab-bar>
template>
<script>
import TabBar from '@/components/common/tabbar/TabBar.vue';
import TabBarItem from '@/components/common/tabbar/TabBarItem.vue';
export default {
name: 'MainTabBar',
components: {
TabBar,
TabBarItem
},
}
script>
<style>
style>
路由router
import Vue from "vue";
import VueRouter from "vue-router"
Vue.use(VueRouter)
const Home=()=>import('views/home/Home')
const Category=()=>import('views/category/Category')
const User=()=>import('views/user/User')
const routes=[
{
path:'',
redirect:'/home'
},
{
path:'/home',
component:Home
},
{
path:'/category',
component:Category
},
{
path:'/user',
component:User
},
]
const router= new VueRouter({
routes,
mode:'history'
})
export default router
Swiper
<template>
<div id="hy-swiper">
<div class="swiper" @touchstart="touchStart" @touchmove="touchMove" @touchend="touchEnd">
<slot>slot>
div>
<slot name="indicator">
slot>
<div class="indicator">
<slot name="indicator" v-if="showIndicator && slideCount>1">
<div v-for="(item, index) in slideCount" class="indi-item" :class="{active: index === currentIndex-1}" :key="index">div>
slot>
div>
div>
template>
<script>
export default {
name: "Swiper",
props: {
interval: {
type: Number,
default: 3000
},
animDuration: {
type: Number,
default: 300
},
moveRatio: {
type: Number,
default: 0.25
},
showIndicator: {
type: Boolean,
default: true
}
},
data: function () {
return {
slideCount: 0, // 元素个数
totalWidth: 0, // swiper的宽度
swiperStyle: {}, // swiper样式
currentIndex: 1, // 当前的index
scrolling: false, // 是否正在滚动
}
},
mounted: function () {
// 1.操作DOM, 在前后添加Slide
setTimeout(() => {
this.handleDom();
// 2.开启定时器
this.startTimer();
}, 100)
},
methods: {
/**
* 定时器操作
ds* 5
*/
startTimer: function () {
this.playTimer = window.setInterval(() => {
this.currentIndex++;
this.scrollContent(-this.currentIndex * this.totalWidth);
}, this.interval)
},
stopTimer: function () {
window.clearInterval(this.playTimer);
},
/**
* 滚动到正确的位置
*/
scrollContent: function (currentPosition) {
// 0.设置正在滚动
this.scrolling = true;
// 1.开始滚动动画
this.swiperStyle.transition ='transform '+ this.animDuration + 'ms';
this.setTransform(currentPosition);
// 2.判断滚动到的位置
this.checkPosition();
// 4.滚动完成
this.scrolling = false
},
/**
* 校验正确的位置
*/
checkPosition: function () {
window.setTimeout(() => {
// 1.校验正确的位置
this.swiperStyle.transition = '0ms';
if (this.currentIndex >= this.slideCount + 1) {
this.currentIndex = 1;
this.setTransform(-this.currentIndex * this.totalWidth);
} else if (this.currentIndex <= 0) {
this.currentIndex = this.slideCount;
this.setTransform(-this.currentIndex * this.totalWidth);
}
// 2.结束移动后的回调
this.$emit('transitionEnd', this.currentIndex-1);
}, this.animDuration)
},
/**
* 设置滚动的位置
*/
setTransform: function (position) {
this.swiperStyle.transform = `translate3d(${position}px, 0, 0)`;
this.swiperStyle['-webkit-transform'] = `translate3d(${position}px), 0, 0`;
this.swiperStyle['-ms-transform'] = `translate3d(${position}px), 0, 0`;
},
/**
* 操作DOM, 在DOM前后添加Slide
*/
handleDom: function () {
// 1.获取要操作的元素
let swiperEl = document.querySelector('.swiper');
let slidesEls = swiperEl.getElementsByClassName('slide');
// 2.保存个数
this.slideCount = slidesEls.length;
// 3.如果大于1个, 那么在前后分别添加一个slide
if (this.slideCount > 1) {
let cloneFirst = slidesEls[0].cloneNode(true);
let cloneLast = slidesEls[this.slideCount - 1].cloneNode(true);
swiperEl.insertBefore(cloneLast, slidesEls[0]);
swiperEl.appendChild(cloneFirst);
this.totalWidth = swiperEl.offsetWidth;
this.swiperStyle = swiperEl.style;
}
// 4.让swiper元素, 显示第一个(目前是显示前面添加的最后一个元素)
this.setTransform(-this.totalWidth);
},
/**
* 拖动事件的处理
*/
touchStart: function (e) {
// 1.如果正在滚动, 不可以拖动
if (this.scrolling) return;
// 2.停止定时器
this.stopTimer();
// 3.保存开始滚动的位置
this.startX = e.touches[0].pageX;
},
touchMove: function (e) {
// 1.计算出用户拖动的距离
this.currentX = e.touches[0].pageX;
this.distance = this.currentX - this.startX;
let currentPosition = -this.currentIndex * this.totalWidth;
let moveDistance = this.distance + currentPosition;
// 2.设置当前的位置
this.setTransform(moveDistance);
},
touchEnd: function (e) {
// 1.获取移动的距离
let currentMove = Math.abs(this.distance);
// 2.判断最终的距离
if (this.distance === 0) {
return
} else if (this.distance > 0 && currentMove > this.totalWidth * this.moveRatio) { // 右边移动超过0.5
this.currentIndex--
} else if (this.distance < 0 && currentMove > this.totalWidth * this.moveRatio) { // 向左移动超过0.5
this.currentIndex++
}
// 3.移动到正确的位置
this.scrollContent(-this.currentIndex * this.totalWidth);
// 4.移动完成后重新开启定时器
this.startTimer();
},
/**
* 控制上一个, 下一个
*/
previous: function () {
this.changeItem(-1);
},
next: function () {
this.changeItem(1);
},
changeItem: function (num) {
// 1.移除定时器
this.stopTimer();
// 2.修改index和位置
this.currentIndex += num;
this.scrollContent(-this.currentIndex * this.totalWidth);
// 3.添加定时器
this.startTimer();
}
}
}
script>
<style scoped>
#hy-swiper {
overflow: hidden;
position: relative;
}
.swiper {
display: flex;
}
.indicator {
display: flex;
justify-content: center;
position: absolute;
width: 100%;
bottom: 8px;
}
.indi-item {
box-sizing: border-box;
width: 5px;
height: 5px;
border-radius: 4px;
background-color: hsla(0,0%,100%,.6);
line-height: 8px;
text-align: center;
font-size: 12px;
margin: 0px 2px;
}
.indi-item.active {
background-color: rgba(212,62,46,1.0);
}
style>
SwiperItem
<template>
<div class="slide">
<slot>slot>
div>
template>
<script>
export default {
name: "Slide"
}
script>
<style scoped>
.slide {
width: 100%;
flex-shrink: 0;
}
.slide img {
width: 100%;
}
style>
HomeSwiper
<template>
<swiper class="swiper-s">
<swiper-item v-for="item in banners" class="swiper-item">
<a href="javascript:;" class="slide-a">
<img :src="item.image" alt="">a>
swiper-item>
swiper>
template>
<script>
import { Swiper,SwiperItem } from '@/components/common/swiper';
export default {
name: 'HomeSwiper',
props:{
banners:{
type:Array,
default() {
return []
}
}
},
components:{
Swiper,
SwiperItem
},
}
script>
<style>
.swiper-s {
position: relative;
top: 43px;
height: 134px;
}
.slide-a {
display: block;
overflow: hidden;
width: calc(100% - 1.25rem);
height: 134px;
margin: 0 auto;
/* top: 2.25rem; */
position: relative;
border-radius: 0.35rem;
}
.swiper-item {
height: 100%;
position: relative;
}
style>
floor-container
<template>
<div class="floor-container">
<ul class="seckill-new-list">
<li class="seckill-new-item" v-for="item in skgoods">
<div class="seckill-item-img">
<a href="javascript:;" class="seckill-new-link">
<img :src="item.image" alt="">
a>
div>
<div class="seckill-item-price">
<span class="seckill-new-price">
<em>¥em>
<span class="j_init_price">{{item.price}}span>
span>
div>
li>
ul>
div>
template>
<script>
export default {
name: 'FloorContainer',
props:{
skgoods:{
type:Array,
default() {
return []
}
}
}
}
script>
<style>
.floor-container {
overflow-x: scroll;
overflow-y: hidden;
width: calc(100% - 0.8rem);
margin: 0 auto;
font-size: 0;
overflow: auto;
padding: 0 1px;
}
.floor-container::-webkit-scrollbar{
display: none;
}
.seckill-new-list {
position: relative;
background-color: #fff;
width: 609.333px;
overflow-y: hidden;
}
.seckill-new-item {
margin-top: 0.62rem;
margin-bottom: 0.62rem;
float: left;
}
.seckill-item-img, .seckill-item-price {
width: 100%;
padding: 0 0.062rem;
}
.seckill-item-price {
margin: 0 auto;
text-align: center;
}
.seckill-new-link {
display: block;
position: relative;
width: 56px;
}
.floor-container img {
width: 100%;
height: auto;
overflow: hidden;
min-height: 1px;
min-width: 1px;
}
.seckill-item-price {
margin: 0 auto;
text-align: center;
width: 100%;
padding: 0 0.05rem;
}
.seckill-new-price {
margin-top: 0.5rem;
display: block;
color: #f2270c;
font-size: .65rem;
line-height: .6rem;
height: 0.6rem;
text-align: center;
}
.seckill-new-price em {
font-size: 11px;
padding-right: 2px;
font-style:normal;
}
style>
floor-title
<template>
<div class="title-wrap">
<a href="javascript:;" class="seckill-left-link">
<div class="seckill-tit-img">京东秒杀div>
<div class="seckill-timer-wrap">
<div class="seckill-nth">
{{time}}
div>
<div class="seckill_timer">
<div class="seckill-time j_sk_h">{{hour}}div>
<span class="seckill-time-separator">:span>
<div class="seckill-time j_sk_m">{{minute}}div>
<span class="seckill-time-separator">:span>
<div class="seckill-time j_sk_s">{{second}}div>
div>
div>
a>
<a href="javascript:;" class="seckill-more-link">
爆款轮番秒
<i class="seckill-more-icon">i>
a>
div>
template>
<script>
export default {
name: 'FloorTitle',
data(){
return {
date:new Date(),
hours:this.time-new Date().getHours()-1,
minutes:60-new Date().getMinutes()-1,
seconds:60-new Date().getSeconds(),
}
},
mounted() {
this.add();
},
props:{
time:Number
},
methods:{
num(n) {
return n < 10 ? '0' + n : ''+n
},
add() {
let time = window.setInterval(()=>{
if(this.hours !== 0&&this.minutes === 0&&this.seconds === 0){
this.hours-=1;
this.minutes=59;
this.seconds=59;
}else if(this.hours === 0&&this.minutes !== 0&&this.seconds===0){
this.minutes-=1;
this.seconds=59;
}else if(this.hours === 0&& this.minutes ===0 &&this.seconds===0){
this.seconds = 0
window.clearInterval(time)
}else if(this.hours!== 0&&this.minutes!==0&&this.seconds===0){
this.minutes-=1;
this.seconds=59;
}else {
this.seconds-=1;
}
},1000)
}
},
watch:{
second:{
handler(newVal) {
this.num(newVal)
}
},
minute:{
handler(newVal) {
this.num(newVal)
}
},
hour: {
handler(newVal){
this.num(newVal)
}
}
},
computed :{
second() {
return this.num(this.seconds)
},
minute() {
return this.num(this.minutes)
},
hour() {
return this.num(this.hours)
}
}
}
script>
<style scoped>
.title-wrap {
background: url(~assets/img/home/seckill/bac.png) no-repeat;
background-size: contain;
height: 2.1rem;
line-height: 2.1rem;
vertical-align: middle;
}
.seckill-left-link {
display: inline-block;
float: left;
}
.seckill-tit-img {
float: left;
display: inline-block;
height: 1.66rem;
margin-top: 0.5rem;
margin-right: 0.31rem;
margin-left: 0.6rem;
font-family: PingFangSC-Semibold;
font-size: 0.9rem;
color: #333;
letter-spacing: 0;
line-height: 0.9rem;
}
.seckill-timer-wrap {
display: inline-block;
border-radius: 0.4rem;
height: 1.1rem;
line-height: 1.1rem;
font-size: 0;
float: left;
margin-top: 0.5rem;
vertical-align: middle;
}
.seckill-nth {
height: 100%;
border-radius: 0.4rem;
font-size: .85rem;
color: #ff2727;
letter-spacing: 0;
padding-right: 0.31rem;
padding-left: 0.31rem;
float: left;
position: relative;
left: -1px;
margin-right: 0.25rem;
line-height: 1.2;
}
.seckill-nth::after {
height: 1.08rem;
width: 1.27rem;
content: "";
display: inline-block;
vertical-align: middle;
background-image: url(~assets/img/home/seckill/bac2.png);
background-repeat: no-repeat;
background-position: 50%;
background-size: contain;
}
.seckill_timer {
margin-right: 0.2rem;
height: 100%;
float: right;
}
.seckill-time {
width: 0.92rem;
color: #fff;
background-image: linear-gradient(-140deg, #ff6152, #fa2c19);
background-color: #fa2c19;
border-radius: 0.2rem;
text-align: center;
font-weight: 400;
float: left;
display: inline-block;
height: 100%;
line-height: 1.4;
font-size: .6rem;
font-family: PingFangSC-Regular;
letter-spacing: 0;
}
.seckill-time-separator {
float: left;
display: inline-block;
height: 100%;
line-height: 1.4;
font-size: .6rem;
text-align: center;
font-family: PingFangSC-Regular;
color: #f2270c;
letter-spacing: 0;
font-weight: 700;
}
.seckill-more-link {
display: inline-block;
color: #f23030;
float: right;
font-size: .6rem;
text-align: right;
position: relative;
padding-right: 1.4rem;
}
.seckill-more-icon {
display: inline-block;
width: 0.68rem;
height: 0.68rem;
background: url(~assets/img/home/seckill/title_more.png) no-repeat;
background-size: cover;
position: absolute;
right: 0.491rem;
top: 0.61rem;
}
style>
category-body
<template>
<div class="category-viewport">
<cate-list :catetab="catetab" @tabClick="tabClick" ref="tabControl">cate-list>
<cate-goods :goods="showGoods"
>cate-goods>
div>
template>
<script>
import CateGoods from '@/components/content/category/CateGoods.vue';
import CateList from '@/components/content/category/CateList.vue';
export default {
name: 'CateGoryBody',
components: {
CateGoods,
CateList
},
data() {
return {
catetab: ['热门推荐', '手机数码', '京东超市', '家用电器',
'电脑办公', '玩具乐器', '家居厨具', '家具家装', '男装', '女装',
'女鞋', '美妆护肤', '医药健保', '酒水饮料', '运动户外', '汽车生活', '礼品鲜花'],
currentNum:'0',
goods:{
'0':{list:[
{
img:require('../../../assets/img/category/0/1.jpg'),
title:'空调'
},
{
img:require('../../../assets/img/category/0/2.jpg'),
title:'冰箱'
},
{
img:require('../../../assets/img/category/0/3.png'),
title:'电脑'
},
{
img:require('../../../assets/img/category/0/4.png'),
title:'手机'
},
{
img:require('../../../assets/img/category/0/5.jpg'),
title:'全面屏手机'
},
{
img:require('../../../assets/img/category/0/6.jpg'),
title:'保健品'
},
{
img:require('../../../assets/img/category/0/7.jpg'),
title:'游戏手机'
},
{
img:require('../../../assets/img/category/0/8.jpg'),
title:'口罩'
},
{
img:require('../../../assets/img/category/0/9.jpg'),
title:'驱蚊用品'
},
{
img:require('../../../assets/img/category/0/10.jpg'),
title:'电磁炉'
},
{
img:require('../../../assets/img/category/0/11.jpg'),
title:'电热水壶'
},
{
img:require('../../../assets/img/category/0/12.jpg'),
title:'数据线'
},
{
img:require('../../../assets/img/category/0/13.jpg'),
title:'图书'
},
{
img:require('../../../assets/img/category/0/14.jpg'),
title:'美妆护肤'
},
{
img:require('../../../assets/img/category/0/15.jpg'),
title:'除菌液'
},
{
img:require('../../../assets/img/category/0/16.jpg'),
title:'休闲零食'
},
{
img:require('../../../assets/img/category/0/17.jpg'),
title:'充电包'
},
{
img:require('../../../assets/img/category/0/18.jpg'),
title:'体温计'
},
{
img:require('../../../assets/img/category/0/19.jpg'),
title:'投影机'
},
{
img:require('../../../assets/img/category/0/20.jpg'),
title:'游戏机'
},
]},
'1':{list:[
{
img:require('../../../assets/img/category/0/1.jpg')
}
]},
'2':{list:[
{
img:require('../../../assets/img/category/0/2.jpg')
}
]},
'3':{list:[
{
img:require('../../../assets/img/category/0/3.png')
}
]},
'4':{list:[
{
img:require('../../../assets/img/category/0/4.png')
}
]},
'5':{list:[
{
img:require('../../../assets/img/category/0/5.jpg')
}
]},
'6':{list:[
{
img:require('../../../assets/img/category/0/6.jpg')
}
]},
'7':{list:[
{
img:require('../../../assets/img/category/0/7.jpg')
}
]},
'8':{list:[
{
img:require('../../../assets/img/category/0/8.jpg')
}
]},
'9':{list:[
{
img:require('../../../assets/img/category/0/9.jpg')
}
]},
'10':{list:[
{
img:require('../../../assets/img/category/0/10.jpg')
}
]},
'11':{list:[
{
img:require('../../../assets/img/category/0/11.jpg')
}
]},
'12':{list:[
{
img:require('../../../assets/img/category/0/12.jpg')
}
]},
'13':{list:[
{
img:require('../../../assets/img/category/0/13.jpg')
}
]},
'14':{list:[
{
img:require('../../../assets/img/category/0/14.jpg')
}
]},
'15':{list:[
{
img:require('../../../assets/img/category/0/15.jpg')
}
]},
'16':{list:[
{
img:require('../../../assets/img/category/0/16.jpg')
}
]}
}
}
},
computed: {
showGoods() {
return this.goods[this.currentNum].list
}
},
methods :{
tabClick(index) {
console.log(this.catetab.length);
for(let i=0;i<this.catetab.length;i++){
if(index===i){
this.currentNum=index
break
}
}
this.$refs.tabControl.currentIndex = index;
}
}
}
script>
<style scoped>
.category-viewport {
height: auto;
min-height: 100%;
margin-top: 45px;
}
style>