增加商品弹窗组件

This commit is contained in:
2025-12-24 17:20:42 +08:00
parent b3c1ab3b8b
commit 65554b043c
4 changed files with 751 additions and 315 deletions

View File

@@ -1,251 +1,623 @@
<template>
<up-popup :show="visible" :round="20" mode="bottom" @close="close">
<view class="shop_sku">
<scroll-view scroll-y style="max-height: 60vh; width: 100%">
<view class="positionabsolute">
<up-icon
name="close-circle"
@click="close"
color="#000"
size="25"
></up-icon>
</view>
<up-swiper :list="specifications.item.images" height="250"></up-swiper>
<view class="shop_sku_name">{{ specifications.item.name }}</view>
<view class="shop_sku_description">
{{
specifications.item.shortTitle ? specifications.item.shortTitle : ""
}}
</view>
<view v-if="specifications.item.type != 'package'">
<view
class="shop_sku_box"
v-for="(specOptions, specType) in specifications.item
.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: isSelected(option),
}"
>
{{ option }}
<view
class="shop_sku_box_item_tip"
v-if="
specifications.item.result &&
specifications.item.result.isSoldStock == 1 &&
selectedSpecs[specType] === option
"
>
<view>售罄</view>
</view>
<view
class="shop_sku_box_item_tip"
v-if="
specifications.item.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 specifications.item.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="specifications.item.isSoldStock == 1"
>
<view>售罄</view>
</view>
</view>
</view>
</view>
</view>
</view>
</scroll-view>
<view class="shop_bottom">
<view class="flex-between">
<view
class="price"
v-if="
specifications.item.type != 'package' &&
specifications.item.result
"
>
<text class="i">¥</text>
<view class="num">
<GoodsPrice
:limitDiscount="limitTimeDiscountRes"
:cart="specifications.item.result"
:shopUserInfo="shopUserInfo"
:shopInfo="shopInfo"
></GoodsPrice>
</view>
<up-popup :show="visible" :round="20" mode="bottom" @close="close" :close-on-click-overlay="true">
<view class="shop_sku">
<view class="positionabsolute">
<up-icon name="arrow-down" @click="close" color="#333" size="22"></up-icon>
</view>
<scroll-view scroll-y style="max-height: 60vh; width: 100%">
<text class="num" v-if="false">
{{
shopInfo.isVip == 1 && shopInfo.isMemberPrice == 1
? specifications.item.result.memberPrice ||
specifications.item.result.salePrice
: specifications.item.result.salePrice
}}
</text>
<text class="i" v-if="specifications.item.unitName"
>/{{ specifications.item.unitName }}</text
>
<text v-if="specifications.item.result.suitNum > 1"
>「{{ specifications.item.result.suitNum
}}{{ specifications.item.result.unitName }}起点」</text
>
</view>
<view class="price" v-else>
<text class="i">¥</text>
<view class="num">
<GoodsPrice
:limitDiscount="limitTimeDiscountRes"
:cart="specifications.item"
:shopUserInfo="shopUserInfo"
:shopInfo="shopInfo"
></GoodsPrice>
</view>
<text class="num" v-if="false">
{{
shopInfo.isVip == 1 && shopInfo.isMemberPrice == 1
? specifications.item.memberPrice ||
specifications.item.salePrice
: specifications.item.salePrice
}}
</text>
<text class="i" v-if="specifications.item.unitName"
>/{{ specifications.item.unitName }}</text
>
<text v-if="specifications.item.suitNum > 1"
>「{{ specifications.item.suitNum
}}{{ specifications.item.unitName }}起点」</text
>
</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="operation-wrap"
v-if="specifications.item.type != 'package'"
>
<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>
<view
v-if="specifications.item.type == 'package'"
class="addShopping"
:class="shopCartNumber > 0 && allConditionsSatisfied ? 'active' : ''"
@click="submitSelection()"
>
{{ skuBtnText }}
</view>
<view
v-else
class="addShopping"
:class="shopCartNumber > 0 && canSubmit ? 'active' : ''"
@click="submitSelection()"
>
{{ skuBtnText }}
</view>
</view>
</view>
</up-popup>
<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="(item,index) in 6" :key="index">
<up-image width="210rpx" height="210rpx" radius="12rpx"></up-image>
<view class="info">
<view class="u-line-2 name u-m-t-10 u-font-32 font-500">
商品名
</view>
<view class="u-flex u-row-between">
<view class="font-bold">
<text class="u-font-24"></text>
<text class="u-font-32">40</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()">
{{ skuBtnText }}
</view>
<view v-else class="addShopping" :class="shopCartNumber > 0 && canSubmit ? 'active' : ''"
@click="submitSelection()">
{{ skuBtnText }}
</view>
</template>
<template v-else>
<view class="addShopping" :class="shopCartNumber > 0 ? 'active' : ''"
@click="submitSelection()">
{{ skuBtnText }}
</view>
</template>
</template>
</view>
</view>
</up-popup>
</template>
<script setup>
import { ref } from "vue";
const props = defineProps({
data: {
type: Object,
default: () => {},
},
specifications: {
type: Object,
default: () => {
return {
item: {},
}
},
},
});
const visible = defineModel({
default: false,
});
function close() {
visible.value = false;
}
const selectedSpecs = ref({});
function isSelected(option) {
return selectedSpecs.value[specType] === option;
}
import {
APIminiAppinfo,
APIminiAppskuinfo,
} from '@/common/api/product/product.js';
import GoodsPrice from '@/components/goods-price.vue';
import {
computed,
inject,
ref
} 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 {}
},
},
});
// 存储用户当前选择的规格,初始为空对象
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 swiperChange(e) {
swiperCurrent.value = e.current;
}
const emits = defineEmits(['prveImgs'])
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 () => {
if (!isProductAvailable(specifications.productListitem.days, specifications.productListitem.startTime,
specifications.productListitem.endTime)) {
uni.showToast({
title: '不在可售时间内'
});
return false;
}
// 判断购物车是否有该选中商品
// 数量不能少于0
if (shopCartNumber.value <= 0) {
return;
}
let res = await shoppingcart();
if ((specifications.item.type == 'package' && allConditionsSatisfied.value) || (specifications.item.type ==
'sku' && canSubmit.value)) {
// 是否是套餐 有就传
if (specifications.item.type == 'package') {
// 需求更改:所所有商品套餐都是add,没有修改
res = null;
} else {
selectedGroupSnap.value = [];
}
console.log('specifications.item', specifications.item);
websocketsendMessage({
id: res ? res.cartListId : '',
type: 'shopping',
suitNum: specifications.productListitem.suitNum,
table_code: uni.cache.get('tableCode'),
shop_id: uni.cache.get('shopId'),
operate_type: res ? 'edit' : 'add',
product_id: specifications.product_id,
sku_id: specifications.sku_id,
number: res ? await calculateValue(res.cartNumber, '+', shopCartNumber.value) :
shopCartNumber.value,
pro_group_info: selectedGroupSnap.value,
goods_type: specifications.item.type == 'package' ? 'package' : '',
memberPrice: specifications.item.memberPrice,
is_print: 1,
product_type: specifications.item.type,
is_time_discount: showLimitDiscount(specifications.item)
});
// 清空套餐选中
selectedGroupSnap.value = [];
showShopsku.value = false;
} else {
websocketsendMessage({
id: res ? res.cartListId : '',
type: 'shopping',
suitNum: specifications.productListitem.suitNum,
table_code: uni.cache.get('tableCode'),
shop_id: uni.cache.get('shopId'),
operate_type: res ? 'edit' : 'add',
product_id: specifications.product_id,
sku_id: specifications.sku_id,
number: res ? await calculateValue(res.cartNumber, '+', shopCartNumber.value) :
shopCartNumber.value,
memberPrice: specifications.item.memberPrice,
is_print: 1,
product_type: specifications.item.type,
is_time_discount: showLimitDiscount(specifications.item)
});
showShopsku.value = false;
return;
}
};
</script>
<style scoped lang="scss">
.box {
width: 100%;
height: 200px;
background-color: #f5f5f5;
}
<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>