浏览代码

Merge branch 'dev' of https://git.citupro.com/zhengnaiwen_citu/menduner into dev

Xiao_123 5 月之前
父节点
当前提交
ab536fb986

+ 1 - 1
src/api/mall.js

@@ -9,7 +9,7 @@ import request from '@/config/axios'
 //   })
 // }
 
-// 获取兑换记录
+// 获取商品详情
 export const getProductDetail = async (params) => {
   return request.get({
     url: '/app-api/product/spu/get-detail',

+ 45 - 14
src/views/mall/components/details.vue

@@ -1,21 +1,30 @@
 <!-- 商品详情 -->
 <template>
-  <div class="default-width px-10">
-    <v-card height="392px" class="carousel mr-3" style="width: 792px; border-radius: 8px;">
-      <!-- <div></div> -->
-      <v-carousel show-arrows="hover" cycle :model-value="0">
-        <v-carousel-item v-for="(item, i) in carouselList" :key="i" @click="null">
-          <div style="height: 392px; overflow: hidden;" :class="{'cursor-pointer': item.link}">
-            <v-img :src="item.img" :lazy-src="item.src" cover style="height: 100%; overflow: hidden;">
-              <template v-slot:placeholder>
-                <v-row align="center" class="fill-height ma-0" justify="center">
-                  <v-progress-circular color="grey-lighten-5" indeterminate></v-progress-circular>
-                </v-row>
-              </template>
-            </v-img>
+  <div class="default-width py-5">
+    <v-card class="carousel border-radius-8 white-bgc pa-5 d-flex" style="width: 100%;">
+      <!-- 图片展示-轮播 -->
+      <v-carousel show-arrows="hover" class="carousel mr-5" cycle :model-value="0" style="width: 450px;">
+        <v-carousel-item v-for="(imgUrl, i) in info.sliderPicUrls" :key="'wareImg'+i" @click="null">
+          <div style="height: 450px; overflow: hidden;">
+            <v-img :src="imgUrl" cover style="height: 100%; overflow: hidden;"></v-img>
           </div>
         </v-carousel-item>
       </v-carousel>
+      <div>
+        <!-- 大标题 -->
+        <div class="title-name">{{ info?.name || '--' }}</div>
+        <!-- 小标题 -->
+        <div class="title-introduction">{{ info?.introduction || '--' }}</div>
+        <!-- 价格 -->
+        <div class="d-flex">
+          <div class="price mr-3">{{ calcPrice(info?.price)}}</div>
+          <div class="marketPrice">{{ calcPrice(info?.marketPrice)}}</div>
+        </div>
+        <!-- 销量 -->
+        <div class="salesCount">已售:{{ info?.salesCount || 0 }}</div>
+        <!-- 属性选择 -->
+        <selectSku v-if="skus?.length" :skus="skus"></selectSku>
+      </div>
     </v-card>
   </div>
 </template>
@@ -23,9 +32,31 @@
 <script setup>
 defineOptions({name: 'wares-details'})
 import { getProductDetail } from '@/api/mall'
+import selectSku from './detailsComponents/s-select-sku.vue'
 import { ref } from 'vue'
 
-const carouselList = ref([])
+const info = ref({})
+const skus = ref([])
+// 获取商品详情
+const getData = async () => {
+  const obj = await getProductDetail({ id: 646 })
+  obj.sliderPicUrls = obj.sliderPicUrls || []
+  info.value = obj
+  skus.value = obj.skus || []
+  console.log('getProductDetail:', info.value)
+}
+getData()
+
+const calcPrice = (price) => { return price && (price-0) ? (price-0)/100 : '--' }
+
 </script>
 <style lang="scss" scoped>
+.carousel {
+  :deep(.v-window) {
+    height: 450px !important;
+  }
+}
+.border-radius-8 {
+  border-radius: 8px;
+}
 </style>

+ 239 - 0
src/views/mall/components/detailsComponents/s-select-sku.vue

@@ -0,0 +1,239 @@
+<template>
+  <div class="ss-modal-box bg-white ss-flex-col">
+    <!-- SKU 信息 - 属性选择 -->
+    <div class="modal-content ss-flex-1">
+      <div class="modal-content-scroll">
+        <div class="sku-item ss-m-b-20" v-for="property in propertyList" :key="property.id">
+          <span class="label-text ss-m-b-20">{{ property.name }}:</span>
+          <span class="ss-flex ss-col-center ss-flex-wrap">
+            <!-- :disabled="value.disabled === true" -->
+            <v-chip
+              class="spec-btn mr-3"
+              :class="[
+                { 'ui-BG-Main-Gradient': state.currentPropertyArray[property.id] === value.id, },
+                { 'disabled-btn': value.disabled === true, },
+              ]"
+              v-for="value in property.values"
+              :key="value.id"
+              size="small"
+              label
+              density="comfortable"
+              color="primary"
+              variant="outlined"
+              :disabled="value.disabled === true"
+              @click="onSelectSku(property.id, value.id)"
+            >
+              {{ value.name }}
+            </v-chip>
+          </span>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { computed, reactive, watch, ref } from 'vue'
+import { convertProductPropertyList } from '@/views/mall/utils'
+
+const emits = defineEmits(['change', 'addCart', 'buy', 'close']);
+const props = defineProps({
+  skus: {
+    type: Array,
+  default: () => []
+  }
+});
+
+const state = reactive({
+  selectedSku: {}, // 选中的 SKU
+  currentPropertyArray: [], // 当前选中的属性,实际是个 Map。key 是 property 编号,value 是 value 编号
+});
+
+// SKU 列表
+const skuList = computed(() => {
+  let skuPrices =  props.skus?.length ? props.skus : []
+  for (let price of skuPrices) {
+    price.value_id_array = price.properties.map((item) => item.valueId);
+  }
+  return skuPrices;
+});
+
+const propertyList = ref([])
+watch(
+  () => props.skus,
+  (newVal) => {
+    if (newVal && newVal.length) {
+      propertyList.value = props.skus?.length ? convertProductPropertyList(props.skus) : []
+      console.log('propertyList:', propertyList.value)
+    }
+  },
+  {
+    immediate: true, // 立即执行
+    // deep: true, // 深度监听
+  },
+);
+
+watch(
+  () => state.selectedSku,
+  (newVal) => {
+    emits('change', newVal);
+  },
+  {
+    immediate: true, // 立即执行
+    deep: true, // 深度监听
+  },
+);
+
+
+// 改变禁用状态:计算每个 property 属性值的按钮,是否禁用
+function changeDisabled(isChecked = false, propertyId = 0, valueId = 0) {
+  let newSkus = []; // 所有可以选择的 sku 数组
+  if (isChecked) {
+    // 情况一:选中 property
+    // 获得当前点击选中 property 的、所有可用 SKU
+    for (let price of skuList.value) {
+      if (price.stock <= 0) {
+        continue;
+      }
+      if (price.value_id_array.indexOf(valueId) >= 0) {
+        newSkus.push(price);
+      }
+    }
+  } else {
+    // 情况二:取消选中 property
+    // 当前所选 property 下,所有可以选择的 SKU
+    newSkus = getCanUseSkuList();
+  }
+
+  // 所有存在并且有库存未选择的 SKU 的 value 属性值 id
+  let noChooseValueIds = [];
+  for (let price of newSkus) {
+    noChooseValueIds = noChooseValueIds.concat(price.value_id_array);
+  }
+  noChooseValueIds = Array.from(new Set(noChooseValueIds)); // 去重
+
+  if (isChecked) {
+    // 去除当前选中的 value 属性值 id
+    let index = noChooseValueIds.indexOf(valueId);
+    noChooseValueIds.splice(index, 1);
+  } else {
+    // 循环去除当前已选择的 value 属性值 id
+    state.currentPropertyArray.forEach((currentPropertyId) => {
+      if (currentPropertyId.toString() !== '') {
+        return;
+      }
+      // currentPropertyId 为空是反选 填充的
+      let index = noChooseValueIds.indexOf(currentPropertyId);
+      if (index >= 0) {
+        // currentPropertyId 存在于 noChooseValueIds
+        noChooseValueIds.splice(index, 1);
+      }
+    });
+  }
+
+  // 当前已选择的 property 数组
+  let choosePropertyIds = [];
+  if (!isChecked) {
+    // 当前已选择的 property
+    state.currentPropertyArray.forEach((currentPropertyId, currentValueId) => {
+      if (currentPropertyId !== '') {
+        // currentPropertyId 为空是反选 填充的
+        choosePropertyIds.push(currentValueId);
+      }
+    });
+  } else {
+    // 当前点击选择的 property
+    choosePropertyIds = [propertyId];
+  }
+
+  for (let propertyIndex in propertyList.value) {
+    // 当前点击的 property、或者取消选择时候,已选中的 property 不进行处理
+    if (choosePropertyIds.indexOf(propertyList.value[propertyIndex]['id']) >= 0) {
+      continue;
+    }
+    // 如果当前 property id 不存在于有库存的 SKU 中,则禁用
+    for (let valueIndex in propertyList.value[propertyIndex]['values']) {
+      propertyList.value[propertyIndex]['values'][valueIndex]['disabled'] =
+        noChooseValueIds.indexOf(propertyList.value[propertyIndex]['values'][valueIndex]['id']) < 0; // true 禁用 or false 不禁用
+    }
+  }
+}
+
+// 当前所选属性下,获取所有有库存的 SKU 们
+function getCanUseSkuList() {
+  let newSkus = [];
+  for (let sku of skuList.value) {
+    if (sku.stock <= 0) {
+      continue;
+    }
+    let isOk = true;
+    state.currentPropertyArray.forEach((propertyId) => {
+      // propertyId 不为空,并且,这个 条 sku 没有被选中,则排除
+      if (propertyId.toString() !== '' && sku.value_id_array.indexOf(propertyId) < 0) {
+        isOk = false;
+      }
+    });
+    if (isOk) {
+      newSkus.push(sku);
+    }
+  }
+  return newSkus;
+}
+
+// 选择规格
+function onSelectSku(propertyId, valueId) {
+  // 清空已选择
+  let isChecked = true; // 选中 or 取消选中
+  if (
+    state.currentPropertyArray[propertyId] !== undefined &&
+    state.currentPropertyArray[propertyId] === valueId
+  ) {
+    // 点击已被选中的,删除并填充 ''
+    isChecked = false;
+    state.currentPropertyArray.splice(propertyId, 1, '');
+  } else {
+    // 选中
+    state.currentPropertyArray[propertyId] = valueId;
+  }
+
+  // 选中的 property 大类
+  let choosePropertyId = [];
+  state.currentPropertyArray.forEach((currentPropertyId) => {
+    if (currentPropertyId !== '') {
+      // currentPropertyId 为空是反选 填充的
+      choosePropertyId.push(currentPropertyId);
+    }
+  });
+
+  // 当前所选 property 下,所有可以选择的 SKU 们
+  let newSkuList = getCanUseSkuList();
+
+  // 判断所有 property 大类是否选择完成
+  if (choosePropertyId.length === propertyList.value.length && newSkuList.length) {
+    newSkuList[0].goods_num = state.selectedSku.goods_num || 1;
+    state.selectedSku = newSkuList[0];
+  } else {
+    state.selectedSku = {};
+  }
+
+  // 改变 property 禁用状态
+  changeDisabled(isChecked, propertyId, valueId);
+}
+
+changeDisabled(false);
+// TODO 芋艿:待讨论的优化点:1)单规格,要不要默认选中;2)默认要不要选中第一个规格
+</script>
+
+<style lang="scss" scoped>
+// 主题色渐变,横向
+.ui-BG-Main-Gradient {
+  // background: linear-gradient(90deg, #ff3000, #ff300099);
+  background: var(--v-primary-base);
+  color: #fff !important;
+}
+
+.disabled-btn {
+  color: #c6c6c6;
+  background: #f8f8f8;
+}
+</style>

+ 10 - 1
src/views/mall/home/components/copy.js

@@ -1,3 +1,12 @@
 
 import { useRouter } from 'vue-router'; const router = useRouter()
-router.push({ path: '/wareDetails/646' })
+router.push({ path: '/wareDetails/646' })
+
+// {
+//   "title": null,
+//   "mark": "1733220155061",
+//   "img": "https://minio.citupro.com/dev/menduner/preferredGroup/IHG-banner-new.gif",
+//   "link": null,
+//   "sort": null,
+//   "status": "0"
+// }

+ 44 - 0
src/views/mall/utils/index.js

@@ -0,0 +1,44 @@
+
+/**
+ * 从商品 SKU 数组中,转换出商品属性的数组
+ *
+ * 类似结构:[{
+ *    id: // 属性的编号
+ *    name: // 属性的名字
+ *    values: [{
+ *      id: // 属性值的编号
+ *      name: // 属性值的名字
+ *    }]
+ * }]
+ *
+ * @param skus 商品 SKU 数组
+ */
+export function convertProductPropertyList(skus) {
+  let result = [];
+  for (const sku of skus) {
+    if (!sku.properties) {
+      continue;
+    }
+    for (const property of sku.properties) {
+      // ① 先处理属性
+      let resultProperty = result.find((item) => item.id === property.propertyId);
+      if (!resultProperty) {
+        resultProperty = {
+          id: property.propertyId,
+          name: property.propertyName,
+          values: [],
+        };
+        result.push(resultProperty);
+      }
+      // ② 再处理属性值
+      let resultValue = resultProperty.values.find((item) => item.id === property.valueId);
+      if (!resultValue) {
+        resultProperty.values.push({
+          id: property.valueId,
+          name: property.valueName,
+        });
+      }
+    }
+  }
+  return result;
+}