[uni-app]小兔鲜-06地址+sku+购物车

news2024/11/25 12:24:05

收址模块

准备 地址管理分包页面 和 添加地址分包页面

新增和修改地址

import type { AddressParams, AddressItem } from '@/types/address'
import { http } from '@/utils/http'

/**
 * 添加收货地址
 * @param data 请求参数
 */
export const postMemberAddressAPI = (data: AddressParams) => {
    return http({
        method: 'POST',
        url: '/member/address',
        data,
    })
}

/**
 * 获取收货地址列表
 */
export const getMemberAddressAPI = () => {
    return http<AddressItem[]>({
        method: 'GET',
        url: '/member/address',
    })
}

/**
 * 获取收货地址详情
 * @param id 地址id(路径参数)
 */
export const getMemberAddressByIdAPI = (id: string) => {
    return http<AddressItem>({
        method: 'GET',
        url: `/member/address/${id}`,
    })
}

/**
 * 修改收货地址
 * @param id 地址id(路径参数)
 * @param data 表单数据(请求体参数)
 */
export const putMemberAddressByIdAPI = (id: string, data: AddressParams) => {
    return http({
        method: 'PUT',
        url: `/member/address/${id}`,
        data,
    })
}

/**
 * 删除收货地址
 * @param id 地址id(路径参数)
 */
export const deleteMemberAddressByIdAPI = (id: string) => {
    return http({
        method: 'DELETE',
        url: `/member/address/${id}`,
    })
}
/** 添加收货地址: 请求参数 */
export type AddressParams = {
    /** 收货人姓名 */
    receiver: string
    /** 联系方式 */
    contact: string
    /** 省份编码 */
    provinceCode: string
    /** 城市编码 */
    cityCode: string
    /** 区/县编码 */
    countyCode: string
    /** 详细地址 */
    address: string
    /** 默认地址,1为是,0为否 */
    isDefault: number
}

/** 收货地址项 */
export type AddressItem = AddressParams & {
    /** 收货地址 id */
    id: string
    /** 省市区 */
    fullLocation: string
}
<script setup lang="ts">
import {
  postMemberAddressAPI,
  getMemberAddressByIdAPI,
  putMemberAddressByIdAPI,
} from '@/services/address'
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'

// 获取页面参数
const query = defineProps<{
  id: string
}>()

// 动态设置标题
uni.setNavigationBarTitle({ title: query.id ? '修改地址' : '新建地址' })

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

// 收集所在地区
const onRegionChange: UniHelper.RegionPickerOnChange = (ev) => {
  // 省市区(前端展示)
  form.value.fullLocation = ev.detail.value.join(' ')
  // 省市区(后端参数)
  const [provinceCode, cityCode, countyCode] = ev.detail.code!
  // 合并数据
  Object.assign(form.value, { provinceCode, cityCode, countyCode })
}

// 收集是否默认收货地址
const onSwitchChange: UniHelper.SwitchOnChange = (ev) => {
  form.value.isDefault = ev.detail.value ? 1 : 0
}

// 定义校验规则
const rules: UniHelper.UniFormsRules = {
  receiver: {
    rules: [{ required: true, errorMessage: '请输入收货人姓名' }],
  },
  contact: {
    rules: [
      { required: true, errorMessage: '请输入联系方式' },
      { pattern: /^1[3-9]\d{9}$/, errorMessage: '手机号格式不正确' },
    ],
  },
  fullLocation: {
    rules: [{ required: true, errorMessage: '请选择所在地区' }],
  },
  address: {
    rules: [{ required: true, errorMessage: '请选择详细地址' }],
  },
}

// 获取表单组件实例,用于调用表单方法
const formRef = ref<UniHelper.UniFormsInstance>()

// 提交表单
const onSubmit = async () => {
  try {
    // 表单校验
    await formRef.value?.validate?.()
    // 判断当前页面是否有地址 id
    if (query.id) {
      // 修改地址请求
      await putMemberAddressByIdAPI(query.id, form.value)
    } else {
      // 新建地址请求
      await postMemberAddressAPI(form.value)
    }
    // 成功提示
    uni.showToast({ icon: 'success', title: query.id ? '修改成功' : '添加成功' })
    // 返回上一页
    setTimeout(() => {
      uni.navigateBack()
    }, 400)
  } catch (error) {
    uni.showToast({ icon: 'error', title: '请填写完整信息' })
  }
}

// 获取收货地址详情数据
const getMemberAddressByIdData = async () => {
  // 有 id 才调用接口
  if (query.id) {
    // 发送请求
    const res = await getMemberAddressByIdAPI(query.id)
    // 把数据合并到表单中
    Object.assign(form.value, res.result)
  }
}

// 页面加载
onLoad(() => {
  getMemberAddressByIdData()
})
</script>

<template>
  <view class="content">
    <uni-forms :rules="rules" :model="form" ref="formRef">
      <!-- 表单内容 -->
      <uni-forms-item name="receiver" class="form-item">
        <text class="label">收货人</text>
        <input class="input" placeholder="请填写收货人姓名" v-model="form.receiver" />
      </uni-forms-item>
      <uni-forms-item name="contact" class="form-item">
        <text class="label">手机号码</text>
        <input class="input" placeholder="请填写收货人手机号码" v-model="form.contact" />
      </uni-forms-item>
      <uni-forms-item name="fullLocation" class="form-item">
        <text class="label">所在地区</text>
        <picker
          @change="onRegionChange"
          class="picker"
          mode="region"
          :value="form.fullLocation.split(' ')"
        >
          <view v-if="form.fullLocation">{{ form.fullLocation }}</view>
          <view v-else class="placeholder">请选择省/市/区(县)</view>
        </picker>
      </uni-forms-item>
      <uni-forms-item name="address" class="form-item">
        <text class="label">详细地址</text>
        <input class="input" placeholder="街道、楼牌号等信息" v-model="form.address" />
      </uni-forms-item>
      <view class="form-item">
        <label class="label">设为默认地址</label>
        <switch
          class="switch"
          color="#27ba9b"
          :checked="form.isDefault === 1"
          @change="onSwitchChange"
        />
      </view>
    </uni-forms>
  </view>
  <!-- 提交按钮 -->
  <button @tap="onSubmit" class="button">保存并使用</button>
</template>

表单校验:

  1. 结构: 使用uni-forms 和 uni-forms-item组件实现表单数据的校验
  2. 规则: 通过rules属性指定校验规则, 通过model属性指定表单数据, 通过name属性指定校验项
  3. 校验: 通过formRef拿到组件实例, 调用组件实例的方法, 触发校验功能

地址管理页列表渲染

<script setup lang="ts">
import { getMemberAddressAPI } from '@/services/address'
import type { AddressItem } from '@/types/address'
import { onShow } from '@dcloudio/uni-app'
import { ref } from 'vue'

// 获取收货地址列表数据
const addressList = ref<AddressItem[]>([])
const getMemberAddressData = async () => {
  const res = await getMemberAddressAPI()
  addressList.value = res.result
}

// 初始化调用(页面显示)
onShow(() => {
  getMemberAddressData()
})
</script>

<template>
  <view class="viewport">
    <!-- 地址列表 -->
    <scroll-view class="scroll-view" scroll-y>
      <view v-if="true" class="address">
        <view class="address-list">
          <!-- 滑动操作分区 -->
          <uni-swipe-action>
            <!-- 滑动操作项 -->
            <uni-swipe-action-item v-for="item in addressList" :key="item.id">
              <!-- 默认插槽 -->
              <!-- 收获地址项 -->
              <view class="item">
                <view class="item-content" @tap="onChangeAddress(item)">
                  <view class="user">
                    {{ item.receiver }}
                    <text class="contact">{{ item.contact }}</text>
                    <text v-if="item.isDefault" class="badge">默认</text>
                  </view>
                  <view class="locate">{{ item.fullLocation }} {{ item.address }}</view>
                  <navigator
                    class="edit"
                    hover-class="none"
                    :url="`/pagesMember/address-form/address-form?id=${item.id}`"
                    @tap.stop="() => {}"
                    @tap.prevent="() => {}"
                  >
                    修改
                  </navigator>
                </view>
              </view>
              <!-- 右侧插槽 -->
              <template #right>
                <button class="delete-button">删除</button>
              </template>
            </uni-swipe-action-item>
          </uni-swipe-action>
        </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>
  1. onLoad() 页面加载,只执行一次
  2. onShow() 页面展示,反复执行

删除地址

<script setup lang="ts">
import { deleteMemberAddressByIdAPI } from '@/services/address'
import type { AddressItem } from '@/types/address'
import { useAddressStore } from '@/stores/modules/address'
import { onShow } from '@dcloudio/uni-app'

// 删除收货地址
const onDeleteAddress = (id: string) => {
  // 二次确认
  uni.showModal({
    content: '删除地址?',
    success: async (res) => {
      if (res.confirm) {
        // 根据id删除收货地址
        await deleteMemberAddressByIdAPI(id)
        // 重新获取收货地址列表
        getMemberAddressData()
      }
    },
  })
}

// 修改收货地址
const onChangeAddress = (item: AddressItem) => {
  // 修改地址
  const addressStore = useAddressStore()
  addressStore.changeSelectedAddress(item)
  // 返回上一页
  uni.navigateBack()
}
</script>

<template>
  <view class="viewport">
    <!-- 地址列表 -->
    <scroll-view class="scroll-view" scroll-y>
      <view v-if="true" class="address">
        <view class="address-list">
          <!-- 滑动操作分区 -->
          <uni-swipe-action>
            <!-- 滑动操作项 -->
            <uni-swipe-action-item v-for="item in addressList" :key="item.id">
              <!-- 默认插槽 -->
              <!-- 收获地址项 -->
              <view class="item">
                <view class="item-content" @tap="onChangeAddress(item)">
                  <view class="user">
                    {{ item.receiver }}
                    <text class="contact">{{ item.contact }}</text>
                    <text v-if="item.isDefault" class="badge">默认</text>
                  </view>
                  <view class="locate">{{ item.fullLocation }} {{ item.address }}</view>
                  <navigator
                    class="edit"
                    hover-class="none"
                    :url="`/pagesMember/address-form/address-form?id=${item.id}`"
                    @tap.stop="() => {}"
                    @tap.prevent="() => {}"
                  >
                    修改
                  </navigator>
                </view>
              </view>
              <!-- 右侧插槽 -->
              <template #right>
                <button class="delete-button" @tap="onDeleteAddress(item.id)">删除</button>
              </template>
            </uni-swipe-action-item>
          </uni-swipe-action>
        </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>

sku模块

概念: 存货单位( Stock Keeping Unit ), 库存管理的最小单元, 通常称为'单品'

SKU常见于电商领域, 对于前端工程师而言, 更多关注SKU算法,基于后端的SKU数据渲染页面并实现交互

uni-app插件市场可以是官方插件集中地, 其中可以找到SKU组件使用

  1. 找到合适的组件后, 直接下载即可
  2. 下载完成后, 跟随文档的指引, 把组件安装到自己的项目中即可
  3. 当前项目使用eslint进行代码校验, 但是上述插件没有进行校验,导致代码无法通过提交检查,
  4. 改动作者代码成本巨高, 所以要禁用当前文件的eslint的校验
  5. 在script标签的第一行, 添加禁用当前文件的语法校验

使用SKU组件, 渲染商品信息

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

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

/** SKU 弹出层实例 */
export type SkuPopupInstance = 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/runtime-core' {
  export interface GlobalComponents {
    'vk-data-goods-sku-popup': SkuPopup
  }
}
<script setup lang="ts">
// 获取商品详情
const goods = ref<GoodsResult>()
const getGoodsData = async () => {
  const res = await getGoodsByIdApi(query.id)
  goods.value = res.result
  // 组织sku组件需要的数据
  localdata.value = {
    _id: res.result.id,
    name: res.result.name,
    goods_thumb: res.result.mainPictures[0],
    spec_list: res.result.specs.map((v) => ({ name: v.name, list: v.values })),
    sku_list: res.result.skus.map((v) => ({
      _id: v.id,
      goods_id: res.result.id,
      goods_name: res.result.name,
      image: v.picture,
      price: v.price * 100, // 注意:需要乘以 100
      stock: v.inventory,
      sku_name_arr: v.specs.map((vv) => vv.valueName),
    })),
  }
}


// 控制sku组件
const isShowSku = ref(false)
// 商品信息
const localdata = ref({} as SkuPopupLocaldata)
</script>

<template>
  <!-- sku弹窗组件 -->
  <vk-data-goods-sku-popup
    v-model="isShowSku"
    :localdata="localdata"
    add-cart-background-color="#FFA868"
    buy-now-background-color="#27BA9B"
    :actived-style="{
      color: '#27BA9B',
      borderColor: '#27BA9B',
      backgroundColor: '#E9F8F5',
    }"
  />
      
  <scroll-view scroll-y class="viewport">
    <!-- 基本信息 -->
    <view class="goods">
      ... ...

      <!-- 操作面板 -->
      <view class="action">
        <view class="item arrow" @tap="isShowSku = true">
          <text class="label">选择</text>
          <text class="text ellipsis"> {{ selectArrText }} </text>
        </view>
       ... ...
      </view>
      
    </view>
  </scroll-view>
      
</template>

设置按钮模式: 加入购物车/立即购买/选择都可以打开sku弹窗, sku弹窗展示的按钮不同

<script setup lang="ts">
import type {
  SkuPopupLocaldata
} from '@/components/vk-data-goods-sku-popup/vk-data-goods-sku-popup'


// 控制sku组件
const isShowSku = ref(false)
// 商品信息
const localdata = ref({} as SkuPopupLocaldata)

// 按钮模式
enum SkuMode {
  Both = 1,
  Cart = 2,
  Buy = 3,
}
const mode = ref<SkuMode>(SkuMode.Cart)
// 打开SKU弹窗修改按钮模式
const openSkuPopup = (val: SkuMode) => {
  // 显示SKU弹窗
  isShowSku.value = true
  // 修改按钮模式
  mode.value = val
}

</script>

<template>
  <!-- sku弹窗组件 -->
  <vk-data-goods-sku-popup
    v-model="isShowSku"
    :localdata="localdata"
    :mode="mode"
    ref="skuPopupRef"
    @add-cart="onAddCart"
    @buy-now="onBuyNow"
    add-cart-background-color="#FFA868"
    buy-now-background-color="#27BA9B"
    :actived-style="{
      color: '#27BA9B',
      borderColor: '#27BA9B',
      backgroundColor: '#E9F8F5',
    }"
  />
      
  <scroll-view scroll-y class="viewport">
    <!-- 基本信息 -->
    <view class="goods">
     ... ...
      <!-- 操作面板 -->
      <view class="action">
        <view class="item arrow" @tap="openSkuPopup(1)">
          <text class="label">选择</text>
          <text class="text ellipsis"> {{ selectArrText }} </text>
        </view>
         ... ...
      </view>
    </view>

  </scroll-view>

  <!-- 用户操作 -->
  <view class="toolbar" :style="{ paddingBottom: safeAreaInsets?.bottom + 'px' }">
    ... ...
    <view class="buttons">
      <view class="addcart" @tap="openSkuPopup(2)"> 加入购物车 </view>
      <view class="buynow" @tap="openSkuPopup(3)"> 立即购买 </view>
    </view>
  </view>
</template>
  1. sku组件可以用过mode选项控制组件内按钮按需展示隐藏, 1展示全部按钮, 2展示加入购物车按钮,3展示立即购买按钮
  2. 直接指定1,2,3语义化不好, 我们使用枚举进行指定

在商品界面展示SKU组件中选中的值

<script setup lang="ts">
// SKU组件实例
const skuPopupRef = ref<SkuPopupInstance>()
// 计算被选中的值
const selectArrText = computed(() => {
  return skuPopupRef.value?.selectArr?.join(' ').trim() || '请选择商品规格'
})
</script>

<template>
  <!-- sku弹窗组件 -->
  <vk-data-goods-sku-popup
    v-model="isShowSku"
    :localdata="localdata"
    :mode="mode"
    ref="skuPopupRef"
    add-cart-background-color="#FFA868"
    buy-now-background-color="#27BA9B"
    :actived-style="{
      color: '#27BA9B',
      borderColor: '#27BA9B',
      backgroundColor: '#E9F8F5',
    }"
  />
  <scroll-view scroll-y class="viewport">
    <!-- 基本信息 -->
    <view class="goods">
      ... ...
      <!-- 操作面板 -->
      <view class="action">
        <view class="item arrow" @tap="openSkuPopup(1)">
          <text class="label">选择</text>
          <text class="text ellipsis"> {{ selectArrText }} </text>
        </view>
        <view class="item arrow" @tap="openPopop('address')">
          <text class="label">送至</text>
          <text class="text ellipsis"> 请选择收获地址 </text>
        </view>
        <view class="item arrow" @tap="openPopop('service')">
          <text class="label">服务</text>
          <text class="text ellipsis"> 无忧退 快速退款 免费包邮 </text>
        </view>
      </view>
    </view>
    ... ...
  </scroll-view>
</template>

加入购物车和立即购买

<script setup lang="ts">
import type {
  SkuPopupEvent,
} from '@/components/vk-data-goods-sku-popup/vk-data-goods-sku-popup'
import { postMemberCartAPI } from '@/services/cart'

// 加入购物车事件
const onAddCart = async (ev: SkuPopupEvent) => {
  await postMemberCartAPI({ skuId: ev._id, count: ev.buy_num })
  uni.showToast({ title: '添加成功' })
  isShowSku.value = false
}

// 立即购买
const onBuyNow = (ev: SkuPopupEvent) => {
  uni.navigateTo({ url: `/pagesOrder/create/create?skuId=${ev._id}&count=${ev.buy_num}` })
  isShowSku.value = false
}
</script>

<template>
  <!-- sku弹窗组件 -->
  <vk-data-goods-sku-popup
    v-model="isShowSku"
    :localdata="localdata"
    :mode="mode"
    ref="skuPopupRef"
    @add-cart="onAddCart"
    @buy-now="onBuyNow"
    add-cart-background-color="#FFA868"
    buy-now-background-color="#27BA9B"
    :actived-style="{
      color: '#27BA9B',
      borderColor: '#27BA9B',
      backgroundColor: '#E9F8F5',
    }"
  />
  ... ...

  <!-- 用户操作 -->
  <view class="toolbar" :style="{ paddingBottom: safeAreaInsets?.bottom + 'px' }">
    ... ...
    <view class="buttons">
      <view class="addcart" @tap="openSkuPopup(2)"> 加入购物车 </view>
      <view class="buynow" @tap="openSkuPopup(3)"> 立即购买 </view>
    </view>
  </view>
</template>

购物车模块

渲染列表

/** 购物车类型 */
export type CartItem = {
    /** 商品 ID */
    id: string
    /** SKU ID */
    skuId: string
    /** 商品名称 */
    name: string
    /** 图片 */
    picture: string
    /** 数量 */
    count: number
    /** 加入时价格 */
    price: number
    /** 当前的价格 */
    nowPrice: number
    /** 库存 */
    stock: number
    /** 是否选中 */
    selected: boolean
    /** 属性文字 */
    attrsText: string
    /** 是否为有效商品 */
    isEffective: boolean
}
import type { CartItem } from '@/types/cart'
import { http } from '@/utils/http'
/**
 * 加入购物车
 * @param data 请求体参数
 */
export const postMemberCartAPI = (data: { skuId: string; count: number }) => {
    return http({
        method: 'POST',
        url: '/member/cart',
        data,
    })
}

/**
 * 获取购物车列表
 */
export const getMemberCartAPI = () => {
    return http<CartItem[]>({
        method: 'GET',
        url: '/member/cart',
    })
}

/**
 * 删除/清空购物车单品
 * @param data 请求体参数 ids SKUID 集合
 */
export const deleteMemberCartAPI = (data: { ids: string[] }) => {
    return http({
        method: 'DELETE',
        url: '/member/cart',
        data,
    })
}

/**
 * 修改购物车单品
 * @param skuId SKUID
 * @param data selected 选中状态 count 商品数量
 */
export const putMemberCartBySkuIdAPI = (
    skuId: string,
    data: { selected?: boolean; count?: number },
) => {
    return http({
        method: 'PUT',
        url: `/member/cart/${skuId}`,
        data,
    })
}

/**
 * 购物车全选/取消全选
 * @param data selected 是否选中
 */
export const putMemberCartSelectedAPI = (data: { selected: boolean }) => {
    return http({
        method: 'PUT',
        url: '/member/cart/selected',
        data,
    })
}
<script setup lang="ts">
import {
  getMemberCartAPI,
} from '@/services/cart'
import { useMemberStore } from '@/stores'
import type { CartItem } from '@/types/cart'
import { onShow } from '@dcloudio/uni-app'
import { ref, computed } from 'vue'

// 获取会员Store
const memberStore = useMemberStore()

// 获取购物车数据
const cartList = ref<CartItem[]>([])
const getMemberCartData = async () => {
  const res = await getMemberCartAPI()
  cartList.value = res.result
}

// 初始化调用: 页面显示触发
onShow(() => {
  // 用户已登录才允许调用
  if (memberStore.profile) {
    getMemberCartData()
  }
})
</script>

<template>
  <scroll-view scroll-y class="scroll-view">
    <!-- 已登录: 显示购物车 -->
    <template v-if="memberStore.profile">
      <!-- 购物车列表 -->
      <view class="cart-list" v-if="cartList.length">
        <!-- 优惠提示 -->
        <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 cartList" :key="item.skuId" class="cart-swipe">
            <!-- 商品信息 -->
            <view class="goods">
              <!-- 选中状态 -->
              <text
                @tap="onChangeSelected(item)"
                class="checkbox"
                :class="{ checked: item.selected }"
              ></text>
              <navigator
                :url="`/pages/goods/goods?id=${item.id}`"
                hover-class="none"
                class="navigator"
              >
                <image mode="aspectFill" class="picture" :src="item.picture"></image>
                <view class="meta">
                  <view class="name ellipsis">{{ item.name }}</view>
                  <view class="attrsText ellipsis">{{ item.attrsText }}</view>
                  <view class="price">{{ item.nowPrice }}</view>
                </view>
              </navigator>
              <!-- 商品数量 -->
              <view class="count">
                <vk-data-input-number-box
                  v-model="item.count"
                  :min="1"
                  :max="item.stock"
                  :index="item.skuId"
                  @change="onChangeCount"
                />
              </view>
            </view>
            <!-- 右侧删除按钮 -->
            <template #right>
              <view class="cart-swipe-right">
                <button @tap="onDeleteCart(item.skuId)" 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" @tap="onChangeSelectedAll" :class="{ checked: isSelectedAll }">全选</text>
        <text class="text">合计:</text>
        <text class="amount">{{ selectedCartListMoney }}</text>
        <view class="button-grounp">
          <view @tap="gotoPayment" class="button payment-button" :class="{ disabled: true }">
            去结算({{ selectedCartListCount }})
          </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>

删除商品

<script setup lang="ts">
import {
  deleteMemberCartAPI,
} from '@/services/cart'

// 点击删除按钮
const onDeleteCart = (skuId: string) => {
  // 弹窗二次确认
  uni.showModal({
    content: '是否删除',
    success: async (res) => {
      if (res.confirm) {
        // 后端删除单品
        await deleteMemberCartAPI({ ids: [skuId] })
        // 重新获取列表
        getMemberCartData()
      }
    },
  })
}
</script>

<template>
  <scroll-view scroll-y class="scroll-view">
    <!-- 已登录: 显示购物车 -->
    <template v-if="memberStore.profile">
      <!-- 购物车列表 -->
      <view class="cart-list" v-if="cartList.length">
       
        <!-- 滑动操作分区 -->
        <uni-swipe-action>
          <!-- 滑动操作项 -->
          <uni-swipe-action-item v-for="item in cartList" :key="item.skuId" class="cart-swipe">
            <!-- 商品信息 -->
            <view class="goods">
              ... ...
            </view>
            <!-- 右侧删除按钮 -->
            <template #right>
              <view class="cart-swipe-right">
                <button @tap="onDeleteCart(item.skuId)" class="button delete-button">删除</button>
              </view>
            </template>
          </uni-swipe-action-item>
        </uni-swipe-action>
  
      </view>
    </template>
  </scroll-view>
</template>

修改商品数量

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/runtime-core' {
  export interface GlobalComponents {
    'vk-data-input-number-box': InputNumberBox
  }
}
<script setup lang="ts">
import type { InputNumberBoxEvent } from '@/components/vk-data-input-number-box/vk-data-input-number-box'
import {
  putMemberCartBySkuIdAPI,
} from '@/services/cart'

// 修改商品数量
const onChangeCount = (ev: InputNumberBoxEvent) => {
  putMemberCartBySkuIdAPI(ev.index, { count: ev.value })
}
</script>

<template>
  <scroll-view scroll-y class="scroll-view">
    <template v-if="memberStore.profile">
      <!-- 购物车列表 -->
      <view class="cart-list" v-if="cartList.length">
        ... ...
        <!-- 滑动操作分区 -->
        <uni-swipe-action>
          <!-- 滑动操作项 -->
          <uni-swipe-action-item v-for="item in cartList" :key="item.skuId" class="cart-swipe">
            <!-- 商品信息 -->
            <view class="goods">
              ... ...
              <!-- 商品数量 -->
              <view class="count">
                <vk-data-input-number-box
                  v-model="item.count"
                  :min="1"
                  :max="item.stock"
                  :index="item.skuId"
                  @change="onChangeCount"
                />
              </view>
            </view>
            ... ...
          </uni-swipe-action-item>
        </uni-swipe-action>
      </view>
    </template>
  </scroll-view>
</template>

商品状态修改

<script setup lang="ts">
import {
  putMemberCartBySkuIdAPI,
  putMemberCartSelectedAPI,
} from '@/services/cart'
import type { CartItem } from '@/types/cart'
import { onShow } from '@dcloudio/uni-app'
import { ref, computed } from 'vue'

// 修改选中状态-单品修改
const onChangeSelected = (item: CartItem) => {
  // 前端数据更新-是否选中取反
  item.selected = !item.selected
  // 后端数据更新
  putMemberCartBySkuIdAPI(item.skuId, { selected: item.selected })
}

// 计算全选状态
const isSelectedAll = computed(() => {
  return cartList.value.length && cartList.value.every((v) => v.selected)
})

// 修改选中状态-全选修改
const onChangeSelectedAll = () => {
  // 全选状态取反
  const _isSelectedAll = !isSelectedAll.value
  // 前端数据更新
  cartList.value.forEach((item) => {
    item.selected = _isSelectedAll
  })
  // 后端数据更新
  putMemberCartSelectedAPI({ selected: _isSelectedAll })
}
</script>

<template>
  <scroll-view scroll-y class="scroll-view">
    <template v-if="memberStore.profile">
      <!-- 购物车列表 -->
      <view class="cart-list" v-if="cartList.length">
        <!-- 滑动操作分区 -->
        <uni-swipe-action>
          <!-- 滑动操作项 -->
          <uni-swipe-action-item v-for="item in cartList" :key="item.skuId" class="cart-swipe">
            <!-- 商品信息 -->
            <view class="goods">
              <!-- 选中状态 -->
              <text
                @tap="onChangeSelected(item)"
                class="checkbox"
                :class="{ checked: item.selected }"
              ></text>
            </view>
            ... ...
          </uni-swipe-action-item>
        </uni-swipe-action>
      </view>
  
      <!-- 吸底工具栏 -->
      <view class="toolbar">
        <text class="all" @tap="onChangeSelectedAll" :class="{ checked: isSelectedAll }">全选</text>
        ... ...
      </view>
    </template>
  </scroll-view>
</template>

底部结算信息

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

// 计算选中单品列表
const selectedCartList = computed(() => {
  return cartList.value.filter((v) => v.selected)
})

// 计算选中总件数
const selectedCartListCount = computed(() => {
  return selectedCartList.value.reduce((sum, item) => sum + item.count, 0)
})

// 计算选中总金额
const selectedCartListMoney = computed(() => {
  return selectedCartList.value
    .reduce((sum, item) => sum + item.count * item.nowPrice, 0)
    .toFixed(2)
})

// 结算按钮
const gotoPayment = () => {
  if (selectedCartListCount.value === 0) {
    return uni.showToast({
      icon: 'none',
      title: '请选择商品',
    })
  }
  // 跳转到结算页
  uni.navigateTo({ url: '/pagesOrder/create/create' })
}
</script>

<template>
  <scroll-view scroll-y class="scroll-view">
    <template v-if="memberStore.profile">
     ... ...
  
      <!-- 吸底工具栏 -->
      <view class="toolbar">
        <text class="text">合计:</text>
        <text class="amount">{{ selectedCartListMoney }}</text>
        <view class="button-grounp">
          <view @tap="gotoPayment" class="button payment-button" :class="{ disabled: true }">
            去结算({{ selectedCartListCount }})
          </view>
        </view>
      </view>
    </template>
  </scroll-view>
</template>

购物车页面复用: 小程序跳转到tabbar页面时, 会关闭其他所有非tabbar页, 所以小程序tabbar页没有后退按钮

<script setup lang="ts">
  // 获取购物车数据
  const cartList = ref<CartItem[]>([])
  const getMemberCartData = async () => {
    const res = await getMemberCartAPI()
    cartList.value = res.result
  }

  // 初始化调用: 页面显示触发
  onShow(() => {
    // 用户已登录才允许调用
    if (memberStore.profile) {
      getMemberCartData()
    }
  })
</script>

<template>
  <scroll-view scroll-y class="scroll-view">
     ... ...
  </scroll-view>
</template>
<script setup lang="ts">
import CartMain from './components/CartMain.vue'
</script>

<template>
  <CartMain />
</template>

<style lang="scss">
page {
  height: 100%;
}
</style>
<script setup lang="ts">
import CartMain from './components/CartMain.vue'
</script>

<template>
  <CartMain />
</template>

<style lang="scss">
page {
  height: 100%;
}
</style>
{
	"pages": [
		... ...
    // 购物车tabber页
		{
			"path": "pages/cart/cart",
			"style": {
				"navigationBarTitleText": "购物车"
			}
		},
    // 购物车普通页
		{
			"path": "pages/cart/cart2",
			"style": {
				"navigationBarTitleText": "购物车"
			}
		},
	  ... ...
	],
}
<template>
  <!-- 用户操作 -->
  <view class="toolbar" :style="{ paddingBottom: safeAreaInsets?.bottom + 'px' }">
    <view class="icons">
      ... ...
      <navigator class="icons-button" url="/pages/cart/cart2" open-type="navigate">
        <text class="icon-cart"></text>购物车
      </navigator>
  
    </view>
    ... ...
  </view>
</template>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2187882.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Stable Diffusion绘画 | 插件-Deforum:动态视频生成(上篇)

Deforum 与 AnimateDiff 不太一样&#xff0c; AnimateDiff 是生成丝滑变化视频的&#xff0c;而 Deforum 的丝滑程度远远没有 AnimateDiff 好。 它是根据对比前面一帧的画面&#xff0c;然后不断生成新的相似图片&#xff0c;来组合成一个完整的视频。 Deforum 的优点在于可…

多模态系统中专家混合(MoE)复杂性的解读

引言 在迅速发展的人工智能领域&#xff0c;整合多种数据模态&#xff08;如文本、图像、音频和传感器数据&#xff09;是一个极具挑战性的任务。传统的单一模型AI系统通常难以应对同时处理多模态时产生的指数级复杂性。而这正是混合专家&#xff08;Mixture of Experts&#…

如何正确拆分数据集?常见方法最全汇总

将数据集划分为训练集&#xff08;Training&#xff09;和测试集&#xff08;Testing&#xff09;是机器学习和统计建模中的重要步骤&#xff1a; 训练集&#xff08;Training&#xff09;&#xff1a;一般来说 Train 训练集会进一步再分为 Train 训练集与 Validation 验证集两…

ElasticSearch备考 -- Update by query

一、题目 有个索引task&#xff0c;里面的文档长这样 现在需要添加一个字段all&#xff0c;这个字段的值是以下 a、b、c、d字段的值连在一起 二、思考 需要把四个字段拼接到一起&#xff0c;组成一个新的字段&#xff0c;这个就需要脚本&#xff0c; 这里有两种方案&#xff…

geodatatool(地图资源下载工具)3.8更新

geodatatool&#xff08;地图资源下载工具&#xff09;3.8&#xff08;新&#xff09;修复更新&#xff0c;修复更新包括&#xff1a; 1.高德POI数据按行政区划下载功能完善。 2.修正高德POI数据类型重复问题。 3.对高德KEY数据访问量超过最大限制时&#xff0c;提示错误并终止…

RK3568平台(显示篇)车机图像显示偏白问题分析

一.显示偏白图片对比 正常图像: 偏白图像: 二.分析过程

手把手教你使用Tensorflow2.7完成人脸识别系统,web人脸识别(Flask框架)+pyqt界面,保姆级教程

目录 前言一、系统总流程设计二、环境安装1. 创建虚拟环境2.安装其他库 三、模型搭建1.采集数据集2. 数据预处理3.构建模型和训练 五、摄像头测试六、web界面搭建与pyqt界面搭建报错了并解决的方法总结 前言 随着人工智能的不断发展&#xff0c;机器学习和深度学习这门技术也越…

YOLO11改进|注意力机制篇|引入注意力与卷积混合的ACmix

目录 一、ACmix注意力机制1.1ACmix注意力介绍1.2ACmix核心代码 二、添加ACmix注意力机制2.1STEP12.2STEP22.3STEP32.4STEP4 三、yaml文件与运行3.1yaml文件3.2运行成功截图 一、ACmix注意力机制 1.1ACmix注意力介绍 ACmix设计为一个结合了卷积和自注意力机制优势的混合模块&am…

Redis: 集群测试和集群原理

集群测试 1 ) SET/GET 命令 测试 set 和 get 因为其他命令也基本相似&#xff0c;我们在 101 节点上尝试连接 103 $ /usr/local/redis/bin/redis-cli -c -a 123456 -h 192.168.10.103 -p 6376我们在插入或读取一个 key的时候&#xff0c;会对这个key做一个hash运算&#xff0c…

判断有向图是否为单连通图的算法

判断有向图是否为单连通图的算法 算法描述伪代码C语言实现解释在图论中,单连通图(singly connected graph)是指对于图中的任意两个顶点 m 和 v,如果存在从 m 到 v 的路径,则该路径是唯一的。为了判断一个有向图是否为单连通图,我们需要确保从任意顶点出发,到任意其他顶点…

开发能够抵御ICS对抗性攻击的边缘弹性机器学习集成

论文标题&#xff1a;《Development of an Edge Resilient ML Ensemble to Tolerate ICS Adversarial Attacks》 作者信息&#xff1a; Likai Yao, NSF Center for Cloud and Autonomic Computing, University of Arizona, Tucson, AZ 85721 USAQinxuan Shi, School of Elect…

【数据库差异研究】别名与表字段冲突,不同数据库在where中的处理行为

目录 ⚛️总结 ☪️1 问题描述 ☪️2 测试用例 ♋2.1 测试单层查询 ♏2.1.1 SQLITE数据库 ♐2.1.2 ORACLE数据库 ♑2.1.3 PG数据库 ♋2.2 测试嵌套查询 ♉2.2.1 SQLITE数据库 ♈2.2.2 ORACLE数据库 &#x1f52f;2.2.3 PG数据库 ⚛️总结 单层查询 数据库类型别名…

字节终面问 Transformer,太难了。。。

最近已有不少大厂都在秋招宣讲了&#xff0c;也有一些在 Offer 发放阶段。 节前&#xff0c;我们邀请了一些互联网大厂朋友、今年参加社招和校招面试的同学。 针对新手如何入门算法岗、该如何准备面试攻略、面试常考点、大模型技术趋势、算法项目落地经验分享等热门话题进行了…

厦门网站设计的用户体验优化策略

厦门网站设计的用户体验优化策略 在信息化快速发展的今天&#xff0c;网站作为企业与用户沟通的重要桥梁&#xff0c;用户体验&#xff08;UX&#xff09;的优化显得尤为重要。尤其是在交通便利、旅游资源丰富的厦门&#xff0c;吸引了大量企业进驻。在这样竞争激烈的环境中&am…

后端向页面传数据(内容管理系统)

一、登录 首先&#xff0c;做一个登录页面。 在这里&#xff0c;注意 内容框里的提示信息用placeholder <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthd…

基于J2EE技术的高校社团综合服务系统

目录 毕设制作流程功能和技术介绍系统实现截图开发核心技术介绍&#xff1a;使用说明开发步骤编译运行代码执行流程核心代码部分展示可行性分析软件测试详细视频演示源码获取 毕设制作流程 &#xff08;1&#xff09;与指导老师确定系统主要功能&#xff1b; &#xff08;2&am…

Visual Studio AI插件推荐

声明&#xff1a;个人喜好&#xff0c;仅供参考。 1、AI插件 Fitten Code&#xff08;免费&#xff09; Fitten Code 是由非十大模型驱动的AI编程助手&#xff0c;支持多种编程语言&#xff0c;支持主流几乎所有的IDE开发工具。包括VS Code、Visual Studio、JetBrains系列I…

Visual Studio 小技巧记录

1、将行距设置成1.15跟舒服一些。 2、括号进行颜色对比。 效果&#xff1a; 3、显示参数内联提示。 效果&#xff1a; 4、保存时规范化代码。 配置文件&#xff1a; 5、将滚动条修改为缩略图 效果&#xff1a;

MongoDB 数据库服务搭建(单机)

下载地址 下载测试数据 作者&#xff1a;程序那点事儿 日期&#xff1a;2023/02/15 02:16 进入下载页&#xff0c;选择版本后&#xff0c;右键Download复制连接地址 下载安装包 ​ wget https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-rhel70-5.0.14.tgz​ …

开放式耳机哪个品牌好?好用且高性价比的开放式蓝牙耳机推荐

相信很多经常运动的朋友都不是很喜欢佩戴入耳式耳机&#xff0c;因为入耳式耳机真的有很多缺点。 安全方面&#xff1a;在安全上就很容易存在隐患&#xff0c;戴上后难以听到周围环境声音&#xff0c;像汽车鸣笛、行人呼喊等&#xff0c;容易在运动中发生意外。 健康方面&…