暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

Vue实现手机端界面的购物车案例

巴韭特锁螺丝 2022-12-08
98

今天用Vue实现的一个手机端的购物车案例,着重阐述组件化思想的优势,将页面的内容分成各个模块进行书写,然后数据传输,父传子、子传父、兄弟数据共享等,这方面知识不牢固的话可以关注一下右方vue的专栏:Vue专栏 里面详解介绍了vue的知识,今天的案例也是借助黑马的相关案例及其接口,既是分享也是回顾。

前期准备

因为本案例是借助vue的框架进行实现的,所以我们需要先搭建vue-cli脚手架,具体的搭建过程请看右方链接  vue-cli脚手架的搭建与使用 ,搭建完成后,需要根据案例的具体的图片功能点要求给components文件下新建子组件文件,案例实现结果以及父子组件的框架如下:

写项目之前先选择好自己想要的手机版本是什么,我这里就用最常见的iPhone 6/7/8 这个手机尺寸了,如果想更换手机版本,可以自行在浏览器进行更换。

为了语义化,这边我把components文件名更改为shopping,不该也没关系。

Header

根据上文图片案例,我们先从上面也是最简单的Header来写,为了便于数据的管理,我们把标题设置为自定义属性,允许使用者自定义标题的内容。

Header.vue子组件代码

  1. <template>

  2. <div class="header-container">{{title}}</div>

  3. </template>

  4. <script>

  5. export default {

  6. props:{

  7. // 声明 title 自定义属性,允许使用者自定义标题的内容

  8. title:{

  9. default:'',

  10. type:String

  11. }

  12. }

  13. }

  14. </script>

  15. <style lang="less" scoped>

  16. .header-container{

  17. font-size: 12px;

  18. height: 45px;

  19. width: 100%;

  20. background-color: #008c8c;

  21. display: flex;

  22. justify-content: center;

  23. align-items: center;

  24. color: #fff;

  25. position: fixed;

  26. top: 0;

  27. z-index: 999;

  28. }

  29. </style>


App.vue父组件代码

  1. <template>

  2. <div class="app-container">

  3. <!-- 头部区域 -->

  4. <Header title="购物车案例"></Header>

  5. </div>

  6. </template>

  7. <script>

  8. // 导入需要的组件

  9. import Header from '@/shopping/Header/Header.vue'

  10. export default {

  11. components:{

  12. Header

  13. }

  14. }

  15. </script>

  16. <style lang="less" scoped>

  17. .app-container{

  18. padding-top: 45px;

  19. padding-bottom:50px

  20. }

  21. </style>



Goods

现在开始从项目的内容入手,因为子组件的内容肯定不能写死了,所以需要将父组件的值传入到子组件,而父组件的值从哪来?这里需要借助接口来获取自己要渲染的商品列表数据,而借助接口传值需要使用ajax或者是axios,所以我们需要先在当前项目下安装 axios,命令如下:

npm install axios -S

安装完成之后,在App.vue 父组件下 使用axios调用接口进行使用:很明显,我们先在data里面定义一个空数组,如果接收到接口里面的数据状态为200,就把接口里面的数据传递到我们定义的list里面,具体方法如下:

在控制台打印的接口数据如下,可以方便的查看接口里面的属性:

Goods.vue子组件代码

因为子组件的复选框的数据是没有和父组件的数据进行联通的,如果不把子组件修改复选框的状态的值传到App.vue父组件上,父组件上的goods_state是不会发生变化的,所以要通过自定义事件进行子向父传值,将复选框的修改状态传递到父组件上面。

  1. <template>

  2. <div class="goods-container">

  3. <!-- 左侧图片 -->

  4. <div class="thumb">

  5. <!-- 复选框 -->

  6. <div class="custom-control custom-checkbox">

  7. <input type="checkbox" class="custom-control-input" :id="'cb'+id" :checked="state" @change="stateChange" >

  8. <label :for="'cb'+id" class="custom-control-label">

  9. <!-- 商品的缩略图 -->

  10. <img :src="pic" alt="">

  11. </label>

  12. </div>

  13. </div>

  14. <!-- 右侧信息区域 -->

  15. <div class="goods-infos">

  16. <!-- 商品标题 -->

  17. <h6 class="goods-title">{{title}}</h6>

  18. <div class="goods-info-bottom">

  19. <!-- 商品价格 -->

  20. <span class="goods-price">¥{{price}}</span>

  21. <!-- 商品的数量 -->

  22. </div>

  23. </div>

  24. </div>

  25. </template>


  26. <script>

  27. export default {

  28. props:{

  29. // 商品的id,将来子组件中商品的勾选状态变化之后,需要通过子 -> 父的形式,通知父组件根据id修改对应商品的修改状态

  30. id:{

  31. require:true,

  32. type:Number

  33. },

  34. // 要渲染的商品的标题

  35. title:{

  36. default:'',

  37. type:String

  38. },

  39. // 要渲染的商品的图片

  40. pic:{

  41. default:'',

  42. type:String

  43. },

  44. // 商品的单价

  45. price:{

  46. default:0,

  47. type:Number

  48. },

  49. // 商品的勾选状态

  50. state:{

  51. default:true,

  52. type:Boolean

  53. },

  54. },

  55. methods:{

  56. // 只有复选框的选中状态发生了变化就会调用这个处理函数

  57. stateChange(e){

  58. const newState = e.target.checked;

  59. this.$emit('state-change', {id:this.id,value:newState});

  60. }

  61. },

  62. }

  63. </script>


  64. <style lang="less" scoped>

  65. .goods-container{

  66. + .goods-container{

  67. border-top:1px solid #efefef

  68. }


  69. padding: 10px;

  70. display: flex;

  71. .thumb{

  72. display: flex;/*display:flex 意思是弹性布局,它能够扩展和收缩 flex 容器内的元素,以最大限度地填充可用空间。*/

  73. align-items: center;/* 设置项目交叉轴方向上的对齐方式 */

  74. img{

  75. width: 80px;

  76. height: 80px;

  77. margin: 10px;

  78. }

  79. .custom-control{

  80. width: 114px;

  81. height: 105px;

  82. }

  83. }

  84. .goods-infos{

  85. display: flex;

  86. flex-direction: column;/*灵活的项目将垂直显示,正如一个列一样。在这里插入图片描述*/

  87. justify-content: space-between;/* 均匀排列每个元素首个元素放置于起点,末尾元素放置于终点 */

  88. height: 100px;

  89. flex: 1;

  90. .goods-title{

  91. font-size: 12px;

  92. font-weight: bold;

  93. }

  94. .goods-info-bottom{

  95. display: flex;

  96. justify-content: space-between;

  97. .goods-price{

  98. font-weight: bold;

  99. color: red;

  100. font-size: 13px;

  101. }

  102. }

  103. }

  104. }

  105. </style>


App.vue父组件代码

父组件通过接收子组件自定义的事件名,通过将函数methods里面的方法判断,来进行动态的改变 list.goods_state 里面的值。

  1. <template>

  2. <div class="app-container">

  3. <!-- 头部区域 -->

  4. <Header title="购物车案例"></Header>

  5. <!-- 循环渲染每一个商品的信息 -->

  6. <Goods

  7. v-for="item in list"

  8. :key="item.id"

  9. :id="item.id"

  10. :title="item.goods_name"

  11. :pic="item.goods_img"

  12. :price="item.goods_price"

  13. :state="item.goods_state"

  14. @state-change="getNewState"

  15. >

  16. </Goods>

  17. </div>

  18. </template>

  19. <script>

  20. // 导入 axios 请求库

  21. import axios from 'axios'

  22. // 导入需要的组件

  23. import Header from '@/shopping/Header/Header.vue'

  24. import Goods from '@/shopping/Goods/Goods.vue'

  25. export default {

  26. data(){

  27. return {

  28. // 用来存储购物车的列表数据,默认为空数组

  29. list:[]

  30. }

  31. },

  32. methods:{

  33. // 封装请求列表数据的方法

  34. async initCarList(){

  35. // 调用 axios 的 get 方法,请求列表数据

  36. const {data:res} = await axios.get("https://www.escook.cn/api/cart")

  37. console.log(res);

  38. if(res.status === 200){

  39. this.list = res.list

  40. }

  41. },

  42. // 接收子组件传递过来的数据

  43. getNewState(val){

  44. this.list.some(item => {

  45. if(item.id === val.id){

  46. item.goods_state = val.value

  47. // 终止后续循环

  48. return true

  49. }

  50. })

  51. },

  52. },

  53. components:{

  54. Header,Goods

  55. },

  56. created(){

  57. // 调用请求数据的方法

  58. this.initCarList()

  59. }

  60. }

  61. </script>

  62. <style lang="less" scoped>

  63. .app-container{

  64. padding-top: 45px;

  65. padding-bottom:50px

  66. }

  67. </style>



Footer

现在实现购物车底部的全选、总计、以及结算的功能样式,因为数据也不能写死了,所以需要我们进行数据绑定,然后通过父组件获取的数据进行传值。

Footer.vue子组件代码

  1. <template>

  2. <div class="footer-container">

  3. <!-- 左侧的全选 -->

  4. <div class="custom-control custom-checkbox">

  5. <input type="checkbox" class="custom-control-input" id="cbFull" :checked="isfull" @change="fullchange">

  6. <label for="cbFull" class="custom-control-label">全选</label>

  7. </div>

  8. <!-- 中间的合计 -->

  9. <div>

  10. <span>合计:</span>

  11. <span class="total-price">¥{{amount.toFixed(2)}}</span>

  12. </div>

  13. <!-- 结算按钮 -->

  14. <button type="button" class="btn btn-primary btn-settle">结算({{all}})</button>

  15. </div>

  16. </template>


  17. <script>

  18. export default {

  19. props:{

  20. // 全选的状态

  21. isfull:{

  22. type:Boolean,

  23. default:true

  24. },

  25. // 总价格

  26. amount:{

  27. type:Number,

  28. default:0

  29. },

  30. // 已勾选的商品的总数量

  31. all:{

  32. type:Number,

  33. default:0

  34. }

  35. },

  36. methods:{

  37. // 监听到了全选的状态变化

  38. fullchange(e){

  39. this.$emit('full-change',e.target.checked)

  40. }

  41. }

  42. }

  43. </script>


  44. <style lang="less" scoped>

  45. .footer-container{

  46. font-size: 12px;

  47. height: 60px;

  48. width: 100%;

  49. border-top: 1px solid #efefef;

  50. position: fixed;

  51. bottom: 0;

  52. background-color: #fff;

  53. display: flex;

  54. justify-content: space-between;

  55. align-items: center;

  56. padding: 0 10px;

  57. .custom-checkbox{

  58. font-size: 13px;

  59. display: flex;

  60. align-items: center;

  61. .custom-control-label{

  62. margin-bottom: -5px;

  63. }

  64. #cbFull{

  65. margin-right: 5px;

  66. }

  67. }

  68. .total-price{

  69. font-weight: bold;

  70. font-size: 14px;

  71. color: red;

  72. }

  73. .btn-settle{

  74. height: 70%;

  75. min-width: 110px;

  76. border-radius: 25px;

  77. font-size: 12px;

  78. }


  79. }

  80. </style>


App.vue父组件代码

  1. <template>

  2. <div class="app-container">

  3. <!-- 头部区域 -->

  4. <Header title="购物车案例"></Header>

  5. <!-- 循环渲染每一个商品的信息 -->

  6. <Goods

  7. v-for="item in list"

  8. :key="item.id"

  9. :id="item.id"

  10. :title="item.goods_name"

  11. :pic="item.goods_img"

  12. :price="item.goods_price"

  13. :state="item.goods_state"

  14. @state-change="getNewState"

  15. >

  16. </Goods>

  17. <!-- Footer区域 -->

  18. <Footer :isfull="fullState" :amount="amt" :all="total" @full-change="getFullState"></Footer>

  19. </div>

  20. </template>

  21. <script>

  22. // 导入 axios 请求库

  23. import axios from 'axios'

  24. // 导入需要的组件

  25. import Header from '@/shopping/Header/Header.vue'

  26. import Goods from '@/shopping/Goods/Goods.vue'

  27. import Footer from '@/shopping/Footer/Footer.vue'

  28. export default {

  29. data(){

  30. return {

  31. // 用来存储购物车的列表数据,默认为空数组

  32. list:[]

  33. }

  34. },

  35. methods:{

  36. // 封装请求列表数据的方法

  37. async initCarList(){

  38. // 调用 axios 的 get 方法,请求列表数据

  39. const {data:res} = await axios.get("https://www.escook.cn/api/cart")

  40. console.log(res);

  41. if(res.status === 200){

  42. this.list = res.list

  43. }

  44. },

  45. // 接收子组件传递过来的数据

  46. getNewState(val){

  47. this.list.some(item => {

  48. if(item.id === val.id){

  49. item.goods_state = val.value

  50. // 终止后续循环

  51. return true

  52. }

  53. })

  54. },

  55. // 接收 Footer 子组件传递过来的全选按钮的状态

  56. getFullState(val){

  57. this.list.forEach(item => item.goods_state = val)

  58. }

  59. },

  60. computed:{

  61. // 动态计算出全选的状态是 true 还是 false

  62. fullState(){

  63. return this.list.every(item => item.goods_state)

  64. },

  65. // 已勾选的商品总价格

  66. amt(){

  67. // 1.先filter过滤

  68. // 2.再reduce累加

  69. return this.list.filter(item=>item.goods_state).reduce((total,item)=>{

  70. return total+=item.goods_price * item.goods_count

  71. },0)

  72. },

  73. // 已勾选商品的总数量

  74. total(){

  75. return this.list.filter(item => item.goods_state).reduce((t,item)=>{

  76. return t+=item.goods_count

  77. },0)

  78. }

  79. },

  80. components:{

  81. Header,Goods,Footer

  82. },

  83. created(){

  84. // 调用请求数据的方法

  85. this.initCarList()

  86. }

  87. }

  88. </script>

  89. <style lang="less" scoped>

  90. .app-container{

  91. padding-top: 45px;

  92. padding-bottom:50px

  93. }

  94. </style>



Counter

因为count是修改商品的数量的,所以修改的数量要直接修改到父组件的App.vue里面的数据,而要想直接修改App.vue数据是不可能的,因为Counter组件外面还嵌套一层Goods组件,App与Counter相当于爷孙的关系,所以我们可以通过eventBus让Counter直接去修改App里面的值。

eventBus.js文件

  1. import Vue from 'vue'

  2. export default new Vue()


 Counter.vue子组件

因为Counter是嵌套在Goods组件里面的,所以我们还需要在Goods组件去引用Counter子组件

给Goods的props属性在设置一个count,用来表明商品的数量。

  1. <template>

  2. <div class="number-container d-flex justify-content-center align-items-center">

  3. <!-- 减 1 的按钮 -->

  4. <button type="button" class="btn btn-light bnt-sm" @click="sub">-</button>

  5. <!-- 购买的数量 -->

  6. <span class="number-box">{{num}}</span>

  7. <!-- 加 1 的按钮 -->

  8. <button type="button" class="btn btn-light bnt-sm" @click="add">+</button>

  9. </div>

  10. </template>


  11. <script>

  12. // 导入eventBus文件

  13. import bus from '@/shopping/eventBus.js'

  14. export default {

  15. props:{

  16. // 接收商品的id值,将来使用 EventBus 方案,把数量传递到 App.vue 的时候,需要通知 App 组件,更新哪个商品的数量

  17. id:{

  18. type:Number,

  19. required:true

  20. },

  21. // 接收到的 num 数量值

  22. num:{

  23. type:Number,

  24. default:1

  25. }

  26. },

  27. methods:{

  28. // 点击按钮,数值+1

  29. add(){

  30. // 要发送给 App 的数据格式为 {id,value}

  31. // 其中,id是商品的id;value是商品最新的购买数量

  32. const obj = {id:this.id,value:this.num+1}

  33. // 要做的事:通过 EventBus 把 obj 对象,发送给 App.vue 组件

  34. bus.$emit('share',obj)

  35. },

  36. sub(){

  37. if(this.num-1 == 0) return

  38. // 要发送给 App 的数据格式为 {id,value}

  39. // 其中,id是商品的id;value是商品最新的购买数量

  40. const obj = {id:this.id,value:this.num-1}

  41. // 要做的事:通过 EventBus 把 obj 对象,发送给 App.vue 组件

  42. bus.$emit('share',obj)

  43. }

  44. }

  45. }

  46. </script>


  47. <style>


  48. </style>


App.vue父组件代码

导入eventBus.js文件,通过bus.$on()方法,调用Counter传来的share里面的数据,通过传来的数据,来修改list里面的goods_count里面的值。

  1. <template>

  2. <div class="app-container">

  3. <!-- 头部区域 -->

  4. <Header title="购物车案例"></Header>

  5. <!-- 循环渲染每一个商品的信息 -->

  6. <Goods

  7. v-for="item in list"

  8. :key="item.id"

  9. :id="item.id"

  10. :title="item.goods_name"

  11. :pic="item.goods_img"

  12. :price="item.goods_price"

  13. :state="item.goods_state"

  14. :count="item.goods_count"

  15. @state-change="getNewState"

  16. >

  17. </Goods>

  18. <!-- Footer区域 -->

  19. <Footer :isfull="fullState" :amount="amt" :all="total" @full-change="getFullState"></Footer>

  20. </div>

  21. </template>

  22. <script>

  23. // 导入 axios 请求库

  24. import axios from 'axios'

  25. // 导入需要的组件

  26. import Header from '@/shopping/Header/Header.vue'

  27. import Goods from '@/shopping/Goods/Goods.vue'

  28. import Footer from '@/shopping/Footer/Footer.vue'

  29. // 导入eventBus文件

  30. import bus from '@/shopping/eventBus.js'

  31. export default {

  32. data(){

  33. return {

  34. // 用来存储购物车的列表数据,默认为空数组

  35. list:[]

  36. }

  37. },

  38. methods:{

  39. // 封装请求列表数据的方法

  40. async initCarList(){

  41. // 调用 axios 的 get 方法,请求列表数据

  42. const {data:res} = await axios.get("https://www.escook.cn/api/cart")

  43. console.log(res);

  44. if(res.status === 200){

  45. this.list = res.list

  46. }

  47. },

  48. // 接收子组件传递过来的数据

  49. getNewState(val){

  50. this.list.some(item => {

  51. if(item.id === val.id){

  52. item.goods_state = val.value

  53. // 终止后续循环

  54. return true

  55. }

  56. })

  57. },

  58. // 接收 Footer 子组件传递过来的全选按钮的状态

  59. getFullState(val){

  60. this.list.forEach(item => item.goods_state = val)

  61. }

  62. },

  63. computed:{

  64. // 动态计算出全选的状态是 true 还是 false

  65. fullState(){

  66. return this.list.every(item => item.goods_state)

  67. },

  68. // 已勾选的商品总价格

  69. amt(){

  70. // 1.先filter过滤

  71. // 2.再reduce累加

  72. return this.list.filter(item=>item.goods_state).reduce((total,item)=>{

  73. return total+=item.goods_price * item.goods_count

  74. },0)

  75. },

  76. // 已勾选商品的总数量

  77. total(){

  78. return this.list.filter(item => item.goods_state).reduce((t,item)=>{

  79. return t+=item.goods_count

  80. },0)

  81. }

  82. },

  83. components:{

  84. Header,Goods,Footer

  85. },

  86. created(){

  87. // 调用请求数据的方法

  88. this.initCarList()

  89. bus.$on('share',val=>{

  90. this.list.some(item=>{

  91. if(item.id === val.id){

  92. item.goods_count = val.value

  93. return true

  94. }

  95. })

  96. })

  97. }

  98. }

  99. </script>

  100. <style lang="less" scoped>

  101. .app-container{

  102. padding-top: 45px;

  103. padding-bottom:50px

  104. }

  105. </style>



这个案例对初学vue者还是有很大的借鉴意义,通过此案例了解组件之间的数据共享的各种方式,不了解的可以先看下右边这篇文章 组件的数据共享。通过项目案例将自己所学知识融会贯通这一点非常重要,多做项目对成长的帮助非常大,希望与诸位共勉。

    版权声明:本文内容始发于CSDN>作者 : 亦世凡华、,遵循CC 4.0 BY-SA版权协议处链接及本声明。
    本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行可。
    始发链接:https://z5qyj5pyi.blog.csdn.net/article/details/128101078?spm=1001.2014.3001.5502
    在此特别鸣谢:CSDN博主>亦世凡华、的创作。
    本文已获原作者亦世凡华、授权发布在本公众号;
    原作者已在本公众号关联运营账号,故在此声明本文原创为CSDN亦世凡华。
    此篇文章的所有版权归原作者所有,商业转载建议请联系原作者,非商业转载请注明出处。

    文章转载自巴韭特锁螺丝,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

    评论