Files
cashier_wx/pages/product/components/goods-modal.vue
2025-12-25 09:43:02 +08:00

698 lines
18 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<up-popup :show="visible" :round="20" mode="bottom" @close="close" :close-on-click-overlay="true">
<view class="shop_sku">
<scroll-view scroll-y style="max-height: 60vh; width: 100%">
<view class="positionabsolute">
<up-icon name="arrow-down" @click="close" color="#333" size="22"></up-icon>
</view>
<up-swiper :list="goods.images" @change="swiperChange" :current="swiperCurrent" radius="6px"
height="250" @click="prveImgs(goods.images, goods.images[swiperCurrent])">
</up-swiper>
<view class="shop_sku_name">{{ goods.name }}</view>
<view class="shop_sku_description" v-if="isSkuGoods">
{{ goods.shortTitle ? goods.shortTitle : '' }}
</view>
<view v-if="goods.type != 'package'">
<view class="shop_sku_box" v-for="(specOptions, specType) in goods.selectSpecInfo" :key="specType">
<view class="shop_sku_box_name">
{{ specType }}
</view>
<view class="flex-start">
<view class="shop_sku_box_item" v-for="option in specOptions" :key="option"
@click="selectSpec(specType, option)" :class="{
shop_sku_box_item_selected: isSkuSelected(specType, option)
}">
{{ option }}
<view class="shop_sku_box_item_tip"
v-if="goods.result && goods.result.isSoldStock == 1 && selectedSpecs[specType] === option">
<view>售罄</view>
</view>
<view class="shop_sku_box_item_tip"
v-if="goods.result == 'kong' && canSubmit == false && selectedSpecs[specType] === option">
<view>已下架</view>
</view>
</view>
</view>
</view>
</view>
<!-- 套餐 -->
<view v-else>
<view class="shop_sku_box">
<view v-for="(setmenu, setmenuindex) in goods.groupSnap" :key="setmenuindex">
<view class="shop_sku_box_name">{{ setmenu.title }} {{ setmenu.count }}
{{ setmenu.number }}</view>
<view class="flex-start">
<view class="shop_sku_box_item" v-for="(option, goodsid) in setmenu.goods"
:key="goodsid" @click="goodsidClick(setmenuindex, option, goodsid)" :class="{
shop_sku_box_item_selected: isOptionSelected(setmenuindex, option)
}" :disabled="isMaxSelected(setmenuindex) && !isOptionSelected(setmenuindex, option)">
{{ option.proName }}
<text v-if="option.unitName">/{{ option.unitName }}</text>
<view class="shop_sku_box_item_tip" v-if="goods.isSoldStock == 1">
<view>售罄</view>
</view>
</view>
</view>
</view>
</view>
</view>
<!-- 推荐搭配 -->
<view class="u-m-t-48 u-p-30">
<view class="font-bold u-font-32 ">推荐搭配</view>
<view class="u-flex recommand-goods">
<view class=" item" v-for="(product,index) in goods.relatedRecommendJson" :key="index">
<up-image width="210rpx" height="210rpx" radius="12rpx" :src="product.coverImg"></up-image>
<view class="info">
<view class="u-line-2 name u-m-t-10 u-font-32 font-500">
{{product.name}}
</view>
<view class="u-flex u-row-between">
<view class="font-bold">
<text class="u-font-24"></text>
<text class="u-font-32">
<GoodsPrice :limitDiscount="cartStore.limitTimeDiscount"
:cart="returnFirstSku(product)" :shopUserInfo="shopUserInfo"
:shopInfo="shopInfo"></GoodsPrice>
</text>
</view>
<view class="btn">
<up-icon name="plus-circle-fill" color="#E9AB7A" size="25"></up-icon>
</view>
</view>
</view>
</view>
</view>
</view>
</scroll-view>
<view class="shop_bottom">
<view class="flex-between">
<view class="price price-sku" v-if="goods.type != 'package' && goods.result">
<text class="i"></text>
<view class="num">
<GoodsPrice :limitDiscount="cartStore.limitTimeDiscount" :cart="selSku"
:shopUserInfo="shopUserInfo" :shopInfo="shopInfo"></GoodsPrice>
</view>
<text class="num" v-if="false">
{{
shopInfo.isVip == 1 && shopInfo.isMemberPrice == 1
? goods.result.memberPrice || goods.result.salePrice
: goods.result.salePrice
}}
</text>
<text class="i" v-if="goods.unitName">/{{ goods.unitName }}</text>
<text
v-if="goods.result.suitNum > 1">{{ goods.result.suitNum }}{{ goods.result.unitName }}起点</text>
</view>
<view class="price price-package" v-else>
<text class="i"></text>
<view class="num">
<GoodsPrice :limitDiscount="cartStore.limitTimeDiscount" :cart="goods"
:shopUserInfo="shopUserInfo" :shopInfo="shopInfo"></GoodsPrice>
</view>
<text class="num" v-if="false">
{{
shopInfo.isVip == 1 && shopInfo.isMemberPrice == 1
? goods.memberPrice || goods.salePrice
: goods.salePrice
}}
</text>
<text class="i" v-if="goods.unitName">/{{ goods.unitName }}</text>
<text v-if="goods.suitNum > 1">{{ goods.suitNum }}{{ goods.unitName }}起点</text>
</view>
<view class="operation-wrap" v-if="goods.type != 'package'&&stockNumberIsFull(goods)">
<view class="btn">
<up-icon name="minus-circle-fill" color="#E9AB7A" size="25"
v-if="shopCartNumber > 0"></up-icon>
<view class="btnClick" @click="shopCart('-')"></view>
</view>
<text class="num">{{ shopCartNumber }}</text>
<view class="btn">
<up-icon name="plus-circle-fill" color="#E9AB7A" size="25"></up-icon>
<view class="btnClick" @click="shopCart('+')"></view>
</view>
</view>
</view>
<view class="shop_skuselect flex-start" v-if="selectedSpecsStr">
<view class="shop_skuselectname">{{ selectedSpecsStr }}</view>
</view>
<template v-if="!stockNumberIsFull(goods)">
<view class="addShopping">
库存不足
</view>
</template>
<template v-else>
<template v-if="isSkuGoods">
<view v-if="goods.type == 'package'" class="addShopping"
:class="shopCartNumber > 0 && allConditionsSatisfied ? 'active' : ''"
@click="submitSelection(goods)">
添加到购物车
</view>
<view v-else class="addShopping" :class="shopCartNumber > 0 && canSubmit ? 'active' : ''"
@click="submitSelection(goods)">
添加到购物车
</view>
</template>
<template v-else>
<view class="addShopping" :class="shopCartNumber > 0 ? 'active' : ''"
@click="submitSelection(goods)">
添加到购物车
</view>
</template>
</template>
</view>
</view>
</up-popup>
</template>
<script setup>
import {
APIminiAppinfo,
APIminiAppskuinfo,
productRelated
} from '@/common/api/product/product.js';
//价格计算辅助函数
import {
limitUtils
} from 'ysk-utils';
import GoodsPrice from '@/components/goods-price.vue';
import {
computed,
inject,
ref,
watch
} from "vue";
const cartStore = inject('cartStore')
const shopUserInfo = inject('shopUserInfo')
const shopInfo = inject('shopInfo')
const props = defineProps({
data: {
type: Object,
default: () => {},
},
goods: {
type: Object,
default: () => {
return {
relatedRecommendJson: []
}
},
},
});
// 存储用户当前选择的规格,初始为空对象
const selectedSpecs = ref({});
const shopCartNumber = ref(0)
function isSkuSelected(specType, option) {
return selectedSpecs.value[specType] === option;
}
const selSku = ref(null)
// 处理规格选择的方法
const selectSpec = async (specType, option) => {
// 规格清零
shopCartNumber.value = 0;
// 更新 selectedSpecs 对象,将当前规格类型的选中值设置为用户点击的选项
const newSelectedSpecs = {};
const specKeys = Object.keys(props.goods.selectSpecInfo);
for (const key of specKeys) {
if (key === specType) {
newSelectedSpecs[key] = option;
} else {
newSelectedSpecs[key] = selectedSpecs.value[key];
}
}
selectedSpecs.value = newSelectedSpecs;
if (allSpecsSelected.value) {
// try {
let result = await APIminiAppskuinfo({
specInfo: selectedSpecsStr.value,
id: props.goods.id
});
// skuList
if (result != true) {
selSku.value = result
} else {}
} else {}
};
const canSubmit = computed(() => {
if (shopCartNumber.value <= 0) {
return false
}
return true
})
// 计算属性,将 selectedSpecs 转换为字符串形式
const selectedSpecsStr = computed(() => {
const values = Object.values(selectedSpecs.value);
return values.join('');
});
// 计算属性,判断是否所有规格类型都有选中项
const allSpecsSelected = computed(() => {
// 获取所有规格类型的键
const specKeys = Object.keys(props.goods.selectSpecInfo);
// skuBtnText.value = selectedSpecsStr.value ? `您还没选择${specKeys[1]}哦` : ``
// 检查每个规格类型是否都在 selectedSpecs 中有对应的选中值
return specKeys.every((key) => selectedSpecs.value[key]);
});
const swiperCurrent = ref(0);
function returnFirstSku(product) {
return product.skuList[0]
}
//返回是否是多规格商品
function returnIsSkuGoods(item) {
if (item.type == 'single' || item.type == 'weight' || (item.type == 'package' && item.groupType == '0')) {
return false;
}
return true;
}
const isSkuGoods = computed(() => {
return returnIsSkuGoods(props.goods);
});
function swiperChange(e) {
swiperCurrent.value = e.current;
}
const emits = defineEmits(['prveImgs', 'websocketsendMessage'])
function prveImgs(images, currentUrl) {
emits('prveImgs', images, currentUrl)
}
const visible = defineModel({
default: false,
});
function stockNumberIsFull(item) {
if (item.isStock && item.stockNumber < item.suitNum) {
return false
}
return true
}
function close() {
visible.value = false;
}
function isSelected(option) {
return selectedSpecs.value[specType] === option;
}
// 判断商品是否在可售时间内
const isProductAvailable = async (sellDaysStr, startTimeStr, endTimeStr) => {
// 将后端返回的字符串转换为数组
const sellDays = sellDaysStr.split(',');
const now = dayjs();
const days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
const currentDay = days[now.day()];
// console.log('当前日期:', currentDay);
// console.log('可售日期列表:', sellDays);
// 检查当前周几是否在可售周几列表中
if (!sellDays.includes(currentDay)) {
// console.log('当前日期不在可售日期列表中');
return false;
}
const startTime = dayjs(`${now.format('YYYY-MM-DD')} ${startTimeStr}`);
let endTime = dayjs(`${now.format('YYYY-MM-DD')} ${endTimeStr}`);
// 处理跨天情况
if (endTime.isBefore(startTime)) {
endTime = endTime.add(1, 'day');
}
// console.log('当前时间:', now.format('YYYY-MM-DD HH:mm:ss'));
// console.log('开始时间:', startTime.format('YYYY-MM-DD HH:mm:ss'));
// console.log('结束时间:', endTime.format('YYYY-MM-DD HH:mm:ss'));
const isInRange = now.isBetween(startTime, endTime, null, '[)');
// console.log('当前时间是否在可售时间范围内:', isInRange);
return isInRange;
};
// 提交选择并执行下一步操作的方法
const submitSelection = async (goods) => {
if (!isProductAvailable(goods.days, goods.startTime,
goods.endTime)) {
uni.showToast({
title: '不在可售时间内'
});
return false;
}
// 判断购物车是否有该选中商品
// 数量不能少于0
if (shopCartNumber.value <= 0) {
return;
}
if ((goods.type == 'package' && allConditionsSatisfied.value) || (goods.type ==
'sku' && canSubmit.value)) {
let res = cartStore.carts.find(cart => cart.product_id == goods.id && cart.sku_id == selSku.value.id)
// 是否是套餐 有就传
if (goods.type == 'package') {
// 需求更改:所所有商品套餐都是add,没有修改
res = null;
} else {
selectedGroupSnap.value = [];
}
console.log('goods', goods);
emits('websocketsendMessage', {
id: res ? res.cartListId : '',
type: 'shopping',
suitNum: selSku.value.suitNum,
table_code: uni.cache.get('tableCode'),
shop_id: uni.cache.get('shopId'),
operate_type: res ? 'edit' : 'add',
product_id: goods.id,
sku_id: selSku.value.id,
number: res ? await calculateValue(res.cartNumber, '+', shopCartNumber.value) :
shopCartNumber.value,
pro_group_info: selectedGroupSnap.value,
goods_type: goods.type == 'package' ? 'package' : '',
memberPrice: goods.memberPrice,
is_print: 1,
product_type: goods.type,
is_time_discount: showLimitDiscount(goods)
})
// 清空套餐选中
selectedGroupSnap.value = [];
showShopsku.value = false;
} else {
let res = cartStore.carts.find(cart => cart.product_id == goods.id && goods.skuList[0].id)
emits('websocketsendMessage', {
id: res ? res.cartListId : '',
type: 'shopping',
suitNum: goods.suitNum,
table_code: uni.cache.get('tableCode'),
shop_id: uni.cache.get('shopId'),
operate_type: res ? 'edit' : 'add',
product_id: goods.id,
sku_id: goods.skuList[0].id,
number: res ? await calculateValue(res.cartNumber, '+', shopCartNumber.value) :
shopCartNumber.value,
memberPrice: goods.memberPrice,
is_print: 1,
product_type: goods.type,
is_time_discount: showLimitDiscount(goods)
})
return;
}
};
function showLimitDiscount(item) {
if (!cartStore.limitTimeDiscount || !cartStore.limitTimeDiscount.id) {
return 0;
}
return limitUtils.canUseLimitTimeDiscount(item, cartStore.limitTimeDiscount, shopInfo, shopUserInfo.value, 'id') ?
1 : 0;
}
// 多规格 套餐 单规格添加数量
const shopCart = async (i) => {
if (i == '-' && shopCartNumber.value <= (props.goods.suitNum || 1)) {
shopCartNumber.value = 0;
return false;
}
let res = null
if (props.goods.result === 'kong') {
//没有满足可添加或者编辑的规格
return;
}
if (i == '-') {
if (!res && shopCartNumber.value == props.goods.suitNum) {
uni.showToast({
title: `起点${props.goods.suitNum}`,
icon: 'none'
});
return false;
} else {
shopCartNumber.value--;
}
} else {
if (!res && shopCartNumber.value < 1) {
if (props.goods.type == 'sku') {
shopCartNumber.value = parseFloat(selSku.value.suitNum);
} else {
shopCartNumber.value = parseFloat(props.goods.suitNum);
}
} else {
shopCartNumber.value++;
}
}
};
</script>
<style lang="scss" scoped>
.shop_sku {
width: 100%;
// height: 100vh;
position: relative;
border-radius: 20rpx;
background: #fff;
box-sizing: border-box;
.positionabsolute {
position: absolute;
top: 30rpx;
z-index: 999;
left: 30rpx;
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
background-color: #eee;
border-radius: 50%;
}
.shop_sku_name {
padding: 0 28rpx;
margin-top: 20rpx;
font-weight: bold;
font-size: 32upx;
margin-bottom: 16rpx;
}
.shop_sku_returned {
padding: 0 28rpx;
font-weight: 400;
font-size: 24rpx;
color: #ff534b;
}
.shop_sku_description {
padding: 0 28rpx;
font-weight: 400;
font-size: 24rpx;
color: #999999;
margin-top: 16rpx;
padding-bottom: 32rpx;
box-sizing: border-box;
border-bottom: 2rpx solid #f0f0f0;
}
.shop_sku_box {
padding: 20rpx;
.shop_sku_box_name {
margin-top: 20rpx;
font-weight: 500;
font-size: 28upx;
color: #333;
}
.flex-start {
.shop_sku_box_item {
margin-top: 16rpx;
padding: 12rpx 28rpx;
border-radius: 8rpx;
font-size: 24upx;
margin-right: 56rpx;
background: #efefef;
border: 2rpx solid #efefef;
position: relative;
color: #666666;
.shop_sku_box_item_tip {
width: 62rpx;
height: 47rpx;
text-align: right;
position: absolute;
top: 0;
right: 0;
background: linear-gradient(45deg, transparent, transparent 50%, #cecece 50%, #cecece 100%);
view {
font-size: 18rpx;
color: #666666;
transform: rotate(45deg);
position: absolute;
top: 5rpx;
right: 3rpx;
}
}
}
.shop_sku_box_item:nth-child(1) {
margin-left: 0;
}
.disabled {
color: #999;
background-color: #f5f5f5;
border: 2rpx solid #f5f5f5;
}
.shop_sku_box_item_noselected {
color: #666;
}
.shop_sku_box_item_selected {
background: #fefaf7;
border-radius: 8rpx;
border: 2rpx solid #e8ad7b;
color: #e8ad7b;
}
}
}
.shop_skuselect {
margin-top: 16rpx;
width: 100%;
// background: #f7f7f7;
color: #999;
font-size: 24upx;
.shop_skuselectname {
color: #000;
font-size: 26upx;
}
}
.shop_bottom {
width: 100%;
box-sizing: border-box;
padding: 30rpx 28rpx;
background-color: #fff;
box-shadow: 0rpx -6rpx 14rpx 2rpx rgba(0, 0, 0, 0.1);
.price {
display: flex;
align-items: flex-end;
.i {
font-size: 28rpx;
color: #e8ad7b;
position: relative;
bottom: 4upx;
font-weight: bold;
}
.num {
font-size: 42rpx;
color: #e8ad7b;
position: relative;
top: 6upx;
padding: 0 4upx;
}
}
.operation-wrap {
display: flex;
align-items: center;
.num {
font-size: 32upx;
padding: 0 16upx;
}
.btn {
display: flex;
align-items: center;
justify-content: center;
position: relative;
.btnClick {
width: 100rpx;
height: 100rpx;
position: absolute;
// bottom: 0;
}
}
}
.addShopping {
width: 100%;
height: 96rpx;
line-height: 96rpx;
text-align: center;
background: #ccc;
border-radius: 48rpx;
font-weight: 400;
font-size: 32rpx;
color: #ffffff;
margin-top: 36rpx;
}
.addShopping.active {
background: #e8ad7b;
}
}
}
.recommand-goods {
display: flex;
flex-wrap: wrap;
margin-top: 24rpx;
.item {
display: flex;
flex-direction: column;
align-items: center;
margin-right: 30rpx;
margin-bottom: 30rpx;
width: 210rpx;
&:nth-child(3n) {
margin-right: 0;
}
.name {
height: 84rpx;
}
.info {
width: 100%;
}
}
}
</style>