基于uniapp vue3.0 uView 做一个点单页面(包括加入购物车动画和左右联动)

news/2024/9/23 11:20:00/

1、实现效果:

下拉有自定义组件(商品卡片、进步器、侧边栏等)源码

2、左右联动功能

使用scroll-view来做右边的菜单页,title的id动态绑定充当锚点

<scroll-view :scroll-into-view="toView" scroll-with-animation="true" class="main" @scroll="scroll" scroll-y><view class="scroll_main"><view class="" v-for="(item,index) in list" :id="'type' + index"><view :id="'title' + index"><u-divider>{{item.meal_name}}</u-divider></view><card v-for="(item2,indax) in item.goods" :data="item2" @change="cardChange"></card></view></view></scroll-view>

侧边栏组件点击事件,返回分类信息,根据分类的id,定位到scroll-view对应的title

<view class="nav"><left-nav :data="list" :current="current" @change="navChange"></left-nav></view>
function navChange(e) {current.value = egetRightScrollDistance()}

 scroll-view属性@scroll用于监听scroll的滚动距离,注意用防抖(我用的是uView里自带的防抖方法),防止nav跳动

获取每个titile距离盒子顶部的距离,用于判断滚动距离是否超出某个分类

onReady(() => {list.value.forEach((item, index) => {uni.createSelectorQuery().select('#title' + index).boundingClientRect(data => {console.log(data);titleH.value.push(data)}).exec()})})

获取“this”:

const {appContext: {app: {config: {globalProperties}}}} = getCurrentInstance()
/* 菜单滚动监听 */function scroll(e) {//防抖globalProperties.$u.debounce(() => {console.log(e.detail.scrollTop);titleH.value.forEach((item, index) => {if ((e.detail.scrollTop + item.height) > item.top) {current.value = index}})scrollH.value = e.detail.scrollTop}, 100)}

3、加入购物车动画

购物车是固定的,我们得给它固定的id以便找到它

<view class="bottom"><view id="left_icon" class="left_icon" ref="cartBtn" @click="showPop = !showPop"><u-icon name="bag" size="80" color="#fff"></u-icon></view><view class="bottom_info"><view>共计:<text style="font-weight: bold;color: #FB3B26;">{{35}}</text>元</view><view>已点:早餐、中餐、晚餐</view></view><view class="submit">确认预订</view></view></view>

定义移动小球的样式,写活它的初始位置

<!-- 小球 --><view class="ball" v-if="showAnimation" :animation="animation":style="{ top: ballTop + 'px', left: ballLeft + 'px' }"></view>
.ball {position: absolute;z-index: 1;width: 40rpx;height: 40rpx;background-color: red;border-radius: 50%;}

写活“+”号的id,以便我们获取实例

<view v-if="id" class="plus" :id="id" @click="addClick"><u-icon :name="plusIcon" size="32" color="#ffffff" :customStyle="iconStyle"></u-icon></view>

 用uni.createAnimation()来制作动画,按钮的位置减去购物车的位置等于偏移的位置

/* 动画效果控制 */function addToCart(item) {const btn = '#id_' + item.id;const car = '.left_icon';console.log('#id_' + item.id);uni.createSelectorQuery().select(btn).boundingClientRect().exec((rect) => {const btnRect = rect[0];const left = btnRect.left;const top = btnRect.top;ballTop.value = top;ballLeft.value = left;uni.createSelectorQuery().select(car).boundingClientRect().exec((rect) => {console.log(rect);const carRect = rect[0];const x = carRect.left;const y = carRect.top;carTop.value = carRect.top;carLeft.value = carRect.left;animationData.value = uni.createAnimation()animationData.value.translate(x - left + 20, y - top).step({duration: 300,})animationTimeout.valueclearTimeout(animationTimeout.value)animation.value = animationData.value.export()showAnimation.value = true;animationTimeout.value = setTimeout(() => {showAnimation.value = false;}, 300);});});}

4、代码

页面booking.vue

<template><view class="booking"><view class="content"><view class="nav"><left-nav :data="list" :current="current" @change="navChange"></left-nav></view><scroll-view :scroll-into-view="toView" scroll-with-animation="true" class="main" @scroll="scroll" scroll-y><view class="scroll_main"><view class="" v-for="(item,index) in list" :id="'type' + index"><view :id="'title' + index"><u-divider>{{item.meal_name}}</u-divider></view><card v-for="(item2,indax) in item.goods" :data="item2" @change="cardChange"></card></view></view></scroll-view></view><view class="bottom"><view id="left_icon" class="left_icon" ref="cartBtn" @click="showPop = !showPop"><u-icon name="bag" size="80" color="#fff"></u-icon></view><view class="bottom_info"><view>共计:<text style="font-weight: bold;color: #FB3B26;">{{35}}</text>元</view><view>已点:早餐、中餐、晚餐</view></view><view class="submit">确认预订</view></view></view><!-- 弹出层 --><u-popup v-model="showPop" mode="bottom" border-radius="20" closeable z-index="1"><scroll-view class="pop_main" scroll-y><view class="pop_title">已选菜品</view><view class="scroll_main"><view class="" v-for="(item,index) in list" :id="'type' + index"><view :id="'title' + index"><u-divider>{{item.meal_name}}</u-divider></view><card v-for="(item2,indax) in item.goods" :data="item2" @change="cardChange" :isAdd="false"></card></view></view></scroll-view></u-popup><!-- 小球 --><view class="ball" v-if="showAnimation" :animation="animation":style="{ top: ballTop + 'px', left: ballLeft + 'px' }"></view>
</template><script setup>import leftNav from "@/components/booking/nav.vue"import card from "@/components/booking/card.vue"import {mockData} from "../binding/mock.js"import {getCurrentInstance,ref} from "vue";import {onLoad,onReady} from '@dcloudio/uni-app';onLoad(e => {mock.value = mockDatalist.value = mock.value.data.datasconsole.log(list.value);})onReady(() => {list.value.forEach((item, index) => {uni.createSelectorQuery().select('#title' + index).boundingClientRect(data => {console.log(data);titleH.value.push(data)}).exec()})})const showPop = ref(false)const animationData = ref()const animation = ref()const animationTimeout = ref()const titleH = ref([])const scrollH = ref(0)const toView = ref("")const current = ref(0)const mock = ref()const list = ref([{}])let ballTop = ref(0);let ballLeft = ref(0);let carTop = ref(0);let carLeft = ref(0);const showAnimation = ref(false);const {appContext: {app: {config: {globalProperties}}}} = getCurrentInstance()/* 菜单滚动监听 */function scroll(e) {//防抖globalProperties.$u.debounce(() => {console.log(e.detail.scrollTop);titleH.value.forEach((item, index) => {if ((e.detail.scrollTop + item.height) > item.top) {current.value = index}})scrollH.value = e.detail.scrollTop}, 100)}function cardChange(e) {addToCart(e)}function navChange(e) {current.value = egetRightScrollDistance()}function getRightScrollDistance() {toView.value = "title" + current.value;}/* 动画效果控制 */function addToCart(item) {const btn = '#id_' + item.id;const car = '.left_icon';console.log('#id_' + item.id);uni.createSelectorQuery().select(btn).boundingClientRect().exec((rect) => {const btnRect = rect[0];const left = btnRect.left;const top = btnRect.top;ballTop.value = top;ballLeft.value = left;uni.createSelectorQuery().select(car).boundingClientRect().exec((rect) => {console.log(rect);const carRect = rect[0];const x = carRect.left;const y = carRect.top;carTop.value = carRect.top;carLeft.value = carRect.left;animationData.value = uni.createAnimation()animationData.value.translate(x - left + 20, y - top).step({duration: 300,})animationTimeout.valueclearTimeout(animationTimeout.value)animation.value = animationData.value.export()showAnimation.value = true;animationTimeout.value = setTimeout(() => {showAnimation.value = false;}, 300);});});}
</script><style lang="scss" scoped>page {background-color: #fff;}.content {min-height: 100vh;display: flex;.nav {flex: 1;min-width: 164rpx;background-color: #F6F6F6;}.main {flex: 3.5;height: 100vh;background-color: #fff;.scroll_main {padding-bottom: 150rpx;}}}.bottom {position: absolute;z-index: 2;bottom: 0;width: 750rpx;height: 132rpx;background: #FFFFFF;box-shadow: 0rpx -2rpx 16rpx 2rpx rgba(164, 164, 164, 0.11);border-radius: 0rpx 0rpx 0rpx 0rpx;display: flex;justify-content: space-between;align-items: center;.bottom_info {flex: 1;margin: 0 20rpx;font-size: 26rpx;line-height: 40rpx;&>view:nth-child(2) {font-size: 24rpx;color: #aaa;}}.submit {color: #FFFFFF;padding: 10rpx 20rpx;background-color: #FB3B26;font-size: 26rpx;border-radius: 30rpx;margin-right: 50rpx;}#left_icon {margin-top: -30rpx;margin-left: 40rpx;width: 120rpx;height: 120rpx;background: #FB3B26;border-radius: 40rpx;line-height: 150rpx;text-align: center;}}.ball {position: absolute;z-index: 1;width: 40rpx;height: 40rpx;background-color: red;border-radius: 50%;}.pop_main {position: relative;max-height: 60vh;padding-top: 100rpx;padding-bottom: 150rpx;&>.pop_title {text-align: center;width: 100vw;height: 100rpx;font-size: 32rpx;font-weight: bold;position: fixed;top: 0;z-index: 1;background-color: #fff;line-height: 100rpx;text-align: center;}}
</style>

侧边栏组件nav.vue

<template><view class="nav_main"><view v-for="(item,index) in data" :class="{'tool-box':true,'item':true,'item_act':current==index}"@click="change(index)">{{item.meal_name}}</view></view>
</template><script setup>const emit = defineEmits(['change'])const props = defineProps({data: {type: Array,default: () => ([])},current: {type: Number,default: () => (0)},});function change(index) {emit('change', index) // 当前值 + 进步值}
</script><style scoped lang="scss">.nav_main {position: fixed;}.item {width: 164rpx;text-align: center;padding: 30rpx 0;font-size: 26rpx;color: #000000;font-weight: 400;position: relative;}.item_act {background-color: #fff;font-size: 26rpx;font-weight: 700;&::before {content: "";display: inline-block;width: 12rpx;height: 34rpx;background: #FC4E3E;border-radius: 0rpx 30rpx 30rpx 0rpx;position: absolute;left: 0;}}
</style>

商品卡片组件card.vue

<template><view class="card_body"><view class="image"></view><view class="foods_info"><view>{{data.name}}</view><view></view><view><view class="">¥{{data.price}}</view><counter v-if="isAdd" :id="'id_' + data.id" :number="data.number ?? 0" @change-click="change"></counter></view></view></view>
</template><script setup>import counter from "@/components/booking/counter.vue"const emit = defineEmits(['change'])const props = defineProps({data: {type: Object,default: () => ({})},isAdd: {type: Boolean,default: () => true}});function change(e) {let obj = props.dataobj.number = econsole.log(obj);emit('change', obj)}
</script><style scoped lang="scss">.card_body {display: flex;margin: 30rpx 20rpx;.image {width: 180rpx;height: 180rpx;background-color: #a1a1a1;border-radius: 10rpx;margin-right: 20rpx;}.foods_info {display: flex;flex-direction: column;justify-content: space-between;flex: 1;&>view:nth-child(1) {font-weight: 700;font-size: 28rpx;color: #000000;}&>view:nth-child(3) {display: flex;align-items: center;font-weight: 400;font-size: 32rpx;color: #000000;justify-content: space-between;}}}
</style>

进步器组件counter.vue

<template><view class="counter"><u-icon v-if="number>0" :name="reduceIcon" size="60" color="#8E8E8E" @click="reduceClick"></u-icon><input v-if="number>0" type="number" :value="number" @blur="inputBlurEvent" @input="inputChangeEvent":disabled="disabled"><view v-if="id" class="plus" :id="id" @click="addClick"><u-icon :name="plusIcon" size="32" color="#ffffff" :customStyle="iconStyle"></u-icon></view></view>
</template><script setup>import {ref,reactive,computed,nextTick} from "vue";const props = defineProps({id: {type: String,default: ""},disabled: {type: Number,default: false},number: {type: Number,default: 0},maxNumber: {type: Number,default: 99999},minNumber: {type: Number,default: 0},progressValue: {type: Number,default: 1},reduceIcon: {type: String,default: "minus-circle"},plusIcon: {type: String,default: "plus"}})const temp = computed(() => {return props.number})const iconStyle = reactive({fontWeight: 'blod'})const emit = defineEmits(['change-click'])// 加function addClick(ev) {emit('change-click', props.number + props.progressValue) // 当前值 + 进步值}// 减function reduceClick() {if (props.number <= props.minNumber) {console.log("不能继续减少啦 ~");return;}if ((props.number - props.progressValue) < props.minNumber) {console.log("不能继续减少");return;}// 3、执行 减操作emit('change-click', props.number - props.progressValue)}function inputBlurEvent(e) {let number = parseInt(e.detail.value)if (isNaN(number) || number === 0) {emit('change-click', 0)return;}// 条件:输入数不为进步值的倍数,则往前取成倍数值let multipie = Math.ceil(number / props.progressValue) // 获取倍数number = multipie * props.progressValue // 向上获取最近的倍数if (number > props.maxNumber) {number = props.maxNumberemit('change-click', number)} else if (number <= props.minNumber) {emit('change-click', props.minNumber)} else {emit('change-click', number)}}function inputChangeEvent(e) {// 限制输入在最大与最小值之间// 注意:因为都是赋值最大或最小值,所以会出现值复用无法重新渲染页面的情况(第一次能重新渲染,之后的都不渲染):已解决let number = parseInt(e.detail.value)if (isNaN(number) || number === 0) {// 为空为0return}if (number > props.maxNumber) {emit('change-click', props.maxNumber)} else if (number <= props.minNumber) {emit('change-click', props.minNumber)} else {emit('change-click', number)}}
</script><style lang="scss" scoped>.counter {display: flex;align-items: center;&>input {width: 2em;font-size: 28rpx;font-family: Source Han Sans CN-Bold, Source Han Sans CN;font-weight: bold;color: #000000;flex: 1;text-align: center;}.plus {margin: 8rpx;width: 48rpx;height: 48rpx;border-radius: 50%;background: #FF3232;display: flex;justify-content: center;align-items: center;&>image {width: 32rpx;height: 30rpx;margin-right: 5rpx;}}}
</style>

模拟数据mock.js

const mockData = {"code": 200,"msg": "","data": {"datas": [{"meal_id": 5,"meal_name": "早餐","meal_type": 1,"goods": [{"id": 4,"name": "牛奶","price": "3.00","img": ""},{"id": 5,"name": "馒头","price": "2.00","img": "http://192.168.1.23:9508/campus_pay_resource/goods/f82315767e959b536f64b0a199f99eb5.png"},{"id": 6,"name": "手抓饼","price": "6.00","img": "http://192.168.1.23:9508/campus_pay_resource/goods/9370838db9f50a2e950070995975e3b7.png"}]},{"meal_id": 5,"meal_name": "午餐","meal_type": 1,"goods": [{"id": 7,"name": "牛奶","price": "3.00","img": ""},{"id": 8,"name": "牛奶","price": "3.00","img": ""},{"id": 9,"name": "牛奶","price": "3.00","img": ""},{"id": 10,"name": "牛奶","price": "3.00","img": ""},{"id": 11,"name": "牛奶","price": "3.00","img": ""},{"id": 12,"name": "牛奶","price": "3.00","img": ""},{"id": 13,"name": "牛奶","price": "3.00","img": ""},{"id": 14,"name": "牛奶","price": "3.00","img": ""},{"id": 15,"name": "牛奶","price": "3.00","img": ""},{"id": 16,"name": "牛奶","price": "3.00","img": ""},{"id": 17,"name": "牛奶","price": "3.00","img": ""},{"id": 18,"name": "牛奶","price": "3.00","img": ""},{"id": 19,"name": "馒头","price": "2.00","img": "http://192.168.1.23:9508/campus_pay_resource/goods/f82315767e959b536f64b0a199f99eb5.png"},{"id": 20,"name": "手抓饼","price": "6.00","img": "http://192.168.1.23:9508/campus_pay_resource/goods/9370838db9f50a2e950070995975e3b7.png"}]},{"meal_id": 5,"meal_name": "晚餐","meal_type": 1,"goods": [{"id": 21,"name": "牛奶","price": "3.00","img": ""},{"id": 22,"name": "馒头","price": "2.00","img": "http://192.168.1.23:9508/campus_pay_resource/goods/f82315767e959b536f64b0a199f99eb5.png"},{"id": 23,"name": "手抓饼","price": "6.00","img": "http://192.168.1.23:9508/campus_pay_resource/goods/9370838db9f50a2e950070995975e3b7.png"}]},{"meal_id": 5,"meal_name": "宵夜","meal_type": 1,"goods": [{"id": 24,"name": "牛奶","price": "3.00","img": ""},{"id": 25,"name": "馒头","price": "2.00","img": "http://192.168.1.23:9508/campus_pay_resource/goods/f82315767e959b536f64b0a199f99eb5.png"},{"id": 26,"name": "手抓饼","price": "6.00","img": "http://192.168.1.23:9508/campus_pay_resource/goods/9370838db9f50a2e950070995975e3b7.png"}]}],"school_name": "测试学校"}
}export {mockData
}


http://www.ppmy.cn/news/1446423.html

相关文章

搭建和配置Stable Diffusion环境,超详细的本地部署教程

跃然纸上的创意、瞬息万变的想象&#xff0c;Stable Diffusion以AI的力量赋予您无限创作可能。在这篇详尽的本地部署教程中&#xff0c;我们将携手走进Stable Diffusion的世界&#xff0c;从零开始&#xff0c;一步步搭建和配置这个强大的深度学习环境。无论您是热衷于探索AI艺…

Go语言nil概念,make与new的区别

nil 在Go语言中&#xff0c;nil 是一种特殊值&#xff0c;主要用于指针、接口、切片、映射、通道这五种引用类型。与其它类型的默认值&#xff08;零值&#xff09;有着显著的区别&#xff1a; nil&#xff1a; nil 表示没有具体的值或不存在的对象引用。它可以赋值给指针、切…

图计算浅谈:主流图存储引擎/图搜索算法

在数据关联与复杂网络越来越突显其价值的今日&#xff0c;图数据库&#xff08;Graph Database&#xff09;逐渐成为在大数据领域不可或缺的一部分。图数据库强调数据项之间的关系&#xff0c;它不仅能存储大量的顶点&#xff08;Vertex&#xff09;和边&#xff08;Edge&#…

Gateway Predicate断言(谓词)

是什么 Spring Cloud Gateway匹配路由作为Spring WebFlux HandlerMapping基础设施的一部分。 Spring Cloud Gateway包含许多内置的路由谓词工厂。 所有这些谓词都匹配HTTP请求的不同属性。 您可以使用逻辑 and 语句来联合收割机组合多个路由谓词工厂。 Predicate就是为了实现一…

虹科Pico汽车示波器 | 免拆诊断案例 | 起动机免拆诊断故障 2 例

电磁开关、换向器烧蚀及炭刷磨损均会导致起动机偶尔不工作&#xff0c;使发动机偶尔无法起动。由于故障是偶发的&#xff0c;且没有故障代码&#xff0c;这往往会让维修人员无从下手&#xff0c;而用Pico示波器测量起动电流&#xff0c;就会让这些“亚健康状态”一目了然。 案例…

Ubuntu 16.04下Firefox版本更新

最近要使用Odoo进行项目管理&#xff0c;Odoo17以上版本对浏览器版本要求较高&#xff0c;如果没有新版本下的函数&#xff0c;将无法运行。而Ubuntu16.04下自带的firefox不满足版本要求&#xff0c;因而需要手动下载安装。 查看当前系统版本apt-get能下载的firefox版本 apt-c…

acwing算法提高之数据结构--可持久化数据结构

目录 1 介绍2 训练 1 介绍 本专题用来记录可持久化数据结构相关的题目。 本专题主要讲如下两类数据结构的可持久化&#xff1a; trie的可持久化线段树的可持久化&#xff0c;即主席树 可持久化的前提&#xff1a;本身的拓扑的结构不变。 解决什么类型的问题&#xff1a;可…

2024年第二十六届“华东杯”(B题)大学生数学建模挑战赛|数学建模完整代码+建模过程全解全析

当大家面临着复杂的数学建模问题时&#xff0c;你是否曾经感到茫然无措&#xff1f;作为2022年美国大学生数学建模比赛的O奖得主&#xff0c;我为大家提供了一套优秀的解题思路&#xff0c;让你轻松应对各种难题。 让我们来看看华东杯 (B题&#xff09;&#xff01; 第一个问题…