Skip to content

购物车案例

分析功能

界面逻辑

  • 商品展示模块:添加减少商品
  • 结算信息模块:计算总价、商品数、配送费、起送价

数据逻辑

  • (单个)设计每个商品的数据结构、并添加属性(选中的数量)
  • (一组)根据原始数据创建商品数据
  • (界面所有)UI 上所需信息(计算总价、总件数、配送费、是否选中商品、是否满起送费)

目录

base
📦shopping-car
┣ 📜data.js
┣ 📜index.html
┗ 📜index.js
📦shopping-car
┣ 📜data.js
┣ 📜index.html
┗ 📜index.js
html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>点单购物车</title>
  </head>
  <style>
    * {
      margin: 0;
      padding: 0;
    }

    html,
    body,
    #container {
      width: 100%;
      height: 100vh;
    }
    .goods-list-container {
      height: calc(100vh - 60px);
    }
    .btn-info {
      display: flex;
      justify-content: end;
      align-items: center;
      gap: 12px;
    }
    li {
      text-align: right;
      margin-bottom: 24px;
      border-bottom: 1px solid #eee;
      padding: 12px;
    }
    .btn {
    }
    .footer-container {
      box-sizing: border-box;
      height: 60px;
      width: 100%;
      display: flex;
      justify-content: space-between;
      align-items: center;
      background-color: aliceblue;
      padding: 0px 12px;
    }
  </style>
  <body>
    <div class="page-container">
      <div class="goods-list-container">
        <ul class="goods-list"></ul>
      </div>
      <div class="footer-container">
        <div>
          <span class="total">总额:0元</span>
          <span class="delivery-price">配送费:5元</span>
        </div>
        <div class="need-price">还差30元起送</div>
      </div>
    </div>
    <script src="./data.js"></script>
    <script src="./index.js"></script>
  </body>
</html>
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>点单购物车</title>
  </head>
  <style>
    * {
      margin: 0;
      padding: 0;
    }

    html,
    body,
    #container {
      width: 100%;
      height: 100vh;
    }
    .goods-list-container {
      height: calc(100vh - 60px);
    }
    .btn-info {
      display: flex;
      justify-content: end;
      align-items: center;
      gap: 12px;
    }
    li {
      text-align: right;
      margin-bottom: 24px;
      border-bottom: 1px solid #eee;
      padding: 12px;
    }
    .btn {
    }
    .footer-container {
      box-sizing: border-box;
      height: 60px;
      width: 100%;
      display: flex;
      justify-content: space-between;
      align-items: center;
      background-color: aliceblue;
      padding: 0px 12px;
    }
  </style>
  <body>
    <div class="page-container">
      <div class="goods-list-container">
        <ul class="goods-list"></ul>
      </div>
      <div class="footer-container">
        <div>
          <span class="total">总额:0元</span>
          <span class="delivery-price">配送费:5元</span>
        </div>
        <div class="need-price">还差30元起送</div>
      </div>
    </div>
    <script src="./data.js"></script>
    <script src="./index.js"></script>
  </body>
</html>
js
// 商品数据
class GoodsItem {
  constructor(g) {
    this.data = g;
    this.choose = 0;
  }
}
// 整个界面数据
class UIData {
  constructor(g) {
    this.data = g.map((goods) => new GoodsItem(goods));
    this.deliveryShould = 30;
    this.deliveryPrice = 5;
  }

  // 添加某件商品数量
  increase(index) {
    this.data[index].choose++;
  }
  // 减少某件商品数量
  decrease(index) {
    !!this.data[index].choose && this.data[index].choose--;
  }
  // 获取总价
  getTotalPrice() {
    return this.data.reduce((pre, cur) => {
      return pre + cur.choose * cur.data.price;
    }, 0);
  }
  // 获取选中商品总数量
  getTotalChooseNumber() {
    return this.data.reduce((pre, cur) => {
      return pre + cur.choose;
    }, 0);
  }
  // 是否选中商品
  getIsHasChoose() {
    return this.getTotalChooseNumber() > 0;
  }
  // 是否满配送费
  getIsShouldDelivery() {
    return this.getTotalPrice() >= this.deliveryShould;
  }
}

// 界面操作
class UI {
  constructor(g) {
    this.uiData = new UIData(g);
    this.doms = {
      goodsContainer: document.querySelector(".goods-list"),
      totalDom: document.querySelector(".total"),
      deliveryPriceDom: document.querySelector(".delivery-price"),
      needPriceDom: document.querySelector(".need-price"),
    };
  }

  // 创建商品元素
  createHtml() {
    let html = "";
    console.log("this.uiData", this.uiData);
    for (let i = 0; i < this.uiData.data.length; i++) {
      const item = this.uiData.data[i];
      html += `<li>
        <div>${item.data.title}</div>
        <div>${item.data.price}</div>
        <div class="btn-info">
          <div class="btn btn-dec">-</div>
          <div class="choose">${item.choose}</div>
          <div class="btn btn-inc">+</div>
        </div>
      </li>`;
    }
    this.doms.goodsContainer.innerHTML = html;
  }
  // 增加某件商品
  increase(index) {
    this.uiData.increase(index);
    this.updateGoodsItem(index);
    this.updateBill();
  }
  // 减少某件商品
  decrease(index) {
    this.uiData.decrease(index);
    this.updateGoodsItem(index);
    this.updateBill();
  }
  // 更新某个商品的状态
  updateGoodsItem(index) {
    const goodsItem = this.doms.goodsContainer.children[index];
    // 更新数量
    const chooseDom = goodsItem.querySelector(".choose");
    chooseDom.textContent = this.uiData.data[index].choose;
  }

  // 更新结算栏状态
  updateBill() {
    const total = this.uiData.getTotalPrice();
    const needPrice = this.uiData.deliveryShould - total;
    this.doms.totalDom.textContent = `商品费用:${total}元`;
    this.doms.deliveryPriceDom.textContent = `配送费:${this.uiData.deliveryPrice}元`;
    this.doms.needPriceDom.textContent = `还差${needPrice >= 0 ? needPrice : 0}元起送`;
  }
}

// 初始化
const ui = new UI(goodsData);
// 创建商品元素
ui.createHtml();
// 注册事件
ui.doms.goodsContainer.addEventListener("click", (e) => {
  if (e.target.classList.contains("btn-inc")) {
    const index = +e.target.getAttribute("index");
    ui.increase(index);
  }
  if (e.target.classList.contains("btn-dec")) {
    const index = +e.target.getAttribute("index");
    ui.decrease(index);
  }
});
// 商品数据
class GoodsItem {
  constructor(g) {
    this.data = g;
    this.choose = 0;
  }
}
// 整个界面数据
class UIData {
  constructor(g) {
    this.data = g.map((goods) => new GoodsItem(goods));
    this.deliveryShould = 30;
    this.deliveryPrice = 5;
  }

  // 添加某件商品数量
  increase(index) {
    this.data[index].choose++;
  }
  // 减少某件商品数量
  decrease(index) {
    !!this.data[index].choose && this.data[index].choose--;
  }
  // 获取总价
  getTotalPrice() {
    return this.data.reduce((pre, cur) => {
      return pre + cur.choose * cur.data.price;
    }, 0);
  }
  // 获取选中商品总数量
  getTotalChooseNumber() {
    return this.data.reduce((pre, cur) => {
      return pre + cur.choose;
    }, 0);
  }
  // 是否选中商品
  getIsHasChoose() {
    return this.getTotalChooseNumber() > 0;
  }
  // 是否满配送费
  getIsShouldDelivery() {
    return this.getTotalPrice() >= this.deliveryShould;
  }
}

// 界面操作
class UI {
  constructor(g) {
    this.uiData = new UIData(g);
    this.doms = {
      goodsContainer: document.querySelector(".goods-list"),
      totalDom: document.querySelector(".total"),
      deliveryPriceDom: document.querySelector(".delivery-price"),
      needPriceDom: document.querySelector(".need-price"),
    };
  }

  // 创建商品元素
  createHtml() {
    let html = "";
    console.log("this.uiData", this.uiData);
    for (let i = 0; i < this.uiData.data.length; i++) {
      const item = this.uiData.data[i];
      html += `<li>
        <div>${item.data.title}</div>
        <div>${item.data.price}</div>
        <div class="btn-info">
          <div class="btn btn-dec">-</div>
          <div class="choose">${item.choose}</div>
          <div class="btn btn-inc">+</div>
        </div>
      </li>`;
    }
    this.doms.goodsContainer.innerHTML = html;
  }
  // 增加某件商品
  increase(index) {
    this.uiData.increase(index);
    this.updateGoodsItem(index);
    this.updateBill();
  }
  // 减少某件商品
  decrease(index) {
    this.uiData.decrease(index);
    this.updateGoodsItem(index);
    this.updateBill();
  }
  // 更新某个商品的状态
  updateGoodsItem(index) {
    const goodsItem = this.doms.goodsContainer.children[index];
    // 更新数量
    const chooseDom = goodsItem.querySelector(".choose");
    chooseDom.textContent = this.uiData.data[index].choose;
  }

  // 更新结算栏状态
  updateBill() {
    const total = this.uiData.getTotalPrice();
    const needPrice = this.uiData.deliveryShould - total;
    this.doms.totalDom.textContent = `商品费用:${total}元`;
    this.doms.deliveryPriceDom.textContent = `配送费:${this.uiData.deliveryPrice}元`;
    this.doms.needPriceDom.textContent = `还差${needPrice >= 0 ? needPrice : 0}元起送`;
  }
}

// 初始化
const ui = new UI(goodsData);
// 创建商品元素
ui.createHtml();
// 注册事件
ui.doms.goodsContainer.addEventListener("click", (e) => {
  if (e.target.classList.contains("btn-inc")) {
    const index = +e.target.getAttribute("index");
    ui.increase(index);
  }
  if (e.target.classList.contains("btn-dec")) {
    const index = +e.target.getAttribute("index");
    ui.decrease(index);
  }
});
js
const goodsData = [
  {
    title: '美式',
    price: 11
  },
  {
    title: '加浓美式',
    price: 15
  },
  {
    title: '生椰拿铁',
    price: 16
  },
  {
    title: '生酪拿铁',
    price: 16
  }
]
const goodsData = [
  {
    title: '美式',
    price: 11
  },
  {
    title: '加浓美式',
    price: 15
  },
  {
    title: '生椰拿铁',
    price: 16
  },
  {
    title: '生酪拿铁',
    price: 16
  }
]

效果

思路总结

  • 一环套一环,各司其职,一步一步实现,思路清晰
  • 从单个商品,根据原始数据结构生成界面渲染和操作所需结构,商品列表只需构造生成即可(工厂模式)
  • 到 UI 数据层,保存界面所有功能模块的数据(商品列表、结算栏),界面所有数据只需从这里获取
  • 再到 UI 操作层,构造渲染页面所需数据并渲染界面、提供用户操作 API

学习视频链接:https://ke.qq.com/course/5892689/13883876927203921#term_id=106109971

思考

工作中都是现有 UI 界面再有数据,对接 API 存在这两种情况:

  1. 不确定接口参数格式
  • 你页面时使用[]保存,后端需要以,分割的字符串
  • 你页面时使用 true,false,后端定义 0,1 接收
  • ...
  1. 不确定接口返回数据格式
  • 如上情况反着来

准备一个参数转化、和一个接口返回数据转化的方法,这样只需在发起请求时调用下转化参数 API、接收返回结果时调用转化数据 API。但为了方便排查,最好还是统一字段、格式。

而关于网络请求方法管理

  1. 独立文件夹

  2. 功能模块下新建文件夹

传统是把所有 api 请求按页面创建文件放在一个 api 文件夹中,但是随着项目越来越大,页面、功能模块越来越多,查阅修改文件目录跨度就变大了,假如一个页面或者功能模块有多个行为发起接口请求,后续这个功能废弃了,需要删除代码,需要关注两处目录下的文件,如果是在功能模块文件夹下建立 API 文件夹的方式管理,后续删除移动、这样就只需关注一处。无论如何,API 最好根据 菜单->页面->功能模块分层,分级方式自定。