列表添加至购物车(Typescript React Shopping cart)

简介: 列表添加至购物车(Typescript React Shopping cart)

效果概述

列表页面显示数据,实现按尺码进行过滤,实现价格高低排序。点击添加至购物车,实现购物车所需内容。

具体效果

点击跳转至效果页面(Typescript React Shopping cart)

1.列表页Tab切换效果实现
2.列表页面点击排序效果
3.请求接口实现商品列表
4.添加购物车的数据保存到本地存储, 数据存放以及操作使用vuex。
5.商品点击加减变化效果
6.计算总数量和总价钱
7.移动端适配

实现后效果图:

image.png

列表添加至购物车(Typescript React Sho)

大致代码思路:

  • 根页面:
router-view 标签接收数据

列表页

在 main.js 页面引入 axios 插件
在 main.js 页面引入 rem 移动端适配代码
通过 rem 进行移动端适配
通过 axios 接口地址请求数据
把请求到的数据赋值给data里的选项
在 html 代码中通过 v-for 实现循环,把数据渲染至视图中
点击添加至购物车
按价格实现从高到低,从低到高
按尺码进行过滤
实现分期购买价格

购物车页:

通过vuex获取添加过来得数据
在 html 代码中通过 v-for 实现循环,把数据渲染至视图中
数量加减
总价和分期价格

main.js代码:

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import axios from 'axios'
Vue.prototype.$axios = axios
import '@/rem'
Vue.config.productionTip = false
new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app')

rem 适配页面代码:

const WIDTH = 375//如果是尺寸的设计稿在这里修改
const setView = () => {
    //设置html标签的fontSize
    document.documentElement.style.fontSize = (100 * document.documentElement.clientWidth / WIDTH) + 'px'
}
window.onresize = setView
setView()

实现路由跳转页代码:

import Vue from 'vue'
import VueRouter from 'vue-router'
import HomeView from '../views/HomeView.vue'
Vue.use(VueRouter)
const routes = [
  {
    path: '/',
    name: 'home',
    component: HomeView
  },
  {
    path: '/shopping',
    name: 'Shopping',
    component:()=>import('@/views/Shopping.vue')
  }
]
const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes
})
export default router

根页面代码:

<template>
  <div id="app">
    <router-view></router-view>
  </div>
</template>
<style lang="scss">
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
  font-size: 16px;
}
</style>

vuex页面代码:

import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
  state: {
    list: JSON.parse(localStorage.getItem('list')) || [],
  },
  getters: {
    // 请求到多少个
    totals(state) {
      let num = 0;
      state.list.map((item) => {
        num += item.num;
      });
      return num;
    },
    // 购物车有多少个
    total(state) {
      let num = 0;
      state.list.map((item) => {
        num += item.num * item.price;
      });
      return num;
    },
    // 购物车总价
    installments(state) {
      let num = 0;
      state.list.map((item) => {
        num += (item.num * item.price) / item.installments;
      });
      return Math.floor(num * 100) / 100;
    },
  },
  mutations: {
    // 添加至购物车
    addShopping(state, data) {
      // 需要添加的内容
      let arr = {id:state.list+1,title:data.title,price:data.price,style:data.style,availableSizes:data.availableSizes,num:1,installments:data.installments}
      // 判断需要添加的内容在购物车中是否存在
      let a = state.list.some((item) => {
        return item.title == data.title;
      });
      // 获取添加的值在购物车中的下标
      let num = 0
      for(let i=0; i<state.list.length; i++){
        if(state.list[i].title == data.title){
          num = i
        }
      }
      // 判断添加的内容购物车中是否存在
      if (a) {
        // 存在则数量加一
        state.list[num].num = state.list[num].num+1
      } else {
        // 不存在则添加至购物车
        state.list.push(arr)
      }
      // 存储到本地
      localStorage.setItem('list',JSON.stringify(state.list))
    },
    // 购物车单个数量减
    jian(state, data) {
      // 判断数量是否大于一,大于一则点击一次减一,否则不变
      data.num > 1 ? data.num = data.num - 1 : data.num 
      // 本地存储
      localStorage.setItem('list',JSON.stringify(state.list))
    },
    // 购物车单个数量加
    jia(state, data) {
      // 点击加一
      data.num = data.num + 1
      // 本地存储
      localStorage.setItem('list',JSON.stringify(state.list))
    },
    // 购物车中删除
    deletes(state, data) {
      // 点击删除
      state.list.splice(data.id - 1, 1)
      // 本地存储
      localStorage.setItem('list',JSON.stringify(state.list))
    },
  },
  actions: {
  },
  modules: {
  }
})

列表页代码:

<template>
  <div class="box">
    <div class="top">
      <div class="topleft"></div>
      <router-link to="/shopping" tag="div" class="topright">
        <img
          src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAQAAADZc7J/AAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAAmJLR0QAAKqNIzIAAAAJcEhZcwAADdcAAA3XAUIom3gAAAAHdElNRQfiCwwQEgYn7+gWAAABQklEQVRIx9WTsUoDQRRF72xMYSFokcRCjBFESGWRwtZe8AtEFFNrI1bB0tLGQuzs/AJB8AO2kZRCxBiRQNKIoIYoyLFwCZuss9nNgpBXzc7be3j3zow09mU48321zVF8gNtbZ5Q1UwmGYQPIxFU5vnVdUuHfARO+OD5oa9GzM6NcbICkem+CLZ0kA1zKHQ2w6tlpqRUN4AwA5knFiTAISGsuHqDfwoOkgp4kUkoP1WI+Azt02ZYk9hle3cAEBh69c7iKEOJ30IJU/71KpqbaKCH6b0LEGgTcaYXpeIj+GJf54pyI70CSTACxq1M5ehmqvDCHfwIkFrQW4S1WzXUCq+E5lNhkMqSfo0ze3t7hlWeqIfJ3GnQo2n644ZhZYMnSL9OQcKnYAHs0ueUeY+nn6eDyRskGcFjngGxIBkUqVvk41g+oBJ136GBf8AAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAxOC0xMS0xMlQxNjoxODowNiswMTowMEVm3zEAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMTgtMTEtMTJUMTY6MTg6MDYrMDE6MDA0O2eNAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAAABJRU5ErkJggg=="
          alt=""
        />
        <span>{{ totals }}</span>
      </router-link>
    </div>
    <div class="sizes">
      <div class="sizes-li">
        <h4>Sizes:</h4>
        <div class="size">
          <button @click="sizes('XS')">XS</button>
          <button @click="sizes('S')">S</button>
          <button @click="sizes('M')">M</button>
          <button @click="sizes('ML')">ML</button>
          <button @click="sizes('L')">L</button>
          <button @click="sizes('XL')">XL</button>
          <button @click="sizes('XXL')">XXL</button>
        </div>
        <p>Leave a star on Github if this repository was useful :)</p>
        <a href="#">Star</a>
      </div>
      <p>{{ listd.length }} Product(s) found</p>
      <select name="" id="" v-model="val" @click="vals">
        <option value="默认">默认</option>
        <option value="升序">高到低</option>
        <option value="降序">低到高</option>
      </select>
    </div>
    <ul class="content">
      <li v-for="item in lists" :key="item.id">
        <div class="img">
          <p v-show="item.isFreeShipping">免运费</p>
        </div>
        <div class="title">{{ item.title }}</div>
        <p class="price">{{ item.currencyFormat }} {{ item.price }}</p>
        <p class="installments" v-show="item.installments == 0 ? false : true">
          or {{ item.installments }} x {{ item.currencyFormat }}
          {{ Math.floor((item.price / item.installments) * 100) / 100 }}
        </p>
        <router-link to="/shopping" class="join"
          ><button @click="addShopping(item)">加入购物车</button></router-link
        >
      </li>
    </ul>
  </div>
</template>
<script>
import { mapState, mapGetters } from "vuex";
export default {
  data() {
    return {
      listd: [],
      lists: [],
      val: "默认",
    };
  },
  // 获取 vuex 里的内容
  computed: {
    ...mapState(["list"]),
    ...mapGetters(["totals"]),
  },
  created() {
    // 进入时调用
    this.request();
  },
  methods: {
    request() {
      // 请求接口
      this.$store.commit("request");
      this.$axios
        .get(`https://react-shopping-cart-67954.firebaseio.com/products.json`)
        .then(({ data }) => {
          console.log(data);
          // 把请求到的值分别赋值给 listd 和 lists
          this.listd = data.products;
          this.lists = data.products;
          // 存储到本地
          localStorage.setItem("listsd", JSON.stringify(this.listd));
        });
    },
    //添加到购物车
    addShopping(item) {
      this.$store.commit("addShopping", item);
    },
    // 点击显示尺码
    sizes(val) {
      // lists 赋值为空
      this.lists = [];
      // 给 listd 进行过滤
      this.listd.filter((item, index) => {
        // listd值得availableSizes进行循环
        item.availableSizes.some((items) => {
          // 判断availableSizes的值是否等于传递过来的参数
          if (items == val) {
            // 等于则追加到 lists 中
            this.lists.push(item);
          }
        });
      });
    },
    // 升序降序
    vals() {
      // 给 lists 用 sort 进行排序
      this.lists.sort((p1, p2) => {
        // 判断val 等于升序 则 2 - 1
        if (this.val == "升序") {
          return p2.price - p1.price;
          // 判断val 等于降序 则 1 - 2
        } else if (this.val == "降序") {
          return p1.price - p2.price;
          // 判断val 等于默认 则把本地存储的值赋值给lists
        } else if (this.val == "默认") {
          this.lists = JSON.parse(localStorage.getItem("listsd"));
        }
      });
    },
  },
};
</script>
<style lang="scss" scoped>
.box {
  width: 100%;
  .top {
    width: 100%;
    height: 0.5rem;
    // background-color: red;
    position: relative;
    .topleft {
      width: 1.2rem;
      height: 1.2rem;
      background-color: #000;
      position: absolute;
      transform: rotate(45deg);
      top: -0.6rem;
      left: -0.6rem;
    }
    .topright {
      width: 0.5rem;
      height: 0.5rem;
      background-color: #000;
      position: absolute;
      right: 0;
      display: flex;
      justify-content: space-between;
      align-items: center;
      span {
        display: inline-block;
        position: absolute;
        bottom: 0;
        left: 0;
        width: 0.2rem;
        height: 0.2rem;
        border-radius: 50%;
        text-align: center;
        line-height: 0.2rem;
        background-color: yellow;
      }
    }
  }
  .sizes {
    width: 100%;
    padding: 0.15rem;
    .sizes-li {
      margin: 0 0.15rem;
      .size {
        margin: 0.2rem 0;
        display: flex;
        justify-content: space-between;
        button {
          width: 0.35rem;
          height: 0.35rem;
          border-radius: 50%;
          background-color: aliceblue;
          border: none;
        }
      }
      p {
        margin: 0.15rem 0;
        font-size: 0.1rem;
      }
      a {
        font-size: 0.15rem;
      }
    }
    p {
      margin: 0.3rem 0 0.2rem;
    }
  }
  .content {
    width: 100%;
    padding: 0 0.15rem;
    background-color: red;
    display: flex;
    justify-content: space-between;
    flex-wrap: wrap;
    li {
      width: 1.675rem;
      height: 4.5rem;
      list-style: none;
      margin: 0.1rem 0;
      text-align: center;
      .img {
        width: 100%;
        height: 2.7rem;
        background-color: pink;
        p {
        }
      }
      .title {
        height: 0.4rem;
        padding: 0 0.2rem;
        margin: 0.15rem 0;
      }
      .price {
      }
      .installments {
        color: gainsboro;
        font-size: 0.12rem;
      }
      .join {
        width: 100%;
        height: 0.6rem;
        padding: 0.15rem 0;
        display: inline-block;
        background-color: #000;
        border: none;
        margin-top: 0.1rem;
        button {
          border: none;
          background-color: #000;
          color: #fff;
        }
      }
    }
  }
}
</style>

购物车页代码:

<template>
  <div class="box">
    <div class="top">
      <router-link to="/" tag="div" class="topleft">X</router-link>
    </div>
    <div class="num">
      <img
        src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAQAAADZc7J/AAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAAmJLR0QAAKqNIzIAAAAJcEhZcwAADdcAAA3XAUIom3gAAAAHdElNRQfiCwwQEgYn7+gWAAABQklEQVRIx9WTsUoDQRRF72xMYSFokcRCjBFESGWRwtZe8AtEFFNrI1bB0tLGQuzs/AJB8AO2kZRCxBiRQNKIoIYoyLFwCZuss9nNgpBXzc7be3j3zow09mU48321zVF8gNtbZ5Q1UwmGYQPIxFU5vnVdUuHfARO+OD5oa9GzM6NcbICkem+CLZ0kA1zKHQ2w6tlpqRUN4AwA5knFiTAISGsuHqDfwoOkgp4kUkoP1WI+Azt02ZYk9hle3cAEBh69c7iKEOJ30IJU/71KpqbaKCH6b0LEGgTcaYXpeIj+GJf54pyI70CSTACxq1M5ehmqvDCHfwIkFrQW4S1WzXUCq+E5lNhkMqSfo0ze3t7hlWeqIfJ3GnQo2n644ZhZYMnSL9OQcKnYAHs0ueUeY+nn6eDyRskGcFjngGxIBkUqVvk41g+oBJ136GBf8AAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAxOC0xMS0xMlQxNjoxODowNiswMTowMEVm3zEAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMTgtMTEtMTJUMTY6MTg6MDYrMDE6MDA0O2eNAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAAABJRU5ErkJggg=="
        alt=""
      />
      <h4>Cart</h4>
    </div>
    <ul class="content">
      <li v-for="item in list" :key="item.id">
        <div class="img"></div>
        <div class="center">
          <p>{{ item.title }}</p>
          <p>{{ item.availableSizes[0] }} | {{ item.style }}</p>
          <p>数量:{{ item.num }}</p>
        </div>
        <div class="right">
          <img
            src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB0AAAAOCAYAAADT0Rc6AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA2hpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDowMzgwMTE3NDA3MjA2ODExODA4MzlFRjgwMkJGMENDMSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDo0NzRFMzQ0QjI3MzgxMUU4QjRFMUVBNEJCODU5RDAzMSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo0NzRFMzQ0QTI3MzgxMUU4QjRFMUVBNEJCODU5RDAzMSIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M2IChNYWNpbnRvc2gpIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6RUE3RjExNzQwNzIwNjgxMUIxQTQ5QTgyNkJBMzJBOEUiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6MDM4MDExNzQwNzIwNjgxMTgwODM5RUY4MDJCRjBDQzEiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz5cNiR0AAAA50lEQVR42qSUaw6EMAiEYY8rB7JcVrYoNaQCNbsk88N2ypc+HICrtq69CyEvNM8mIuCk33sXTuNeaJ5zrRZ1HV361RIw2pyYd4Cp65CrWgJGmxPz6gbvZpKAMfJYw9FMEjBGntGUE3AEVC+6ppyAI6B60e8mAldAWIBD4LjTCvwAqjG5txkcAmdoBWZ/z8UL5RVQ9YF3JfB7lWtXx9v+ON4WHW8E5GTszUPiZOx+SBkQq7kFEKs5yH6LxbFn4cBBOHAUDj4GuYhBdjFIQQxyEYPsYpBGUypyd45DmppSkbtzHJ5rvwIMAKXLCXxfiHXkAAAAAElFTkSuQmCC"
            @click="deletes(item)"
          />
          <p>$ {{ item.price * item.num }}</p>
          <div>
            <button @click="jian(item)">-</button>
            <button @click="jia(item)">+</button>
          </div>
        </div>
      </li>
    </ul>
    <div class="bottom">
      <div>
        <span>小计</span>
        <span>
          $ {{ total }}
          <p v-if="list.length < 1 ? false : true">
            or up to {{ list[list.length - 1].installments }} X $
            {{ installments }}
          </p>
        </span>
      </div>
      <button @click="collection">收款处</button>
    </div>
  </div>
</template>
<script>
import { mapGetters, mapState } from "vuex";
export default {
  computed: {
    // 获取 vuex 里的内容
    ...mapState(["list", "num"]),
    ...mapGetters(["total", "installments"]),
  },
  methods: {
    // 减
    jian(item) {
      this.$store.commit("jian", item);
    },
    // 加
    jia(item) {
      this.$store.commit("jia", item);
    },
    // 收款
    collection() {
      alert("Checkout - Subtotal: $ " + this.total);
    },
    // 删除
    deletes(item) {
      this.$store.commit("deletes", item);
    },
  },
};
</script>
<style lang="scss" scoped>
.box {
  width: 100%;
  height: 100vh;
  background-color: #000;
  opacity: 0.9;
  color: #fff;
  .top {
    .topleft {
      width: 0.5rem;
      height: 0.5rem;
      text-align: center;
      line-height: 0.5rem;
    }
  }
  .num {
    width: 100%;
    // background-color: red;
    text-align: center;
    display: flex;
    justify-content: center;
    align-items: center;
    h4 {
      margin-left: 0.2rem;
    }
  }
  .content {
    li {
      width: 100%;
      padding: 0 0.15rem;
      border-top: 1px solid #000;
      opacity: 1;
      display: flex;
      justify-content: space-between;
      .img {
        width: 0.5rem;
        height: 0.75rem;
        background-color: #fff;
      }
    }
  }
  .bottom {
    width: 100%;
    position: fixed;
    bottom: 0;
    div {
      width: 100%;
      display: flex;
      justify-content: space-between;
    }
    button {
      width: 100%;
      height: 0.5rem;
      border: none;
    }
  }
}
</style>

以上就是列表添加至购物车(Typescript React Shopping cart)的代码,不懂得也可以在评论区里问我,以后会持续添加一些新的功能,敬请关注。

相关文章
|
4月前
|
前端开发 JavaScript 安全
TypeScript在React Hooks中的应用:提升React开发的类型安全与可维护性
【7月更文挑战第17天】TypeScript在React Hooks中的应用极大地提升了React应用的类型安全性和可维护性。通过为状态、依赖项和自定义Hooks指定明确的类型,开发者可以编写更加健壮、易于理解和维护的代码。随着React和TypeScript的不断发展,结合两者的优势将成为构建现代Web应用的标准做法。
|
29天前
|
前端开发 JavaScript
手敲Webpack 5:React + TypeScript项目脚手架搭建实践
手敲Webpack 5:React + TypeScript项目脚手架搭建实践
|
1月前
|
JavaScript 前端开发 安全
使用 TypeScript 加强 React 组件的类型安全
【10月更文挑战第1天】使用 TypeScript 加强 React 组件的类型安全
38 3
|
1月前
|
前端开发 JavaScript API
React 列表 & Keys
10月更文挑战第9天
14 0
|
1月前
|
JavaScript 前端开发 算法
写 React / Vue 项目时为什么要在列表组件中写 key
在React或Vue项目中,为列表组件中的每个元素添加唯一的key属性,有助于框架高效地更新和渲染列表。Key帮助虚拟DOM识别哪些项已更改、添加或删除,从而优化性能并减少不必要的重新渲染。
|
3月前
|
JavaScript 前端开发 安全
[译] 使用 TypeScript 开发 React Hooks
[译] 使用 TypeScript 开发 React Hooks
|
3月前
|
开发者 自然语言处理 存储
语言不再是壁垒:掌握 JSF 国际化技巧,轻松构建多语言支持的 Web 应用
【8月更文挑战第31天】JavaServer Faces (JSF) 框架提供了强大的国际化 (I18N) 和本地化 (L10N) 支持,使开发者能轻松添加多语言功能。本文通过具体案例展示如何在 JSF 应用中实现多语言支持,包括创建项目、配置语言资源文件 (`messages_xx.properties`)、设置 `web.xml`、编写 Managed Bean (`LanguageBean`) 处理语言选择,以及使用 Facelets 页面 (`index.xhtml`) 显示多语言消息。通过这些步骤,你将学会如何配置 JSF 环境、编写语言资源文件,并实现动态语言切换。
42 0
|
3月前
|
前端开发 JavaScript 安全
【前端开发新境界】React TypeScript融合之路:从零起步构建类型安全的React应用,全面提升代码质量和开发效率的实战指南!
【8月更文挑战第31天】《React TypeScript融合之路:类型安全的React应用开发》是一篇详细教程,介绍如何结合TypeScript提升React应用的可读性和健壮性。从环境搭建、基础语法到类型化组件、状态管理及Hooks使用,逐步展示TypeScript在复杂前端项目中的优势。适合各水平开发者学习,助力构建高质量应用。
64 0
|
1月前
|
JavaScript 前端开发 安全
深入理解TypeScript:增强JavaScript的类型安全性
【10月更文挑战第8天】深入理解TypeScript:增强JavaScript的类型安全性
49 0
|
1月前
|
JavaScript 前端开发 开发者
深入理解TypeScript:类型系统与实用技巧
【10月更文挑战第8天】深入理解TypeScript:类型系统与实用技巧