uni-app D7 实战(小兔鲜)

63 阅读16分钟

1.地址模块

image.png

1.1 静态结构

1.1.1 address的静态结构

<script setup lang="ts">
//
</script>

<template>
  <view class="viewport">
    <!-- 地址列表 -->
    <scroll-view class="scroll-view" scroll-y>
      <view v-if="true" class="address">
        <view class="address-list">
          <!-- 收货地址项 -->
          <view class="item">
            <view class="item-content">
              <view class="user">
                黑马小王子
                <text class="contact">13111111111</text>
                <text v-if="true" class="badge">默认</text>
              </view>
              <view class="locate">广东省 广州市 天河区 黑马程序员</view>
              <navigator
                class="edit"
                hover-class="none"
                :url="`/pagesMember/address-form/address-form?id=1`"
              >
                修改
              </navigator>
            </view>
          </view>
          <!-- 收货地址项 -->
          <view class="item">
            <view class="item-content">
              <view class="user">
                黑马小公主
                <text class="contact">13222222222</text>
                <text v-if="false" class="badge">默认</text>
              </view>
              <view class="locate">北京市 北京市 顺义区 黑马程序员</view>
              <navigator
                class="edit"
                hover-class="none"
                :url="`/pagesMember/address-form/address-form?id=2`"
              >
                修改
              </navigator>
            </view>
          </view>
        </view>
      </view>
      <view v-else class="blank">暂无收货地址</view>
    </scroll-view>
    <!-- 添加按钮 -->
    <view class="add-btn">
      <navigator hover-class="none" url="/pagesMember/address-form/address-form">
        新建地址
      </navigator>
    </view>
  </view>
</template>

<style lang="scss">
page {
  height: 100%;
  overflow: hidden;
}

/* 删除按钮 */
.delete-button {
  display: flex;
  justify-content: center;
  align-items: center;
  width: 50px;
  height: 100%;
  font-size: 28rpx;
  color: #fff;
  border-radius: 0;
  padding: 0;
  background-color: #cf4444;
}

.viewport {
  display: flex;
  flex-direction: column;
  height: 100%;
  background-color: #f4f4f4;

  .scroll-view {
    padding-top: 20rpx;
  }
}

.address {
  padding: 0 20rpx;
  margin: 0 20rpx;
  border-radius: 10rpx;
  background-color: #fff;

  .item-content {
    line-height: 1;
    padding: 40rpx 10rpx 38rpx;
    border-bottom: 1rpx solid #ddd;
    position: relative;

    .edit {
      position: absolute;
      top: 36rpx;
      right: 30rpx;
      padding: 2rpx 0 2rpx 20rpx;
      border-left: 1rpx solid #666;
      font-size: 26rpx;
      color: #666;
      line-height: 1;
    }
  }

  .item:last-child .item-content {
    border: none;
  }

  .user {
    font-size: 28rpx;
    margin-bottom: 20rpx;
    color: #333;

    .contact {
      color: #666;
    }

    .badge {
      display: inline-block;
      padding: 4rpx 10rpx 2rpx 14rpx;
      margin: 2rpx 0 0 10rpx;
      font-size: 26rpx;
      color: #27ba9b;
      border-radius: 6rpx;
      border: 1rpx solid #27ba9b;
    }
  }

  .locate {
    line-height: 1.6;
    font-size: 26rpx;
    color: #333;
  }
}

.blank {
  margin-top: 300rpx;
  text-align: center;
  font-size: 32rpx;
  color: #888;
}

.add-btn {
  height: 80rpx;
  text-align: center;
  line-height: 80rpx;
  margin: 30rpx 20rpx;
  color: #fff;
  border-radius: 80rpx;
  font-size: 30rpx;
  background-color: #27ba9b;
}
</style>

点击跳转 image.png

1.1.2 address-form的静态结构

<script setup lang="ts">
import { ref } from 'vue'

// 表单数据
const form = ref({
  receiver: '', // 收货人
  contact: '', // 联系方式
  fullLocation: '', // 省市区(前端展示)
  provinceCode: '', // 省份编码(后端参数)
  cityCode: '', // 城市编码(后端参数)
  countyCode: '', // 区/县编码(后端参数)
  address: '', // 详细地址
  isDefault: 0, // 默认地址,1为是,0为否
})
</script>

<template>
  <view class="content">
    <form>
      <!-- 表单内容 -->
      <view class="form-item">
        <text class="label">收货人</text>
        <input class="input" placeholder="请填写收货人姓名" value="" />
      </view>
      <view class="form-item">
        <text class="label">手机号码</text>
        <input class="input" placeholder="请填写收货人手机号码" value="" />
      </view>
      <view class="form-item">
        <text class="label">所在地区</text>
        <picker class="picker" mode="region" value="">
          <view v-if="false">广东省 广州市 天河区</view>
          <view v-else class="placeholder">请选择省/市/区(县)</view>
        </picker>
      </view>
      <view class="form-item">
        <text class="label">详细地址</text>
        <input class="input" placeholder="街道、楼牌号等信息" value="" />
      </view>
      <view class="form-item">
        <label class="label">设为默认地址</label>
        <switch class="switch" color="#27ba9b" :checked="true" />
      </view>
    </form>
  </view>
  <!-- 提交按钮 -->
  <button class="button">保存并使用</button>
</template>

<style lang="scss">
page {
  background-color: #f4f4f4;
}

.content {
  margin: 20rpx 20rpx 0;
  padding: 0 20rpx;
  border-radius: 10rpx;
  background-color: #fff;

  .form-item,
  .uni-forms-item {
    display: flex;
    align-items: center;
    min-height: 96rpx;
    padding: 25rpx 10rpx 40rpx;
    background-color: #fff;
    font-size: 28rpx;
    border-bottom: 1rpx solid #ddd;
    position: relative;
    margin-bottom: 0;

    // 调整 uni-forms 样式
    .uni-forms-item__content {
      display: flex;
    }

    .uni-forms-item__error {
      margin-left: 200rpx;
    }

    &:last-child {
      border: none;
    }

    .label {
      width: 200rpx;
      color: #333;
    }

    .input {
      flex: 1;
      display: block;
      height: 46rpx;
    }

    .switch {
      position: absolute;
      right: -20rpx;
      transform: scale(0.8);
    }

    .picker {
      flex: 1;
    }

    .placeholder {
      color: #808080;
    }
  }
}

.button {
  height: 80rpx;
  margin: 30rpx 20rpx;
  color: #fff;
  border-radius: 80rpx;
  font-size: 30rpx;
  background-color: #27ba9b;
}
</style>

跳转成功

image.png

1.1.2.1 动态设置标题

如果是新建地址的话是没有id值的,如果是修改地址的话是有id值的,所以我们可以根据是否有id值来动态渲染标题

image.png

代码实现: image.png image.png

image.png

1.2 新建地址

image.png

1.2.1 封装API接口并定义参数类型

注意注意:后端接收的地址数据是地址的编码,而前端渲染的是中文 image.png 定义接口:

image.png

定义参数类型:

image.png

1.2.2 收集表单数据

1.2.2.1 收获人姓名(直接使用vue的v-model进行双向绑定)

image.png

1.2.2.2 手机号码(直接使用vue的v-model进行双向绑定)
1.2.2.3 地区(使用picker事件获取)

前端 image.png 后端

image.png

1.2.2.4 收集是否为默认收货地址

image.png image.png

1.2.2.5 绑定提交表单事件

image.png image.png 提交成功后给个轻提示

image.png

1.3 列表渲染

image.png

1.3.1 封装API接口

image.png

1.3.2 页面中使用接口

image.png

1.3.3 数据类型声明文件

image.png 合并复用类型

image.png

1.3.4 保存数组

image.png

1.3.5 渲染页面

image.png 成功:

image.png

1.3.5 新加页面后不会自动更新,因为只写了onLoad,即在页面加载的时候刷新,所以要有所更改

改为onShow,每次页面加载的时候就刷新 image.png

1.4 修改地址-数据回显

image.png

image.png

1.4.1 封装API接口

image.png

1.4.2 获取数据并使用onLoad加载(点击获取id)

image.png

1.4.3 表单数据的回显

image.png

1.4.4 成功回显

image.png

1.5 修改地址-保存修改

image.png

1.5.1 根据id的有无来判断是修改地址还是新建地址

image.png

1.5.2 封装修改收货地址的API接口

image.png

image.png 结果:

image.png

image.png

1.6 表单校验

image.png

image.png

1.6.1 指定校验规则

image.png 按照vue文档,结合自己的变量名,编写rules: image.png

1.6.2 修改表单结构

使用uniapp中的uni-forms

image.png

1.6.3 表单组件实例

image.png

image.png 成功:

image.png

1.6.4 和前面流程一样,把其他字段也进行一个校验(手机号码,再给校验规则额外加个手机格式)

image.png

image.png 结果:

image.png 其他的字段校验步骤一模一样,此处做省略

1.6.5 效果展示:

image.png

1.7 地址-删除地址(使用uni-swiper-action)

image.png

1.7.1 修改列表结构

image.png 结果:

image.png

1.7.2 绑定删除事件

image.png

image.png 效果展示:

image.png

1.7.3 删除收获地址的API

image.png

1.7.4 删除成功

image.png

2 SKU模块-了解SKU(stock-keeping-unit)

image.png

image.png

2.1 安装插件后将文件夹放置项目根目录下的components文件夹中

image.png

2.2 复制示例代码


<!-- 静态数据演示版本 适合任何后端 -->
<template>
    <view class="app">
        <button @click="openSkuPopup()">打开SKU组件</button>

        <vk-data-goods-sku-popup
            ref="skuPopup"
            v-model="skuKey"
            border-radius="20"
            :z-index="990"
            :localdata="goodsInfo"
            :mode="skuMode"
            @open="onOpenSkuPopup"
            @close="onCloseSkuPopup"
            @add-cart="addCart"
            @buy-now="buyNow"
        ></vk-data-goods-sku-popup>
    </view>
</template>

<script>
export default {
    data() {
        return {
            // 是否打开SKU弹窗
            skuKey: false,
            // SKU弹窗模式
            skuMode: 1,
            // 后端返回的商品信息
            goodsInfo: {}
        };
    },
    // 监听 - 页面每次【加载时】执行(如:前进)
    onLoad(options) {
        this.init(options);
    },
    methods: {
        // 初始化
        init(options = {}) {},
        // 获取商品信息,并打开sku弹出
        openSkuPopup() {
            /**
             * 获取商品信息
             * 这里可以看到每次打开SKU都会去重新请求商品信息,为的是每次打开SKU组件可以实时看到剩余库存
             */
            // 此处写接口请求,并将返回的数据进行处理成goodsInfo的数据格式,
            // goodsInfo是后端返回的数据
            this.goodsInfo = {
                "_id": "001",
                "name": "iphone11",
                "goods_thumb": "https://img14.360buyimg.com/n0/jfs/t1/59022/28/10293/141808/5d78088fEf6e7862d/68836f52ffaaad96.jpg",
                "sku_list": [
                    {
                        "_id": "001",
                        "goods_id": "001",
                        "goods_name": "iphone11",
                        "image": "https://img14.360buyimg.com/n0/jfs/t1/79668/22/9987/159271/5d780915Ebf9bf3f4/6a1b2703a9ed8737.jpg",
                        "price": 19800,
                        "sku_name_arr": ["红色", "128G", "公开版"],
                        "stock": 1000
                    },
                    {
                        "_id": "002",
                        "goods_id": "001",
                        "goods_name": "iphone11",
                        "image": "https://img14.360buyimg.com/n0/jfs/t1/52252/35/10516/124064/5d7808e0E46202391/7100f3733a1c1f00.jpg",
                        "price": 9800,
                        "sku_name_arr": ["白色", "256G", "公开版"],
                        "stock": 100
                    },
                    {
                        "_id": "003",
                        "goods_id": "001",
                        "goods_name": "iphone11",
                        "image": "https://img14.360buyimg.com/n0/jfs/t1/79668/22/9987/159271/5d780915Ebf9bf3f4/6a1b2703a9ed8737.jpg",
                        "price": 19800,
                        "sku_name_arr": ["红色", "256G", "公开版"],
                        "stock": 1
                    }
                ],
                "spec_list": [
                    {
                        "name": "颜色",
                        "list": [
                            { "name": "红色" },
                            { "name": "黑色" },
                            { "name": "白色" }
                        ]
                    },
                    {
                        "name": "内存",
                        "list": [
                            { "name": "128G" },
                            { "name": "256G" }
                        ],
                    },
                    {
                        "name": "版本",
                        "list": [
                            { "name": "公开版" },
                            { "name": "非公开版" }
                        ]
                    }
                ]
            };
            this.skuKey = true;
        },
        // sku组件 开始-----------------------------------------------------------
        onOpenSkuPopup() {
            console.log("监听 - 打开sku组件");
        },
        onCloseSkuPopup() {
            console.log("监听 - 关闭sku组件");
        },
        // 加入购物车前的判断
        addCartFn(obj) {
            let { selectShop } = obj;
            // 模拟添加到购物车,请替换成你自己的添加到购物车逻辑
            let res = {};
            let name = selectShop.goods_name;
            if (selectShop.sku_name != "默认") {
                name += "-" + selectShop.sku_name_arr;
            }
            res.msg = `${name} 已添加到购物车`;
            if (typeof obj.success == "function") obj.success(res);
        },
        // 加入购物车按钮
        addCart(selectShop) {
            console.log("监听 - 加入购物车");
            this.addCartFn({
                selectShop: selectShop,
                success: res => {
                    // 实际业务时,请替换自己的加入购物车逻辑
                    this.toast(res.msg);
                    setTimeout(() => {
                        this.skuKey = false;
                    }, 300);
                }
            });
        },
        // 立即购买
        buyNow(selectShop) {
            console.log("监听 - 立即购买");
            this.addCartFn({
                selectShop: selectShop,
                success: res => {
                    // 实际业务时,请替换自己的立即购买逻辑
                    this.toast("立即购买");
                }
            });
        },
        toast(msg) {
            uni.showToast({
                title: msg,
                icon: "none"
            });
        }
    }
};
</script>

<style lang="scss" scoped>
.app {
    padding: 30rpx;
    font-size: 28rpx;
}
</style>

2.3 打开对应页面

image.png

image.png

2.4 SKU实战-渲染商品信息

image.png

2.4.1 类型声明文件

import { Component } from '@uni-helper/uni-app-types'

/** SKU 弹出层 */
export type SkuPopup = Component<SkuPopupProps>

/** SKU 弹出层实例 */
export type SkuPopupInstanceType = InstanceType<SkuPopup>

/** SKU 弹出层属性 */
export type SkuPopupProps = {
  /** 双向绑定,true 为打开组件,false 为关闭组件 */
  modelValue: boolean
  /** 商品信息本地数据源 */
  localdata: SkuPopupLocaldata
  /** 按钮模式 1:都显示 2:只显示购物车 3:只显示立即购买 */
  mode?: 1 | 2 | 3
  /** 该商品已抢完时的按钮文字 */
  noStockText?: string
  /** 库存文字 */
  stockText?: string
  /** 点击遮罩是否关闭组件 */
  maskCloseAble?: boolean
  /** 顶部圆角值 */
  borderRadius?: string | number
  /** 最小购买数量 */
  minBuyNum?: number
  /** 最大购买数量 */
  maxBuyNum?: number
  /** 每次点击后的数量 */
  stepBuyNum?: number
  /** 是否只能输入 step 的倍数 */
  stepStrictly?: boolean
  /** 是否隐藏库存的显示 */
  hideStock?: false
  /** 主题风格 */
  theme?: 'default' | 'red-black' | 'black-white' | 'coffee' | 'green'
  /** 默认金额会除以100(即100=1元),若设置为0,则不会除以100(即1=1元) */
  amountType?: 1 | 0
  /** 自定义获取商品信息的函数(已知支付宝不支持,支付宝请改用localdata属性) */
  customAction?: () => void
  /** 是否显示右上角关闭按钮 */
  showClose?: boolean
  /** 关闭按钮的图片地址 */
  closeImage?: string
  /** 价格的字体颜色 */
  priceColor?: string
  /** 立即购买 - 按钮的文字 */
  buyNowText?: string
  /** 立即购买 - 按钮的字体颜色 */
  buyNowColor?: string
  /** 立即购买 - 按钮的背景颜色 */
  buyNowBackgroundColor?: string
  /** 加入购物车 - 按钮的文字 */
  addCartText?: string
  /** 加入购物车 - 按钮的字体颜色 */
  addCartColor?: string
  /** 加入购物车 - 按钮的背景颜色 */
  addCartBackgroundColor?: string
  /** 商品缩略图背景颜色 */
  goodsThumbBackgroundColor?: string
  /** 样式 - 不可点击时,按钮的样式 */
  disableStyle?: object
  /** 样式 - 按钮点击时的样式 */
  activedStyle?: object
  /** 样式 - 按钮常态的样式 */
  btnStyle?: object
  /** 字段名 - 商品表id的字段名 */
  goodsIdName?: string
  /** 字段名 - sku表id的字段名 */
  skuIdName?: string
  /** 字段名 - 商品对应的sku列表的字段名 */
  skuListName?: string
  /** 字段名 - 商品规格名称的字段名 */
  specListName?: string
  /** 字段名 - sku库存的字段名 */
  stockName?: string
  /** 字段名 - sku组合路径的字段名 */
  skuArrName?: string
  /** 字段名 - 商品缩略图字段名(未选择sku时) */
  goodsThumbName?: string
  /** 被选中的值 */
  selectArr?: string[]

  /** 打开弹出层 */
  onOpen: () => void
  /** 关闭弹出层 */
  onClose: () => void
  /** 点击加入购物车时(需选择完SKU才会触发)*/
  onAddCart: (event: SkuPopupEvent) => void
  /** 点击立即购买时(需选择完SKU才会触发)*/
  onBuyNow: (event: SkuPopupEvent) => void
}

/**  商品信息本地数据源 */
export type SkuPopupLocaldata = {
  /** 商品 ID */
  _id: string
  /** 商品名称 */
  name: string
  /** 商品图片 */
  goods_thumb: string
  /** 商品规格列表 */
  spec_list: SkuPopupSpecItem[]
  /** 商品SKU列表 */
  sku_list: SkuPopupSkuItem[]
}

/** 商品规格名称的集合 */
export type SkuPopupSpecItem = {
  /** 规格名称 */
  name: string
  /** 规格集合 */
  list: { name: string }[]
}

/** 商品SKU列表 */
export type SkuPopupSkuItem = {
  /** SKU ID */
  _id: string
  /**  商品 ID */
  goods_id: string
  /** 商品名称 */
  goods_name: string
  /** 商品图片 */
  image: string
  /** SKU 价格 * 100, 注意:需要乘以 100 */
  price: number
  /** SKU 规格组成, 注意:需要与 spec_list 数组顺序对应 */
  sku_name_arr: string[]
  /** SKU 库存 */
  stock: number
}

/** 当前选择的sku数据 */
export type SkuPopupEvent = SkuPopupSkuItem & {
  /** 商品购买数量 */
  buy_num: number
}

/** 全局组件类型声明 */
declare module 'vue' {
  export interface GlobalComponents {
    'vk-data-goods-sku-popup': SkuPopup
  }
}

2.4.2 添加弹窗组件

image.png

2.4.3 准备localdata以及与SKU的商品格式进行关联(与我们的真实的后端数据格式进行匹配)

image.png

2.4.4 将后端数据处理为SKU所需的格式

处理后端数据为我们所需的 有了类型声明文件,现在所需的字段为:

image.png

image.png
前三个是简单的字段,从后端的数据中.提取出来就好了,后两个是数组,我们需要先去类型声明文件观察数组类型

image.png

2.4.4.1 spec_list:规格

image.png

2.4.4.2 sku_list

image.png

2.4.5 注意,通过点击‘选择商品规格’,才显示为true

image.png image.png

2.4.6 成功

image.png

image.png

2.5 SKU-打开弹窗交互

image.png

2.5.1 绑定mode(通过传参来实现打开购物车还是购买还是两者都有)

image.png

2.5.2 绑定函数(与h5标签)

image.png

image.png

image.png

2.5.3 成功

image.png

image.png

image.png

2.6 渲染被选中的值

image.png 需要使用到组件内部的值:selectArr

2.6.1 通过获取组件的实例,再获取组件内部的值‘selectArr’

image.png

image.png

2.6.2 计算被选中的值

image.png

2.6.3 效果:

image.png

2.7 加入购物车

image.png

2.7.1 绑定一个加入购物车事件

image.png

image.png

2.7.2 加入购物车接口

image.png

2.7.3 页面中调用接口

image.png

2.7.4 成功

image.png

2.8 渲染购物车列表

image.png

2.8.1 购物车静态结构

<script setup lang="ts">
//
</script>

<template>
  <scroll-view scroll-y class="scroll-view">
    <!-- 已登录: 显示购物车 -->
    <template v-if="true">
      <!-- 购物车列表 -->
      <view class="cart-list" v-if="true">
        <!-- 优惠提示 -->
        <view class="tips">
          <text class="label">满减</text>
          <text class="desc">满1件, 即可享受9折优惠</text>
        </view>
        <!-- 滑动操作分区 -->
        <uni-swipe-action>
          <!-- 滑动操作项 -->
          <uni-swipe-action-item v-for="item in 2" :key="item" class="cart-swipe">
            <!-- 商品信息 -->
            <view class="goods">
              <!-- 选中状态 -->
              <text class="checkbox" :class="{ checked: true }"></text>
              <navigator
                :url="`/pages/goods/goods?id=1435025`"
                hover-class="none"
                class="navigator"
              >
                <image
                  mode="aspectFill"
                  class="picture"
                  src="https://yanxuan-item.nosdn.127.net/da7143e0103304f0f3230715003181ee.jpg"
                ></image>
                <view class="meta">
                  <view class="name ellipsis">人手必备,儿童轻薄透气防蚊裤73-140cm</view>
                  <view class="attrsText ellipsis">黄色小象 140cm</view>
                  <view class="price">69.00</view>
                </view>
              </navigator>
              <!-- 商品数量 -->
              <view class="count">
                <text class="text">-</text>
                <input class="input" type="number" value="1" />
                <text class="text">+</text>
              </view>
            </view>
            <!-- 右侧删除按钮 -->
            <template #right>
              <view class="cart-swipe-right">
                <button class="button delete-button">删除</button>
              </view>
            </template>
          </uni-swipe-action-item>
        </uni-swipe-action>
      </view>
      <!-- 购物车空状态 -->
      <view class="cart-blank" v-else>
        <image src="/static/images/blank_cart.png" class="image" />
        <text class="text">购物车还是空的,快来挑选好货吧</text>
        <navigator open-type="switchTab" url="/pages/index/index" hover-class="none">
          <button class="button">去首页看看</button>
        </navigator>
      </view>
      <!-- 吸底工具栏 -->
      <view class="toolbar">
        <text class="all" :class="{ checked: true }">全选</text>
        <text class="text">合计:</text>
        <text class="amount">100</text>
        <view class="button-grounp">
          <view class="button payment-button" :class="{ disabled: true }"> 去结算(10) </view>
        </view>
      </view>
    </template>
    <!-- 未登录: 提示登录 -->
    <view class="login-blank" v-else>
      <text class="text">登录后可查看购物车中的商品</text>
      <navigator url="/pages/login/login" hover-class="none">
        <button class="button">去登录</button>
      </navigator>
    </view>
    <!-- 猜你喜欢 -->
    <XtxGuess ref="guessRef"></XtxGuess>
    <!-- 底部占位空盒子 -->
    <view class="toolbar-height"></view>
  </scroll-view>
</template>

<style lang="scss">
// 根元素
:host {
  height: 100vh;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  background-color: #f7f7f8;
}

// 滚动容器
.scroll-view {
  flex: 1;
}

// 购物车列表
.cart-list {
  padding: 0 20rpx;

  // 优惠提示
  .tips {
    display: flex;
    align-items: center;
    line-height: 1;
    margin: 30rpx 10rpx;
    font-size: 26rpx;
    color: #666;

    .label {
      color: #fff;
      padding: 7rpx 15rpx 5rpx;
      border-radius: 4rpx;
      font-size: 24rpx;
      background-color: #27ba9b;
      margin-right: 10rpx;
    }
  }

  // 购物车商品
  .goods {
    display: flex;
    padding: 20rpx 20rpx 20rpx 80rpx;
    border-radius: 10rpx;
    background-color: #fff;
    position: relative;

    .navigator {
      display: flex;
    }

    .checkbox {
      position: absolute;
      top: 0;
      left: 0;

      display: flex;
      align-items: center;
      justify-content: center;
      width: 80rpx;
      height: 100%;

      &::before {
        content: '\e6cd';
        font-family: 'erabbit' !important;
        font-size: 40rpx;
        color: #444;
      }

      &.checked::before {
        content: '\e6cc';
        color: #27ba9b;
      }
    }

    .picture {
      width: 170rpx;
      height: 170rpx;
    }

    .meta {
      flex: 1;
      display: flex;
      flex-direction: column;
      justify-content: space-between;
      margin-left: 20rpx;
    }

    .name {
      height: 72rpx;
      font-size: 26rpx;
      color: #444;
    }

    .attrsText {
      line-height: 1.8;
      padding: 0 15rpx;
      font-size: 24rpx;
      align-self: flex-start;
      border-radius: 4rpx;
      color: #888;
      background-color: #f7f7f8;
    }

    .price {
      line-height: 1;
      font-size: 26rpx;
      color: #444;
      margin-bottom: 2rpx;
      color: #cf4444;

      &::before {
        content: '¥';
        font-size: 80%;
      }
    }

    // 商品数量
    .count {
      position: absolute;
      bottom: 20rpx;
      right: 5rpx;

      display: flex;
      justify-content: space-between;
      align-items: center;
      width: 220rpx;
      height: 48rpx;

      .text {
        height: 100%;
        padding: 0 20rpx;
        font-size: 32rpx;
        color: #444;
      }

      .input {
        height: 100%;
        text-align: center;
        border-radius: 4rpx;
        font-size: 24rpx;
        color: #444;
        background-color: #f6f6f6;
      }
    }
  }

  .cart-swipe {
    display: block;
    margin: 20rpx 0;
  }

  .cart-swipe-right {
    display: flex;
    height: 100%;

    .button {
      display: flex;
      justify-content: center;
      align-items: center;
      width: 50px;
      padding: 6px;
      line-height: 1.5;
      color: #fff;
      font-size: 26rpx;
      border-radius: 0;
    }

    .delete-button {
      background-color: #cf4444;
    }
  }
}

// 空状态
.cart-blank,
.login-blank {
  display: flex;
  justify-content: center;
  align-items: center;
  flex-direction: column;
  height: 60vh;
  .image {
    width: 400rpx;
    height: 281rpx;
  }
  .text {
    color: #444;
    font-size: 26rpx;
    margin: 20rpx 0;
  }
  .button {
    width: 240rpx !important;
    height: 60rpx;
    line-height: 60rpx;
    margin-top: 20rpx;
    font-size: 26rpx;
    border-radius: 60rpx;
    color: #fff;
    background-color: #27ba9b;
  }
}

// 吸底工具栏
.toolbar {
  position: fixed;
  left: 0;
  right: 0;
  bottom: var(--window-bottom);
  z-index: 1;

  height: 100rpx;
  padding: 0 20rpx;
  display: flex;
  align-items: center;
  border-top: 1rpx solid #ededed;
  border-bottom: 1rpx solid #ededed;
  background-color: #fff;
  box-sizing: content-box;

  .all {
    margin-left: 25rpx;
    font-size: 14px;
    color: #444;
    display: flex;
    align-items: center;
  }

  .all::before {
    font-family: 'erabbit' !important;
    content: '\e6cd';
    font-size: 40rpx;
    margin-right: 8rpx;
  }

  .checked::before {
    content: '\e6cc';
    color: #27ba9b;
  }

  .text {
    margin-right: 8rpx;
    margin-left: 32rpx;
    color: #444;
    font-size: 14px;
  }

  .amount {
    font-size: 20px;
    color: #cf4444;

    .decimal {
      font-size: 12px;
    }

    &::before {
      content: '¥';
      font-size: 12px;
    }
  }

  .button-grounp {
    margin-left: auto;
    display: flex;
    justify-content: space-between;
    text-align: center;
    line-height: 72rpx;
    font-size: 13px;
    color: #fff;

    .button {
      width: 240rpx;
      margin: 0 10rpx;
      border-radius: 72rpx;
    }

    .payment-button {
      background-color: #27ba9b;

      &.disabled {
        opacity: 0.6;
      }
    }
  }
}
// 底部占位空盒子
.toolbar-height {
  height: 100rpx;
}
</style>

2.8.3 获取会员Store,判断是否登录

image.png 登录状态时的购物车栏:

image.png 在‘我的’页面退出登录,退出登录状态的购物车栏:

image.png

2.8.4 封装获取购物车列表接口

image.png

2.8.5 处理接口获取到的数据

image.png

2.8.6 购物车列表的类型声明文件

image.png

image.png

2.8.7 页面中使用

image.png

2.8.8 页面中渲染

image.png

2.8.9 成功

image.png 再添加一件商品

image.png

2.9 删除单品

image.png 侧滑显示删除的插槽,

image.png

2.9.1 封装删除的API

image.png

2.9.2 给按钮绑定事件

需要传入参数来确认删除的目标购物车单品

image.png 使用模态弹窗向用户确认是否删除购物车单品

image.png

2.9.3 效果

image.png

image.png

2.10 修改购物车单品数量

这是sku组件里已经自带了的

image.png

2.10.1 准备步进器的类型声明文件

// 步进器类型声明文件
import { Component } from '@uni-helper/uni-app-types';

/** 步进器 */
export type InputNumberBox = Component<InputNumberBoxProps>;

/** 步进器实例 */
export type InputNumberBoxInstance = InstanceType<InputNumberBox>;

/** 步进器属性 */
export type InputNumberBoxProps = {
    /** 输入框初始值(默认1) */
    modelValue: number;
    /** 用户可输入的最小值(默认0) */
    min: number;
    /** 用户可输入的最大值(默认99999) */
    max: number;
    /**  步长,每次加或减的值(默认1) */
    step: number;
    /** 是否禁用操作,包括输入框,加减按钮 */
    disabled: boolean;
    /** 输入框宽度,单位rpx(默认80) */
    inputWidth: string | number;
    /**  输入框和按钮的高度,单位rpx(默认50) */
    inputHeight: string | number;
    /** 输入框和按钮的背景颜色(默认#F2F3F5) */
    bgColor: string;
    /** 步进器标识符 */
    index: string;
    /** 输入框内容发生变化时触发 */
    onChange: (event: InputNumberBoxEvent) => void;
    /** 输入框失去焦点时触发 */
    onBlur: (event: InputNumberBoxEvent) => void;
    /** 点击增加按钮时触发 */
    onPlus: (event: InputNumberBoxEvent) => void;
    /** 点击减少按钮时触发 */
    onMinus: (event: InputNumberBoxEvent) => void;
};

/** 步进器事件对象 */
export type InputNumberBoxEvent = {
    /** 输入框当前值 */
    value: number;
    /** 步进器标识符 */
    index: string;
};

/** 全局组件类型声明 */
declare module 'vue' {
    export interface GlobalComponents {
        'vk-data-input-number-box': InputNumberBox;
    }
}

2.10.2 在页面中使用sku的官方步近期,并基于步进器设置商品最小值(1),和商品最大值(库存)

image.png

image.png

2.10.3 绑定事件

image.png

image.png

2.10.4 封装修改购物车的API

image.png

image.png 实现:

image.png

2.11 购物车-修改选中状态

image.png

2.11.1 给选中状态绑定点击事件

image.png

2.11.2 前端勾选改变

image.png

2.11.3 后端选中状态发送

image.png 实现: image.png

2.11.4 与全选勾选统一

image.png

image.png 实时更新:当购物车中 任何一个商品 的 selected 状态发生变化时, isSelectedAll 会自动重新计算 使用every,computed,根据购物车列表中的每个的selected属性来计算全选的勾选

image.png

2.11.5 全选状态取反

2.11.5.1 前端

image.png

2.11.5.2 后端
2.11.5.2.1 需要调用切封装购物车取消全选的API

image.png

image.png

2.11.5.2.2 主页面调用API

image.png 结果:

image.png

2.12 购物车-底部结算信息

image.png

2.12.1 计算被选中的单品,并计算选中的总数

image.png

2.12.2 在页面中导入

image.png 效果:

image.png

2.12.3 计算被选中的单品的总价格

image.png

image.png 效果:

image.png

2.13 提示去支付

image.png

2.13.1 如果购物车没有内容但是有点击了去‘去结算’

image.png

image.png

2.14 购物车模块-两个购物车页面

image.png

2.14.1 封装购物车为一个组件

image.png

2.14.2 在原来CART页面引入原来的组件