cashier_wx/pages/product/index.vue

1931 lines
49 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>
<view>
<Nav v-if="store.scrollTop>=44" />
<!-- 顶部面板 -->
<view class="top--panel">
<image class="panelimgbackground"
:src="shopExtend?shopExtend.value:'https://czg-qr-order.oss-cn-beijing.aliyuncs.com/shopDetails/topBanner.png'"
mode="aspectFill">
</image>
<view class="panelone">
{{ shopInfo.shopName }}
</view>
<view class="paneltow">
距离您{{ distance }}
</view>
<view class="panelthere flex-between">
<view class="paneltheretext">
营业时间:{{ (shopInfo.businessStartDay || '--') +' 至 '+ (shopInfo.businessEndDay || '--') +' '+ (shopInfo.businessTime || '')}}
</view>
<view class="flex-start" @click="callChildMethod">
<view style="margin-right: 14rpx;">查看</view>
<up-icon name="arrow-down" color="#999999" size="14"></up-icon>
</view>
</view>
<!-- 本店招牌菜 -->
<view class="panelfour" v-if="shopProductList.hots && shopProductList.hots.length > 0">
本店招牌菜
</view>
<view class="panelfive">
<scroll-view :scroll-x="true" :scroll-with-animation="false">
<view class="panelfive_list">
<view class="panelfiveitem" @click="clickspecifications(item,index,index,'热销')"
v-for="(item,index) in shopProductList.hots" :key="index">
<image class="panelfiveitemimage" :src="item.coverImg" mode="aspectFill"></image>
<view class="panelfiveitemone">
{{ item.name }}
</view>
<view class="panelfiveitemtow">
本店回头客第{{index+1}}名
</view>
<view class="panelfiveitemthere flex-start">
<text>招牌</text>
</view>
<!-- <view class="panelfiveitemfour">
{{item.shortTitle?item.shortTitle:''}}
</view>
<view class="panelfiveitemfive">
月售{{item.stockNumber}}
</view> -->
<view v-if="item.isSoldStock == 1 || item.isSaleTime == 0" class="flex-between"
style="margin-top: 32rpx;margin-bottom: 48rpx;">
<view class="panelfiveitemsex flex-between">
<view class="panelfiveitemsex_oen">
<text class="tips"
:class="shopInfo.isVip ==0 || shopInfo.isMemberPrice==0?'lineThrough':''">¥</text>
<!-- 会员价与价格 -->
<text
class="price">{{shopInfo.isVip ==1 && shopInfo.isMemberPrice==1?item.memberPrice:item.salePrice}}</text>
<!-- 原价 -->
<text class="originalprice" v-if="item.originPrice">¥{{item.originPrice}}</text>
<!-- 单位 -->
<text class="unit" v-if="item.unitName">/{{item.unitName}}</text>
</view>
</view>
<view class="panelfiveitemNum">
<view class="sku-wrap flex-center" style="background-color: #CECECE;">
<text class="t" v-if="item.isSaleTime == 0">非可售时间</text>
<text class="t" v-if="item.isSoldStock == 1">已售罄</text>
</view>
</view>
</view>
<view v-else class="flex-between" style="margin-top: 32rpx;margin-bottom: 48rpx;">
<view class="panelfiveitemsex flex-between">
<view class="panelfiveitemsex_oen">
<text class="tips"
:class="shopInfo.isVip ==0 || shopInfo.isMemberPrice==0?'lineThrough':''">¥</text>
<!-- 会员价与价格 -->
<text
class="price">{{shopInfo.isVip ==1 && shopInfo.isMemberPrice==1?item.memberPrice:item.salePrice}}</text>
<!-- 原价 -->
<text class="originalprice" v-if="item.originPrice">¥{{item.originPrice}}</text>
<!-- 单位 -->
<text class="unit" v-if="item.unitName">/{{item.unitName}}</text>
</view>
</view>
<!-- single-单规格商品 sku-多规格商品 package-套餐商品 weight-称重商品 coupon-团购券 -->
<view class="panelfiveitemNum">
<!-- v-if="item.type == 'single'|| item.type == 'weight' || item.type == 'coupon' || (item.type == 'package' && item.groupType=='0')" -->
<view class="sku-wrap flex-center"
v-if="item.type == 'sku' || (item.type == 'package' && item.groupType == '1')"
@click.stop="clickspecifications(item,index,index1)">
<text class="t" v-if="item.groupType == '1'">选择套餐</text>
<text class="t" v-else>选规格</text>
<text class="dot num"
v-if="item.cartNumber >0">{{ ifcartNumber(item) <99?ifcartNumber(item):'99+'}}</text>
</view>
<view class="Controls" v-else>
<view class="btn" v-if="item.cartNumber != '0'">
<up-icon name="minus-circle-fill" color="#E9AB7A" size="25"></up-icon>
<view class="btnClick" @click.stop="shoppingcart(item,index,'-','单')">
</view>
</view>
<text class="num"> {{ ifcartNumber(item) }} </text>
<text class="dot num"
v-if="item.suitNum>1">{{item.suitNum<99?item.suitNum:'99+'}}</text>
<view class="btn">
<up-icon name="plus-circle-fill" color="#E9AB7A" size="25"></up-icon>
<view class="btnClick" @click.stop="shoppingcart(item,index,'+','单')">
</view>
</view>
</view>
</view>
</view>
<view v-if="item.isSoldStock == 1 || item.isSaleTime == 0"
style="width: 100%;height: 100%;position: absolute;top: 0;left: 0;z-index: 9,background-color: rgba(255,255,255,0.5);">
</view>
</view>
</view>
</scroll-view>
</view>
</view>
<!-- 滚动区域 -->
<view class="scroll-panel" id="scroll-panel">
<view class="list-box">
<view class="left" :style="{top: `${store.height}px`}">
<scroll-view :scroll-into-view="leftIntoView" :scroll-with-animation="false" :scroll-y="true"
:style="{height:`${store.screenHeight - store.height}px`}">
<view class="item" v-for="(item,index) in shopProductList.productInfo" :key="index"
:class="{ 'active':index==leftIndex }" :id="'left-'+index" :data-index="index"
@click="leftTap(index)"><text>{{item.name}}</text></view>
</scroll-view>
</view>
<view class="main">
<view>
<view class="item main-item" v-for="(item,index) in shopProductList.productInfo" :key="index"
:id="'item-'+index">
<view class="title">
<view>{{item.name}}</view>
</view>
<view class="goods" @click="clickspecifications(item1,index,index1)"
v-for="(item1,index1) in item.productList" :key="index1">
<image class="goodsImg" v-if="item1.coverImg!=null" :lazy-load="true"
@load="imageLoaded(item1,index,index1)"
:src="`${item1.coverImg}${!item1.imgLoad?'?x-oss-process=image/resize,m_lfit,w_300,h_300':''}`"
mode="aspectFill"></image>
<image class="goodsImg"
src="https://czg-qr-order.oss-cn-beijing.aliyuncs.com/index/1.gif" mode="" v-else
:lazy-load="true">
</image>
<view v-if="index=='0'" class="topSort" :class="'c'+(index1+1)">TOP{{index1+1}}</view>
<view class="goods_right" style="overflow: hidden;">
<view class="name">{{ item1.name }}</view>
<!-- <view class="lookBack" v-if="index=='0'">本店销量第{{index1+1}}名</view>-->
<view class="lookBack" v-if="specifications.item.isHot == 1">热销</view>
<view class="describe"> {{item1.shortTitle?item1.shortTitle:''}} </view>
<view v-if="item1.isSoldStock == 1 || item1.isSaleTime == 0" class="flex-between">
<view class="money">
<view>¥</view>
<text class="money_num"
v-if="shopInfo.isVip ==1 && shopInfo.isMemberPrice==1"
style="margin-right: 10rpx;">{{ item1.memberPrice }}</text>
<view v-if="shopInfo.isVip ==1 && shopInfo.isMemberPrice==1"
:class="{lineThrough: shopInfo.isVip ==1 && shopInfo.isMemberPrice==1}">
</view>
<text class="money_num"
:class="{lineThrough: shopInfo.isVip ==1 && shopInfo.isMemberPrice==1}">{{ item1.salePrice }}</text>
<text v-if="item1.unitName">/{{item1.unitName}}</text>
</view>
<view class="flex-end">
<view class="sku-wrap flex-center" style="background-color: #CECECE;">
<text class="t" v-if="item1.isSaleTime == 0">非可售时间</text>
<text class="t" v-else-if="item1.isSoldStock == 1">已售罄</text>
</view>
</view>
</view>
<view v-else class="flex-between">
<view class="money">
<view>¥</view>
<text class="money_num"
v-if="shopInfo.isVip ==1 && shopInfo.isMemberPrice==1"
style="margin-right: 10rpx;">{{ item1.memberPrice }}</text>
<view v-if="shopInfo.isVip ==1 && shopInfo.isMemberPrice==1"
:class="{lineThrough: shopInfo.isVip ==1 && shopInfo.isMemberPrice==1}">
</view>
<text class="money_num"
:class="{lineThrough: shopInfo.isVip ==1 && shopInfo.isMemberPrice==1}">{{ item1.salePrice }}</text>
<text v-if="item1.unitName">/{{item1.unitName}}</text>
</view>
<view class="flex-end"
v-if="item1.type == 'sku' || (item1.type == 'package' && item1.groupType == '1')">
<view class="sku-wrap flex-center"
@click.stop="clickspecifications(item1,index,index1)">
<text class="t" v-if="item1.groupType == '1'">选择套餐</text>
<text class="t" v-else>选规格</text>
<text class="dot num"
v-if="item1.cartNumber >0">{{ ifcartNumber(item1) <99?ifcartNumber(item1):'99+'}}</text>
</view>
</view>
<view class="Controls" v-else>
<view class="btn" v-if="item1.cartNumber != '0'">
<up-icon name="minus-circle-fill" color="#E9AB7A" size="25"></up-icon>
<view class="btnClick" @click.stop="singleclick(item1,'-')">
</view>
</view>
<text class="num"> {{ ifcartNumber(item1) }} </text>
<view class="btn">
<up-icon name="plus-circle-fill" color="#E9AB7A" size="25"></up-icon>
<view class="btnClick" @click.stop="singleclick(item1,'+')">
</view>
</view>
</view>
</view>
</view>
<view v-if="item1.isSoldStock != 0 || item1.isSale == 0"
style="width: 100%;height: 100%;position: absolute;top: 0;left: 0;z-index: 9background-color: rgba(255,255,255,0.5);">
</view>
</view>
</view>
<view class="fill-last" :style="{ 'height':fillHeight + 'px' }"></view>
</view>
</view>
</view>
</view>
<!-- 店铺详情 -->
<shopindex ref="showShopInfoRef"></shopindex>
<!-- 购物车 -->
<shoppingCartes :cartLists_count="cartLists_count" :cartList="matchedProducts" :showCart="showCart"
@customevent='websocketsendMessage' @close="showCart = !showCart">
</shoppingCartes>
<!-- 购物车 -->
<view class="cart-wrap" v-if="cartLists_count>0">
<view class="cart-content">
<view class="left">
<view class="iconBox">
<image class="icon"
src="https://czg-qr-order.oss-cn-beijing.aliyuncs.com/shopDetails/shopIcon.png"
mode="aspectFill" @click="showCart = !showCart">
</image>
<text class="u-badge"> {{cartLists_count<99?cartLists_count:'99+'}} </text>
</view>
<text class="i">¥</text>
<!-- <text class="num"
v-if="shopInfo.isVip == 1 && cartLists.memberAmount > 0">{{cartLists.memberAmount||'0.00'}}</text>
<text class="num" v-else>{{cartLists.amount||'0.00'}}</text> -->
</view>
<view class="btn" @tap="$u.debounce(orderdetail, 500)">
<text class="t">去结算</text>
</view>
</view>
</view>
<!-- 规格 -->
<up-popup :show="showShopsku" :round="20" mode="bottom" @close="showShopsku = false;pagemetashow=false"
:safeAreaInsetBottom='false'>
<view class="shop_sku" style="max-height:70vh;">
<view class="positionabsolute">
<up-icon name="close-circle" @click="showShopsku = false;pagemetashow=false" 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: selectedSpecs[specType] === option }">
{{option}}
<view class="shop_sku_box_item_tip" v-if="specifications.item.isPauseSale == 1">
<view>售罄</view>
</view>
</view>
</view>
</view>
</view>
<view v-else>
<view class="shop_sku_box">
<view class="shop_sku_box_name">
{{specifications.groupSnap.count }} 选 {{specifications.groupSnap.number }}
</view>
<view class="flex-start">
<view class="shop_sku_box_item" v-for="(option,goodsid) in specifications.groupSnap.goods"
:key="goodsid" @click="goodsidClick(option, goodsid)"
:class="{ shop_sku_box_item_selected: isOptionSelected(option)}"
:disabled="isMaxSelected() &&!isOptionSelected(option)">
{{option.proName}}/{{option.skuName}}
<view class="shop_sku_box_item_tip" v-if="specifications.item.isPauseSale == 1">
<view>售罄</view>
</view>
</view>
</view>
</view>
</view>
<view class="shop_bottom">
<view class="flex-between">
<view class="price">
<text class="i">¥</text>
<text
class="num">{{shopInfo.isVip ==1 && shopInfo.isMemberPrice==1?specifications.item.memberPrice:specifications.item.salePrice}}</text>
<text class="i">/{{specifications.item.unitName}}</text>
</view>
<view class="operation-wrap">
<view class="btn">
<up-icon name="minus-circle-fill" color="#E9AB7A" size="25"></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 class="addShopping" :class="(shopCartNumber>0&&canSubmit)?'active':''"
@click="submitSelection()">
{{skuBtnText}}
</view>
</view>
</view>
</up-popup>
<!-- <view>
<button @click="manualClose">手动关闭连接</button>
<p>连接状态: {{ isConnected ? '已连接' : '未连接' }}</p>
<view>收到的消息:</view>
<view>
<view v-for="(message, index) in receivedMessages" :key="index">{{ message }}</view>
</view>
</view> -->
</view>
</template>
<script setup>
import {
ref,
reactive,
onMounted,
watchEffect,
getCurrentInstance,
computed
} from "vue";
import {
onLoad,
onReady,
onShow,
onPageScroll
} from '@dcloudio/uni-app'
//导航栏
import Nav from '@/components/CustomNavbar.vue';
import shopindex from './components/shopindex.vue'
import shoppingCartes from './components/shoppingCartes.vue'
// 获取全局属性
const {
proxy
} = getCurrentInstance();
//接口引入
import {
productminiApphotsquery,
APIgroupquery,
APIminiAppinfo,
APIminiAppskuinfo
} from "@/common/api/product/product.js";
// websocket
import useWebSocket from '@/common/js/websocket.js';
// pinia管理
import {
useNavbarStore
} from '@/stores/navbarStore';
const store = useNavbarStore();
import {
productStore
} from '@/stores/user.js';
const userStore = productStore();
// 动态更新导航栏配置
store.updateNavbarConfig({
showBack: true, //左边返回键
rightText: '', //右边文字
showSearch: true, //true是标题其他事文字
title: '',
isTransparent: false,
hasPlaceholder: false //是否要占位符
});
const userInfo = uni.cache.get('userInfo')
const shopInfo = uni.cache.get('shopInfo')
const shopTable = uni.cache.get('shopTable')
const distance = uni.cache.get('distance') //距离
//店铺详情
const showShopInfoRef = ref(null)
//调用shop组件
const callChildMethod = () => {
if (showShopInfoRef.value) {
showShopInfoRef.value.childMethod();
}
}
const shopExtend = uni.cache.get('shopTable').shopExtendMap.index_bg
// 计算高度
const navScroll = ref(null)
// 获取商品数据
const shopProductList = reactive({
hots: [],
productInfo: [],
})
// * 图片加载
const imageLoaded = (item, index, index1) => {
// shopProductList.productInfo[index].products[index1]['imgLoad'] = true;
}
// 计算左侧位置
const leftIndex = ref(0)
//距离顶部的高度
const scrollTopSiz = ref(0)
//元素最低端的距离
const lastbottom = ref('')
//储存元素的下标
const topArr = ref([])
// 填充高度,用于最后一项低于滚动区域时使用
const fillHeight = ref(0)
//距离顶部的高度
const scrollTopSize = ref(0)
//左侧导航点击
const leftTap = (index) => {
uni.pageScrollTo({
scrollTop: topArr.value[index] - store.height,
duration: 0
});
leftIndex.value = index
}
/* 计算左侧滚动位置定位 */
const leftIntoView = computed(() => {
return `left-${leftIndex.value? leftIndex.value:0}`
})
/* 获取元素顶部信息 */
const getElementTop = () => {
new Promise((resolve, reject) => {
let view = uni.createSelectorQuery().selectAll('.main-item');
view.boundingClientRect(async data => {
resolve(data);
}).exec();
proxy.$uGetRect('.scroll-panel').then(res => {
scrollTopSize.value = res.top; //元素距离顶部的距离
})
}).then((res) => {
try {
topArr.value = res.map((item) => {
return item.top; /* 减去滚动容器距离顶部的距离 加导航栏高度*/
});
// 获取最后一个元素最低端的盒子到顶点的距离
for (let i = 0; i <= (topArr.value.length - 1); i++) {
if ((store.screenHeight - 200) >= (topArr.value[topArr.value.length - 1] - topArr.value[
i])) {
// 获取距离最后一个index
lastbottom.value = i;
break;
}
}
/* 获取最后一项的高度,设置填充高度。判断和填充时做了 +-20 的操作,是为了滚动时更好的定位 */
let last = res[res.length - 1].height;
if (last - 20 < store.screenHeight) {
fillHeight.value = 200;
}
} catch (error) {
//TODO handle the exception
}
});
}
// 定时器
const mainThrottle = ref(null)
/* 主区域滚动监听 */
const mainScroll = (e) => {
// 节流方法
clearTimeout(mainThrottle.value);
mainThrottle.value = setTimeout(() => {
scrollFn();
}, 10);
let scrollFn = () => {
let top = e.scrollTop;
let index = 0;
// 判断左边是否可以滑动
for (let i = (topArr.value.length - 1); i >= 0; i--) {
/* 在部分安卓设备上因手机逻辑分辨率与rpx单位计算不是整数滚动距离与有误差增加2px来完善该问题 */
if ((top + 2) >= topArr.value[i] - store.height) {
index = i;
break;
}
}
/* 查找当前滚动距离 */
if (index >= lastbottom.value) { //
leftIndex.value = index > leftIndex.value ? index : leftIndex.value
// this.leftIndex = this.leftIndex
} else {
leftIndex.value = (index < 0 ? 0 : index);
}
}
}
//动态导航栏滑动距离
onPageScroll((res) => {
uni.$u.debounce(store.scrollTop = res.scrollTop, 500)
uni.$u.debounce(navScroll.value = res.scrollTop, 500)
uni.$u.debounce(mainScroll(res), 500)
});
// 点击详情
const showShopsku = ref(false)
// 规格信息
const specifications = reactive({
item: {},
index: '',
inedxs: '',
type: '',
product_id: '',
sku_id: '',
groupSnap: {
goods: []
}
})
/* 价格 */
const salePrice = computed(() => {
// return shopInfo.isVip == 1 && memberPrice > 0 ? res.data.memberPrice : res.data.salePrice
return 0
})
// 用于判断接口数据是否加载完成
const isDataLoaded = ref(false);
// 存储用户当前选择的规格,初始为空对象
const selectedSpecs = ref({});
// 能否提交的状态
const canSubmit = ref(false);
//字形判断
const skuBtnText = ref('添加到购物车')
// 计算属性,判断是否所有规格类型都有选中项
const allSpecsSelected = computed(() => {
// 获取所有规格类型的键
const specKeys = Object.keys(specifications.item.selectSpecInfo);
// skuBtnText.value = selectedSpecsStr.value ? `您还没选择${specKeys[1]}哦` : ``
// 检查每个规格类型是否都在 selectedSpecs 中有对应的选中值
return specKeys.every(key => selectedSpecs.value[key]);
});
// 处理规格选择的方法
const selectSpec = async (specType, option) => {
// 规格清零
shopCartNumber.value = 0
// 更新 selectedSpecs 对象,将当前规格类型的选中值设置为用户点击的选项
selectedSpecs.value = {
...selectedSpecs.value,
[specType]: option
};
if (allSpecsSelected.value) {
try {
let result = await APIminiAppskuinfo({
specInfo: selectedSpecsStr.value,
id: specifications.item.id
});
specifications.sku_id = result.id
specifications.product_id = result.productId
specifications.item = Object.assign({}, result, specifications.item);
if (result.isPauseSale == 0) {
canSubmit.value = true;
skuBtnText.value = '添加到购物车'
}
} catch (error) {
canSubmit.value = false;
}
} else {
canSubmit.value = false;
}
};
// 存储选中的选项
const selectedOptions = ref([]);
// 选择规格的方法
const goodsidClick = (option, goodsid) => {
if (isOptionSelected(option)) {
// 如果已经选中,取消选中
selectedOptions.value = selectedOptions.value.filter(item => item.proId !== option.proId);
} else if (!isMaxSelected()) {
// 如果未达到最大选择数量,添加到选中列表
selectedOptions.value.push(option);
}
};
// 判断选项是否已选中
const isOptionSelected = (option) => {
return selectedOptions.value.some(item => item.proId == option.proId);
};
// 判断是否达到最大选择数量
const isMaxSelected = () => {
// 初始他为可点击
if (selectedOptions.value.length >= specifications.groupSnap.number) {
canSubmit.value = true;
} else {
canSubmit.value = false;
}
return selectedOptions.value.length >= specifications.groupSnap.number;
};
// 计算属性,将 selectedSpecs 转换为字符串形式
const selectedSpecsStr = computed(() => {
const values = Object.values(selectedSpecs.value);
return values.join('');
});
//添加购物车数量
const shopCartNumber = ref(0)
// 多规格 去添加购物车
const shopCart = (i) => {
if (i == '+') {
shopCartNumber.value++;
} else {
shopCartNumber.value--;
}
}
// 使用 find 方法查找购物车是否有匹配的数组
const matchingProduct = async (data) => {
return matchedProducts.value.find(product => {
if (data.type === 'single') {
return product.skuId == data.skuId && product.id == data.id
} else {
return product.skuId == data.sku_id && product.id == data.product_id
}
});
}
// 提交选择并执行下一步操作的方法
const submitSelection = async () => {
if (!canSubmit.value) {
return false;
}
let res = await matchingProduct(specifications)
if (res) {
await calculateValue(res.cartNumber, '+', shopCartNumber.value)
}
// 是否是套餐 有就传
if (selectedOptions.value) {
specifications.groupSnap.goods = selectedOptions.value
}
websocketsendMessage({
type: 'shopping',
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: specifications.groupSnap
})
showShopsku.value = false
// 这里可以添加更多的业务逻辑,例如发送请求到后端等
}
//获取多规格数据
const clickspecifications = async (item, index, indexs, type) => {
console.log(item, index, indexs, type)
// 数量清零
shopCartNumber.value = 0
// 初始化
let data = {
item: item,
index: '',
inedxs: '',
type: '',
product_id: '',
sku_id: ''
}
Object.assign(specifications, data);
// 初始化 多规格选中
selectedSpecs.value = {}
canSubmit.value = false
skuBtnText.value = '请选择规格'
// single-单规格商品 sku-多规格商品 package-套餐商品 weight-称重商品 coupon-团购券
let res = await APIminiAppinfo(item.id)
specifications.item = res
if (specifications.item.type == "package") {
selectedOptions.value = []
specifications.product_id = item.id
specifications.sku_id = item.skuId
specifications.groupSnap = res.groupSnap[0]
}
// 购物车是否有商品
specifications.item.cartListId = await matchingProduct(specifications.item) ? item.cartListId : ''
specifications.index = index
specifications.indexs = indexs
specifications.type = type
specifications.item.selectSpecInfo = Object.fromEntries(
Object.entries(specifications.item.selectSpecInfo).filter(([_, value]) => value.length > 0)
);
// 给默认数量
specifications.item.amountcartNumber = 0
showShopsku.value = true
}
// 单规格
const singleclick = async (item, i) => {
if (selectedOptions.value.length > 0) {
specifications.groupSnap.goods = selectedOptions.value
}
// 判断购物车是否有该选中商品
let res = await matchingProduct(item)
console.log(res)
websocketsendMessage({
id: res ? item.cartListId : '',
type: 'shopping',
table_code: uni.cache.get('tableCode'),
shop_id: uni.cache.get('shopId'),
operate_type: await calculateValue(item.cartNumber, i) == 'del' ? 'del' : res ? 'edit' : 'add',
product_id: item.id,
sku_id: item.skuId,
number: await calculateValue(item.cartNumber, i),
pro_group_info: specifications.groupSnap,
})
}
// 封装加法函数
const calculateValue = (cartNumber, i, step = 1) => {
if (i == '+') {
const result = parseFloat(cartNumber) + step;
return result.toFixed(2);
} else {
// 当减到0返回del
const result = parseFloat(cartNumber) - step;
return result == 0 ? 'del' : result.toFixed(2);
}
}
// WebSocket处理 // 初始化配置
const options = {
initMessage: {
type: 'shopping',
operate_type: 'init',
table_code: uni.cache.get('tableCode'),
shop_id: uni.cache.get('shopId')
}
}
const {
isConnected,
sendMessage,
closeSocket: manualClose,
receivedMessages
} = useWebSocket(options);
//购物车显示
const showCart = ref(false)
// 购物车数组
const cartList = ref([])
// 更新商品数量的方法
const updateProductQuantities = () => {
// 遍历购物车数组
if (cartListFilter.value.length > 0) {
cartListFilter.value.forEach((cartItem) => {
shopProductList.productInfo.forEach((group) => {
group.productList.forEach((product) => {
if (product.id == cartItem.product_id && product.skuId == cartItem
.sku_id) {
product.cartNumber = cartItem.number
product.cartListId = cartItem.id
}
});
});
});
// 遍历购物车数组
cartListFilter.value.forEach((cartItem) => {
// 遍历商品列表二维数组
shopProductList.hots.forEach((group) => {
// 商品 id 匹配
if (group.id == cartItem.product_id) {
// 更新商品的数量
group.cartListId = cartItem.id
group.cartNumber = cartItem.number
}
});
});
} else {
shopProductList.hots.forEach((i) => {
i.cartNumber = 0
})
// 遍历商品列表二维数组
shopProductList.productInfo.forEach((group) => {
group.productList.forEach((product) => {
product.cartNumber = 0
});
});
}
}
//websocket产值
const websocketsendMessage = (data) => {
sendMessage(data)
}
// 监听接收到的消息变化
watchEffect(async () => {
if (isDataLoaded.value && receivedMessages.value.length > 0) {
const Message = receivedMessages.value[receivedMessages.value.length - 1];
if (Message) {
// 心跳返回 过滤
if (Message.type == "p") {
return false
}
// 清空购物车
if (Message.operate_type == 'shopping_cleanup') {
cartList.value = []
showCart.value = false
}
if (Message.type == 'bc') {
console.log(Message)
}
// 初始化购物车数据
if (Message.operate_type == "shopping_init") {
cartList.value = Message.data
}
// 删除除购物车
if (Message.operate_type == 'shopping_del') {
cartList.value = cartList.value.filter(item => item.id !== Message.data.id);
}
// 添加或者减少购物后返回
if (Message.operate_type == 'shopping_add' || Message.operate_type == 'sopping_edit' || Message.type == 'bc') {
[Message.data].forEach((objA) => {
const index = cartList.value.findIndex((objB) => objB.id == objA.id);
if (index !== -1) {
cartList.value[index] = objA;
} else {
cartList.value.push(objA);
}
});
}
//除去p 每次返回都回执消息
await websocketsendMessage({
type: 'receipt',
msg_id: Message.msg_id
})
// if(Message.status != 1){
// uni.showToast({
// title:'操作失败请稍后重试~'
// })
// }
// 初始化商品数量
await updateProductQuantities()
}
}
})
// 更新购物车数据
const matchedProducts = computed(() => {
return cartList.value.map((cartItem) => {
for (const group of shopProductList.productInfo) {
for (const product of group.productList) {
if (product.id == cartItem.product_id) {
console.log(cartItem, product)
// 找到匹配的商品,添加 cartListId 属性
return {
...product,
// cartListinfo:cartItem
cartListId: cartItem.id,
cartNumberToAdd: product.type == 'weight' ? 1 : cartItem.number //增加一个数量算法
};
}
}
}
// 如果没找到匹配的商品,返回 null 或者其他默认值,这里返回 null
return null;
}).filter(item => item !== null);
})
// 计算购物车商品总数量
const cartLists_count = computed(() => {
return matchedProducts.value.reduce((sum, item) => {
// 将 cartNumberToAdd 转换为数字
const num = typeof item.cartNumberToAdd === 'string' ? parseFloat(item.cartNumberToAdd) : item
.cartNumberToAdd;
return sum + num;
}, 0);
});
// 定义 ifcartNumber 计算属性方法 展示数量
const ifcartNumber = computed(() => {
return (item) => {
// 如果 item 为空或者 cartNumber 不是字符串类型,返回 0
if (!item || typeof item.cartNumber !== 'string') {
return 0;
}
let numValue = parseFloat(item.cartNumber);
if (isNaN(numValue)) {
// 如果转换结果是 NaN说明 cartNumber 不是有效的数字字符串,返回 0
return 0;
}
// type string 商品类型 single-单规格商品 sku-多规格商品 package-套餐商品 weight-称重商品 coupon-团购券
if (item.type === 'weight') {
// 如果类型是称重重量,将值保留两位小数
return parseFloat(numValue.toFixed(2));
} else {
// 如果类型是整数,将值转换为整数
return Math.round(numValue);
}
// 如果类型不匹配,返回原始值
return item.cartNumber;
};
})
// 计算处理后的购物车列表 // 用于筛选后的购物车数组
const cartListFilter = computed(() => {
console.log(cartList.value)
// 使用 reduce 方法对 cartList 进行处理
const grouped = cartList.value.reduce((acc, item) => {
const productId = item.product_id;
const num = parseFloat(item.number);
if (!acc[productId]) {
// 如果 acc 中还没有该 product_id 的记录,创建一个新对象
acc[productId] = {
...item,
number: num
};
} else {
// 如果已经有该 product_id 的记录,将当前对象的 number 值累加到已有记录的 number 属性上
acc[productId].number += num;
}
return acc;
}, {});
// 将累加结果保留两位小数,并将对象转换为数组
return Object.values(grouped).map(item => ({
...item,
number: item.number.toFixed(2)
}));
})
// 结账
const orderdetail = () => {
if (this.cartLists.data.length == 0) {
uni.showToast({
title: '请先添加商品',
icon: 'none'
})
return false
}
}
// 列表请求
const productqueryProduct = async () => {
shopProductList.hots = await productminiApphotsquery()
shopProductList.productInfo = await APIgroupquery()
//第一步:将所有商品的 cartNumber 初始化为 0
shopProductList.productInfo.forEach((group) => {
group.productList.forEach((product) => {
product.cartNumber = 0;
});
});
shopProductList.hots.forEach((i) => {
i.cartNumber = 0
})
scrollTopSize.value = 0
topArr.value = []
userStore.actionsAPIuser()
userInfo.value = uni.cache.get('userInfo')
// 数据可以更新
isDataLoaded.value = true;
}
onLoad(async (e) => {
await proxy.$onLaunched;
})
onMounted(async () => {
await productqueryProduct()
setTimeout(() => {
getElementTop()
}, 500)
})
</script>
<style lang="scss" scoped>
.top--panel {
// padding: 0 20rpx;
background-color: #fff;
.panelimgbackground {
width: 100%;
height: 272rpx;
}
.panelone {
padding: 20rpx 20rpx 0 20rpx;
font-family: Source Han Sans CN, Source Han Sans CN;
font-weight: 500;
font-size: 32rpx;
color: #333333;
}
.paneltow {
padding: 0 20rpx;
margin-top: 16rpx;
font-family: Source Han Sans CN, Source Han Sans CN;
font-weight: 400;
font-size: 24rpx;
color: #999999;
}
.panelthere {
margin-top: 32rpx;
margin-bottom: 50rpx;
font-family: Source Han Sans CN, Source Han Sans CN;
font-weight: 400;
font-size: 24rpx;
color: #999999;
padding: 0 20rpx;
.paneltheretext {
font-family: Source Han Sans CN, Source Han Sans CN;
font-weight: 400;
font-size: 24rpx;
color: #999999;
}
.flex-start {
view {
font-family: Source Han Sans CN, Source Han Sans CN;
font-weight: 400;
font-size: 24rpx;
color: #999999;
}
}
}
.panelfour {
font-family: Source Han Sans CN, Source Han Sans CN;
font-weight: bold;
font-size: 32rpx;
color: #333333;
padding: 0 20rpx;
}
.panelfive {
width: 100%;
padding: 0 20rpx;
.panelfive_list {
display: flex;
}
.panelfiveitem {
margin-top: 16rpx;
width: 340rpx;
margin-right: 30rpx;
position: relative;
flex-shrink: 0;
.panelfiveitemimage {
border-radius: 20rpx 20rpx 0rpx 0rpx;
width: 100%;
height: 204rpx;
}
.panelfiveitemone {
margin-top: 32rpx;
font-family: Source Han Sans CN, Source Han Sans CN;
font-weight: 500;
font-size: 28rpx;
color: #333333;
}
.panelfiveitemtow {
margin-top: 16rpx;
font-family: Source Han Sans CN, Source Han Sans CN;
font-weight: 400;
font-size: 24rpx;
color: #FF534B;
}
.panelfiveitemthere {
margin-top: 12rpx;
// text:nth-child(1) {
// margin-left: 0;
// }
text {
padding: 4rpx 24rpx;
background: #F6F6F6;
border-radius: 8rpx 8rpx 8rpx 8rpx;
font-family: Source Han Sans CN, Source Han Sans CN;
font-weight: 400;
font-size: 20rpx;
color: #999999;
}
}
.panelfiveitemfour {
margin-top: 12rpx;
width: 100%;
height: 36rpx;
line-height: 36rpx;
overflow: hidden; //超出的文本隐藏
text-overflow: ellipsis; //溢出用省略号显示
white-space: nowrap; //溢出不换行
font-family: Source Han Sans CN, Source Han Sans CN;
font-weight: 400;
font-size: 24rpx;
color: #999999;
}
.panelfiveitemfive {
margin-top: 8rpx;
font-family: Source Han Sans CN, Source Han Sans CN;
font-weight: 400;
font-size: 24rpx;
color: #999999;
}
.panelfiveitemsex {
.panelfiveitemsex_oen {
.tips {
font-family: Source Han Sans CN, Source Han Sans CN;
font-weight: 400;
font-size: 24rpx;
color: #333333;
}
.price {
font-family: Source Han Sans CN, Source Han Sans CN;
font-weight: bold;
font-size: 36rpx;
color: #333333;
}
.originalprice {
margin-left: 10rpx;
font-family: Source Han Sans CN, Source Han Sans CN;
font-weight: 400;
font-size: 20rpx;
text-decoration: line-through;
}
.unit {
font-family: Source Han Sans CN, Source Han Sans CN;
font-weight: 400;
font-size: 36rpx;
color: #333333;
}
}
}
.panelfiveitemNum {
// position: absolute;
// bottom: 82rpx;
// right: 0;
display: flex;
align-items: center;
justify-content: flex-end;
.sku-wrap {
padding: 10rpx 20rpx;
background: #E3AD7F;
border-radius: 32rpx;
position: relative;
align-items: center;
.t {
font-size: 28rpx;
color: #fff;
}
.dot {
position: absolute;
top: -15rpx;
right: -10rpx;
background-color: #FF4B33;
color: #fff;
border-radius: 58rpx;
padding: 5rpx 14rpx;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
font-size: 20rpx;
}
}
.Controls {
display: flex;
align-items: center;
.num {
margin: 8rpx 8rpx 0 8rpx;
}
.btn {
display: flex;
align-items: center;
justify-content: center;
position: relative;
.btnClick {
width: 100rpx;
height: 100rpx;
position: absolute;
// bottom: 0;
}
}
}
}
}
}
}
.scroll-panel {
.list-box {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: flex-start;
align-items: flex-start;
align-content: flex-start;
font-size: 28rpx;
position: relative;
.left {
width: 200rpx;
background-color: #f6f6f6;
line-height: normal;
box-sizing: border-box;
font-size: 32rpx;
position: sticky;
top: 0;
// padding-bottom: 200rpx;
.item {
padding: 30rpx 30rpx;
// height: 92rpx;
// line-height: 92rpx;
position: relative;
font-weight: 400;
font-size: 24rpx;
color: #333333;
display: flex;
align-items: center;
// overflow: hidden;
// text-overflow: ellipsis;
// white-space: nowrap;
&+.item {
margin-top: 1px;
&::after {
// content: '';
// display: block;
// height: 0;
// border-top: #d6d6d6 solid 1px;
// width: 620upx;
// position: absolute;
// top: -1px;
// right: 0;
// transform: scaleY(0.5);
/* 1px像素 */
}
}
&.active {
color: #333;
background-color: #fff;
position: relative;
&::before {
content: '';
display: block;
position: absolute;
top: 0;
left: 0;
border-left: #7E5C23 solid 4px;
height: 100%;
width: 0;
}
&::after {
content: '';
display: block;
position: absolute;
top: 0;
left: -15rpx;
bottom: 0;
margin: auto;
height: 30rpx;
width: 30rpx;
border-radius: 50%;
background-color: #7E5C23;
}
}
}
.items {
height: 90px;
position: relative;
font-weight: 400;
font-size: 24rpx;
color: #333333;
}
.fill-last {
height: 0;
width: 100%;
background: none;
}
}
.main {
background-color: #fff;
padding-left: 20rpx;
width: 0;
flex-grow: 1;
box-sizing: border-box;
position: relative;
.title {
line-height: normal;
padding: 30rpx 0;
font-size: 24rpx;
font-weight: bold;
color: #666;
background-color: #fff;
position: sticky;
top: 0;
z-index: 19;
}
.item {
padding-bottom: 16rpx;
border-bottom: #eee solid 1px;
}
.goods {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: flex-start;
align-content: center;
position: relative;
&+.goods {
margin-top: 16rpx;
}
&>image {
width: 120rpx;
height: 120rpx;
margin-right: 18rpx;
margin-left: 2px;
}
.goodsImg {
width: 200rpx;
height: 200rpx;
border-radius: 18rpx;
flex-shrink: 0;
}
.topSort {
width: 92rpx;
height: 38rpx;
text-align: center;
line-height: 38rpx;
position: absolute;
top: 0;
left: 0;
font-weight: 400;
font-size: 24rpx;
color: #FFFFFF;
border-radius: 20rpx 0rpx 20rpx 0rpx;
}
.topSort.c1 {
background: #FC5C2E;
}
.topSort.c2 {
background: #EF994E;
}
.topSort.c3 {
background: #F4B951;
}
.goods_right {
width: 100%;
display: flex;
flex-direction: column;
justify-content: space-around;
position: relative;
padding-top: 5rpx;
padding-right: 20rpx;
box-sizing: border-box;
.name {
font-size: 28rpx;
color: #333;
font-weight: bold;
margin-bottom: 8rpx;
}
.describe,
.monthlySale,
.lookBack {
font-weight: 400;
font-size: 20rpx;
color: #999999;
margin-bottom: 8rpx;
}
.describe,
.name {
width: 270rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.lookBack {
color: #FF534B;
}
.money {
font-weight: bold;
font-size: 24rpx;
color: #333;
display: flex;
align-items: flex-end;
.money_num {
font-size: 28rpx;
color: #333;
font-weight: bold;
}
}
.suit {
font-weight: 400;
font-size: 24rpx;
color: #666666;
}
.sku-wrap {
padding: 10rpx 20rpx;
background: #E3AD7F;
border-radius: 32rpx;
// position: absolute;
// bottom: 10rpx;
// right: 20rpx;
align-items: center;
position: relative;
.t {
font-size: 24rpx;
color: #fff;
}
.dot {
position: absolute;
top: -15rpx;
right: -10rpx;
background-color: #FF4B33;
color: #fff;
border-radius: 58rpx;
padding: 5rpx 14rpx;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
font-size: 20rpx;
}
}
.Controls {
display: flex;
align-items: center;
justify-content: flex-end;
// position: absolute;
// bottom: 10rpx;
// right: 20rpx;
.num {
margin: auto 16rpx;
}
.btn {
display: flex;
align-items: center;
justify-content: center;
position: relative;
.btnClick {
width: 100rpx;
height: 100rpx;
position: absolute;
// bottom: 0;
}
}
}
}
}
}
}
.lineThrough {
font-weight: normal;
text-decoration: line-through;
color: #999 !important;
font-size: 24rpx !important;
}
.money_num.lineThrough {}
}
.shop_sku {
width: 100%;
position: relative;
border-radius: 20rpx;
background: #fff;
overflow-y: auto;
padding-bottom: 320rpx;
box-sizing: border-box;
.positionabsolute {
position: absolute;
top: 30rpx;
right: 30rpx;
}
.shop_skucimage {
width: 100%;
height: 500rpx !important;
border-radius: 20rpx 20rpx 0 0;
}
.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: 0 28rpx;
.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;
// background-color: #CECECE;
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);
position: fixed;
left: 0;
bottom: 0;
.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;
}
}
}
.shop-info-wrap {
padding: 0 30rpx 50rpx;
box-sizing: border-box;
.info-wrap {
padding: 30rpx 0;
box-sizing: border-box;
.shopName {
align-self: center;
font-size: 32rpx;
color: #333;
font-weight: bold;
}
}
.info-wrap-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.row {
.col {
margin-bottom: 10rpx;
}
.l,
.t {
font-size: 24rpx;
color: #999;
}
}
}
.cart-wrap {
width: 100%;
padding: 0 20rpx;
box-sizing: border-box;
position: fixed;
bottom: 40rpx;
left: 0;
z-index: 99;
.cart-content {
display: flex;
align-items: center;
height: 128rpx;
background: #FFFFFF;
box-shadow: 0rpx 0rpx 20rpx 2rpx rgba(0, 0, 0, 0.15);
border-radius: 58rpx;
padding: 0 36rpx;
box-sizing: border-box;
.left {
flex: 1;
display: flex;
align-items: center;
position: relative;
.iconBox {
position: relative;
}
.icon {
width: 76rpx;
height: 88rpx;
margin-left: 22rpx;
}
.u-badge {
position: absolute;
top: -30rpx;
right: -30rpx;
background-color: #FF4B33;
color: #fff;
border-radius: 58rpx;
padding: 5rpx 14rpx;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
font-size: 20rpx;
}
.i,
.num {
color: #333;
}
.i {
font-size: 20upx;
position: relative;
top: 4upx;
margin-left: 64rpx;
}
.num {
font-size: 42upx;
font-weight: bold;
}
}
.btn {
height: 100%;
width: 40%;
display: flex;
align-items: center;
justify-content: flex-end;
.t {
width: 160rpx;
height: 64rpx;
line-height: 64rpx;
text-align: center;
background: #E7AE7B;
border-radius: 36rpx 36rpx 36rpx 36rpx;
border: 2rpx solid #E8AD7B;
font-weight: bold;
font-size: 28rpx;
color: #FFFFFF;
}
}
}
}
</style>