android美团购物车效果实现 美团小程序购物车_sed

1、mall.vue

<template>
	<view class="container">
		<!-- 头部背景 -->
		<view class="fixed-header">
			<NavBar
				titleTxt=""
				showLeft
				bgColor="transparent"
				textColor="#FFFFFF"
				leftArrowColor="#FFFFFF"
			/>
			<image
				:src="(mallInfo.imgs && mallInfo.imgs[0]) ? mallInfo.imgs[0].imgUrl : `${baseImgHost}/default-img1.png`"
				mode="aspectFill"
				@tap="toMallImgs"
			/>
		</view>
		<view class="relative-container">
			<!-- 商家信息 -->
			<view class="mall-info">
				<view
					class="name-phone flex f-ai-c f-jc-sb"
					@tap="makeCall"
				>
					<view class="left flex f-d-c">
						<text class="fz-40">
							{{ mallInfo.merchantName }}
						</text>
						<text class="fz-24">
							{{ mallInfo.phone }}
						</text>
					</view>
					<image
						:src="`${baseImgHost}/phone3-g.png`"
						mode="aspectFill"
					/>
				</view>
				<view
					class="address flex f-d-c"
					@tap="switchLocation"
				>
					<text class="fz-24 p-name">
						{{ mallInfo.address }}
					</text>
					<text class="fz-24 p-dis">
						距您{{ shortenNumber(mallInfo.meter) }}{{ mallInfo.meter > 1000 ? 'km' : 'm' }}
					</text>
					<image
						class="nav-bg"
						:src="`${baseImgHost}/navigation-bg.png`"
						mode="aspectFill"
					/>
					<image
						class="nav icon-box"
						:src="`${baseImgHost}/navigation.png`"
						mode="aspectFill"
					/>
				</view>
			</view>
			<!-- 商品分类列表+商品列表 -->
			<view class="goods-type-g">
				<BgTitle
					title="商品列表"
					:fontSize="30"
					style="margin-left: 20rpx;"
				/>
				<view class="type-goods-container flex">
					<!-- 商品类别滚动区域 -->
					<view class="goods-type">
						<view
							v-for="item in Object.values(goodsTypeList)"
							:key="item.id"
							class="goods-type-item flex f-jc-c f-ai-c"
							:class="{active: item.id === activeTypeId}"
							@tap="goodsTypeClickHandle(item)"
						>
							<view class="fz-24 type-name flex f-jc-c f-ai-c">
								{{ item.typeName }}
								<view
									v-if="item.count"
									class="badge fz-20"
									:class="[item.count > 9 ? 'rectangle' : 'circle']"
								>
									{{ item.count }}
								</view>
							</view>
						</view>
					</view>
					<!-- 商品滚动区域 -->
					<view
						v-if="goodsList.length"
						class="flex flex1 f-d-c"
						style="height: 100%; position: relative;"
					>
						<view
							v-for="item in goodsList"
							:key="item.id"
							class="goods-item flex"
							@tap.stop="switchGoods(item.id)"
						>
							<image
								class="goods-img"
								:src="item.imgs[0]?.imgUrl || `${baseImgHost}/default-img2.png`"
								mode="aspectFill"
							/>
							<view class="flex f-d-c f-jc-sb goods-info">
								<text class="goods-name fz-28">
									{{ item.goodsName }}
								</text>
								<view class="flex f-jc-sb f-ai-fe">
									<view class="flex f-d-c">
										<text class="goods-sell fz-24">
											月销 {{ item.monthSell }} 单
										</text>
										<text class="goods-price fz-24">
											¥<text class="fz-34">
												{{ item.specifications[0].price }}
											</text>
										</text>
									</view>
									<!-- 选规格 -->
									<view
										v-if="item.specifications.length > 1"
										class=" count-opt specification fz-26"
									>
										<view
											class="gg"
											@tap.stop="specificationClick(item)"
										>
											选规格
											<text
												v-if="item.count"
												class="spec-item-count fz-20"
												:class="[item.count > 9 ? 'rectangle' : 'circle']"
											>
												{{ item.count }}
											</text>
										</view>
									</view>
									<!-- 增减数量 -->
									<view
										v-else
										class="flex f-ai-c count-opt"
									>
										<view
											v-if="item.count"
											class="opt-b reduce fz-36"
											@tap.stop="goodsCountOpt($event, item)"
										>
											-
										</view>
										<text
											v-if="item.count"
											class="fz-28 choosed"
										>
											{{ item.count }}
										</text>
										<view
											class="opt-b add fz-36"
											@tap.stop="goodsCountOpt($event, item, 1)"
										>
											+
										</view>
									</view>
								</view>
							</view>
						</view>
					</view>
					<NoData
						v-else
						style="margin-top: 20rpx; height: 290rpx;"
					/>
				</view>
				<view class="fixed-bottom" />
			</view>
		</view>
		<!-- 底部下单、购物袋等 -->
		<view class="func-bar flex f-ai-c f-jc-sb">
			<view class="flex f-ai-c">
				<view
					class="shopping-bag"
					@tap.stop="bagClickHandle($event)"
				>
					<image
						id="bagIcon"
						:src="`${baseImgHost}/bag.png`"
						mode="aspectFie"
					/>
					<view
						v-if="total"
						class="badge fz-20"
						:class="[total > 9 ? 'rectangle' : 'circle']"
					>
						{{ total }}
					</view>
					<view
						v-else
						class="badge-hidden"
					/>
				</view>
				<text class="fz-24 total-price">
					¥ <text class="fz-34">
						{{ totalPrice }}
					</text>
				</text>
				<text class="fz-22 total">
					共 {{ total }} 件
				</text>
			</view>
			<view
				class="place-order fz-30"
				@tap="confirmOrder"
			>
				去下单
			</view>
		</view>
		<!-- 选规格弹框 -->
		<Overlay
			:show="showSpecification"
			@overlayClick="closeSpecDialog"
		/>
		<view
			class="spec"
			:class="showSpecContainer ? 'show' : 'hidden'"
		>
			<view class="spec-container flex f-d-c">
				<view class="goods-name fz-34">
					{{ specGoods?.goodsName }}
				</view>
				<!-- 规格信息 -->
				<view class="flex flex1 f-d-c spec-con">
					<text class="fz-24">
						规格
					</text>
					<view class="specs flex">
						<view
							v-for="(item, index) in specGoods?.specifications"
							:key="item.id"
							class="spec-item fz-24"
							:class="{active: index === activeSpecIndex}"
							@tap.stop="specClick(item, index)"
						>
							{{ item.name }}
						</view>
					</view>
				</view>
				<text class="spec-choosed fz-24">
					已选择规格:{{ specGoods?.specNames?.join(',') }}
				</text>
				<!-- 购买数量 -->
				<view class="flex f-jc-sb f-ai-c spec-count-opt">
					<view class="unit-price flex f-ai-c">
						<text
							class="fz-28"
							style="font-weight: 600;"
						>
							单价
						</text>
						<view style="color: #FF4200;">
							<text class="fz-24">
								¥
							</text>
							<text
								class="fz-38"
								style="font-weight: 600;"
							>
								{{ specGoods?.specifications[activeSpecIndex].price }}
							</text>
						</view>
					</view>
					<view
						v-if="!specGoods?.specifications[activeSpecIndex].count"
						class="add-bag flex f-jc-c f-ai-c"
						@tap.stop="specCountOptThrottle($event, specGoods?.specifications[activeSpecIndex], specGoods, 1)"
					>
						<text class="fz-26">
							+
						</text>
						<text class="fz-26">
							加入购物袋
						</text>
					</view>
					<view
						v-else
						class="flex f-ai-c count-opt"
					>
						<view
							class="opt-b reduce fz-28"
							@tap.stop="specCountOptThrottle($event, specGoods?.specifications[activeSpecIndex], specGoods)"
						>
							-
						</view>
						<text class="fz-28 choosed">
							{{ specGoods?.specifications[activeSpecIndex].count || 0 }}
						</text>
						<view
							class="opt-b add fz-28"
							@tap.stop="specCountOptThrottle($event, specGoods?.specifications[activeSpecIndex], specGoods, 1)"
						>
							+
						</view>
					</view>
				</view>
				<image
					class="close-btn"
					:src="`${baseImgHost}/close-2.png`"
					mode="aspectFill"
					@tap.stop="closeSpecDialog"
				/>
			</view>
		</view>
		<!-- 购物袋 -->
		<PageContainer
			:show="showBag"
			height="70vh"
			:round="true"
			:z-index="9990"
			:showShadow="true"
			@overlayClick="showBag = false;"
		>
			<view class="bag-container">
				<view class="flex f-ai-c f-jc-sb bag-header">
					<view>
						<text class="fz-28">
							购物袋
						</text>
						<text class="fz-24 total">
							(共{{ total }}件商品)
						</text>
					</view>
					<view class="flex f-ai-c">
						<image :src="`${baseImgHost}/del.png`" />
						<text
							class="fz-24 total"
							@tap.stop="clearBagHandle"
						>
							清空购物袋
						</text>
					</view>
				</view>
				<view class="bg-list">
					<view
						v-for="item in Object.values(shoppingBag)"
						:key="item.id"
						class="bg-item flex f-jc-sb"
					>
						<view class="flex">
							<image :src="(item.goodsImgs ?? [])[0]?.imgUrl || `${baseImgHost}/default-img2.png`" />
							<view class="flex f-d-c">
								<text class="fz-28">
									{{ item.goodsName }}
								</text>
								<text
									class="fz-24"
									style="color: #828180;"
								>
									{{ item.specificationName }}
								</text>
								<view
									class="fz-20"
									style="color: #FF4200"
								>
									¥<text class="fz-28">
										{{ item.price }}
									</text>
								</view>
							</view>
						</view>
						<view
							class="flex f-ai-c count-opt"
						>
							<view
								class="opt-b reduce fz-36"
								@tap.stop="bagCoutOpt(item)"
							>
								-
							</view>
							<text
								class="fz-28 choosed"
							>
								{{ item.count }}
							</text>
							<view
								class="opt-b add fz-36"
								@tap.stop="bagCoutOpt(item, 1)"
							>
								+
							</view>
						</view>
					</view>
				</view>
			</view>
		</PageContainer>
		<view
			class="dot"
			:style="{ opacity: dotOpacity, left: dotLeft, top: dotTop }"
		/>
	</view>
</template>
<script>
import './mall.less';
import { NavBar, BgTitle, Overlay, PageContainer, NoData } from '@/components';
import Taro from '@tarojs/taro';
import { throttle, twoBezier } from '@/utils/util';
import { shortenNumber } from '@/utils/number';
import { getGoodsTypeList, getGoodsList, merchantDetail, bagAdd, bagList, bagDelete, bagClear } from '@/apis/goods';

export default {
	name: 'MallDetail',
	components: { NavBar, BgTitle, Overlay, PageContainer, NoData },
	provide () {
		return {
			activeBackground: 'linear-gradient(90deg, #FFA00C, rgba(255,159,10,0)) !important'
		};
	},
	data () {
		return {
			isInit: true, // 是否是页面数据初始化
			showBag: false,
			merchantId: null, // 商家ID
			keyword: '',
			mallInfo: {}, // 商家信息
			goodsList: [], // 商品列表
			goodsTypeList: {}, // 商品类型列表
			shortenNumber,
			activeTypeId: 0, // 当前选中的商品类型id
			activeType: null, // 当前选中的商品类型
			showSpecification: false, // 是否展示选规格弹框
			showSpecContainer: false, // 是否展示选规格容器
			activeSpecIndex: 0, // 任何一个规格弹框中,选中的规格的索引
			specGoods: null, // 当前查看规格的商品
			/**
			 * 购物袋, key: 某个规格的某个商品的记录id;
			 */
			shoppingBag: {},
			totalPrice: 0, // 总价
			total: 0, // 总数量
			pager: {
				currentPage: 1,
				pageSize: 10,
				pageCount: 0
			},
			specCountOptThrottle: null,
			dotTop: 0,
			dotLeft: 0,
			endLeft: 0,
			endTop: 0,
			dotOpacity: 0
		};
	},
	onLoad (options) {
		this.merchantId = options.id;
	},
	async onShow () {
		this.pager = {
			currentPage: 1,
			pageSize: 10,
			pageCount: 0
		};
		this.goodsList = [];
		this.wxToken = Taro.getStorageSync('token');
		this.initLngLat(this.getMallInfo);
		const storageActiveTypeId = await Taro.getStorageSync('activeGoodsType');
		if (storageActiveTypeId) Taro.removeStorageSync('activeGoodsType');
		await this.getGoodsTypeList(storageActiveTypeId);
		await this.getGoodsList(this.activeTypeId);
		this.specCountOptThrottle = throttle(this.specCountOpt, 500);
		// 初始化购物袋的位置坐标
		const query = Taro.createSelectorQuery();
		query.select('#bagIcon').boundingClientRect((res) => {
			this.endLeft = res.left + 12.5;
			this.endTop = res.top;
		}).exec();
	},
	onHide () {
		this.showBag = false;
	},
	methods: {
		makeCall () {
			if (this.mallInfo.phone) {
				Taro.makePhoneCall({
					phoneNumber: this.mallInfo.phone
				}).catch(() => {});
			}
		},
		switchLocation () {
			const that = this;
			Taro.openLocation({
				latitude: Number(that.mallInfo.lat),
				longitude: Number(that.mallInfo.lng),
				scale: 18,
				name: that.mallInfo.merchantName,
				address: that.mallInfo.address
			});
		},
		clearBagHandle () {
			bagClear({ merchantId: this.merchantId }, this.wxToken, false).then(async res => {
				if (res) {
					Taro.showToast({
						title: '操作成功',
						icon: 'none',
						duration: 1000
					});
					await this.getGoodsTypeList();
					await this.getGoodsList(this.activeTypeId);
				}
			});
		},
		toMallImgs () {
			Taro.navigateTo({ url: `/pages/mallImgs/mallImgs?type=mall&merchantId=${this.merchantId}` });
		},
		confirmOrder () {
			if (this.total) {
				Taro.navigateTo({ url: `/pages/orderConfirm/orderConfirm?merchantId=${this.merchantId}` });
			}
		},
		// 购物袋被点击
		bagClickHandle () {
			if (!this.total) return;
			if (this.showBag) {
				this.showBag = false;
				return;
			}
			this.showBag = true;
			this.getBagList();
		},
		// 获取商家信息
		getMallInfo () {
			merchantDetail(this.merchantId, { lat: this.lat, lng: this.lng }, this.wxToken).then(res => {
				this.mallInfo = res;
			});
		},
		// 商品类型列表
		async getGoodsTypeList (storageActiveTypeId) {
			const res = await getGoodsTypeList({ merchantId: this.merchantId, page: 1, limit: 100 }, this.wxToken);
			if (res && res.list.length) {
				if (storageActiveTypeId) {
					this.activeTypeId = storageActiveTypeId;
					this.activeType = res.list.find(l => l.id === storageActiveTypeId);
				} else {
					this.activeTypeId = res.list[0].id;
					this.activeType = res.list[0];
				}
				res.list.map(l => {
					this.goodsTypeList[l.id] = { ...l, count: 0 };
				});
			}
		},
		// 商品列表
		async getGoodsList (id, showLoading) {
			const res = await getGoodsList({
				merchantId: this.merchantId,
				typeId: id,
				page: this.pager.currentPage,
				limit: this.pager.pageSize
			}, this.wxToken, showLoading);
			if (this.triggered) {
				this.triggered = false;
				Taro.stopPullDownRefresh();
			}
			if (res && res.list) {
				if (this.pager.currentPage === 1) {
					this.goodsList = res.list;
				} else {
					this.goodsList = this.goodsList.concat(res.list);
				}
				this.pager = res.page;
			}
			this.getBagList();
		},
		// 获取购物袋中物品列表
		getBagList () {
			this.goodsList = this.goodsList.map(gl => {
				gl.count = 0;
				gl.specifications = gl.specifications.map(sp => {
					sp.count = 0;
					return sp;
				});
				return gl;
			});
			bagList({ merchantId: this.merchantId }, this.wxToken).then(res => {
				this.shoppingBag = {};
				// 购物袋每条记录中带有该商品的类别id,字段名为: typeId
				if (res && res.length) {
					const _goodsTypeList = {};
					const { count, price } = res.reduce((pre, curr, index, arr) => {
						// 填充购物袋物品
						this.shoppingBag[curr.id] = curr;
						// 各商品类型回显已选中的商品总数
						if (_goodsTypeList[curr.goodsTypeId]) {
							_goodsTypeList[curr.goodsTypeId].count += curr.count;
						} else {
							_goodsTypeList[curr.goodsTypeId] = { count: curr.count };
						}
						const goods = this.goodsList.find(g => g.id === curr.goodsId);
						if (goods) {
							// 各商品回显已选中的商品总数
							goods.count = (goods.count ?? 0) + curr.count;
							// 某商品中的各个规格回显总数
							const spec = goods.specifications.find(s => s.id === curr.specificationId);
							if (spec) {
								spec.count = curr.count;
							}
						}
						return {
							count: pre.count + curr.count,
							price: +(pre.price + (curr.price || 0)).toFixed(2)
						};
					}, { count: 0, price: 0 });
					// 各商品类型回显已选中的商品总数
					for (const gt in _goodsTypeList) {
						this.goodsTypeList[gt].count = _goodsTypeList[gt].count;
					}
					this.total = count;
					this.totalPrice = price;
				} else {
					this.showBag = false; // 如果购物袋中物品数为0时关闭购物袋
					this.total = 0;
					this.totalPrice = 0;
					for (const key in this.goodsTypeList) {
						this.goodsTypeList[key].count = 0;
					}
				}
			});
		},
		tabClickHandle (id) {
			Taro.navigateTo({ url: `/pages/mallDetail/mallDetail?id=${id}` });
		},
		// 某个商品类别被点击
		goodsTypeClickHandle (item) {
			this.activeTypeId = item.id;
			this.activeType = item;
			this.pager.currentPage = 1;
			this.getGoodsList(item.id, true);
		},
		dotAnimate (event) {
			// 设置小红点初始位置
			const { clientX, clientY } = event.changedTouches[0];
			this.dotLeft = clientX + 'px';
			this.dotTop = clientY + 'px';
			const num = 30;
			const track = [];
			for (let i = 0; i < num + 1; i++) {
				const [x, y] = twoBezier(i / num, [clientX, clientY], [clientX - 60, clientY - 120], [this.endLeft, this.endTop]);
				track.push({ opacity: 1, left: `${x}px`, top: `${y}px` });
			}
			track[num].opacity = 0;
			let i = 0;
			const inter = setInterval(() => {
				if (i === num + 1) {
					clearInterval(inter);
					return;
				}
				const { opacity, left, top } = track[i];
				this.dotOpacity = opacity;
				this.dotLeft = left;
				this.dotTop = top;
				i++;
			}, 10);
		},
		// 商品列表中的加减号
		goodsCountOpt (event, item, type) {
			if (!type && !item.count) return;
			(type ? bagAdd : bagDelete)({
				merchantId: this.merchantId, // 商户id
				goodsId: item.id, // 商品id
				specificationId: item.specifications[0].id // 规格id
			}, this.wxToken).then(res => {
				if (res) {
					if (type) {
						this.dotAnimate(event);
						item.count = (item.count ?? 0) + 1;
					} else {
						item.count--;
					}
					this.updateOther(type, item.specifications[0].price);
					this.updateShoppingBagDatas(item.id, item.specifications[0].id, type, res);
				}
			});
		},
		// 更新总数、总价、各商品类型已选购总数
		updateOther (type, price) {
			if (type) {
				this.total++;
				this.goodsTypeList[this.activeTypeId].count = (this.goodsTypeList[this.activeTypeId].count ?? 0) + 1;
				this.totalPrice = +(this.totalPrice + price).toFixed(2);
			} else {
				if (this.total) {
					this.goodsTypeList[this.activeTypeId].count--;
					this.total--;
					this.totalPrice = +(this.totalPrice - price).toFixed(2);
				}
			}
		},
		// 更新购物袋中的物品
		updateShoppingBagDatas (goodsId, specificationId, type, res) {
			for (const i in this.shoppingBag) {
				if (this.shoppingBag[i].goodsId === goodsId && this.shoppingBag[i].specificationId === specificationId) {
					// 如果是添加物品
					if (type) {
						this.shoppingBag[i].count++;
					} else {
						this.shoppingBag[i].count--;
						if (!this.shoppingBag[i].count) {
							delete this.shoppingBag[i];
						}
					}
				}
			}
			// 将购物袋中没有的物品同步进去
			if (type && !this.shoppingBag[res.id]) {
				this.shoppingBag[res.id] = res;
			}
		},
		// 点击某个货物
		switchGoods (id) {
			Taro.setStorageSync('activeGoodsType', this.activeTypeId);
			Taro.navigateTo({
				url: `/pages/mallGoodsDetail/mallGoodsDetail?id=${id}&merchantId=${this.merchantId}`
			});
		},
		// 商品列表中的规格按钮点击
		specificationClick (item) {
			item.specNames = [];
			this.showSpecification = true;
			this.showSpecContainer = true;
			item.count = 0;
			Object.values(this.shoppingBag).forEach(gr => {
				const spec = item.specifications.find(s => {
					if (item.id === gr.goodsId && s.id === gr.specificationId) return s;
				});
				if (spec) {
					spec.count = gr.count; // 将购物袋中该商品的某个规格的购买数量赋值给该商品的该规格
					item.count = (item.count ?? 0) + gr.count;
					item.specNames.push(spec.name);
				}
			});
			this.specGoods = item;
		},
		// 关闭规格弹框
		closeSpecDialog () {
			this.activeSpecIndex = 0;
			this.showSpecContainer = false;
			this.showSpecification = false;
		},
		// 规格弹框中的某个规格点击
		specClick (item, index) {
			this.activeSpecIndex = index;
		},
		// 商品规格弹框中的加减号
		specCountOpt (event, item, specGoods, type) {
			(type ? bagAdd : bagDelete)({
				merchantId: this.merchantId, // 商户id
				goodsId: specGoods.id, // 商品id
				specificationId: item.id // 规格id
			}, this.wxToken).then(res => {
				if (res) {
					if (type) {
						this.dotAnimate(event);
						if (!item.count) {
							specGoods.specNames = Array.from(new Set([...specGoods.specNames, item.name]));
						}
						item.count = (item.count ?? 0) + 1; // 该商品选中的某规格的总数
						specGoods.count = (specGoods.count ?? 0) + 1; // 该商品选中的各规格的总数
					} else {
						item.count--;
						if (!item.count) {
							specGoods.specNames.splice(specGoods.specNames.indexOf(item.name), 1);
						}
						specGoods.count--;
					}
					this.updateOther(type, item.price);
					this.updateShoppingBagDatas(specGoods.id, item.id, type, res);
				}
			});
		},
		// 购物袋弹框中的加减号
		bagCoutOpt (item, type) {
			(type ? bagAdd : bagDelete)({
				merchantId: this.merchantId, // 商户id
				goodsId: item.goodsId, // 商品id
				specificationId: item.specificationId // 规格id
			}, this.wxToken).then(res => {
				if (res) {
					// 如果是减号,且该商品选购数量==1,则将其从购物袋中清除
					if (!type && item.count === 1) {
						delete this.shoppingBag[item.id];
					}
					this.getBagList();
				}
			});
		}
	},
	async onPullDownRefresh () {
		if (this.triggered) return;
		this.triggered = true;
		await this.getGoodsTypeList();
		this.pager = {
			currentPage: 1,
			pageSize: 10,
			pageCount: 0
		};
		await this.getGoodsList(this.activeTypeId);
	},
	async onReachBottom () {
		this.onTolowerMixin(() => this.getGoodsList(this.activeTypeId));
	}
};
</script>

2、mall.less

page {
	height: 100vh;
	overflow-y: scroll;
	.container {
		padding-top: 400rpx;
		.fixed-header {
			position: fixed;
			z-index: 998;
			top: 0;
			width: 100vw;
			height: 434rpx;
			image {
				width: 100vw;
			}
		}
		.relative-container {
			width: 100vw;
			z-index: 998;
    		background-color: #FFFFFF;
			border-radius: 35rpx 35rpx 0 0;
			.mall-info {
				margin-top: 38rpx;
				padding: 0 20rpx;
				box-sizing: border-box;
				.name-phone {
					.left {
						text:nth-child(1) {
							font-family: PingFang-SC-Bold;
							font-weight: bold;
							color: #333333;
						}
						text:nth-child(2n) {
							display: inline-block;
							margin-top: 25rpx;
							font-family: PingFangSC-Light;
							font-weight: 300;
							color: #666666;
						}
					}
					image {
						width: 50rpx;
						height: 50rpx;
					}
				}
				.address {
					position: relative;
					margin-top: 30rpx;
					height: 130rpx;
					padding-top: 14rpx;
					box-sizing: border-box;
					.p-name {
						width: 500rpx;
						color: #333333;
						font-weight: bold;
						font-family: PingFang-SC-Bold;
					}
					.p-dis {
						margin-top: 21rpx;
						color: #666666;
						font-family: PingFangSC-Regular;
						font-weight: 400;
					}
					image {
						position: absolute;
					}
					.nav-bg {
						width: 290rpx;
						height: 129rpx;
						right: 0;
						top: 0;
						z-index: -1;
					}
					.nav {
						right: 0;
						top: 50%;
						transform: translateY(-50%);
					}
				}
			}
			.goods-type-g {
				margin-top: 45rpx;
				.type-goods-container {
					padding-bottom: 200rpx;
					box-sizing: border-box;
					.goods-type {
						width: 152rpx;
						background-color: #F8F8F8;
						&-item {
							.type-name {
								position: relative;
								width: 152rpx;
								height: 61rpx;
								color: #9B9B9B;
								background-color: #F8F8F8;
								text-align: center;
								margin: 18rpx 0;
								.badge {
									position: absolute;
									top: 0;
									right: 0rpx;
									min-width: 30rpx;
									height: 30rpx;
									line-height: 30rpx;
									text-align: center;
									color: #FFFFFF;
									background: #FE3A46;
									&.rectangle {
										padding: 0 8rpx;
										border-radius: 16rpx;
									}
									&.circle {
										border-radius: 50%;
									}
								}
								.badge-hidden {
									opacity: 0;
									position: absolute;
									top: 0;
									right: 0rpx;
									width: 30rpx;
									height: 30rpx;
								}
							}
							&.active {
								.type-name {
									background-color: #FFFFFF !important;
									color: #010101 !important;
									&::after {
										content: '';
										position: absolute;
										width: 6rpx;
										height: 45rpx;
										left: 0;
										top: 50%;
										transform: translateY(-50%);
										background: #FFA00C;
										border-radius: 0 7rpx 7rpx 0;
									}
								}
							}
						}
					}
					.goods-item {
						position: relative;
						height: 232rpx;
						padding: 25rpx 21rpx;
						box-sizing: border-box;
						background-color: #FFFFFF;
						.goods-img {
							width: 183rpx;
							height: 183rpx;
							border-radius: 30rpx;
							margin-right: 21rpx;
						}
						.goods-info {
							// width: 353rpx;
							.goods-name {
								width: 335rpx;
								display: -webkit-box;
								overflow: hidden;
								text-overflow: ellipsis;
								-webkit-line-clamp: 2;
								-webkit-box-orient: vertical;
								font-weight: 800;
								color: #3E3A39;
								font-family: PingFang-SC-Heavy;
							}
							.goods-sell {
								display: inline-block;
								margin: 10rpx 0 22rpx 0;
								font-family: PingFang-SC-Medium;
								font-weight: 500;
								color: #828180;
							}
							.goods-price {
								width: 115rpx;
								color: #FF4200;
								font-family: PingFang-SC-Bold;
								font-weight: bold;
							}
						}
						.specification {
							.gg {
								position: relative;
								width: 93rpx;
								height: 41rpx;
								line-height: 41rpx;
								font-family: PingFang-SC-Medium;
								text-align: center;
								background: #FFA00C;
								border-radius: 10rpx;
								color: #FFFFFF;
								.spec-item-count {
									display: inline-block;
									position: absolute;
									top: -16rpx;
									right: -13rpx;
									min-width: 30rpx;
									height: 30rpx;
									line-height: 30rpx;
									text-align: center;
									color: #FFFFFF;
									background: #FE3A46;
									&.rectangle {
										padding: 0 8rpx;
										border-radius: 16rpx;
									}
									&.circle {
										border-radius: 50%;
									}
								}
							}
						}
						.count-opt {
							.opt-b {
								width: 43rpx;
								height: 43rpx;
								border-radius: 50%;
								line-height: 43rpx;
								text-align: center;
							}
							.reduce {
								border: 1rpx solid #FFA00C;
							}
							.choosed {
								display: inline-block;
								margin: 0 16rpx;
							}
							.add {
								border: 1rpx solid #FFEBCB;
								background: #FFEBCB;
								color: #FFA00C;
							}
						}
					}
				}
				.fixed-bottom {
					position: fixed;
					width: 100vw;
					height: 74rpx;
					bottom: 0;
					background-color: #FFFFFF;
				}
			}
		}
		.func-bar {
			position: fixed;
			z-index: 9999;
			width: 708rpx;
			height: 98rpx;
			left: 50%;
			transform: translateX(-50%);
			top: 90vh;
			// bottom: 50rpx;
			overflow: hidden;
			background: #FFFFFF;
			box-shadow: 0 2rpx 10rpx 0 rgba(255, 160, 12, 0.2);
			border-radius: 49rpx;
			.shopping-bag {
				position: relative;
				width: 52rpx;
				height: 60rpx;
				margin-left: 42rpx;
				margin-right: 38rpx;
				image {
					width: 52rpx;
					height: 60rpx;
				}
				.badge {
					position: absolute;
					top: 0;
					right: -13rpx;
					min-width: 30rpx;
					height: 30rpx;
					line-height: 30rpx;
					text-align: center;
					color: #FFFFFF;
					background: #FE3A46;
					&.rectangle {
						padding: 0 8rpx;
						border-radius: 16rpx;
					}
					&.circle {
						border-radius: 50%;
					}
				}
			}
			.total-price {
				display: inline-block;
				margin-right: 15rpx;
				color: #FF4200;
				font-weight: bold;
			}
			.total {
				font-weight: 400;
				color: #828180;
			}
			.place-order {
				width: 207rpx;
				height: 98rpx;
				line-height: 98rpx;
				text-align: center;
				background-color: #FE3A46;
				color: #FFFFFF;
			}
		}
		.spec {
			position: fixed;
			z-index: 9999;
			top: 16vh;
			opacity: 0;
			width: 710rpx;
			height: 58vh;
			left: 50%;
			transition: all 0.2s ease-in;
			transform: translateX(-50%) scale(0);
			&.show {
				opacity: 1;
				transform: translateX(-50%) scale(1) !important;
			}
			&.hidden {
				opacity: 0;
				transform: translateX(-50%) scale(0) !important;
			}
			.spec-container {
				position: relative;
				width: 100%;
				// height: 644rpx;
				height: 50vh;
				border-radius: 10rpx;
				padding-top: 30rpx;
				box-sizing: border-box;
				background-color: #FFFFFF;
				.goods-name {
					padding: 0 23rpx;
					font-family: PingFangSC-Semibold;
					font-weight: 600;
					color: #000000;
				}
				.spec-con {
					margin-top: 30rpx;
					text {
						padding: 0 23rpx;
						font-family: PingFangSC-Regular;
						font-weight: 400;
						color: #8A8A8A;
					}
					.specs {
						margin-top: 20rpx;
						padding: 0 23rpx;
						height: 100%;
						flex-wrap: wrap;
						overflow-y: scroll;
						.spec-item {
							height: 58rpx;
							padding: 0 25rpx;
							margin-right: 30rpx;
							margin-bottom: 20rpx;
							line-height: 58rpx;
							text-align: center;
							background: #F2F2F4;
							border: 1rpx solid #F2F2F4;
							border-radius: 10rpx;
							&.active {
								border-color: #FFA00C;
								background-color: #FFF6E9;
								color: #FFA00C;
							}
						}
					}
				}
				.spec-choosed {
					display: inline-block;
					width: 100%;
					padding: 22rpx 30rpx;
					box-sizing: border-box;
					background: #FAFAFA;
					color: #656565;
				}
				.spec-count-opt {
					// position: relative;
					height: 110rpx;
					margin-top: 20rpx;
					padding: 5rpx 23rpx;
					.add-bag {
						position: relative;
						width: 185rpx;
						height: 61rpx;
						line-height: 61rpx;
						text-align: center;
						background: #FFA00C;
						border-radius: 10rpx;
						font-family: PingFang SC;
						font-weight: 400;
						color: #FFFFFF;
						text:nth-child(1) {
							display: inline-block;
							margin-right: 10rpx;
							transform: scale(1.5);
						}
					}
					.count-opt {
						// position: absolute;
						// top: 50%;
						// transform: translateY(-50%);
						right: 22rpx;
						.opt-b {
							width: 43rpx;
							height: 43rpx;
							border-radius: 50%;
							line-height: 43rpx;
							text-align: center;
						}
						.reduce {
							border: 1rpx solid #FFA00C;
						}
						.choosed {
							display: inline-block;
							margin: 0 16rpx;
						}
						.add {
							border: 1rpx solid #FFEBCB;
							background: #FFEBCB;
							color: #FFA00C;
						}
					}
				}
				.close-btn {
					position: absolute;
					width: 77rpx;
					height: 77rpx;
					bottom: -130rpx;
					left: 50%;
					border-radius: 50%;
					transform: translateX(-50%);
				}
			}
		}
		.bag-container {
			position: relative;
			height: 100%;
			padding: 90rpx 20rpx 170rpx 29rpx;
			box-sizing: border-box;
			background-color: #FFFFFF;
			.bag-header {
				position: absolute;
				padding: 0 20rpx 0 29rpx;
				top: 43rpx;
				left: 0;
				right: 0;
				.total {
					color: #A09F9E;
				}
				image {
					width: 24rpx;
					height: 27rpx;
					margin-right: 13rpx;
				}
			}
			.bg-list {
				height: 100%;
				overflow-y: scroll;
				.bg-item {
					position: relative;
					padding: 20rpx 0;
					image {
						width: 113rpx;
						height: 113rpx;
						border-radius: 20rpx;
						margin-right: 20rpx;
					}
					.count-opt {
						position: absolute;
						bottom: 28rpx;
						right: 22rpx;
						.opt-b {
							width: 43rpx;
							height: 43rpx;
							border-radius: 50%;
							line-height: 43rpx;
							text-align: center;
						}
						.reduce {
							border: 1rpx solid #FFA00C;
						}
						.choosed {
							display: inline-block;
							margin: 0 16rpx;
						}
						.add {
							border: 1rpx solid #FFEBCB;
							background: #FFEBCB;
							color: #FFA00C;
						}
					}
				}
			}
		}
		.dot {
			position: fixed;
			opacity: 0;
			width: 20rpx;
			height: 20rpx;
			z-index: 9999;
			border-radius: 50%;
			background-color: #FF4200;
		}
	}
}

3、mall.config.js

export default definePageConfig({
	'navigationStyle': 'custom',
	'navigationBarTextStyle': 'white',
	'enablePullDownRefresh': true, // 当前页
	'backgroundTextStyle': 'dark', // 顶部显示颜色为深色的三个点
	onReachBottomDistance: 10
});

4、 twoBezier

/**
     * @desc 二阶贝塞尔
     * @param {number} t 当前百分比
     * @param {Array} p1 起点坐标
     * @param {Array} cp 控制点
     * @param {Array} p2 终点坐标
     */
const twoBezier = (t, p1, cp, p2) => {
	const [x1, y1] = p1;
	const [cx, cy] = cp;
	const [x2, y2] = p2;
	const x = (1 - t) * (1 - t) * x1 + 2 * t * (1 - t) * cx + t * t * x2;
	const y = (1 - t) * (1 - t) * y1 + 2 * t * (1 - t) * cy + t * t * y2;
	return [x, y];
};