菜大王uniapp开发
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

979 lines
25 KiB

<script setup>
import { onShow } from "@dcloudio/uni-app";
import { nextTick, ref } from "vue";
import customTabBar from "@/components/custom-tab-bar/my-tab-bar.vue";
import navigation from "@/components/navigation/navigation.vue";
import {
addCartApi,
getCartInfoApi,
getProductsApi,
getProductTypesApi,
previewApi,
} from "@/libs/api";
import { getHeight, useHeight, validates } from "@/libs/utils";
import useStore from "@/store";
const store = useStore();
/**
* 购物车商品id列表
*/
const keys = [];
// const plugin = requirePlugin("WechatSI")
// const manager = plugin.getRecordRecognitionManager()
/**
* 搜索关键字
*/
const keyword = ref("");
/**
* 搜索框的高度
*/
// const searchHeight = ref(0);
/**
* 状态栏高度
*/
// const statusBarHeight = ref(0);
/**
* 屏幕的高度
*/
// const screenHeight = ref(0);
/**
* 各种距离
*/
const safeHeight = ref({});
/**
* 当前标签
*/
const currentTab = ref(0);
/**
* 当前子类标签
*/
const currentType = ref(0);
/**
* 右侧商品列表的高度
*/
const bottomHeight = ref(0);
/**
* 右侧商品列表的滚动高度
*/
const scrollTop = ref(0);
/**
* 顶部展开栏左右偏移量
*/
const navScrollLeft = ref(0);
/**
* 顶部类型数据
*/
const navData = ref([]);
/**
* 展开顶部类型数据
*/
const isShowAll = ref(false);
/**
* 子类型数据
*/
const typeData = ref([]);
/**
* 商品数据
*/
const products = ref([]);
/**
* 购物车商品数量
*/
const cartCount = ref(0);
/**
* 购物车商品列表
*/
const cartList = ref([]);
/**
* 购物车商品总价
*/
const totalPrice = ref(0);
/**
* 服务费用
*/
const serviceFee = ref(0);
/**
* 配送费
*/
const shippingFee = ref(0);
/**
* 是否显示购物车
*/
const showCart = ref(false);
/**
* 判断商品列表滚动到底部是否还有数据
*/
const unLoading = ref(false);
/**
* 判断商品列表有无数据
*/
const showList = ref(true);
/**
* 当前页码
*/
const pageIndex = ref(1);
/**
* 每页显示数量
*/
const pageSize = ref(20);
/**
* 总页数
*/
const pageCount = ref(0);
/**
* 判断是否滚动到了底部
*/
const isBottom = ref(false);
/**
* 下拉刷新状态
*/
const triggered = ref(false);
/**
* 触摸开始时的X坐标
*/
const touchDotX = ref(0);
/**
* 触摸开始时的Y坐标
*/
const touchDotY = ref(0);
/**
* 是否触摸开始
*/
const touchStar = ref(false);
/**
* 右侧商品列表的高度
*/
const rightHeight = ref(0);
/**
* 滚动加载数据时当前商品类型id
*/
const scrollId = ref("");
/**
* 是否在搜索中
*/
const isSearching = ref(false);
/**
* 滚动页面的高度
*/
const pageContainerHeight = ref(0);
/**
* 标记是否已获取过购物车(首次加载后设为true)
*/
const hasFetchedCart = ref(true);
/**
* 上次点击加入购物车的时间
*/
// const actionTime = ref(true);
/**
* 输入购买数量弹框的值
*/
const inputValue = ref("");
/**
* 隐藏购买数量弹框
*/
// const hiddenModal = ref(true);
function onSearch() {}
function onChangeKeyword() {}
/**
* 展开顶部类型数据
*/
function onShowAllTypes() {
isShowAll.value = !isShowAll.value;
}
/**
* 点击一级分类,并修改二级分类数据
*/
function onSwitchNav(item, index) {
navScrollLeft.value = (index - 2) * 50;
if (currentTab.value === index)
return;
currentTab.value = index;
currentType.value = 0;
products.value = [];
unLoading.value = false;
showList.value = true;
pageIndex.value = 1;
isShowAll.value = false;
keyword.value = "";
isSearching.value = false;
getLeftTypes(item.id);
}
function onOpenRecord() {}
/**
* 点击了二级分类,并修改列表
*/
function onSwitchType(item, index) {
if (currentType.value === index)
return;
currentType.value = index;
products.value = [];
unLoading.value = false;
showList.value = true;
pageIndex.value = 1;
scrollId.value = item.id;
getProducts(scrollId.value);
}
/**
* 获取顶部tab标签列表,对应左侧类别
*/
async function getProductTypes(id, isSearch) {
const res = await getProductTypesApi();
if (res.code === "0") {
navData.value = res.data;
if (id) {
getLeftTypes(id);
res.data.forEach((item, index) => {
if (item.id === id) {
currentTab.value = index;
navScrollLeft.value = (index - 2) * 50;
}
});
}
else {
const idx = isSearch ? 0 : currentTab.value;
getLeftTypes(navData.value[idx].id, isSearch ? true : undefined);
}
}
}
/**
* 获取左侧子类别
* @param parentId
* @param isSearch
*/
async function getLeftTypes(parentId, isSearch) {
const res = await getProductTypesApi({ parentId });
if (res.code === "0") {
typeData.value = res.data;
if (res.total > 0) {
scrollId.value = typeData.value[currentType.value].id;
getProducts(isSearch ? "" : scrollId.value);
}
}
}
/**
* 获取商品信息
*/
async function getProducts(parentId, tips) {
if (!hasFetchedCart.value) {
const params = {
warehouseId: uni.getStorageSync("warehousId"),
addrId: uni.getStorageSync("addressId"),
};
const res = await getCartInfoApi(params);
store.changeCartList(res.code === "0" ? res.data : []);
// app.globalData.cartList = res.code === "0" ? res.data : [];
hasFetchedCart.value = res.code === "0";
}
loadProductList(parentId, tips);
}
/**
* 菜品列表 全局只加载一次
*/
async function loadProductList(parentId, tips) {
showList.value = true;
// const params = {
// warehouseId: wx.getStorageSync("warehousId"),
// addrId: wx.getStorageSync("addressId"),
// };
const cartList = store.cartList;
const total = cartList.reduce((cur, acc) => acc + cur.quantity, 0);
store.increment(total);
const params = {
typeId: parentId,
promotion: false,
orderByField: "name",
ase: true,
search: keyword.value,
pageNum: pageIndex.value,
pageSize: pageSize.value,
warehouseid: uni.getStorageSync("warehousId"),
};
const res = await getProductsApi(params);
if (res.code !== "0")
return;
pageCount.value = res.pageCount;
if (!(res.total > 0)) {
showList.value = false;
return;
}
res.data.forEach((item) => {
// 初始化购物车映射表(格式:specId_price -> quantity)
const cartMap = cartList.reduce((acc, cur) => {
return { ...acc, [`${cur.specId}_${cur.price}`]: cur.quantity };
}, {});
item.specs.forEach((specsItem) => {
// 为每个规格项生成相同的组合键
const specKey = `${specsItem.id}_${specsItem.price}`;
// 从映射表中获取数量(不存在则设为0)
specsItem.sum = cartMap[specKey] || 0;
});
// 判断是否显示“选规格”按钮
item.showChoose = item.specs.length > 1 ? 1 : 0;
item.showChild = item.specs.length > 1 ? false : item.showChild;
});
products.value = pageIndex.value === 1
? res.data
: [...products.value, ...(res.data || [])];
rightHeight.value = (await getHeight("#scroll-page")) + 1;
if (tips === 1) {
currentType.value -= 1;
}
else if (tips === 2) {
currentType.value += 1;
scrollTop.value = 0;
bottomHeight.value = 0;
}
uni.hideNavigationBarLoading(); // 停止下拉刷新
uni.stopPullDownRefresh();
}
/**
* 滚动事件
* @param {Event} e - 滚动事件参数
*/
function onScroll(e) {
isBottom.value = (e.detail.scrollHeight - e.detail.scrollTop) <= rightHeight.value;
}
/**
* 翻页数据
*/
function onLoadList() {
const pageIdx = pageIndex.value + 1;
if (pageIdx <= pageCount.value) {
pageIndex.value = pageIdx;
unLoading.value = false;
getProducts(scrollId.value);
}
else {
unLoading.value = true;
}
}
/**
* 下拉刷新
*/
function onRefresherrefresh() {
triggered.value = false;
if (currentType.value !== 0) {
getProducts(typeData[currentType.value - 1].id, 1);
}
}
/**
* 触摸开始事件
* @param e 触摸事件参数
*/
function onTouchStart(e) {
touchDotX.value = e.touches[0].pageX;
touchDotY.value = e.touches[0].pageY;
touchStar.value = false;
}
/**
* 触摸移动事件
* @param e 触摸事件参数
*/
function onTouchMove(e) {
const pageX = e.touches[0].pageX;
const pageY = e.touches[0].pageY;
if (!touchStar.value) {
return;
}
const moveY = Math.abs(pageY - touchDotY.value);
/**
* 左滑手势:横向滑动距离 ≥ 40 且纵向偏移 < 10,且当前不是最后一个一级分类时,切换到下一个一级分类
*/
const isLeft = pageX - touchDotX.value <= -40
&& moveY < 10
&& (currentTab.value < navData.value.length - 1);
/**
* 右滑手势:横向滑动距离 ≥ 40 且纵向偏移 < 10,且当前不是第一个一级分类时,切换到上一个一级分类
*/
const isRight = pageX - touchDotX.value >= 40
&& moveY < 10
&& (currentTab.value !== 0);
// 左右滑动切换一级分类时,重置当前二级分类为第一个
if (isLeft || isRight) {
currentType.value = 0;
products.value = [];
unLoading.value = false;
showList.value = true;
pageIndex.value = 1;
touchStar.value = false;
currentTab.value = isLeft ? currentTab.value + 1 : currentTab.value - 1;
const navId = navData.value[currentTab.value]?.id;
navScrollLeft.value = (currentTab.value - 2) * 50;
getLeftTypes(navId);
}
bottomHeight.value = touchDotY.value > pageY
? bottomHeight.value + 0.2 // 向上
: bottomHeight.value - 0.2; // 往下
}
/**
* 触摸结束事件
*/
function onTouchEnd() {
if (
// 当底部手势滑动距离 ≥ 4 且当前二级分类索引未越界时,触发加载下一二级分类
bottomHeight.value >= 4
&& currentType.value <= typeData.value.length - 1
) {
bottomHeight.value = 0;
if (!isSearching.value) {
getProducts(typeData.value[currentType.value + 1].id, 2);
}
}
if (
// 当底部手势滑动距离大于 0 且已滚动到底部时,重置手势滑动距离
bottomHeight.value > 0
&& bottomHeight.value >= 0
&& isBottom.value
) {
bottomHeight.value = 0;
}
}
function onGotoNext() {
if (currentType.value === typeData.value.length - 1) {
return;
}
currentType.value += 1;
scrollTop.value = 0;
bottomHeight.value = 0;
}
function onGotoDetail(item) {
uni.navigateTo({
url: `/pages/detail/detail?id=${item.id}`,
});
}
/**
* 初始化购物车信息
*/
async function initCartInfo() {
const data = {
warehouseId: uni.getStorageSync("warehousId"),
addrId: uni.getStorageSync("addressId"),
};
const res = await getCartInfoApi(data);
if (res.code !== 0)
return;
cartCount.value = res.total;
cartList.value = res.data || [];
store.goodsCheckedItems = res.data.map(item => item.id); ;
totalPrice.value = res.data.reduce((acc, item) => acc + item.amount, 0).toFixed(2);
if (store.goodsCheckedItems > 0) {
preview(store.goodsCheckedItems);
}
else if (showCart.value) {
showList.value = false;
}
}
/**
* 订单预览
* @param itemIds 商品id列表
*/
async function preview(itemIds) {
const data = {
itemIds: itemIds.join(","),
addrId: uni.getStorageSync("addressId"),
};
const res = await previewApi(data);
serviceFee.value = res.data.serviceFee;
shippingFee.value = res.data.shippingFee;
totalPrice.value = res.data.itemAmount;
}
/**
* 加入购物车
* @param item 商品项
*/
async function onAdd(item) {
// item.specs[0].sum = 9;
// if (item.sum === "") {
// item.sum = inputValue.value;
// }
const isPass = validates([
() => item.stock == item.sum && "采购数量不能大于库存数量",
() => item.stock == 0 && "库存数量为0无法添加",
]);
if (!isPass) {
return;
}
// 判空为 0, 否则转换为数字
item.sum = Number(item.sum) || 0;
// 起订量(item.minNum), 否则每次+1
item.sum += item.sum === 0 ? (item.minNum || 1) : 1;
// 保持输入框与购物车数量同步
inputValue.value = item.sum;
toCart(item);
}
/**
* 更新购物车
* @param item 商品项
*/
async function toCart(item) {
const data = {
quantity: item.sum,
specId: item.id,
Chuxiao: item.chuxiao,
warehouseId: uni.getStorageSync("warehousId"),
addrId: uni.getStorageSync("addressId"),
};
if (!data.specId) {
uni.showModal({
title: "提示",
content: "当前商品规格错误,请稍候再试",
showCancel: false,
confirmText: "确定",
});
}
if (data.Chuxiao === "" || data.Chuxiao === undefined) {
data.Chuxiao = false;
}
if (!data.warehouseId || !data.addrId) {
uni.showModal({
title: "提示",
content: "请先选择收货地址,再添加商品",
showCancel: false,
confirmText: "确定",
});
}
keys.push(item.id);
const res = await addCartApi(data);
keys.splice(item.id, 1);
if (res.code === "0") {
initCartInfo();
}
}
/**
* 减少购买数量
* @param item 商品项
*/
function onMinus(item) {
// if (item.sum === "") {
// item.sum = inputValue.value;
// }
const isPass = validates([
() => item.stock == item.sum && "采购数量不能大于库存数量",
() => item.stock == 0 && "库存数量为0无法添加",
() => item.sum < 0 && "显示错误,采购数量不能小于0",
]);
if (!isPass) {
return;
}
// 判空为 0, 否则转换为数字
item.sum = Number(item.sum) || 0;
// 达到起订量(item.minNum)直接清空, 否则每次-1
item.sum -= item.sum === (item.minNum || 1) ? (item.minNum || 1) : 1;
// 保持输入框与购物车数量同步
inputValue.value = item.sum;
toCart(item);
}
function onChooseNorm(item) {
item.showChild = !item.showChild;
}
// function onModelConfirm() { }
// function onModelCancel() { }
function onShowKeyboard() { }
onShow(() => {
getProductTypes();
safeHeight.value = useHeight();
nextTick(async () => {
const boxHeight = await getHeight(".first-box");
const { screen, status, menu, bottom } = safeHeight.value;
pageContainerHeight.value = screen - status - menu - bottom - boxHeight - 60;
// searchHeight.value = await getHeight(".header-box");
});
});
</script>
<template>
<!-- 微信的胶囊以及搜索框 -->
<navigation background="#fff" class="header-box">
<div
class="box"
:style="{
height: `${safeHeight.menu || 45}px`,
width: safeHeight.menu ? '70%' : '100%',
}"
>
<text class="iconfont icon-search1" />
<input
class="input"
type="text"
placeholder="请输入商品名称"
placeholder-style="color: #999999"
:value="keyword"
@onConfirm="onSearch"
@input="onChangeKeyword"
>
<text class="iconfont icon-yuyin" @click="onOpenRecord" />
</div>
</navigation>
<!-- 横向的分类 一级分类 -->
<view class="first-box">
<view class="left">
<scroll-view
class="scroll-view"
scroll-x
scroll-with-animation
:scroll-left="navScrollLeft"
>
<view
v-for="(navItem, idx) in navData"
:key="idx"
class="list-box"
@click="() => onSwitchNav(navItem, idx)"
>
<view class="type-list">
<image class="image" :src="navItem.imageUrl" mode="" />
<text
class="name" :class="[currentTab === idx ? 'name-active' : ''] "
>
{{ navItem.name }}
</text>
</view>
</view>
</scroll-view>
</view>
<!-- 展开 -->
<view class="right" @click="onShowAllTypes">
<text class="text">
</text>
<text class="text">
</text>
<text class="iconfont icon-down" />
</view>
</view>
<!-- 遮罩层 -->
<view v-show="isShowAll" class="first-type-mask" @click="onShowAllTypes" />
<!-- 展开全部分类 -->
<view
class="first-big-type"
:style="{
top: isShowAll
? `${safeHeight.status + (safeHeight.menu || 45) + 5}px`
: '-1600rpx',
}"
>
<view class="title">
全部分类
</view>
<view class="list-box">
<view
v-for="(navItem, idx) in navData"
:key="idx"
class="listx"
@click="() => onSwitchNav(navItem, idx)"
>
<image
class="img"
:class="[currentTab == idx ? 'nav-item-img-active' : '']"
:src="navItem.imageUrl"
/>
<view
class="name"
:class="[currentTab == idx ? 'active' : '']"
>
{{ navItem.name }}
</view>
</view>
</view>
<view
class="retract"
@click="onShowAllTypes"
>
<view class="left">
点击收起
</view>
<text class="iconfont icon-up1" />
</view>
</view>
<!-- 内容 可滚动 -->
<view
class="page-container"
:style="{ height: `${pageContainerHeight}px` }"
>
<view v-if="!isSearching" class="left">
<view
v-for="(typeItem, typex) in typeData"
:key="typex"
class="text"
@click="() => onSwitchType(typeItem, typex)"
>
<text
class="name"
:class="[currentType == typex ? 'text-active' : '']"
>
{{ typeItem.name }}
</text>
</view>
</view>
<!--
:scroll-top="scrollTop" 设置滚动条位置
scroll-y 允许纵向滚动
scroll-with-animation 滚动时带动画
show-scrollbar 显示滚动条
enhanced 启用增强特性
enable-passive 启用 passive 滚动,提升性能
refresher-enabled 启用下拉刷新
refresher-default-style="white" 下拉刷新默认样式为白色
:refresher-threshold="20" 下拉刷新触发阈值 20px
:refresher-triggered="triggered" 控制下拉刷新状态
:throttle="false" 关闭滚动节流
@scrolltolower="onLoadList" 滚动到底部时触发加载下一页
@scroll="onScroll" 滚动过程中实时监听滚动位置
@refresherrefresh="onRefresherrefresh" 下拉刷新时触发重新加载当前分类数据
-->
<scroll-view
id="scroll-page"
class="right"
:scroll-top="scrollTop"
scroll-y
scroll-with-animation
show-scrollbar
enhanced
enable-passive
refresher-enabled
refresher-default-style="white"
:refresher-threshold="20"
:refresher-triggered="triggered"
:throttle="false"
:style="{
width: isSearching ? '100%' : '80%',
height: '100%',
marginTop: `${-(bottomHeight)}%`,
transition: '.01s all',
}"
@scrolltolower="onLoadList"
@scroll="onScroll"
@refresherrefresh="onRefresherrefresh"
>
<view
v-if="showList"
class="content-right"
@touchstart="onTouchStart"
@touchmove="onTouchMove"
@touchend="onTouchEnd"
>
<view
v-for="(item, idx) in products"
:key="idx"
class="right-list"
>
<view v-if="!item.showChoose && item.specs[0].stock == 0" class="of-stock" />
<view class="right-list-top">
<view
class="right-list-left"
@click="() => onGotoDetail(item)"
>
<image class="image" :src="item.imageUrl" mode="" />
<view
v-if="!item.showChoose && item.specs[0].stock == 0"
class="of-stock-text"
>
补货中
</view>
</view>
<view class="right-list-right">
<view class="top" @click="() => onGotoDetail(item)">
<view class="name">
{{ item.name }}
</view>
<view class="inventory">
{{
// 仅当商品只有1个规格且库存不为“无限库存”(-1)时才显示库存数量
item.specs.length == 1 && !(item.specs[0].stock == -1)
? `库存:${item.specs[0].stock}`
: ''
}}
</view>
</view>
<view class="under">
<view class="price-box" @click="() => onGotoDetail(item)">
<view class="price">
<text>¥{{ item.specs[0].price }}</text>
</view>
<text v-if="item.specs[0].chuxiao" class="original">
正价:¥{{ item.specs[0].oldPrice }}/{{ item.specs[0].unit }}
</text>
<text v-else class="no-original">
/{{ item.specs[0].unit }}
</text>
</view>
<view v-if="!item.showChoose && item.specs[0].stock != 0" class="choose-box">
<block v-if="item.specs[0].sum !== 0">
<view class="line-one" />
<view class="line-two" />
<view class="minus" @click.stop="() => onMinus(item)">
<image class="icon" src="/static/home/minus.png" mode="" />
</view>
<text class="input" @click="() => onShowKeyboard(item)">
{{ item.specs[0].sum }}
</text>
</block>
<view class="add" @click.stop="() => onAdd(item)">
<image class="icon" src="/static/home/add.png" mode="" />
</view>
</view>
<view
v-if="item.showChoose"
class="specifications"
@click.stop="() => onChooseNorm(item)"
>
{{ item.showChild ? '收起' : '选规格' }}
</view>
</view>
</view>
</view>
<view v-if="item.showChild" class="right-list-under">
<view
v-for="(childs) in item.specs"
:key="childs.id"
class="goods-under-list"
>
<view class="price-box">
<view class="price">
<text>¥{{ childs.price }}</text>
</view>
<text v-if="item.specs[0].chuxiao" class="original">
/{{ childs.unit }}
</text>
<text v-else class="no-original">
/{{ childs.unit }}
</text>
<view class="no-original">
{{ childs.stock == -1 ? '' : `库存:${childs.stock}` }}
</view>
</view>
<view class="choose-box">
<block v-if="childs.sum !== 0 || childs.sum != ''">
<view class="line-one" />
<view class="line-two" />
<view
class="minus"
data-quantity="-1"
data-exa="sum"
@click="onSetSum"
>
<image class="icon" src="/static/home/minus.png" mode="" />
</view>
<text
class="input"
@click="() => showKeyboard()"
>
{{ childs.sum }}
</text>
</block>
<view
v-if="childs.stock != 0"
class="add"
data-quantity="1"
data-id="{{childs.id}}"
data-chuxiao="{{childs.chuxiao}}"
data-exa="sum"
@click="() => setSum(childs)"
>
<image class="icon" src="/static/home/add.png" mode="" />
</view>
<text v-if="childs.stock == 0" class="no-stock">
补货中
</text>
</view>
</view>
</view>
</view>
<view
v-if="!isSearching
&& products.length !== 0
&& currentType != typeData.length - 1"
class="next"
@click="onGotoNext"
>
<text>上划或点击进入</text>
<text style="color: #3aa24b;">
{{ typeData[currentType + 1].name }}
</text>
</view>
</view>
<view
v-if="products.length === 0"
:style="{
fontSize: '29rpx',
color: '#666666',
textAlign: 'center',
marginTop: '30rpx',
}"
>
暂无商品~
</view>
</scroll-view>
</view>
<!-- 输入购买数量弹框 -->
<!-- <modal
v-show="!hiddenModal"
title="修改购买数量"
confirm-text="确定"
cancel-text="取消"
@confirm="onModelConfirm"
@cancel="onModelCancel"
>
<input
placeholder="输入所需数量"
focus="{{focus}}"
:value="inputValue"
type="number"
@input="input"
>
</modal> -->
<customTabBar tab-index="1" />
</template>
<style lang="scss" scoped>
@import './classification.scss';
</style>