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

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.hqwc.cn/news/660679.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

linus下Anaconda创建虚拟环境pytorch

一、虚拟环境 1.创建 输入下面命令 conda create -n env_name python3.8 输入y 2.激活环境 输入 conda activate env_name 二、一些常用的命令 在Linux的控制平台 切换到当前的文件夹 cd /根目录/次目录 查看conda目录 conda list 查看pip目录 pip list查看历史命…

【JAVA】javadoc,如何生成标准的JAVA API文档

目录 1.什么是JAVA DOC 2.标签 3.命令 1.什么是JAVA DOC 当我们写完JAVA代码&#xff0c;别人要调用我们的代码的时候要是没有API文档是很痛苦的&#xff0c;只能跟进源码去一个个的看&#xff0c;一个个方法的猜&#xff0c;并且JAVA本来就不是一个重复造轮子的游戏&#…

基于JSP/Servlet校园二手交易平台

摘 要 本系统采用JSP/servlet技术&#xff0c;是使用Java编程语言编写的一套校园网二手交易平台软件。系统采用的是最近几年流行的B/S开发模式&#xff0c;以互联网方式运行&#xff0c;服务器端只需要安装本系统&#xff0c;而客户端用户只要可以上网&#xff0c;就可以非常方…

软考之零碎片段记录(二十九)+复习巩固(十七、十八)

学习 1. 后缀式&#xff08;逆波兰式&#xff09; 2. c/c语言编译 类型检查是语义分析 词法分析。分析单词。如单词的字符拼写等语法分析。分析句子。如标点符号、括号位置等语言上的错误语义分析。分析运算符、运算对象类型是否合法 3. java语言特质 即时编译堆空间分配j…

idea生成双击可执行jar包

我这里是一个生成xmind,解析sql的一个main方法,可以通过配置文件来修改有哪些类会执行 我们经常会写一个处理文件的main方法,使用时再去寻找,入入会比较麻烦,这里就可以把我们写过的main方法打成jar包,放到指定的目录来处理文件并生成想要的结果 1.写出我们自己的main方法,本地…

记一次使用Notepad++正则表达式批量替换SQL语句

目录 一、需求二、解决方案三、正则解析 一、需求 存在如下SQL建表脚本&#xff1a; CREATE TABLE "BUSINESS_GOODS" ( "ID" VARCHAR(32) NOT NULL, "GOODS_CODE" VARCHAR(50), "GOODS_NAME" VARCHAR(100), ... NOT CLUSTER PRIMARY…

申请DigiCert代码签名证书的费用大概是多少?

在数字化转型的当下&#xff0c;代码签名证书成为维护软件及应用程序安全性和信誉度不可或缺的一环。DigiCert&#xff0c;作为全球首屈一指的数字证书供应商&#xff0c;其产品线涵盖了多种证书解决方案&#xff0c;其中便包括至关重要的代码签名证书&#xff0c;旨在通过数字…

tableau基础学习——添加标靶图、甘特图、瀑布图

标靶图 添加参考线 添加参考分布 甘特图 创建新的字段 如设置延迟天数****计划交货日期-实际交货日期 为正代表提前交货&#xff0c;负则代表延迟交货 步骤&#xff1a;创建——计算新字段 把延迟天数放在颜色、大小里面就可以 瀑布图 两个表按照地区连接 先做个条形图&…

TCP协议关于速率的优化机制-滑动窗口详解

在上一章中&#xff0c;我们讲述了TCP协议在传输过程中的可靠性http://t.csdnimg.cn/BsImO&#xff0c;这里衔接上一篇文章继续讲&#xff0c;TCP协议的特性&#xff0c;TCP协议写完之后就写&#xff0c;Http和Https等内容吧 1. 滑动窗口 这里的滑动窗口不是指算法里面的双指…

Vue3管理系统-路由设置+表单校验

一、配置路由规则 1.在views 下创建文件夹分类,搭好架子 2.配置路由规则 在router下Index.js import { createRouter, createWebHistory } from vue-routerconst router createRouter({history: createWebHistory(import.meta.env.BASE_URL),routes: [//一级路由//这里可以…

vue路由(路由基本使用,传参,多级路由)

目录 vue-router简介路由配置和使用嵌套&#xff08;多级&#xff09;路由路由传参方式1&#xff1a;路由的query参数方式2&#xff1a;路由的params参数props配置 命名路由取消路由组件在前进后退 vue-router简介 vue的一个插件库&#xff0c;专门用来实现SPA应用 路由配置…

机器人系统ros2-开发实践04-ROS2 中 tf2的定义及示例说明

1. what ros2 tf2 &#xff1f; tf2的全称是transform2&#xff0c;在ROS&#xff08;Robot Operating System&#xff09;中&#xff0c;它是专门用于处理和变换不同坐标系间位置和方向的库。这个名字来源于“transform”这个词&#xff0c;表示坐标变换&#xff0c;而“2”则…