小米商城 -- vue项目实战

仅以该文记录项目过程。

github: 小米商城源码

       该项目是对小米商城系统的模仿,实现了从浏览商品到结算商品的整个过程,其中包括了商品列表、根据价格筛选商品、对商品 排序、登录、加入购物车、结算等功能 前台使用vue-cli构建了请求服务器,使用了Vue框架,还使用了vue-router、axios、Vuex等中间件 后台使用了node.js,express框架构建了后台服务器

1.  项目初始化

全局环境下安装vue,vue-cli 脚手架

npm install vue -g
npm install vue-cli -g

初始化项目:

$ vue init webpack MiMall

? Project name (MiMall) mistore
? Project name mistore
? Project description (A Vue.js project) xiaomi store with vue
? Project description xiaomi store with vue
? Author (Spock <[email protected]>)
? Author Spock <[email protected]>
? Vue build (Use arrow keys)
? Vue build standalone
? Install vue-router? (Y/n)
? Install vue-router? Yes
? Use ESLint to lint your code? (Y/n) n
? Use ESLint to lint your code? No
? Set up unit tests (Y/n) n
? Set up unit tests No
? Setup e2e tests with Nightwatch? (Y/n) n
? Setup e2e tests with Nightwatch? No
? Should we run `npm install` for you after the project has been created? (recom
? Should we run `npm install` for you after the project has been created? (recom
mended) npm

先安装几个插件:

npm i babel-runtime fastclick babel-polyfill

    "babel-polyfill": "^6.26.0",//es6的API转义
    "babel-runtime": "^6.26.0",//对es6的语法进行转义
    "fastclick": "^1.0.6",//解决移动端300ms延迟的问题

main.js中的设置:

import 'babel-polyfill'
import fastclick from 'fastclick'

fastclick.attach(document.body)//这样就能解决body下按钮点击300ms的延迟

2. 配置路由

   先配置路径别名:(    // 别名,只针对于js库,css的引入还是要写相对路径,不能省略)

build/webpack.base.conf.js:

    alias: {
      'vue$': 'vue/dist/vue.esm.js',
      '@': resolve('src'),
      'components': resolve('src/components'), 
       //当在js文件中import其他文件时路径直接写commont相当于../src/components
      'api': resolve('src/api')   //后面会用到
    }

src/router : 

配置别名的好处就在下面import的时候体现出来了。

先配置主页,购物车,及地址栏的路由。

import Vue from 'vue'
import Router from 'vue-router'
import Goods from 'components/goods'
import Car from 'components/car'
import Address from 'components/address'

Vue.use(Router)

export default new Router ({
  routes: [
  {
    path: '/',
    component: Goods
  },
  {
    path: '/car',
    component: Car
  },
  {
    path: '/address',
    component: Address
  }
  ]
})

回到主页面配置显示的信息:

src/App.vue: 

<template>
  <div id="app">
    <m-header></m-header>
    <tab></tab>
    <keep-alive>
      <router-view></router-view>
    </keep-alive>
  </div>
</template>

<script>
import MHeader from 'components/m-header'
import Tab from 'components/tab'

export default {
  components: {
    MHeader,
    Tab
  }
}
</script>

<style>
</style>

可以看到引用了几个组件,还未创建。接下来创建这几个页面:

src/ components/goods.vue  (示例:car.vue,address.vue,m-header.vue,tab.vue也类似该结构创建)

<template>
<p>商品页面</p>
</template>

<script>

</script>
<style type="text/css">

</style>

现在 控制台 输入命令: npm run dev , 打开localhost:8080 就可以看到主页面的信息了。

3.   “Sticky Footer”布局:

  指的就是一种网页效果: 如果页面内容不足够长时,页脚固定在浏览器窗口的底部;如果内容足够长时,页脚固定在页面的最底部。但如果网页内容不够长,置底的页脚就会保持在浏览器窗口底部。

src/ components/footer.vue:

<template>
<div class="footer">
  <div class="footer-contain">
    <div class="area-select">
      <span>地区:</span>
      <select>
        <option value="中国">中国</option>
        <option value="USA">USA</option>
        <option value="India">India</option>
      </select>
    </div>
    <ul>
      <li>隐私策略</li>
      <li>团队合作</li>
      <li>关于我们</li>
      <li>&copy;2018 taoMall.com 版权所有</li>
    </ul>
  </div>
</div>
</template>

<script>

</script>
<style type="text/css">
.footer {
  margin-top: -100px;
  width: 100%;
  height: 100px;
  background-color: #bbb;
  overflow: hidden;
}
.footer-contain {
  padding:0 120px;
  height: 100%;
  width: 100%;
  box-sizing: border-box;
  display: flex;
  align-items: center;
  justify-content: space-between;
}
.footer-contain .area-select {
  flex-width: 200px;
  width: 200px;
}
.footer-contain ul {
  display: flex;
}
.footer-contain ul li {
  margin-left: 10px;
}
</style>

然后更改App.vue中的内容:

<template>
  <div id="app">
    <div class="content-wrap">
      <div class="content">
        <m-header></m-header>
        <tab></tab>
        <keep-alive>
          <router-view></router-view>
        </keep-alive>
      </div>
    </div>
    <m-footer></m-footer>
  </div>
</template>

<script>
import MHeader from 'components/m-header'
import Tab from 'components/tab'
import Footer from 'components/footer'

export default {
  components: {
    MHeader,
    Tab,
    MFooter:Footer,
  }
}
</script>

<style>
#app {
  height: 100%;
}
.content-wrap {
  min-height: 100%;
}
.content {
  padding-bottom: 100px;
}
</style>

(缺点: 添加了两个div标签)

done!    ✿✿ヽ(°▽°)ノ✿

4. 根据路由地址显示不同的文本信息

src/components/tab.vue:  利用计算属性及this.$route.path 得到路由地址

<template>
<div class="tab">
  <ul class="tab-contain">
    <li class="tab-item"><router-link  tag="a" to="/">主页</router-link></li>
    <li class="tab-item"><span>{{tabTitle}}</span></li>
  </ul>
</div>
</template>

<script>
export default {
  data() {
    return {
      pathMap: {
        '/': '商品信息',
        '/car': '购物车信息',
        '/address': '地址信息',
      }
    }
  },
  //计算属性:逻辑计算,根据pathMap数据中的地址绑定显示的文字
  computed: {
    tabTitle() {
      return this.pathMap[this.$route.path]
    }
  }
}
</script>
<style type="text/css">
 .tab {
  padding: 20px 100px;
  box-sizing: border-box;
  background-color: #f3f0f0;
 }
 .tab-contain {
  font-size: 0;
 }
 .tab-contain .tab-item {
  display: inline-block;
  font-size: 18px;
  margin-right: 10px;
 }
 .tab-contain .tab-item span {
  margin-left:10px;
  color: #de1442;
}
</style>

5.  利用axios获取数据

(ps: 本打算利用axios伪造referer获取数据,服务器原api获取有困惑,遂败。直接设置跨域处理)

在前端 good.js 请求地址,不是直接请求服务端,而是请求我们自己的server端,然后我们的地址再去请求服务端(使用axios发送http请求)

下载axios:

npm install axios --save

src/api/goods.js:

import axios from 'axios'

export function getGoodsList() {
  const url = '/goods/list'

  const data = {
    page: 0,
    pageSize: 8,
    orderFlag: true,
    priceLeave: 'All'
  }

  return axios.get(url, {
    params:data
  }).then((res) => {
    return Promise.resolve(res)
  })
}

config/index.js:(设置跨域请求)

proxyTable: {
  '/goods/list': {
      target: 'http://hotemotion.fun:3389',
      changeOrigin: true
    }
  },

src/components/goods.vue:(请求数据)

<template>
<p>商品页面</p>
</template>

<script>
import {getGoodsList} from 'api/goods'

export default {
  data() {
    return {
      goods : {}
    }
  },
  created() {
    this._getGoodsList()
  },
  methods: {
    _getGoodsList() {
      getGoodsList().then((res) => {
        this.goods = res.data
        console.log(this.goods)
      })
    }
  }
}

</script>
<style type="text/css">

</style>

重新启动npm run dev,就能够在控制台看到输出的数据。

6.  实现商品页面数据运用

src/compnents/goods.vue : 样式还是用stylu写比较好啊(´థ౪థ)σ

<template>
<div class="goods-contain">
  <div class="goods-sort">
    <p>排序:</p>
    <p>默认</p>
    <p class="goods-price">价格<span class="icon-arrow">↑</span></p>
  </div>
  <div class="goods-items">
    <ul class="price-inter">
      <li class="price-name">价格:</li>
      <li class="price-item active">全部</li>
      <li class="price-item" v-for="(item, index) in price">{{item.startPrice}}-{{item.endPrice}}</li>
    </ul>
    <ul class="goods-info">
      <li class="goods-des" v-for="(item, index) in goods" :key="index">
        <div class="good-all">
          <div class="good-image">
            <img :src="'/static/images/' + item.productImg">
          </div>
          <p class="good-name">{{item.productName}}</p>
          <p class="good-price">¥{{item.productPrice}}</p>
          <div class="add-car">加入购物车</div>
        </div>
      </li>
    </ul>
  </div>
</div>
</template>

<script>
import {getGoodsList} from 'api/goods'

export default {
  data() {
    return {
      goods : [],
      price:[
        {
          "startPrice":"0.00",
          "endPrice":"100.00"
        },
        {
          "startPrice":"100.00",
          "endPrice":"500.00"
        },
        {
          "startPrice":"500.00",
          "endPrice":"1000.00"
        },
        {
          "startPrice":"1000.00",
          "endPrice":"8000.00"
        },
      ],
    }
  },
  created() {
    this._getGoodsList()
  },
  methods: {
    _getGoodsList() {
      getGoodsList().then((res) => {
        this.goods = res.data
        console.log(this.goods)
      })
    }
  }
}

</script>
<style type="text/css">

</style>

7.  图片懒加载

安装插件:

npm i vue-lazyload

main.js 中引入:(先在static地址中放入图片资源)

import VueLazyLoad from 'vue-lazyload'

Vue.use(VueLazyLoad,{
  loading: '/static/images/Loading/loading-balls.svg'
})

src/components/goods.vue :   在图片的使用地址上更改为 v-lazy 即可。

<div class="good-image">
   <img v-lazy="'/static/images/' + item.productImg">
</div>

这下刷新页面就能够看到有滚动的小球的加载过程。

8.   价格排序

逻辑:通过观察 Headers

通过点击左边不同区间的价格发现: priceLevel 控制显示区间(全部时为all,其他时候为0,1,2,3)

通过点击价格排序发现:orderPrice升序为true,降序为false。

page 应该是拿来实现下拉加载图片的优化。

实现:所以这些参数应该在传递的时候变化,所以需要修改api请求的参数

src/api/goods.js: (将axios请求分离出来,不会让页面显得冗长)

import axios from 'axios'

export function getGoodsList(page, pageSize,orderFlag,priceLevel) {
  const url = '/goods/list'

  const data = {
    page,
    pageSize,
    orderFlag,
    priceLevel
  }

  return axios.get(url, {
    params:data
  }).then((res) => {
    return Promise.resolve(res)
  })
}

src/components/goods.vue: 

<template>
<div class="goods-contain">
  <div class="goods-sort">
    <p>排序:</p>
    <p>默认</p>
    <p class="goods-price" @click="sortBy()">价格<span class="icon-arrow" :class="{arrow_turn:!orderFlag}">↑</span></p>
  </div>
  <div class="goods-items">
    <ul class="price-inter">
      <li class="price-name">价格:</li>
      <li class="price-item" :class="{active: priceLevel === 'all'}" @click="selectInter('all')">全部</li>
      <li class="price-item" :class="{active: priceLevel === index}" v-for="(item, index) in price" @click="selectInter(index)">{{item.startPrice}}-{{item.endPrice}}</li>
    </ul>
    <ul class="goods-info">
      <li class="goods-des" v-for="(item, index) in goods" :key="index">
        <div class="good-all">
          <div class="good-image">
            <img v-lazy="'/static/images/' + item.productImg">
          </div>
          <p class="good-name">{{item.productName}}</p>
          <p class="good-price">¥{{item.productPrice}}</p>
          <div class="add-car">加入购物车</div>
        </div>
      </li>
    </ul>
  </div>
</div>
</template>

<script>
import {getGoodsList} from 'api/goods'

export default {
  data() {
    return {
      goods : [],
      price:[
        {
          "startPrice":"0.00",
          "endPrice":"100.00"
        },
        {
          "startPrice":"100.00",
          "endPrice":"500.00"
        },
        {
          "startPrice":"500.00",
          "endPrice":"1000.00"
        },
        {
          "startPrice":"1000.00",
          "endPrice":"8000.00"
        },
      ],
      page: 0,        //下拉加载
      pageSize: 8,        
      orderFlag: true,    //升序还是降序
      priceLevel: 'all',  //显示的区间
    }
  },
  created() {
    this._getGoodsList()
  },
  methods: {
    _getGoodsList() {
      getGoodsList(this.page, this.pageSize, this.orderFlag, this.priceLevel).then((res) => {
          this.goods = res.data  //得到商品列表数据存在goods变量中
      })
    },
    sortBy() {
      this.orderFlag = !this.orderFlag
      this.page = 0
      this._getGoodsList(false)
    },
    selectInter(index) {
      this.priceLevel = index
      this.page = 0
      this._getGoodsList(false)
    }
  }
}

</script>
<style type="text/css">
//
</style>

9.  下拉加载

建议看过此文再接着学习: vue 组件实现下拉加载

下载组件:

npm install vue-infinite-scroll --save

main.js : 全局环境下设置

import InfiniteScroll from 'vue-infinite-scroll'

Vue.use(InfiniteScroll)

api/ components/ goods.vue:

<template>
    <div v-infinite-scroll="loadMore" infinite-scroll-disabled="busy" infinite-scroll-distance="30">
    <!-- 加载更多 -->
    </div>
</template>

<script>
export default {
  data() {
    return {
      busy: false,
   }
 },
  mounted() {      //将获取数据的函数放在mounted中执行是为了能够在刷新的时候也得到数据
    this._getGoodsList(false)
  },
 methods: {
    _getGoodsList(flag) {
      getGoodsList(this.page, this.pageSize, this.orderFlag, this.priceLevel).then((res) => {
        if (flag) {
          //多次加载数据,则需要把数据相加
          this.goods = this.goods.concat(res.data)
          if (res.data.length === 0) {
            //没有数据可加载就关闭无限滚动
            this.busy = true
          } else {
            //否则仍可以触发无限滚动
            this.busy = false
          }
        } else {
          //第一次加载数据并且允许滚动
          this.goods = res.data
          this.busy = false
        }
      })
    },
    loadMore() {
      this.busy = true
      //0.3s 后加载下一页的数据
      setTimeout(() => {
        this.page ++
        this._getGoodsList(true)  //滚动的时候调用axios加载数据,参数判断不是第一次加载
        }, 300)
    }
  }
}
</script>

10. 登录页面

逻辑: 点击登录后,显示登录窗口,如果信息正确则显示登录后的信息。所以添加一个事件showLogin() 控制窗口的登录。

<template>
    <div class="sign">
        <p class="signin" v-if="showLoginOut" @click = "showLogin()">登录</p>
        <div class="signout" v-if="!showLoginOut">
          <span class="sign-name"></span>
          <span class="sign-out">退出</span>
          <router-link to="/car" class="sign-car">购物车</router-link>
        </div>
    </div>
  </div>
</template>
<script>
import axios from 'axios'

export default {
  data() {
    return {
      showLoginOut : true,
    }
  },
  methods: {

    // 显示登录窗口
    showLogin() {
      this.showLogDialog = true
    }
  }
</script>

       看了两眼登录页面,其实可以写成一个子组件的形式,用于登录、注册等的载体(没有注册页啊!!!(ノ`Д)ノ   ),然後利用插槽插入基本內容。(运用了组件之间的参数传递)

创建一个基础组件:

src/components/base/dialog.vue:

<template>
  <!-- 设计弹窗的框架样式,再利用slot插槽插进不同的内容 -->
  <div>
    <div class="dialog-wrap" v-if="isShow">
        <div class="dialog-cover" @click="closeMyself"></div>
<!-- 动画效果 -->
        <transition name="drop">
        <div class="dialog-content" v-if="isShow">
          <p class="dialog-close" @click="closeMyself">X</p>
          <!-- 插槽的位置 -->
          <slot>hello</slot>
        </div>
      </transition>

    </div>
  </div>
</template>

<script>
export default {
  props:{
    isShow:{
      type:Boolean,
      default:false
    }
  },
  methods:{
    closeMyself(){
      this.$emit('on-close')  //发送给父组件处理
    }
  }
};
</script>



<style scoped>
.drop-enter-active {
  transition: all .5s ease;
}
.drop-leave-active {
  transition: all .3s ease;
}
.drop-enter {
  transform: translateY(-500px);
}
.drop-leave-active {
  transform: translateY(-500px);
}

.dialog-wrap {
  position: fixed;
  width: 100%;
  height: 100%;
}
.dialog-cover {
  background: #000;
  opacity: .3;
  position: fixed;
  z-index: 5;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
}
.dialog-content {
  width: 50%;
  position: fixed;
  max-height: 50%;
  overflow: auto;
  background: #fff;
  top: 20%;
  left: 50%;
  margin-left: -25%;
  z-index: 10;
  border: 2px solid #464068;
  padding: 2%;
  line-height: 1.6;
}
.dialog-close {
  position: absolute;
  right: 5px;
  top: 5px;
  width: 20px;
  height: 20px;
  text-align: center;
  cursor: pointer;
}
.dialog-close:hover {
  color: #4fc08d;
}
</style>

在头部引用:

src/components/m-header.vue:(父组件传递值给子组件,并且处理子组件传递来的事件)

<template>
  <my-dialog :is-show="showLogDialog" @on-close="closeDialog('showLogDialog')">
    <!-- my-dialog 插件控制弹窗,父组件绑定is-show属性传递给子组件,并且根据值判断弹窗是否展示 -->
  </my-dialog>

</template>

<script>
import Dialog from './base/dialog'

export default {
  data() {
    return {
      showLogDialog: false //该变量控制窗口是否显示
    }
  },
  methods: {
    closeDialog(attr) {
      this[attr] = false
    },
    showLogin() {
      this.showLogDialog = true
    }
  },
  components: {
    MyDialog: Dialog // 名称
  }
}
</script>

然后填充插槽内容即可。

11. 往插槽里面填内容,并且丰富登录后的显示

先在config/index.js,配置跨域访问路由:

    proxyTable: {
    '/goods/*': {
        target: 'http://hotemotion.fun:3389',
        changeOrigin: true
      },
    '/users/*':{
      target: 'http://hotemotion.fun:3389',
      changeOrigin: true
    }

逻辑:点击登录后应该post一个用户的信息(账号、密码),检查用户的登录状态。并且get一个carList的数据

登录事件:showSignout() 传递用户名和密码给后端,判断正确后显示登陆后的信息

退出事件:_getLogout()  将信息清空

(用户名和密码都用了v-model 绑定输入的数据)

src/components/m-header.vue: 

<template>
<div>
  <div class="header">
    <div class="logo">
      <img src="/static/images/logo.jpg">
    </div>
    <div class="sign">
        <p class="signin" v-if="showLoginOut" @click = "showLogin()">登录</p>
        <div class="signout" v-if="!showLoginOut">
          <span class="sign-name" v-text="userName"></span>
          <span class="sign-out" @click="_getLogout">退出</span>
          <router-link to="/car" class="sign-car">购物车</router-link>
        </div>
    </div>
  </div>
  <my-dialog :is-show="showLogDialog" @on-close="closeDialog('showLogDialog')">
    <!-- my-dialog 插件控制弹窗,父组件绑定is-show属性传递给子组件,并且根据值判断弹窗是否展示 -->
    <div class="signin-slot">
      <p class="signin-logo">登录:</p>
      <form>
        <div class="signin-name">
          <span class="name-icon icon">1</span>
          <input type="input" name="username" placeholder="用户名" v-model="userName"/>
        </div>
        <div class="signin-psd">
          <span class="pwd-icon icon">2</span>
          <input type="password" name="password" placeholder="密码" v-model="userPwd">
        </div>
        <button type="button" class="signin-submit" @click="showSignout">登录</button>
      </form>
    </div>
  </my-dialog>
</div>
</template>

<script>
import axios from 'axios'
import Dialog from './base/dialog'

export default {
  data() {
    return {
      showLogDialog: false,
      showLoginOut : true,
      userName:'',
      userPwd:'',
    }
  },
  // 刷新后能够保持登录状态
  mounted() {
    this._getCheckLogin();
  },
  methods: {
    closeDialog(attr) {
      this[attr] = false
    },
    // 显示登录窗口
    showLogin() {
      this.showLogDialog = true
    },
    //检查登录状态
    _getCheckLogin() {
      axios.get('/users/checkLogin').then((res) => {
        if (res.data.status == '0') {
          this.showLoginOut = false
          this.userName = res.data.result.userName
          this.getCartList()
        }
      })
    },
    // 登出
    _getLogout() {
      axios.post('/users/logout').then((res) => {
        if (res.data.status == '0') {
          this.showLoginOut = true
          this.userName = ''
          this.userPwd = ''
        }
      })
    },
    // 登入
    showSignout() {
      axios.post('/users/login',{
        userName: this.userName,
        userPwd: this.userPwd,
      }).then((res) => {
        if (res.data.status == '0') {
          // console.log(res)
          this.showLogDialog = false;
          this.showLoginOut = false;
        }
      })
    }
  },
  components: {
    MyDialog: Dialog
  }
}
</script>
<style type="text/css" scoped>

</style>

此时重新启动npm run dev,就能够在控制台看到登录后取得的数据。此时加上页面样式,该页面基本信息完成。

11. 购物车页面

学习一个flex的布局技巧: width:1%;

子组件count.vue的运用,以及父组件的传参

route(路线), router(路由) 傻傻分不清楚。

    this,$route.query.addressId (得到路径上的参数)

    this.$router.push()       ( 跳转到某一路由)

12.   使用Vuex管理数据

安装:

npm install vuex --save

利用Vuex得到商品的数量,用于在购物车中显示。

src/store/store.js: 

state 用来存储变量的状态

mutations:记录变量状态的变化(setCartCount是设置cartCount初始值,updateCartCount 改变cartCount数值)

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

const store = new Vuex.Store({
  state: {
    cartCount:0
  },
  mutations: {
    setCartCount(state,cartCount){
      state.cartCount = cartCount;
    },
    updateCartCount(state,cartCount){
      state.cartCount += cartCount;
    }
  }
});
export default store

src/ components/goods.vue:

_addToCar() 添加到购物车就加一个数。

import store from './../store/store'

export default {
    methods: {
    _addToCar(productId) {
      //post 提交数据
      addToCar(productId).then((res) => {
        //mmp, 这个状态码是字符串
        if (res.data.status == '0') {
          this.showAddCart = true
          // 如果请求成功,数据存入store中
          store.commit('updateCartCount', 1)
        } else {
          this.showErrDialog = true
        }
      })
    },
  }
}

src/components/car.vue: 

点击按钮改变数量的时候状态加1或减1, 删除的时候则删除当前的数量

import store from '../store/store'

export default {
  methods: { 
   editNum(flag, item) {
      if (flag === 'min') {
        if (item.productNum === 1) {
          return
        }
        item.productNum--
        store.commit('updateCartCount', -1)
      } else {
        item.productNum++
        store.commit('updateCartCount', 1)
      }
    },
    delItem() {
      axios.post('/users/carDel', {
        productId: this.productId
      }).then((res) => {
        if (res.data.status == '0') {
          this.showDelDialog = false
          this.getCarList()
          store.commit('updateCartCount', -this.productNum)
        }
      })
    }
  }

在m-header.vue中使用:

<template>
  <router-link to="/car" class="sign-car">购物车{{carCount}}</router-link>
</template>
<script>
import store from '../store/store'

export default {
  // 刷新的时候也检查登录状态
  mounted() {
    this._getCheckLogin();
  },
  computed: {
    carCount() {
      return store.state.cartCount
    }
  },
  methods: {
    // 登出
    _getLogout() {
          store.commit('setCartCount', 0)
    },
    // 获得商品数据,存入store中
    getCartList() {
      axios.get('/users/carList').then((res) => {
        if (res.data.status == '0') {
          let cartCount = 0
          const carList = res.data.result
          carList.forEach((item) => {
            cartCount += item.productNum
            // console.log(cartCount)
            store.commit('setCartCount', cartCount)
          })
        }
      })
    }
}

</script>

猜你喜欢

转载自blog.csdn.net/weixin_41892205/article/details/81435008