小兔鲜--项目总结3

目录

结算模块-地址切换交互实现

地址切换交互需求分析

打开弹框交互实现

地址激活交互实现

订单模块-生成订单功能实现 

 支付模块-实现支付功能

支付业务流程

 支付模块-支付结果展示

支付模块-封装倒计时函数

理解需求

 实现思路分析

会员中心-个人中心信息渲染

分页逻辑实现

SKU组件封装

认识SKU组件

 点击规格更新选中状态

 点击规格更新禁用状态 - 生成有效路径字典(1)

点击规格更新禁用状态 - 生成有效路径字典(2)

点击规格更新禁用状态 - 初始化规格禁用

击规格更新禁用状态 - 点击时组合禁用更新

产出有效的SKU信息

完整代码


结算模块-地址切换交互实现

地址切换交互需求分析

1. 打开弹框交互:点击切换地址按钮,打开弹框,回显用户可选地址列表
2. 切换地址交互:点击切换地址,点击确定按钮,激活地址替换默认收货地址

打开弹框交互实现

1. 准备弹框模版

<el-dialog title="切换收货地址" width="30%" center>
  <div class="addressWrapper">
    <div class="text item" v-for="item in checkInfo.userAddresses"  :key="item.id">
      <ul>
      <li><span>收<i />货<i />人:</span>{{ item.receiver }} </li>
      <li><span>联系方式:</span>{{ item.contact }}</li>
      <li><span>收货地址:</span>{{ item.fullLocation + item.address }}</li>
      </ul>
    </div>
  </div>
  <template #footer>
    <span class="dialog-footer">
      <el-button>取消</el-button>
      <el-button type="primary">确定</el-button>
    </span>
  </template>
</el-dialog>

 2. 控制弹框打开

const showDialog = ref(false)

<el-button size="large" @click="showDialog = true">切换地址</el-button>

<el-dialog v-model="showDialog" title="切换收货地址" width="30%" center>
    <!-- 省略 -->
</el-dialog>

地址激活交互实现

原理:地址切换是我们经常遇到的 `tab切换类` 需求,这类需求的实现逻辑都是相似的
1. 点击时记录一个当前激活地址对象activeAddress, 点击哪个地址就把哪个地址对象记录下来
2. 通过动态类名:class 控制激活样式类型 active是否存在,判断条件为:激活地址对象的id === 当前项id

<script setup>
// 切换地址
const activeAddress = ref({})
const switchAddress = (item) => {
  activeAddress.value = item
}
</script>

<template>
<div class="text item" 
  :class="{ active: activeAddress.id === item.id }" 
  @click="switchAddress(item)"
  :key="item.id">
  <!-- 省略... -->
</div>
</template>

切换地址属于哪类通用型交互功能?

tab切换类交互
记录激活项(整个对象/id/index) + 动态类名控制

订单模块-生成订单功能实现 

业务需求说明

确定结算信息没有问题之后,点击提交订单按钮,需要做以下俩个事情:

1. 调用接口生成订单id,并且携带id跳转到支付页
2. 调用更新购物车列表接口,更新购物车状态

<script setup>
import { createOrderAPI } from '@/apis/checkout'

// 创建订单
const createOrder = async () => {
  const res = await createOrderAPI({
    deliveryTimeType: 1,
    payType: 1,
    payChannel: 1,
    buyerMessage: '',
    goods: checkInfo.value.goods.map(item => {
      return {
        skuId: item.skuId,
        count: item.count
      }
    }),
    addressId: curAddress.value.id
  })
  const orderId = res.result.id
  router.push({
    path: '/pay',
    query: {
      id: orderId
    }
  })
}

</script>

<template>
    <!-- 提交订单 -->
    <div class="submit">
      <el-button @click="createOrder" type="primary" size="large">提交订单</el-button>
    </div>
</template>

 支付模块-实现支付功能

支付业务流程

// 支付地址
const baseURL = 'http://pcapi-xiaotuxian-front-devtest.itheima.net/'
const backURL = 'http://127.0.0.1:5173/paycallback'
const redirectUrl = encodeURIComponent(backURL)
const payUrl = `${baseURL}pay/aliPay?orderId=${route.query.id}&redirect=${redirectUrl}`

 支付模块-支付结果展示

业务需求理解

<script setup>
import { getOrderAPI } from '@/apis/pay'
import { onMounted, ref } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
const orderInfo = ref({})

const getOrderInfo = async () => {
  const res = await getOrderAPI(route.query.orderId)
  orderInfo.value = res.result
}

onMounted(() => getOrderInfo())

</script>


<template>
  <div class="xtx-pay-page">
    <div class="container">
      <!-- 支付结果 -->
      <div class="pay-result">
        <!-- 路由参数获取到的是字符串而不是布尔值 -->
        <span class="iconfont icon-queren2 green" v-if="$route.query.payResult === 'true'"></span>
        <span class="iconfont icon-shanchu red" v-else></span>
        <p class="tit">支付{{ $route.query.payResult === 'true' ? '成功' : '失败' }}</p>
        <p class="tip">我们将尽快为您发货,收货期间请保持手机畅通</p>
        <p>支付方式:<span>支付宝</span></p>
        <p>支付金额:<span>¥{{ orderInfo.payMoney?.toFixed(2) }}</span></p>
        <div class="btn">
          <el-button type="primary" style="margin-right:20px">查看订单</el-button>
          <el-button>进入首页</el-button>
        </div>
        <p class="alert">
          <span class="iconfont icon-tip"></span>
          温馨提示:小兔鲜儿不会以订单异常、系统升级为由要求您点击任何网址链接进行退款操作,保护资产、谨慎操作。
        </p>
      </div>
    </div>
  </div>
</template>

支付模块-封装倒计时函数

理解需求

 实现思路分析

import { computed, onUnmounted, ref } from 'vue'
import dayjs from 'dayjs'
// 封装倒计时逻辑
export const useCountDown = () => {
    const formatTime = computed(() => dayjs.unix(time.value).format('mm分ss秒'))
    // 1. 响应式的数据
    let timer = null
    const time = ref(0)
    // 2. 开启倒计时的函数
    const start = (currentTime) => {
        // 开始倒计时的逻辑
        // 核心逻辑的编写:每隔1s就减一
        time.value = currentTime
        timer = setInterval(() => {
            time.value--
        }, 1000)
    }
    // 组件销毁时清除定时器
    onUnmounted(() => {
        timer && clearInterval(timer)
    })

    return {
        formatTime,
        start
    }
}

会员中心-个人中心信息渲染

分页逻辑实现

页数 = 总条数 / 每页条数 

<script setup>
// 补充总条数
const total = ref(0)
const getOrderList = async () => {
  const res = await getUserOrder(params.value)
  // 存入总条数
  total.value = res.result.counts
}
// 页数切换
const pageChange = (page) => {
  params.value.page = page
  getOrderList()
}
</script>

<template>
   <el-pagination 
     :total="total" 
     @current-change="pageChange" 
     :page-size="params.pageSize" 
     background
     layout="prev, pager, next" />
</template>

SKU组件封装

认识SKU组件

SKU组件的作用是为了让用户能够选择商品的规格,从而提交购物车,在选择的过程中,组件的选中状态要进行更新, 组件还要提示用户当前规格是否禁用,每次选择都要产出对应的Sku数据\

 点击规格更新选中状态

 

基本思路:

  1. 每一个规格按钮都拥有自己的选中状态数据-selected,true为选中,false为取消选中
  2. 配合动态class,把选中状态selected作为判断条件,true让active类名显示,false让active类名不显示
  3. 点击的是未选中,把同一个规格的其他取消选中,当前点击项选中;点击的是已选中,直接取消
script setup>
// 省略代码

// 选中和取消选中实现
const changeSku = (item, val) => {
  // 点击的是未选中,把同一个规格的其他取消选中,当前点击项选中,点击的是已选中,直接取消
  if (val.selected) {
    val.selected = false
  } else {
    item.values.forEach(valItem => valItem.selected = false)
    val.selected = true
  }
}

</script>

<template>
  <div class="goods-sku">
    <dl v-for="item in goods.specs" :key="item.id">
      <dt>{{ item.name }}</dt>
      <dd>
        <template v-for="val in item.values" :key="val.name">
          <img v-if="val.picture" 
            @click="changeSku(item, val)" 
            :class="{ selected: val.selected }" 
            :src="val.picture"
            :title="val.name">
          <span v-else 
            @click="changeSku(val)" 
            :class="{ selected: val.selected }">{{ val.name }}</span>
        </template>
      </dd>
    </dl>
  </div>
</template>

 点击规格更新禁用状态 - 生成有效路径字典(1)

规格禁用的判断依据是什么?

 核心原理:当前的规格Sku,或者组合起来的规格Sku,在skus数组中对应项的库存为零时,当前规格会被禁用,生成 路径字典是为了协助和简化这个匹配过程

点击规格更新禁用状态 - 生成有效路径字典(2)

 

实现步骤:
1. 根据库存字段得到有效的Sku数组
2. 根据有效的Sku数组使用powerSet算法得到所有子集 3. 根据子集生成路径字典对象

export default function bwPowerSet (originalSet) {
  const subSets = []

  // We will have 2^n possible combinations (where n is a length of original set).
  // It is because for every element of original set we will decide whether to include
  // it or not (2 options for each set element).
  const numberOfCombinations = 2 ** originalSet.length

  // Each number in binary representation in a range from 0 to 2^n does exactly what we need:
  // it shows by its bits (0 or 1) whether to include related element from the set or not.
  // For example, for the set {1, 2, 3} the binary number of 0b010 would mean that we need to
  // include only "2" to the current set.
  for (let combinationIndex = 0; combinationIndex < numberOfCombinations; combinationIndex += 1) {
    const subSet = []

    for (let setElementIndex = 0; setElementIndex < originalSet.length; setElementIndex += 1) {
      // Decide whether we need to include current element into the subset or not.
      if (combinationIndex & (1 << setElementIndex)) {
        subSet.push(originalSet[setElementIndex])
      }
    }

    // Add current subset to the list of all subsets.
    subSets.push(subSet)
  }

  return subSets
}
// 创建生成路径字典对象函数
const getPathMap = (goods) => {
  const pathMap = {};
  // 1. 得到所有有效的Sku集合
  const effectiveSkus = goods.filter((sku) => sku.inventory > 0);

  // 2. 根据有效的Sku集合使用powerSet算法得到所有子集 [1,2] => [[1], [2], [1,2]]
  effectiveSkus.forEach((sku) => {
    // 2.1 获取可选规格值数组
    const selectedValArr = sku.specs.map((val) => val.valueName);
    // 2.2 获取可选值数组的子集
    const valueArrPowerSet = bwPowerSet(selectedValArr);
    // 3. 根据子集生成路径字典对象
    // 3.1 遍历子集 往pathMap中插入数据
    valueArrPowerSet.forEach((arr) => {
      // 根据Arr得到字符串的key,约定使用-分割 ['蓝色','美国'] => '蓝色-美国'
      const key = arr.join("-");
      // 给pathMap设置数据
      if (pathMap[key]) {
        pathMap[key].push(sku.id);
      } else {
        pathMap[key] = [sku.id];
      }
    });
  });
  console.log(pathMap);
  return pathMap;
};

点击规格更新禁用状态 - 初始化规格禁用

思路:遍历每一个规格对象,使用name字段作为key去路径字典pathMap中做匹配,匹配不上则禁用

 思路:判断规格的name属性是否能在有效路径字典中找到,如果找不到就禁用

// 1. 定义初始化函数
// specs:商品源数据 pathMap:路径字典
const initDisabledState = (specs, pathMap) => {
  // 约定:每一个按钮的状态由自身的disabled进行控制
  specs.forEach((item) => {
    item.values.forEach((val) => {
      console.log(val);
      if (pathMap[val.name]) {
        val.disabled = false;
      } else {
        val.disabled = true;
      }
    });
  });
};

击规格更新禁用状态 - 点击时组合禁用更新

思路(点击规格时):
1. 按照顺序得到规格选中项的数组 [‘蓝色’,‘20cm’, undefined]

2. 遍历每一个规格

2.1 把name字段的值填充到对应的位置
2.2 过滤掉undefined项使用join方法形成一个有效的key
2.3 使用key去pathMap中进行匹配,匹配不上,则当前项禁用

// 获取选中匹配数组 ['黑色',undefined,undefined]
const getSelectedValues = (specs) => {
  const arr = []
  specs.forEach(spec => {
    const selectedVal = spec.values.find(value => value.selected)
    arr.push(selectedVal ? selectedVal.name : undefined)
  })
  return arr
}

// 切换时更新选中状态
const updateDisabledState = (specs, pathMap) => {
  // 约定:每一个按钮的状态由自身的disabled进行控制
  specs.forEach((item, i) => {
    const selectedValues = getSelectedValues(specs);
    console.log(selectedValues);
    item.values.forEach((val) => {
      selectedValues[i] = val.name;
      const key = selectedValues.filter((value) => value).join("-");
      console.log(key);
      if (pathMap[key]) {
        val.disabled = false;
      } else {
        val.disabled = true;
      }
    });
  });
};

产出有效的SKU信息

什么是有效的SKU?

如何判断当前用户已经选择了所有有效的规格?
已选择项数组 [‘蓝色’,‘20cm’, undefined] 中找不到undefined, 那么用户已经选择了所有的有效规格,此时可以产出数据

如何获取当前的SKU信息对象?

把已选择项数组拼接为路径字典的key,去路径字典pathMap中找即可

 

// 选中和取消选中实现
const changeSku = (item, val) => {
  if (val.disabled) return;
  // 点击的是未选中,把同一个规格的其他取消选中,当前点击项选中,点击的是已选中,直接取消
  if (val.selected) {
    val.selected = false;
  } else {
    item.values.forEach((valItem) => (valItem.selected = false));
    val.selected = true;
  }
  updateDisabledState(goods.value.specs, pathMap);
  const index = getSelectedValues(goods.value.specs).findIndex(
    (item) => item === undefined
  );
  if (index > -1) {
    console.log("找到了,信息不完整");
  } else {
    console.log("没有找到,信息完整,可以产出");
    // 获取sku对象
    const key = getSelectedValues(goods.value.specs).join("-");
    const skuIds = pathMap[key];
    console.log(skuIds);
    // 以skuId作为匹配项去goods.value.skus数组中找
    const skuObj = goods.value.skus.find((item) => item.id === skuIds[0]);
    console.log("sku对象为", skuObj);
  }
};

完整代码

<script setup>
import { onMounted, ref } from "vue";
import axios from "axios";
import { bwPowerSet } from "@/utils/getPathMap";
// 商品数据
const goods = ref({});
let pathMap = {};
const getGoods = async () => {
  // 1135076  初始化就有无库存的规格
  // 1369155859933827074 更新之后有无库存项(蓝色-20cm-中国)
  const res = await axios.get(
    "http://pcapi-xiaotuxian-front-devtest.itheima.net/goods?id=1369155859933827074"
  );
  goods.value = res.data.result;

  // 后端返回的库存列表
  let skus = res.data.result.skus;
  // 规格列表
  let specs = res.data.result.specs;
  pathMap = getPathMap(skus);
  initDisabledState(specs, pathMap);
};
onMounted(() => getGoods());
// 创建生成路径字典对象函数
const getPathMap = (goods) => {
  const pathMap = {};
  // 1. 得到所有有效的Sku集合
  const effectiveSkus = goods.filter((sku) => sku.inventory > 0);

  // 2. 根据有效的Sku集合使用powerSet算法得到所有子集 [1,2] => [[1], [2], [1,2]]
  effectiveSkus.forEach((sku) => {
    // 2.1 获取可选规格值数组
    const selectedValArr = sku.specs.map((val) => val.valueName);
    // 2.2 获取可选值数组的子集
    const valueArrPowerSet = bwPowerSet(selectedValArr);
    // 3. 根据子集生成路径字典对象
    // 3.1 遍历子集 往pathMap中插入数据
    valueArrPowerSet.forEach((arr) => {
      // 根据Arr得到字符串的key,约定使用-分割 ['蓝色','美国'] => '蓝色-美国'
      const key = arr.join("-");

      // 给pathMap设置数据
      if (pathMap[key]) {
        pathMap[key].push(sku.id);
      } else {
        pathMap[key] = [sku.id];
      }
    });
  });
  console.log(pathMap);
  return pathMap;
};
// 1. 定义初始化函数
// specs:商品源数据 pathMap:路径字典
const initDisabledState = (specs, pathMap) => {
  // 约定:每一个按钮的状态由自身的disabled进行控制
  specs.forEach((item) => {
    item.values.forEach((val) => {
      console.log(val);
      if (pathMap[val.name]) {
        val.disabled = false;
      } else {
        val.disabled = true;
      }
    });
  });
};
// 获取选中匹配数组 ['黑色',undefined,undefined]
const getSelectedValues = (specs) => {
  const arr = [];
  specs.forEach((spec) => {
    const selectedVal = spec.values.find((value) => value.selected);
    arr.push(selectedVal ? selectedVal.name : undefined);
  });
  return arr;
};
// 切换时更新选中状态
const updateDisabledState = (specs, pathMap) => {
  // 约定:每一个按钮的状态由自身的disabled进行控制
  specs.forEach((item, i) => {
    const selectedValues = getSelectedValues(specs);
    console.log(selectedValues);
    item.values.forEach((val) => {
      selectedValues[i] = val.name;
      const key = selectedValues.filter((value) => value).join("-");
      console.log(key);
      if (pathMap[key]) {
        val.disabled = false;
      } else {
        val.disabled = true;
      }
    });
  });
};
// 选中和取消选中实现
const changeSku = (item, val) => {
  if (val.disabled) return;
  // 点击的是未选中,把同一个规格的其他取消选中,当前点击项选中,点击的是已选中,直接取消
  if (val.selected) {
    val.selected = false;
  } else {
    item.values.forEach((valItem) => (valItem.selected = false));
    val.selected = true;
  }
  updateDisabledState(goods.value.specs, pathMap);
  const index = getSelectedValues(goods.value.specs).findIndex(
    (item) => item === undefined
  );
  if (index > -1) {
    console.log("找到了,信息不完整");
  } else {
    console.log("没有找到,信息完整,可以产出");
    // 获取sku对象
    const key = getSelectedValues(goods.value.specs).join("-");
    const skuIds = pathMap[key];
    console.log(skuIds);
    // 以skuId作为匹配项去goods.value.skus数组中找
    const skuObj = goods.value.skus.find((item) => item.id === skuIds[0]);
    console.log("sku对象为", skuObj);
  }
};
</script>

<template>
  <div class="goods-sku">
    <dl v-for="item in goods.specs" :key="item.id">
      <dt>{{ item.name }}</dt>
      <dd>
        <template v-for="val in item.values" :key="val.name">
          <img
            v-if="val.picture"
            @click="changeSku(item, val)"
            :class="{ selected: val.selected, disabled: val.disabled }"
            :src="val.picture"
            :title="val.name"
          />
          <span
            v-else
            @click="changeSku(item, val)"
            :class="{ selected: val.selected, disabled: val.disabled }"
            >{{ val.name }}</span
          >
        </template>
      </dd>
    </dl>
  </div>
</template>
<style scoped lang="scss">
@mixin sku-state-mixin {
  border: 1px solid #e4e4e4;
  margin-right: 10px;
  cursor: pointer;

  &.selected {
    border-color: #27ba9b;
  }

  &.disabled {
    opacity: 0.6;
    border-style: dashed;
    cursor: not-allowed;
  }
}

.goods-sku {
  padding-left: 10px;
  padding-top: 20px;

  dl {
    display: flex;
    padding-bottom: 20px;
    align-items: center;

    dt {
      width: 50px;
      color: #999;
    }

    dd {
      flex: 1;
      color: #666;

      > img {
        width: 50px;
        height: 50px;
        margin-bottom: 4px;
        @include sku-state-mixin;
      }

      > span {
        display: inline-block;
        height: 30px;
        line-height: 28px;
        padding: 0 20px;
        margin-bottom: 4px;
        @include sku-state-mixin;
      }
    }
  }
}
</style>

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值