uniapp【修复】:sku优化多层选择,默认,置灰等问题

This commit is contained in:
15093570141
2024-11-14 17:28:10 +08:00
parent 01e9111943
commit 9be74e793d
3 changed files with 268 additions and 211 deletions

View File

@@ -134,6 +134,7 @@ export interface GoodsSkuListType {
sku_name_arr ?: Array<string>; sku_name_arr ?: Array<string>;
stock ?: number; stock ?: number;
_id ?: string _id ?: string
[key:string]:any;
} }
export interface GoodsSpecListType { export interface GoodsSpecListType {
@@ -141,6 +142,7 @@ export interface GoodsSpecListType {
name ?: string; name ?: string;
select ?: boolean; select ?: boolean;
show ?: boolean; show ?: boolean;
[key:string]:any;
} }
export interface GoodsFootprintType { export interface GoodsFootprintType {

View File

@@ -56,11 +56,10 @@
.tag { .tag {
position: relative; position: relative;
font-size: 27rpx; font-size: 27rpx;
padding: 15rpx 20rpx; border: 1rpx solid #f4f4f4;
border: 1rpx solid #6e737d;
border-radius: 15rpx; border-radius: 15rpx;
text-align: center; margin: inherit;
height: max-content; background-color: #fff;
.icon-select { .icon-select {
position: absolute; position: absolute;
right: 0; right: 0;
@@ -68,6 +67,9 @@
width: 28rpx; width: 28rpx;
height: 30rpx; height: 30rpx;
} }
&:after{
content: none;
}
} }
.active { .active {
border: 1px solid #d33123; border: 1px solid #d33123;
@@ -75,9 +77,9 @@
background-color: rgba(211, 49, 35, 0.04); background-color: rgba(211, 49, 35, 0.04);
} }
.gray { .gray {
border: 1px solid #eee; color: #c3c3c3;
background-color: #eee; border-color: #f6f6f6;
color: #6e737d; background-color: #f6f6f6;
} }
} }
} }

View File

@@ -1,230 +1,283 @@
<template> <template>
<uv-popup ref="skuPopup" mode="bottom" :closeable="true" :safeAreaInsetBottom="props.safeAreaInsetBottom" <uv-popup ref="skuPopup" mode="bottom" :closeable="true" :safeAreaInsetBottom="props.safeAreaInsetBottom"
@change="handleChangePopup"> @change="handleChangePopup">
<view class="sku-content"> <view class="sku-content">
<view class="godds-box"> <view class="godds-box">
<image class="img" :src="state?.chooseSku?.image"></image> <image class="img" :src="state?.chooseSku?.image"></image>
<view class="price-box"> <view class="price-box">
<view class="price">{{ state.chooseSku?.price }}</view> <view class="price">{{ state.chooseSku?.price }}</view>
<view class="stock">库存{{ state.chooseSku?.stock }}</view> <view class="stock">库存{{ state.chooseSku?.stock }}</view>
<view class="specs">规格{{ state.chooseSku?.sku_name_arr?.join(',') }}</view> <view class="specs">规格{{ state.chooseSku?.sku_name_arr?.join(',') }}</view>
</view> </view>
</view> </view>
<view class="specs-box"> <view class="specs-box">
<view class="item-box" v-for="item, index in state?.spec_list" :key="index"> <view class="item-box" v-for="item, index in state?.spec_list" :key="index">
<view class="title">{{ item.name }}</view> <view class="title">{{ item.name }}</view>
<view class="tag-box"> <view class="tag-box">
<view v-for="itemChild, indexChild in item?.list" :key="indexChild" <button v-for="itemChild, indexChild in item?.list" :key="indexChild"
:class="['tag', { 'active': itemChild.select }, { 'gray': !itemChild.show && state.skuLength == 1 }]" :class="['tag',{'active':getSkuSelected(itemChild.name)},{'gray':getSkuDisabled(itemChild.name)}]"
@click="handleChooseSku(index, itemChild)"> @click="handleChooseSku(itemChild.name)" :disabled="getSkuDisabled(itemChild.name)">
{{ itemChild.name }} {{ itemChild.name }}
<image v-if="itemChild.select" class="icon-select" <image v-if="getSkuSelected(itemChild.name)" class="icon-select"
:src="handleStaticResources('/static/images/tag-select.png')"> </image> :src="handleStaticResources('/static/images/tag-select.png')"> </image>
</view> </button>
</view> </view>
</view> </view>
</view> </view>
<view class="number-box"> <view class="number-box">
<view class="tit">数量</view> <view class="tit">数量</view>
<uv-number-box v-model="state.numberVal" :disabledInput="true" <uv-number-box v-model="state.numberVal" :disabledInput="true"
@change="handleChangeNumberVal"></uv-number-box> @change="handleChangeNumberVal"></uv-number-box>
</view> </view>
<view class="btn-box"> <view class="btn-box">
<!-- <view v-if="props.isShowAddCartBtn" class="btn add-cart" @click="handleAddCart">加入购物车</view> --> <!-- <view v-if="props.isShowAddCartBtn" class="btn add-cart" @click="handleAddCart">加入购物车</view> -->
<view v-if="props.isShowAddCartBtn" class="core-button-confirm add-cart"> <view v-if="props.isShowAddCartBtn" class="core-button-confirm add-cart">
<coreshop-button :loading="props.addCartloading" class="core-button-confirm_" :radius="0" title="加入购物车" <coreshop-button :loading="props.addCartloading" class="core-button-confirm_" :radius="0"
:customStyle="addCartCustomStyle" @onClick="handleAddCart()"></coreshop-button> title="加入购物车" :customStyle="addCartCustomStyle" @onClick="handleAddCart()"></coreshop-button>
</view> </view>
<!-- <view class="btn buy-now" @click="handleBuyNow">{{ props.btnBuyTitlt }}</view> --> <!-- <view class="btn buy-now" @click="handleBuyNow">{{ props.btnBuyTitlt }}</view> -->
<view class="core-button-confirm"> <view class="core-button-confirm">
<coreshop-button :loading="props.buyNowNowloading" class="core-button-confirm_" :radius="0" <coreshop-button :loading="props.buyNowNowloading" class="core-button-confirm_" :radius="0"
:title="props.btnBuyTitlt" :customStyle="buyNowCustomStyle" @onClick="handleBuyNow()"></coreshop-button> :title="props.btnBuyTitlt" :customStyle="buyNowCustomStyle"
</view> @onClick="handleBuyNow()"></coreshop-button>
</view> </view>
</view> </view>
</uv-popup> </view>
</uv-popup>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { watch, reactive, ref } from 'vue'; import { watch, reactive, ref, computed } from 'vue';
import type { GoodsSkuListType, GoodsSpecListType } from '@/core/models'; import type { GoodsSkuListType, GoodsSpecListType } from '@/core/models';
import { handleStaticResources, handleShowToast } from '@/core/utils'; import { handleStaticResources, handleShowToast } from '@/core/utils';
import { deepClone } from '@/uni_modules/uv-ui-tools/libs/function/index.js'; import { deepClone } from '@/uni_modules/uv-ui-tools/libs/function/index.js';
const buyNowCustomStyle = { type SpecWeight = Record<string, {
'border': 'none', weight : number;
'background-color': '#d33123', sort : number;
'flex': 1, }>
'padding': '32rpx 0',
'font-size': '27rpx',
'text-align': 'center',
'color': '#fff',
'height': '90rpx'
}
const addCartCustomStyle = { type SelectedSpec = {
...buyNowCustomStyle, name : string;
'background-color': '#000', weight : number;
} sort : number;
}
const buyNowCustomStyle = {
'border': 'none',
'background-color': '#d33123',
'flex': 1,
'padding': '32rpx 0',
'font-size': '27rpx',
'text-align': 'center',
'color': '#fff',
'height': '90rpx'
}
const addCartCustomStyle = {
...buyNowCustomStyle,
'background-color': '#000',
}
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
showSku : boolean, showSku : boolean,
goodsDetailData : any, goodsDetailData : any,
isShowAddCartBtn : boolean, isShowAddCartBtn : boolean,
btnBuyTitlt : string, btnBuyTitlt : string,
safeAreaInsetBottom : boolean; safeAreaInsetBottom : boolean;
buyNowCustomStyle : { [key : string] : any }, buyNowCustomStyle : any,
buyNowNowloading : boolean, buyNowNowloading : boolean,
addCartloading : boolean addCartloading : boolean
}>(), { }>(), {
showSku: false, showSku: false,
goodsDetailData: {}, goodsDetailData: {},
isShowAddCartBtn: true, isShowAddCartBtn: true,
btnBuyTitlt: '立即购买', btnBuyTitlt: '立即购买',
safeAreaInsetBottom: true, safeAreaInsetBottom: true,
buyNowCustomStyle: { buyNowCustomStyle: {
'border': 'none', 'border': 'none',
'background-color': '#d33123', 'background-color': '#d33123',
'flex': 1, 'flex': 1,
'padding': '25rpx 0', 'padding': '25rpx 0',
'font-size': '27rpx', 'font-size': '27rpx',
'text-align': 'center', 'text-align': 'center',
'color': '#fff', 'color': '#fff',
'height': '90rpx' 'height': '90rpx'
}, },
buyNowNowloading: false, buyNowNowloading: false,
addCartloading: false addCartloading: false
}); });
const emits = defineEmits(['handleChangePopup', 'handleAddCart', 'handleBuyNow', 'handleChangeGoodSku']); const emits = defineEmits(['handleChangePopup', 'handleAddCart', 'handleBuyNow', 'handleChangeGoodSku']);
const skuPopup = ref(); const skuPopup = ref();
const state = reactive<{ const state = reactive<{
numberVal : number; numberVal : number;
spec_list : Array<GoodsSpecListType>; spec_list : Array<GoodsSpecListType>;
chooseSku : GoodsSkuListType; chooseSku : GoodsSkuListType;
skuLength : number; specCombinationList : Array<GoodsSkuListType>;
}>({ specMap : SpecWeight;
numberVal: 1, selectedSpecs : Array<SelectedSpec>
spec_list: [], }>({
chooseSku: {}, numberVal: 1,
skuLength: 1, spec_list: [],
}); chooseSku: {},
specCombinationList: [],
specMap: {},
selectedSpecs: [],
});
watch(() => props.goodsDetailData, (newVal : any) => { watch(() => props.goodsDetailData, (newVal : any) => {
if (newVal) { if (newVal) {
/** 获取sku有几层 */ // 构造规格权重
state.skuLength = newVal?.skuList?.spec_list.length; let i = 0, j = 0;
/** 默认选择第一个sku */ state.spec_list = deepClone(newVal?.skuList?.spec_list.map((item : GoodsSpecListType) => {
state.chooseSku = newVal?.skuList?.sku_list.find((item : any) => item.stock != 0); item.list.forEach((itemChild : GoodsSpecListType) => {
state.specMap[itemChild.name] = {
sort: i,
weight: 1 << j
}
j++;
})
i++;
return item;
}));
state.spec_list = deepClone(newVal?.skuList?.spec_list.map((item : GoodsSpecListType) => { state.specCombinationList = (props.goodsDetailData?.skuList?.sku_list.filter((item : GoodsSkuListType) => item.stock > 0)).map((item : GoodsSkuListType) => {
item.list.forEach((itemChild : GoodsSpecListType, indexChild : number) => { return {
let findSku = newVal?.skuList?.sku_list.find((item : any) => item.sku_name_arr.includes(itemChild.name)) ...item,
if (findSku && state.skuLength == 1) { weight: item.sku_name_arr.reduce((prev, current) => prev + state.specMap[current]?.weight, 0),
itemChild['show'] = true; }
} else { })
itemChild['show'] = false;
}
let findChooseSku = state.chooseSku.sku_name_arr.includes(itemChild.name); /** 获取数据返回的默认sku,如果有就用没有就找第一个库存不为0的sku */
if (findChooseSku) { let systemDefaultSku:Array<string> = (props.goodsDetailData?.product?.spesDesc?.split(',')).map((item:string)=> item.split(':')[1]);
itemChild['select'] = true; let skuDefaultSku:GoodsSkuListType = state.specCombinationList.find((item:GoodsSkuListType)=> item.sku_name_arr.every((name:string)=> systemDefaultSku.includes(name)));
} else { if(!systemDefaultSku || !skuDefaultSku){
itemChild['select'] = false; state.specCombinationList[0].sku_name_arr.forEach((item:string)=>{
} handleChooseSku(item);
}) });
return item; return ;
})) }
} skuDefaultSku.sku_name_arr.forEach((item:string)=>{
}, { handleChooseSku(item);
deep: true });
}) }
}, {
deep: true
})
watch(() => props.showSku, (newVal : boolean) => { watch(() => props.showSku, (newVal : boolean) => {
if (newVal) { if (newVal) {
skuPopup.value.open(); skuPopup.value.open();
} else { } else {
skuPopup.value.close(); skuPopup.value.close();
state.numberVal = 1; state.numberVal = 1;
} }
}) })
/** 商品数量加减变化 */ const getSkuDisabled = computed(() => {
const handleChangeNumberVal = (e : any) => { return (item : string) => {
state.numberVal = e.value; // 每次选中之后要判断其他规格是否能够被选中
} const weights = state.selectedSpecs.reduce((prev, current) => prev + current.weight, 0);
// 所有是初始状态则所有属性均可选因此disabled为false.
if (weights === 0) return false;
const current = state.specMap[item];
const weight = current.weight;
// 假如选择该属性,这是选中后的“权重之和”
let sum = weight + weights;
// 找到是否存在同一维度的规格已被选中的规格
const existEle = state.selectedSpecs.find(o => o.sort === current.sort);
if (existEle) {
// 如果存在,则减去即前选中的规格权重,即替换为当前权重。
sum -= existEle.weight;
}
for (let i = 0; i < state.specCombinationList.length; i++) {
const sWeight = state.specCombinationList[i].weight!;
// 与“权重之和”按位与运算后与其进行比较,如果相等,说明该属性是可选的
if ((sWeight & sum) === sum) {
return false;
}
}
return state.selectedSpecs.findIndex(o => o.name === item) == -1;
};
})
/** sku弹框显示或者隐藏 */ const getSkuSelected = computed(() => {
const handleChangePopup = (e : any) => { return (item : string) => {
emits('handleChangePopup', e.show) return state.selectedSpecs.findIndex(o => o.name === item) > -1;
} };
})
/** sku选择 */ /** 商品数量加减变化 */
const handleChooseSku = (index : number, sku : GoodsSpecListType) => { const handleChangeNumberVal = (e : any) => {
let chooseSku : GoodsSkuListType = {}; // 选中的sku state.numberVal = e.value;
}
if (state.skuLength === 1) { /** sku只有一层的时候 */ /** sku弹框显示或者隐藏 */
chooseSku = props.goodsDetailData?.skuList?.sku_list.find((item : GoodsSkuListType) => item.sku_name_arr.includes(sku.name)); const handleChangePopup = (e : any) => {
} else { /** 当sku多层的时候 */ emits('handleChangePopup', e.show)
let nameArr : Array<string> = [sku.name]; }
let noClickSpecList = state.spec_list.filter((item : any, idx : number) => idx !== index);
noClickSpecList.forEach((item : GoodsSpecListType) => {
item.list.forEach((itemChild : GoodsSpecListType) => {
if (itemChild.select) {
nameArr.push(itemChild.name)
}
})
})
chooseSku = props.goodsDetailData.skuList?.sku_list.find((item : GoodsSkuListType) => item.sku_name_arr.every((itemChild : string) => nameArr.includes(itemChild)));
}
if (!chooseSku || chooseSku.stock <= 0) { /** sku选择 */
handleShowToast('没有库存了,请选择其它规格'); const handleChooseSku = (name : string) => {
return false; const spec = state.specMap[name];
} const eleIndex = state.selectedSpecs.findIndex(item => item.name === name);
const sortIndex = state.selectedSpecs.findIndex(item => item.sort === spec.sort);
// 如果当前元素已经存在,则从列表中删除
if (eleIndex > -1) {
state.selectedSpecs.splice(eleIndex, 1);
return;
}
// 如果列表中的元素存在和当前元素的维度相同,则从列表中把那个元素删除,保证同一纬度只有一个元素。
if (sortIndex > -1) {
state.selectedSpecs.splice(sortIndex, 1);
}
state.selectedSpecs.push({ name: name, weight: spec.weight, sort: spec.sort });
state.spec_list[index].list.forEach((item : any) => item.select = false); /** 判断sku是否选择完毕 */
sku.select = !sku.select; if(state.selectedSpecs.length === state.spec_list.length){
state.chooseSku = chooseSku; let skuName:Array<string> = state.selectedSpecs.map(item => item.name);
emits('handleChangeGoodSku', state.chooseSku); state.chooseSku = state.specCombinationList.find((item:GoodsSkuListType)=> item.sku_name_arr.every((name:string)=> skuName.includes(name)));
}; emits('handleChangeGoodSku', state.chooseSku);
}
};
/** 加入购物车 */ /** 加入购物车 */
const handleAddCart = () => { const handleAddCart = () => {
if (!handleHaveStock()) { if (!handleHaveStock()) {
return; return;
} }
emits('handleAddCart', { emits('handleAddCart', {
productId: state.chooseSku._id, productId: state.chooseSku._id,
nums: state.numberVal, nums: state.numberVal,
}); });
} }
/** 立即购买 */ /** 立即购买 */
const handleBuyNow = () => { const handleBuyNow = () => {
if (!handleHaveStock()) { if (!handleHaveStock()) {
return; return;
} }
emits('handleBuyNow', { emits('handleBuyNow', {
productId: state.chooseSku._id, productId: state.chooseSku._id,
nums: state.numberVal, nums: state.numberVal,
}); });
} }
/** 判断库存 */ /** 判断库存 */
const handleHaveStock = () => { const handleHaveStock = () => {
if (state.chooseSku.stock === 0) { if (state.selectedSpecs.length !== state.spec_list.length) {
handleShowToast('没有库存了,请选择其它规格'); handleShowToast('请选择sku');
return false; return false;
} }
return true; return true;
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import './goods-detail-sku.scss'; @import './goods-detail-sku.scss';
</style> </style>