365 lines
10 KiB
JavaScript
365 lines
10 KiB
JavaScript
|
|
/* eslint-disable no-param-reassign */
|
|||
|
|
/* eslint-disable no-nested-ternary */
|
|||
|
|
import Toast from 'tdesign-miniprogram/toast/index';
|
|||
|
|
|
|||
|
|
Component({
|
|||
|
|
options: {
|
|||
|
|
multipleSlots: true,
|
|||
|
|
addGlobalClass: true,
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
properties: {
|
|||
|
|
src: {
|
|||
|
|
type: String,
|
|||
|
|
},
|
|||
|
|
title: String,
|
|||
|
|
show: {
|
|||
|
|
type: Boolean,
|
|||
|
|
value: false,
|
|||
|
|
},
|
|||
|
|
limitBuyInfo: {
|
|||
|
|
type: String,
|
|||
|
|
value: '',
|
|||
|
|
},
|
|||
|
|
isStock: {
|
|||
|
|
type: Boolean,
|
|||
|
|
value: true,
|
|||
|
|
},
|
|||
|
|
limitMaxCount: {
|
|||
|
|
type: Number,
|
|||
|
|
value: 999,
|
|||
|
|
},
|
|||
|
|
limitMinCount: {
|
|||
|
|
type: Number,
|
|||
|
|
value: 1,
|
|||
|
|
},
|
|||
|
|
skuList: {
|
|||
|
|
type: Array,
|
|||
|
|
value: [],
|
|||
|
|
observer(skuList) {
|
|||
|
|
if (skuList && skuList.length > 0) {
|
|||
|
|
if (this.initStatus) {
|
|||
|
|
this.initData();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
specList: {
|
|||
|
|
type: Array,
|
|||
|
|
value: [],
|
|||
|
|
observer(specList) {
|
|||
|
|
if (specList && specList.length > 0) {
|
|||
|
|
this.initData();
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
outOperateStatus: {
|
|||
|
|
type: Boolean,
|
|||
|
|
value: false,
|
|||
|
|
},
|
|||
|
|
hasAuth: {
|
|||
|
|
type: Boolean,
|
|||
|
|
value: false,
|
|||
|
|
},
|
|||
|
|
count: {
|
|||
|
|
type: Number,
|
|||
|
|
value: 1,
|
|||
|
|
observer(count) {
|
|||
|
|
this.setData({
|
|||
|
|
buyNum: count,
|
|||
|
|
});
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
initStatus: false,
|
|||
|
|
selectedSku: {},
|
|||
|
|
selectSpecObj: {},
|
|||
|
|
|
|||
|
|
data: {
|
|||
|
|
buyNum: 1,
|
|||
|
|
isAllSelectedSku: false,
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
methods: {
|
|||
|
|
initData() {
|
|||
|
|
const { skuList } = this.properties;
|
|||
|
|
const { specList } = this.properties;
|
|||
|
|
// 为每个规格值计算库存信息
|
|||
|
|
specList.forEach((item) => {
|
|||
|
|
if (item.specValueList.length > 0) {
|
|||
|
|
item.specValueList.forEach((subItem) => {
|
|||
|
|
const obj = this.checkSkuStockQuantity(subItem, skuList, item);
|
|||
|
|
subItem.hasStockObj = obj;
|
|||
|
|
// 初始化未选中状态
|
|||
|
|
subItem.isSelected = false;
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 默认选择策略:优先选择第一个有库存的SKU组合,其次选择第一个SKU
|
|||
|
|
const getQty = (sku) => {
|
|||
|
|
return Number((sku.stockInfo && sku.stockInfo.stockQuantity) ?? sku.stock ?? sku.quantity ?? 0) || 0;
|
|||
|
|
};
|
|||
|
|
const defaultSku = (skuList || []).find((s) => getQty(s) > 0) || (skuList || [])[0];
|
|||
|
|
|
|||
|
|
// 构建 selectedSku 映射,并在 specList 中标记选中项
|
|||
|
|
const selectedSkuMap = {};
|
|||
|
|
if (defaultSku && Array.isArray(defaultSku.specInfo)) {
|
|||
|
|
defaultSku.specInfo.forEach((spec) => {
|
|||
|
|
const specId = spec.specId;
|
|||
|
|
const specValueId = spec.specValueId;
|
|||
|
|
const specTitle = spec.specTitle;
|
|||
|
|
const specValue = spec.specValue;
|
|||
|
|
|
|||
|
|
// 在对应的规格组内标记选中项
|
|||
|
|
specList.forEach((group) => {
|
|||
|
|
const sameGroup = String(group.specId) === String(specId) || (!!specTitle && group.title === specTitle);
|
|||
|
|
if (sameGroup && Array.isArray(group.specValueList)) {
|
|||
|
|
group.specValueList.forEach((sv) => {
|
|||
|
|
const matchById = specValueId != null && String(sv.specValueId) === String(specValueId);
|
|||
|
|
const matchByText = specValue != null && sv.specValue != null && sv.specValue === specValue;
|
|||
|
|
sv.isSelected = matchById || matchByText;
|
|||
|
|
});
|
|||
|
|
// 记录所选项的ID(优先使用ID)
|
|||
|
|
const chosen = group.specValueList.find((sv) => sv.isSelected);
|
|||
|
|
if (chosen) {
|
|||
|
|
selectedSkuMap[group.specId] = chosen.specValueId;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
} else {
|
|||
|
|
// 当没有SKU或SKU缺少specInfo时,退化为每组选择第一个有库存的规格值
|
|||
|
|
specList.forEach((group) => {
|
|||
|
|
const firstAvailable = (group.specValueList || []).find((sv) => sv.hasStockObj?.hasStock) || (group.specValueList || [])[0];
|
|||
|
|
if (firstAvailable) {
|
|||
|
|
group.specValueList.forEach((sv) => { sv.isSelected = sv === firstAvailable; });
|
|||
|
|
selectedSkuMap[group.specId] = firstAvailable.specValueId;
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 更新组件状态
|
|||
|
|
const isAllSelectedSku = specList.every((spec) => !!selectedSkuMap[spec.specId]);
|
|||
|
|
this.selectedSku = selectedSkuMap;
|
|||
|
|
this.selectSpecObj = {};
|
|||
|
|
this.setData({
|
|||
|
|
specList,
|
|||
|
|
isAllSelectedSku,
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 通知父页面已完成默认选择,便于价格与图片联动
|
|||
|
|
this.triggerEvent('change', {
|
|||
|
|
selectedSku: this.selectedSku,
|
|||
|
|
isAllSelectedSku,
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
this.initStatus = true;
|
|||
|
|
},
|
|||
|
|
// 兼容两种数据结构:
|
|||
|
|
// 1) sku.specInfo 使用 { specId, specValueId }
|
|||
|
|
// 2) sku.specInfo 使用 { specTitle, specValue }
|
|||
|
|
// 同时 specList.subItem 具备 { specValueId, specValue }
|
|||
|
|
checkSkuStockQuantity(specValueItem, skuList, specGroupItem) {
|
|||
|
|
let hasStock = false;
|
|||
|
|
let stockQuantity = 0;
|
|||
|
|
skuList.forEach((sku) => {
|
|||
|
|
if (sku.specInfo) {
|
|||
|
|
sku.specInfo.forEach((spec) => {
|
|||
|
|
// 统一以字符串比较,避免类型不一致导致匹配失败
|
|||
|
|
const matchById =
|
|||
|
|
spec.specValueId != null && specValueItem.specValueId != null &&
|
|||
|
|
String(spec.specValueId) === String(specValueItem.specValueId);
|
|||
|
|
|
|||
|
|
const sameGroupByTitle =
|
|||
|
|
spec.specTitle && specGroupItem && specGroupItem.title && spec.specTitle === specGroupItem.title;
|
|||
|
|
|
|||
|
|
const matchByText =
|
|||
|
|
spec.specValue != null && specValueItem.specValue != null && spec.specValue === specValueItem.specValue &&
|
|||
|
|
(sameGroupByTitle || true); // 若无 title 也放行文本匹配
|
|||
|
|
|
|||
|
|
if (matchById || matchByText) {
|
|||
|
|
// 兼容多种库存字段来源,优先 stockInfo.stockQuantity
|
|||
|
|
const qty = Number(
|
|||
|
|
(sku.stockInfo && sku.stockInfo.stockQuantity) ?? sku.stock ?? sku.quantity ?? 0
|
|||
|
|
) || 0;
|
|||
|
|
stockQuantity += qty;
|
|||
|
|
if (qty > 0) {
|
|||
|
|
hasStock = true;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
return {
|
|||
|
|
hasStock,
|
|||
|
|
stockQuantity,
|
|||
|
|
};
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
toChooseItem(e) {
|
|||
|
|
const { specid, id, val } = e.currentTarget.dataset;
|
|||
|
|
// 放开点击,避免因错误的 hasStock 计算而阻断选择
|
|||
|
|
|
|||
|
|
const { specList } = this.data;
|
|||
|
|
const { skuList } = this.properties;
|
|||
|
|
|
|||
|
|
specList.forEach((spec) => {
|
|||
|
|
if (String(spec.specId) === String(specid)) {
|
|||
|
|
spec.specValueList.forEach((specValue) => {
|
|||
|
|
if (String(specValue.specValueId) === String(id)) {
|
|||
|
|
specValue.isSelected = !specValue.isSelected;
|
|||
|
|
} else {
|
|||
|
|
specValue.isSelected = false;
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
this.selectSpecObj[specid] = {
|
|||
|
|
specId: specid,
|
|||
|
|
specValueId: id,
|
|||
|
|
specValue: val,
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
this.selectedSku[specid] = id;
|
|||
|
|
|
|||
|
|
const isAllSelectedSku = this.isAllSelectedSku();
|
|||
|
|
|
|||
|
|
this.setData({
|
|||
|
|
specList,
|
|||
|
|
isAllSelectedSku,
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 与父页面绑定的事件名保持一致:父页面使用 bind:change="chooseSpecItem"
|
|||
|
|
this.triggerEvent('change', {
|
|||
|
|
selectedSku: this.selectedSku,
|
|||
|
|
isAllSelectedSku,
|
|||
|
|
});
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
isAllSelectedSku() {
|
|||
|
|
const { specList } = this.properties;
|
|||
|
|
let isAllSelectedSku = true;
|
|||
|
|
specList.forEach((spec) => {
|
|||
|
|
if (!this.selectedSku[spec.specId]) {
|
|||
|
|
isAllSelectedSku = false;
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
return isAllSelectedSku;
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
getSelectedSkuId() {
|
|||
|
|
const { skuList } = this.properties;
|
|||
|
|
const { selectedSku } = this;
|
|||
|
|
let selectedSkuId = '';
|
|||
|
|
|
|||
|
|
skuList.forEach((sku) => {
|
|||
|
|
let isMatch = true;
|
|||
|
|
if (sku.specInfo) {
|
|||
|
|
sku.specInfo.forEach((spec) => {
|
|||
|
|
// 统一字符串比较避免类型不一致导致匹配失败
|
|||
|
|
if (String(selectedSku[spec.specId]) !== String(spec.specValueId)) {
|
|||
|
|
isMatch = false;
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
if (isMatch) {
|
|||
|
|
selectedSkuId = sku.skuId;
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
return selectedSkuId;
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
handlePopupHide() {
|
|||
|
|
// 触发与父页面绑定一致的事件名,确保可以关闭弹窗
|
|||
|
|
this.triggerEvent('closeSpecsPopup');
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
specsConfirm() {
|
|||
|
|
const { isAllSelectedSku } = this.data;
|
|||
|
|
if (!isAllSelectedSku) {
|
|||
|
|
Toast({
|
|||
|
|
context: this,
|
|||
|
|
selector: '#t-toast',
|
|||
|
|
message: '请选择完整的商品规格',
|
|||
|
|
theme: 'warning',
|
|||
|
|
direction: 'column',
|
|||
|
|
});
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
this.triggerEvent('specsConfirm', {
|
|||
|
|
selectedSku: this.selectedSku,
|
|||
|
|
buyNum: this.data.buyNum,
|
|||
|
|
});
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
addCart() {
|
|||
|
|
const { isAllSelectedSku } = this.data;
|
|||
|
|
if (!isAllSelectedSku) {
|
|||
|
|
Toast({
|
|||
|
|
context: this,
|
|||
|
|
selector: '#t-toast',
|
|||
|
|
message: '请选择完整的商品规格',
|
|||
|
|
theme: 'warning',
|
|||
|
|
direction: 'column',
|
|||
|
|
});
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
this.triggerEvent('addCart', {
|
|||
|
|
selectedSku: this.selectedSku,
|
|||
|
|
buyNum: this.data.buyNum,
|
|||
|
|
});
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
buyNow() {
|
|||
|
|
const { isAllSelectedSku } = this.data;
|
|||
|
|
if (!isAllSelectedSku) {
|
|||
|
|
Toast({
|
|||
|
|
context: this,
|
|||
|
|
selector: '#t-toast',
|
|||
|
|
message: '请选择完整的商品规格',
|
|||
|
|
theme: 'warning',
|
|||
|
|
direction: 'column',
|
|||
|
|
});
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
this.triggerEvent('buyNow', {
|
|||
|
|
selectedSku: this.selectedSku,
|
|||
|
|
buyNum: this.data.buyNum,
|
|||
|
|
});
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
setBuyNum(e) {
|
|||
|
|
const { value } = e.detail;
|
|||
|
|
this.setData({
|
|||
|
|
buyNum: value,
|
|||
|
|
});
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
changeNum(e) {
|
|||
|
|
const { buyNum } = e.detail;
|
|||
|
|
this.setData({
|
|||
|
|
buyNum,
|
|||
|
|
});
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
handleBuyNumChange(e) {
|
|||
|
|
const { value } = e.detail;
|
|||
|
|
this.setData({
|
|||
|
|
buyNum: value,
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 触发父组件的 changeNum 事件,通知价格更新
|
|||
|
|
this.triggerEvent('changeNum', {
|
|||
|
|
buyNum: value,
|
|||
|
|
});
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
});
|