使用VUE构建商品界面
index.html<!DOCTYPE html><html><head><meta charset="utf-8"><title>Webpack App</title><link rel=&quo
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Webpack App</title>
<link rel="stylesheet" type="text/css" href="/dist/main.css">
</head>
<body>
<div id="app">
</div>
<script type="text/javascript" src="/dist/main.js"></script>
</body>
</html>
商品列表页主要有两个模块,
路由组件views / list.vue,负责数据的请求,国剧相关逻辑。
另一个是商品简介组件的组件/ product.vue,鼠标经过时,显示加入购物车的按钮;
prodect.vue
首先分析接收的数据结构:
//信息
{
ID:1,
名称: 'Airpods',
品牌:“苹果”,
图片: 'HTTP://ordfm6aah.bkt.clouddn.com/shop/1.jpeg',
销售:10000,
费用:1288
颜色: '白色'
}
根据产品数据在素文字进行配置,因为颜色比较特殊,中文中无法对应具体的色值,所以在数据中定义一个映射,用于映射颜色和色值。
export default {
/*
info数据格式如下:
{
id:1,
name:'Airpods',
brand:'Apple',
image:'http://ordfm6aah.bkt.clouddn.com/shop/1.jpeg',
sales:10000,
cost:1288,
color:'白色'
}
*/
props:{info:Object},
data(){
return{
colors:{
'白色':'#ffffff',
'金色':'#dac272',
'蓝色':'#233472',
'红色':'#2352e'
}
}
}
}
鼠标悬停在卡片上时候,右上角会显示“加入购物车”按钮,
product.vue
<template>
<div class="product">
<router-link
:to="'/product/'+info.id"
class="product-main">
<img :src="info.image">
<h4>{{info.name}}</h4>
<div class="product-color"
:style="{background:colors[info.color]}">
<!--
中括号内部根据色号匹配CSS样式
-->
</div>
<div class="product-cost">
¥{{info.cost}}
</div>
<div
class="product-add-cart"
@click.prevent="handleCart"
>
加入购物车
</div>
</router-link>
</div>
</template>
<script>
export default {
/*
info数据格式如下:
{
id:1,
name:'Airpods',
brand:'Apple',
image:'http://ordfm6aah.bkt.clouddn.com/shop/1.jpeg',
sales:10000,
cost:1288,
color:'白色'
}
*/
props:{info:Object},
data(){
return{
colors:{
'白色':'#ffffff',
'金色':'#dac272',
'蓝色':'#233472',
'红色':'#2352e'
}
}
},
methods:{
hadleCart(){
//VUEX共用数据调用,提交ID
this.$store.commit('addCart',this.info.id);
}
}
}
</script>
<style scoped>
.product{
width:25%;
float:left;
}
.product-main{
display:block;
margin:16px;
padding:16px;
border:1px solid #dddee1;
border-radius:6px;
overflow:hidden;
background:#fff;
text-align:center;
position:relative;
}
.product-main img{
width:100%;
}
h4{
color:#222;
overflow:hidden;
text-overflow:hidden;
white-space:nowrap
}
.product-main:hover h4{
color:#0070c9
}
.product-color{
display:block;
width:16px;
height:16px;
border:1px solid #dddee1;
border-radius:50%;
margin:6px auto;
}
.prodyct-cost{
color:#de4037;
margin-top:6px;
}
.product-add-cart{
display:none;
padding:4px 8px;
background:#2d8cf0;
color:#fff;
font-size:12px;
border-radius:3px;
cursor:pointer;
position:absolute;
top:5px;
right:5px;
}
.product-main:hover .product-add-cart{
display:inline-block;
}
</style>
产品数据:(异步模拟)
product.js
export default [
{
id: 1,
name: 'AirPods',
brand: 'Apple',
image: 'http://ordfm6aah.bkt.clouddn.com/shop/1.jpeg',
sales: 10000,
cost: 1288,
color: '白色'
},
{
id: 2,
name: 'BeatsX 入耳式耳机',
brand: 'Beats',
image: 'http://ordfm6aah.bkt.clouddn.com/shop/2.jpeg',
sales: 11000,
cost: 1188,
color: '白色'
},
{
id: 3,
name: 'Beats Solo3 Wireless 头戴式式耳机',
brand: 'Beats',
image: 'http://ordfm6aah.bkt.clouddn.com/shop/3.jpeg',
sales: 5000,
cost: 2288,
color: '金色'
},
{
id: 4,
name: 'Beats Pill+ 便携式扬声器',
brand: 'Beats',
image: 'http://ordfm6aah.bkt.clouddn.com/shop/4.jpeg',
sales: 3000,
cost: 1888,
color: '红色'
},
{
id: 5,
name: 'Sonos PLAY:1 无线扬声器',
brand: 'Sonos',
image: 'http://ordfm6aah.bkt.clouddn.com/shop/5.jpeg',
sales: 8000,
cost: 1578,
color: '白色'
},
{
id: 6,
name: 'Powerbeats3 by Dr. Dre Wireless 入耳式耳机',
brand: 'Beats',
image: 'http://ordfm6aah.bkt.clouddn.com/shop/6.jpeg',
sales: 12000,
cost: 1488,
color: '金色'
},
{
id: 7,
name: 'Beats EP 头戴式耳机',
brand: 'Beats',
image: 'http://ordfm6aah.bkt.clouddn.com/shop/7.jpeg',
sales: 25000,
cost: 788,
color: '蓝色'
},
{
id: 8,
name: 'B&O PLAY BeoPlay A1 便携式蓝牙扬声器',
brand: 'B&O',
image: 'http://ordfm6aah.bkt.clouddn.com/shop/8.jpeg',
sales: 15000,
cost: 1898,
color: '金色'
},
{
id: 9,
name: 'Bose® QuietComfort® 35 无线耳机',
brand: 'Bose',
image: 'http://ordfm6aah.bkt.clouddn.com/shop/9.jpeg',
sales: 14000,
cost: 2878,
color: '蓝色'
},
{
id: 10,
name: 'B&O PLAY Beoplay H4 无线头戴式耳机',
brand: 'B&O',
image: 'http://ordfm6aah.bkt.clouddn.com/shop/10.jpeg',
sales: 9000,
cost: 2298,
color: '金色'
}
]
list组件:
<template>
<div v-show="list.length">
<div class="list-control">
<span>排序</span>
<span class="list-control-order-item"
:class="{on:order===''}"
@click="handleOrderDefault"
>默认</span>
<span
class="list-control-order-item"
:class="{on:order==='sales'}"
@click="handleOrderSales"
>销量
<template v-if="order==='sales'">↓</template>
</span>
<span
class="list-control-order-item"
:class="{on:order.indexOf('cost')>-1}"
@click="handleOrderCost"
>
价格
<template v-if="order==='cost-asc'"
>↑</template>
<template v-if="order==='cost-desc'">↓</template>
</span>
</div>
<Product v-for="item in list" :info="item" :key="item.id">
</Product>
<div class="product-not-found"
v-show="!filteredAndOrderedList.length"
>
暂无相关商品
</div>
</div>
</template>
<script>
import Product from '../components/product.vue';
export default {
components:{Product},
data(){
return{
//排序依据
//sales(销量))
//cost-desc(价格降序)
//cost-asc(价格升序)
order:''
}
},
methods:{
handleOrderDefault(){
this.order='';
},
handleOrderSales(){
this.order="sales";
},
handleOrderCost(){
if(this.order==='cost-desc'){
this.order='cost-asc';
}else{
this.order='cost-desc';
}
}
},
computed:{
list(){
//从Vuex获取商品列表数据
return this.$store.state.productList;
},
filteredAndOrderedList(){
//复制原始数据
let list=[...this.list];
//todo按品牌过滤
//todo按颜色过滤
//排序
if(this.order!==''){
if(this.order==='sales'){
list=list.sort((a,b)=>b.sales-a.sales);
}else if(this.order==='cost-desc'){
list=list.sort((a,b)=>b.cost-a.cost);
}else if(this.order==='cost-asc'){
list=list.sort((a,b)=>a.cost-b.cost);
}
}
return list;
}
},
mounted(){
//初始化时,通过Vuex的action请求数据
this.$store.dispath('getProductList');
}
}
</script>
<style lang="scss" scoped>
.product-not-found{
text-align: center;
padding: 32px;
}
.list-control{
background: #fff;
border-radius: 6px;
margin:16px;
padding: 16px;
box-shadow: 0 1px 1px rgba(0,0,0,2);
}
.list-control-filter{
margin-bottom: 16px;
}
.list-control-filter-item,
.list-control-order-item{
cursor: pointer;
display: inline-block;
border: 1px solid #e9eaec;
border-radius: 4px;
margin-right: 6px;
padding: 2px 6px;
}
.list-control-filter-item.on,
.list-control-order-item.on{
background:#f2352e;
border:1px solid #f2352e;
color: #fff;
}
</style>
在router.js中配置list的路由
const routers=[
{
path:'/list',
meta:{
title:'商品列表'
},
component:(resolve)=>require(['./views/list.vue'],resolve)
},
{
path:'*',
redirect:'/list'
}
];
export default routers;
在main.js中导入产品数据
import product_data from './product.js';
const store=new Vuex.Store({
state:{
//商品列表数据
productList:[],
//购物车shujv
cartList:[]
},
getters:{
brands:state=>{
const brands=state.productList.map(item=>item.brand);
}
},
mutations:{
//添加商品列表
setProductList(state,data){
state.productList=data;
}
},
actions:{
//请求商品列表
getProductList(context){
//真实环境通过Ajax获取,这里用异步模拟
setTimeout(()=>{
context.commit('setProductList',product_data);
//product_data是异步从product.js获取的数据
})
}
在APP.vue中挂载路由并设置导航条
<template>
<div>
<div class="header">
<router-link
to="./list"
class="header-title">电商网站示例</router-link>
<div class="header-menu">
<router-link to="/cart" class="header-menu-cart">
购物车
<span v-if="cartList.length">{{cartList.length}}</span>
</router-link>
</div>
</div>
<router-view></router-view>
</div>
</template>
<script>
export default {
computed:{
cartList(){
return this.$store.state.cartList;
}
}
}
</script>
全局设置css:
style.css
*{
margin: 0;
padding: 0;
}
a{
text-decoration: none;
}
body{
background: #f8f8f9;
}
.header{
height: 48px;
line-height: 48px;
background: rgba(0,0,0,.8);
color: #fff;
}
.header-title{
padding: 0 32px;
float: left;
color: #fff;
}
.header-menu{
float: right;
margin-right: 32px;
}
.header-menu-cart{
color: #fff;
}
.header-menu-cart span{
display: inline-block;
width: 16px;
height: 16px;
line-height: 16px;
text-align: center;
border-radius: 50%;
background: #ff5500;
color: #fff;
font-size: 12px;
}
在views中新建product.vue
<template>
<div v-if="product">
<div class="product">
<div class="product-image">
<img :src="product.image">
</div>
<div class="product-info">
<h1 class="product-name">{{product.name}}</h1>
<div class="product-cost">¥{{product.cost}}</div>
<div class="product-add-cart"
@click="handleAddToCart">加入购物车</div>
</div>
</div>
<div class="product-desc">
<h2>产品介绍</h2>
<img :src="'http://ordfm6aah.bkt.clouddn.com/shop'+n+'jpeg'" v-for="n in 10" :key="n">
</div>
</div>
</template>
<script>
//导入本地数据匹配使用,真实场景不需要
import product_data from '../product.js'
export default {
data(){
return{
//获取路由中的参数
id:parseInt(this.$route.param.id),
product:null
}
},
methods:{
//计入购物车
handleAddToCart(){
this.$store.commit('addCart',this.id);
},
getProduct(){
//真实环境通过Ajax获取,此处异步模拟
setTimeout(()=>{
this.product=product_data.find(item=>item.id===this.id);
},500);
}
},
mounter(){
//初始化,请求数据
this.getProduct();
}
}
</script>
<style scoped>
.product{
margin: 32px;
padding: 32px;
background: #fff;
border: 1px solid #dddee1;
border-radius: 10px;
overflow: hidden;
}
.product-image{
width: 50%;
height: 550px;
float: left;
text-align: center;
}
.product-image img{
height: 100%;
}
.product-info{
width: 50%;
padding: 150px 0 250px;
height: 150px;
float: left;
text-align: center;
}
.product-cost{
color: #f2352e;
margin: 8px 0;
}
.product-add-cart{
display: inline-block;
padding: 8px 64px;
margin: 8px 0;
background: #2d8cf0;
color: #fff;
border-radius: 4px;
cursor: pointer;
}
.product-desc{
background: #fff;
margin: 32px;
padding: 32px;
border: 1px solid #dddee1;
border-radius: 10px;
text-align: center;
}
.product-desc img{
display: block;
width: 50%;
margin: 32px auto;
padding: 32px;
border-bottom: 1px solid #dddee1;
}
</style>
添加购物车组件在views:
购物车应具有:
vuex中的购物车数据cartList;
product.js中的所有商品数据;
将product.js中的数组转换为字典productDictList.
商品总数countAll
总费用costAll
cart.vue
<template>
<div class="cart">
<div class="cart-header">
<div class="cart-header-title">
购物清单
</div>
<div class="cart-header-main">
<div class="cart-info">商品信息</div>
<div class="cart-price">单价</div>
<div class="cart-count">数量</div>
<div class="cart-cost">小计</div>
<div class="cart-delete">删除</div>
</div>
</div>
<div class="cart-content">
<div class="cart-content-main" v-for="(item,index) in cartList" :key="item.id">
<div class="cart-info">
<img src="productDictList[item.id].image" >
<span >{{productDictList[item.id].name}}</span>
</div>
<div class="cart-price">
¥{{productDictList[item.id].cost}}
</div>
<div
class="cart-count"><span
class="cart-control-minus"
@click="handleCount(index,-1)">-
</span>
{{item.count}}
<span class="cart-control-add"
@click="handleCount(index,1)">
+
</span>
</div>
<div class="cart-cost">
¥{{productDictList[item.id].cost*item.count}}
</div>
<div class="cart-delete">
<span
class="cart-control-delete"
@click="handleDelete(index)">
删除
</span>
</div>
</div>
<div class="cart-empty" v-if="!cartList.length">购物车为空</div>
</div>
</div>
</template>
<script>
import product_data from '../product.js'
export default {
methods:{
handleCount(index,count){
if(count<0&&this.cartList[index].count===1)return;
this.$store.commit('editCartCount',{
id:this.cartList[index].id,
count:count
});
},
handleDelete(index){
this.$store.commit('deleteCart',this.cartList[index].id);
}
},
computed:{
cartList(){
return this.$store.state.cartList;
},
productDocList(){
const dict={};
this.productList.forEach(item => {
dict[item.id]=item;
});
return dict;
},
countAll(){
let count=0;
this.cartList.forEach(item=>{
count+=item.count;
});
return count;
},
costAll(){
let cost=0;
this.cartList.forEach(item=>{
cost+=this.productDictList[item.id].cost*item.count;
});
return cost;
}
},
data(){
return{
productList:product_data
}
}
}
</script>
<style scoped>
.cart{
margin: 32px;
background: #fff;
border: 1px solid #dddee1;
border-radius: 10px;
}
.cart-header-title{
padding: 16px 32px;
border-bottom: 1px solid #dddee1;
border-radius: 10px 10px 0 0;
background: #f8f8f9;
}
.cart-header-main{
padding: 8px 32px;
overflow: hidden;
border-bottom: 1px solid #dddee1;
background: #eee;
overflow: hidden;
}
.cart-empty{
text-align: center;
padding: 32px;
}
.cart-header-main div{
text-align: center;
float: left;
font-size: 14px;
}
div.cart-info{
width: 60%;
text-align: left;
}
.cart-price{
width: 10%;
}
.cart-count{
width: 10%;
}
.cart-cost{
width: 10%;
}
.cart-delete {
width: 10%;
}
.cart-content-main{
padding: 0 32px;
height: 60px;
line-height: 60px;
text-align: center;
border-bottom: 1px dashed #e9eaec;
overflow: hidden;
}
.cart-content-main div{
float: left;
}
.cart-content-main img{
width: 40px;
height: 40px;
position: relative;
top: 10px;
}
.cart-control-minus,
.cart-control-add{
display: inline-block;
margin: 0 4px;
width: 24px;
height: 24px;
line-height: 22px;
text-align: center;
background: #f8f8f9;
border-radius: 50%;
box-shadow: 0 1px 1px rgba(0,0,0,.2);
cursor: pointer;
}
.cart-control-delete{
cursor: pointer;
color: #2d8cf0;
}
.cart-promotion{
padding: 16px 32px;
}
.cart-control-promotion,
.cart-control-order{
display: inline-block;
padding: 8px 32px;
border-radius: 6px;
background: #2d8cf0;
color: #fff;
cursor: pointer;
}
.cart-control-promotion{
padding: 2px 6px;
font-size: 12px;
border-radius: 3px;
}
.cart-footer{
padding: 32px;
text-align: right;
}
.cart-footer-desc{
display: inline-block;
padding: 0 16px;
}
.cart-footer-desc span{
color: #f2352e;
font-size: 20px;
}
</style>
设置路由引入:
//router.js
const routers = [
{
path: '/list',
meta: {
title: '商品列表'
},
component: (resolve) => require(['./views/list.vue'], resolve)
},
{
path: '/product/:id',
meta: {
title: '商品详情'
},
component: (resolve) => require(['./views/product.vue'], resolve)
},
{
path: '/cart',
meta: {
title: '购物车'
},
component: (resolve) => require(['./views/cart.vue'], resolve)
},
{
path: '*',
redirect: '/list'
}
];
export default routers;
main.js
import Vue from 'vue';
import VueRouter from 'vue-router';
import Routers from './router';
import Vuex from 'vuex';
import App from './app.vue';
import './style.css';
import product_data from './product';
Vue.use(VueRouter);
Vue.use(Vuex);
// 路由配置
const RouterConfig = {
// 使用 HTML5 的 History 路由模式
mode: 'history',
routes: Routers
};
const router = new VueRouter(RouterConfig);
router.beforeEach((to, from, next) => {
window.document.title = to.meta.title;
next();
});
router.afterEach((to, from, next) => {
window.scrollTo(0, 0);
});
// 数组排重
function getFilterArray (array) {
const res = [];
const json = {};
for (let i = 0; i < array.length; i++){
const _self = array[i];
if(!json[_self]){
res.push(_self);
json[_self] = 1;
}
}
return res;
}
const store = new Vuex.Store({
state: {
productList: [],
cartList: []
},
getters: {
brands: state => {
const brands = state.productList.map(item => item.brand);
return getFilterArray(brands);
},
colors: state => {
const colors = state.productList.map(item => item.color);
return getFilterArray(colors);
}
},
mutations: {
// 添加商品列表
setProductList (state, data) {
state.productList = data;
},
// 添加到购物车
addCart (state, id) {
// 先判断购物车是否已有,如果有,数量+1
const isAdded = state.cartList.find(item => item.id === id);
if (isAdded) {
isAdded.count ++;
} else {
state.cartList.push({
id: id,
count: 1
})
}
},
// 修改商品数量
editCartCount (state, payload) {
const product = state.cartList.find(item => item.id === payload.id);
product.count += payload.count;
},
// 删除商品
deleteCart (state, id) {
const index = state.cartList.findIndex(item => item.id === id);
state.cartList.splice(index, 1);
},
// 清空购物车
emptyCart (state) {
state.cartList = [];
}
},
actions: {
// 请求商品列表
getProductList (context) {
// 真实环境通过 ajax 获取,这里用异步模拟
setTimeout(() => {
context.commit('setProductList', product_data);
}, 500);
},
// 购买
buy (context) {
// 真实环境应通过 ajax 提交购买请求后再清空购物列表
return new Promise(resolve=> {
setTimeout(() => {
context.commit('emptyCart');
resolve();
}, 500)
});
}
}
});
new Vue({
el: '#app',
router: router,
store: store,
render: h => {
return h(App)
}
});
app.vue
<template>
<div>
<div class="header">
<router-link to="/list" class="header-title">电商网站示例</router-link>
<div class="header-menu">
<router-link to="/cart" class="header-menu-cart">
购物车
<span v-if="cartList.length">{{ cartList.length }}</span>
</router-link>
</div>
</div>
<router-view></router-view>
</div>
</template>
<script>
export default {
computed: {
cartList () {
return this.$store.state.cartList;
}
}
}
</script>
更多推荐
所有评论(0)