uni-app 数据绑定和事件处理,用一个筛选表单讲清楚

发布时间:2026/7/20 14:00:09
uni-app 数据绑定和事件处理,用一个筛选表单讲清楚 页面真正变复杂往往不是因为元素多而是因为用户开始操作了。输入关键词。选择分类。调整价格。点搜索。页面要跟着这些操作变化。数据变了视图要更新用户点了按钮方法要执行某个字段变了可能还要触发请求。这一篇不用零散例子讲ref、v-model、computed、watch。我们写一个商品筛选表单把它们串起来。太长不看版这一篇用商品筛选表单讲交互链路。用户输入或选择以后事件函数更新响应式数据computed负责生成展示文案和派生值点击搜索时再把页面状态整理成请求参数。能用computed表达的派生数据不要重复存一份。这个表单要做什么页面上有四件事输入关键词。选择分类。输入最低价和最高价。点击搜索得到请求参数。交互链路是这样。用户输入或选择事件函数修改响应式数据computed 生成派生结果视图自动更新点击搜索时组装参数文件位置pages/goods/filter.vue也别忘了注册到pages.json。{path:pages/goods/filter,style:{navigationBarTitleText:筛选商品}}先把响应式数据建好在 Vue3 的script setup里最常用的是ref和reactive。这里表单字段属于一个整体用reactive很顺手。script setup import { reactive, computed, watch } from vue const categories [全部, 食品, 日用品, 数码, 服饰] const form reactive({ keyword: , categoryIndex: 0, minPrice: , maxPrice: }) /scriptreactive适合管理一组相关字段比如表单。如果只是一个简单值比如count、visible、status用ref更直接。有一个新手很容易混的点ref在脚本里要.value在模板里不用。reactive对象字段正常用点操作就行。constcountref(0)count.value模板里写text{{ count }}/text不用写count.value。输入框有两种写法商品关键词可以用v-model。input classinput v-modelform.keyword placeholder输入商品关键词 /v-model适合普通输入框能少写一段事件赋值逻辑。但你仍然要理解事件写法因为 uni-app 里很多组件的值都在事件对象里。比如价格输入我们故意用input写。input classinput typedigit :valueform.minPrice placeholder最低价 inputhandleMinPriceInput /consthandleMinPriceInput(e){form.minPricee.detail.value}在 uni-app 和小程序场景里表单事件值经常在e.detail.value。你可以把它理解成组件把用户操作结果放进事件详情里再交给你的方法。视图页面状态handleInputinput用户视图页面状态handleInputinput用户输入价格触发 input 事件读取 e.detail.value 并写入 form视图更新picker 也走事件分类选择用picker。picker :rangecategories :valueform.categoryIndex changehandleCategoryChange view classpicker-value {{ currentCategory }} /view /pickerconsthandleCategoryChange(e){form.categoryIndexNumber(e.detail.value)}picker返回的是索引当前分类文本可以通过计算属性得到。constcurrentCategorycomputed((){returncategories[form.categoryIndex]})computed 适合派生数据不要重复存如果一个值可以由其他字段算出来就不要再存一份。比如当前分类文本、是否填写价格、搜索按钮文案都可以算出来。consthasPriceRangecomputed((){returnBoolean(form.minPrice||form.maxPrice)})constsearchSummarycomputed((){constparts[]if(form.keyword.trim()){parts.push(关键词${form.keyword.trim()})}if(currentCategory.value!全部){parts.push(分类${currentCategory.value})}if(hasPriceRange.value){parts.push(价格${form.minPrice||0}-${form.maxPrice||不限})}returnparts.length0?parts.join():还没有设置筛选条件})这里不要额外维护一个summary字段。否则你要在关键词变化、分类变化、价格变化时到处同步一漏就错。数据绑定里很重要的一条经验是源数据只留一份能算出来的就算出来。watch 适合副作用但不要滥用computed适合算值。watch适合做副作用。什么叫副作用比如数据变化以后你要请求接口、写缓存、打印日志、上报埋点这些都不是单纯展示。比如关键词变化后你想提示用户正在准备搜索。watch(()form.keyword,(newKeyword){if(!newKeyword.trim())returnconsole.log(关键词变化了新的关键词是,newKeyword)})但初学阶段要克制使用watch。如果你只是想显示一个由其他数据计算出来的文本用computed。如果你想在关键词变化后自动请求接口要考虑防抖或取消旧请求。否则用户连续输入a、ab、abc你可能瞬间打三次请求旧请求还可能比新请求更晚回来。这个问题会在网络请求那篇专门讲。点击搜索时组装参数搜索按钮不要直接把整个form原样扔给接口。页面状态是给页面用的请求参数是给接口用的中间最好做一次整理。constbuildSearchParams(){constparams{}constkeywordform.keyword.trim()if(keyword){params.keywordkeyword}if(currentCategory.value!全部){params.categorycurrentCategory.value}if(form.minPrice){params.minPriceNumber(form.minPrice)}if(form.maxPrice){params.maxPriceNumber(form.maxPrice)}returnparams}consthandleSearch(){constparamsbuildSearchParams()uni.showToast({title:Object.keys(params).length0?开始搜索:请先设置条件,icon:none})console.log(搜索参数,params)}这样做有两个好处。页面字段可以保留页面需要的形态比如categoryIndex。接口参数可以整理成接口需要的形态比如category、minPrice、maxPrice。这也是项目里很常见的分层页面状态归页面请求参数归请求。完整页面代码template view classpage view classpanel view classform-item text classlabel关键词/text input classinput v-modelform.keyword placeholder输入商品关键词 / /view view classform-item text classlabel分类/text picker :rangecategories :valueform.categoryIndex changehandleCategoryChange view classpicker-value {{ currentCategory }} /view /picker /view view classprice-row view classprice-item text classlabel最低价/text input classinput typedigit :valueform.minPrice placeholder0 inputhandleMinPriceInput / /view view classprice-item text classlabel最高价/text input classinput typedigit :valueform.maxPrice placeholder不限 inputhandleMaxPriceInput / /view /view view classsummary {{ searchSummary }} /view button typeprimary clickhandleSearch 搜索商品 /button /view /view /template script setup import { reactive, computed, watch } from vue const categories [全部, 食品, 日用品, 数码, 服饰] const form reactive({ keyword: , categoryIndex: 0, minPrice: , maxPrice: }) const currentCategory computed(() { return categories[form.categoryIndex] }) const hasPriceRange computed(() { return Boolean(form.minPrice || form.maxPrice) }) const searchSummary computed(() { const parts [] if (form.keyword.trim()) { parts.push(关键词${form.keyword.trim()}) } if (currentCategory.value ! 全部) { parts.push(分类${currentCategory.value}) } if (hasPriceRange.value) { parts.push(价格${form.minPrice || 0} - ${form.maxPrice || 不限}) } return parts.length 0 ? parts.join() : 还没有设置筛选条件 }) const handleCategoryChange (e) { form.categoryIndex Number(e.detail.value) } const handleMinPriceInput (e) { form.minPrice e.detail.value } const handleMaxPriceInput (e) { form.maxPrice e.detail.value } const buildSearchParams () { const params {} const keyword form.keyword.trim() if (keyword) { params.keyword keyword } if (currentCategory.value ! 全部) { params.category currentCategory.value } if (form.minPrice) { params.minPrice Number(form.minPrice) } if (form.maxPrice) { params.maxPrice Number(form.maxPrice) } return params } const handleSearch () { const params buildSearchParams() uni.showToast({ title: Object.keys(params).length 0 ? 开始搜索 : 请先设置条件, icon: none }) console.log(搜索参数, params) } watch( () form.keyword, (newKeyword) { if (!newKeyword.trim()) return console.log(关键词变化了, newKeyword) } ) /script style langscss .page { min-height: 100vh; padding: 32rpx; background: #f6f7fb; } .panel { padding: 28rpx; border-radius: 16rpx; background: #ffffff; } .form-item { margin-bottom: 28rpx; } .label { display: block; margin-bottom: 12rpx; font-size: 26rpx; color: #374151; } .input, .picker-value { min-height: 80rpx; padding: 0 24rpx; border: 1rpx solid #d1d5db; border-radius: 10rpx; font-size: 28rpx; line-height: 80rpx; } .price-row { display: flex; gap: 20rpx; } .price-item { flex: 1; } .summary { margin: 8rpx 0 28rpx; padding: 20rpx; border-radius: 10rpx; background: #f3f4f6; font-size: 26rpx; color: #4b5563; } /style这条链路比背语法重要数据绑定和事件处理的核心不是某个单独 API。它是一条链路用户操作组件。组件触发事件。事件函数读取e.detail.value或直接通过v-model更新数据。响应式数据变化。模板自动更新。computed负责派生展示。点击搜索时把页面状态整理成请求参数。这条链路顺了页面就从静态模板变成了真正能交互的应用。后面做列表、路由、请求、状态管理本质上都还在处理同一件事数据放哪里谁来改改完以后谁会受到影响。这里容易翻车在脚本里忘记.valueref在脚本里读写要用.value模板里会自动解包。把 computed 能算出的值再存一份比如筛选摘要可以由表单字段算出来就不要额外维护summary状态。watch 里直接请求接口关键词一变就请求用户连续输入时会打很多请求。网络请求篇会讲防抖、取消旧请求和请求序号校验参见08-网络请求与页面状态设计.md。页面状态和请求参数混在一起页面可以存categoryIndex但接口可能要的是category。提交前做一次buildSearchParams项目后面会清楚很多。自己试试给筛选表单加一个“只看有库存”的开关并把它放进搜索参数。给价格区间加一个校验最低价不能大于最高价。参考资料Vue Composition API 文档https://vuejs.org/api/composition-api-setup.htmlVue computed 文档https://vuejs.org/guide/essentials/computed.htmlDCloud input 组件文档https://uniapp.dcloud.net.cn/component/input.html