小言_互联网的博客

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

508人阅读  评论(0)

目录

前期准备

Header

Goods

Footer

Counter


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

前期准备

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

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

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

根据上文图片案例,我们先从上面也是最简单的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.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者还是有很大的借鉴意义,通过此案例了解组件之间的数据共享的各种方式,不了解的可以先看下右边这篇文章 组件的数据共享 。通过项目案例将自己所学知识融会贯通这一点非常重要,多做项目对成长的帮助非常大,希望与诸位共


转载:https://blog.csdn.net/qq_53123067/article/details/128101078
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场