数据绑定和事件处理 - 让页面动起来
⚡ 静态页面只是开始,动态交互才是精彩!今天我们来学习如何让页面活起来
1. 数据绑定基础
文本插值
vue
<template>
<view class="text-demo">
<!-- 基础文本绑定 -->
<text>\{\{ message \}\}</text>
<!-- 表达式绑定 -->
<text>\{\{ count * 2 \}\}</text>
<!-- 三元运算符 -->
<text>\{\{ isVip ? 'VIP用户' : '普通用户' \}\}</text>
<!-- 方法调用 -->
<text>\{\{ formatDate(currentTime) \}\}</text>
</view>
</template>
<script>
export default {
data() {
return {
message: 'Hello UniApp',
count: 10,
isVip: true,
currentTime: new Date()
}
},
methods: {
formatDate(date) {
return date.toLocaleDateString()
}
}
}
</script>
属性绑定
vue
<template>
<view class="attr-demo">
<!-- 动态class -->
<view :class="{ active: isActive, disabled: isDisabled }">
动态class
</view>
<!-- 动态style -->
<view :style="{ color: textColor, fontSize: fontSize + 'px' }">
动态样式
</view>
<!-- 动态属性 -->
<image :src="imageUrl" :alt="imageAlt" class="dynamic-image" />
<!-- 条件渲染 -->
<view v-if="showContent">显示的内容</view>
<view v-else>隐藏时显示这个</view>
<!-- 列表渲染 -->
<view v-for="(item, index) in list" :key="item.id" class="list-item">
\{\{ index + 1 \}\}. \{\{ item.name \}\}
</view>
</view>
</template>
<script>
export default {
data() {
return {
isActive: true,
isDisabled: false,
textColor: '#007aff',
fontSize: 16,
imageUrl: '/static/demo.jpg',
imageAlt: '演示图片',
showContent: true,
list: [
{ id: 1, name: '苹果' },
{ id: 2, name: '香蕉' },
{ id: 3, name: '橙子' }
]
}
}
}
</script>
<style>
.active {
background-color: #007aff;
color: white;
}
.disabled {
opacity: 0.5;
}
.dynamic-image {
width: 100px;
height: 100px;
}
.list-item {
padding: 10px;
border-bottom: 1px solid #eee;
}
</style>
2. 双向数据绑定
表单控件绑定
vue
<template>
<view class="form-demo">
<!-- 输入框 -->
<view class="form-group">
<text class="label">用户名:</text>
<input v-model="formData.username" placeholder="请输入用户名" />
<text class="value">当前值:\{\{ formData.username \}\}</text>
</view>
<!-- 多行文本 -->
<view class="form-group">
<text class="label">个人简介:</text>
<textarea v-model="formData.bio" placeholder="请输入个人简介" />
</view>
<!-- 开关 -->
<view class="form-group">
<text class="label">接收通知:</text>
<switch v-model="formData.notifications" />
<text class="value">\{\{ formData.notifications ? '开启' : '关闭' \}\}</text>
</view>
<!-- 滑块 -->
<view class="form-group">
<text class="label">音量:\{\{ formData.volume \}\}%</text>
<slider
v-model="formData.volume"
min="0"
max="100"
@change="onVolumeChange"
/>
</view>
<!-- 选择器 -->
<view class="form-group">
<text class="label">城市:</text>
<picker
:range="cities"
:value="cityIndex"
@change="onCityChange"
>
<view class="picker-text">\{\{ cities[cityIndex] \}\}</view>
</picker>
</view>
<!-- 显示所有数据 -->
<view class="data-display">
<text class="title">表单数据:</text>
<text class="data">\{\{ JSON.stringify(formData, null, 2) \}\}</text>
</view>
</view>
</template>
<script>
export default {
data() {
return {
formData: {
username: '',
bio: '',
notifications: true,
volume: 50,
city: '北京'
},
cities: ['北京', '上海', '广州', '深圳'],
cityIndex: 0
}
},
methods: {
onVolumeChange(e) {
console.log('音量变化:', e.detail.value)
},
onCityChange(e) {
this.cityIndex = e.detail.value
this.formData.city = this.cities[this.cityIndex]
}
}
}
</script>
<style>
.form-demo {
padding: 20px;
}
.form-group {
margin-bottom: 20px;
}
.label {
display: block;
margin-bottom: 8px;
font-weight: bold;
}
.value {
color: #666;
font-size: 14px;
margin-left: 10px;
}
.picker-text {
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
}
.data-display {
margin-top: 30px;
padding: 15px;
background: #f5f5f5;
border-radius: 8px;
}
.title {
font-weight: bold;
display: block;
margin-bottom: 10px;
}
.data {
font-family: monospace;
white-space: pre-wrap;
font-size: 12px;
}
</style>
3. 事件处理
基础事件
vue
<template>
<view class="event-demo">
<!-- 点击事件 -->
<button @click="handleClick">普通点击</button>
<!-- 带参数的事件 -->
<button @click="handleClickWithParam('Hello')">带参数点击</button>
<!-- 事件对象 -->
<button @click="handleClickWithEvent">获取事件对象</button>
<!-- 阻止默认行为 -->
<button @click.stop="handleStopPropagation">阻止冒泡</button>
<!-- 长按事件 -->
<view @longpress="handleLongPress" class="long-press-area">
长按我试试
</view>
<!-- 触摸事件 -->
<view
@touchstart="handleTouchStart"
@touchmove="handleTouchMove"
@touchend="handleTouchEnd"
class="touch-area"
>
触摸区域
</view>
<!-- 自定义事件计数器 -->
<view class="counter">
<button @click="decrement">-</button>
<text class="count">\{\{ count \}\}</text>
<button @click="increment">+</button>
</view>
</view>
</template>
<script>
export default {
data() {
return {
count: 0,
touchStartTime: 0
}
},
methods: {
handleClick() {
uni.showToast({
title: '按钮被点击了',
icon: 'success'
})
},
handleClickWithParam(message) {
uni.showToast({
title: `参数:\${message}`,
icon: 'none'
})
},
handleClickWithEvent(e) {
console.log('事件对象:', e)
uni.showToast({
title: '查看控制台',
icon: 'none'
})
},
handleStopPropagation(e) {
e.stopPropagation()
uni.showToast({
title: '阻止了事件冒泡',
icon: 'none'
})
},
handleLongPress() {
uni.showToast({
title: '长按触发',
icon: 'none'
})
},
handleTouchStart(e) {
this.touchStartTime = Date.now()
console.log('触摸开始:', e.touches[0])
},
handleTouchMove(e) {
console.log('触摸移动:', e.touches[0])
},
handleTouchEnd(e) {
const duration = Date.now() - this.touchStartTime
console.log('触摸结束,持续时间:', duration + 'ms')
},
increment() {
this.count++
},
decrement() {
this.count--
}
}
}
</script>
<style>
.event-demo {
padding: 20px;
}
.event-demo button {
margin: 10px 0;
width: 100%;
}
.long-press-area {
background: #f0f0f0;
padding: 20px;
text-align: center;
margin: 10px 0;
border-radius: 8px;
}
.touch-area {
background: #e0f7fa;
padding: 30px;
text-align: center;
margin: 10px 0;
border-radius: 8px;
}
.counter {
display: flex;
align-items: center;
justify-content: center;
margin: 20px 0;
}
.counter button {
width: 50px;
height: 50px;
margin: 0 10px;
}
.count {
font-size: 24px;
font-weight: bold;
min-width: 50px;
text-align: center;
}
</style>
4. 计算属性和侦听器
计算属性
vue
<template>
<view class="computed-demo">
<view class="shopping-cart">
<text class="title">购物车</text>
<view v-for="item in cartItems" :key="item.id" class="cart-item">
<text class="item-name">\{\{ item.name \}\}</text>
<text class="item-price">¥\{\{ item.price \}\}</text>
<view class="quantity-control">
<button @click="decreaseQuantity(item.id)">-</button>
<text class="quantity">\{\{ item.quantity \}\}</text>
<button @click="increaseQuantity(item.id)">+</button>
</view>
<text class="item-total">¥\{\{ itemTotal(item) \}\}</text>
</view>
<view class="cart-summary">
<text class="summary-item">商品数量:\{\{ totalQuantity \}\}</text>
<text class="summary-item">商品总价:¥\{\{ totalPrice \}\}</text>
<text class="summary-item">运费:¥\{\{ shippingFee \}\}</text>
<text class="summary-total">总计:¥\{\{ finalTotal \}\}</text>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
cartItems: [
{ id: 1, name: 'iPhone 15', price: 5999, quantity: 1 },
{ id: 2, name: 'iPad Air', price: 4599, quantity: 2 },
{ id: 3, name: 'AirPods Pro', price: 1999, quantity: 1 }
]
}
},
computed: {
// 计算总数量
totalQuantity() {
return this.cartItems.reduce((total, item) => total + item.quantity, 0)
},
// 计算商品总价
totalPrice() {
return this.cartItems.reduce((total, item) => {
return total + (item.price * item.quantity)
}, 0)
},
// 计算运费
shippingFee() {
return this.totalPrice >= 5000 ? 0 : 20
},
// 计算最终总价
finalTotal() {
return this.totalPrice + this.shippingFee
}
},
methods: {
itemTotal(item) {
return item.price * item.quantity
},
increaseQuantity(id) {
const item = this.cartItems.find(item => item.id === id)
if (item) {
item.quantity++
}
},
decreaseQuantity(id) {
const item = this.cartItems.find(item => item.id === id)
if (item && item.quantity > 1) {
item.quantity--
}
}
}
}
</script>
<style>
.shopping-cart {
padding: 20px;
}
.title {
font-size: 20px;
font-weight: bold;
margin-bottom: 20px;
}
.cart-item {
display: flex;
align-items: center;
padding: 15px;
border-bottom: 1px solid #eee;
}
.item-name {
flex: 1;
font-weight: bold;
}
.item-price {
width: 80px;
color: #666;
}
.quantity-control {
display: flex;
align-items: center;
margin: 0 15px;
}
.quantity-control button {
width: 30px;
height: 30px;
margin: 0 5px;
}
.quantity {
min-width: 30px;
text-align: center;
}
.item-total {
width: 80px;
text-align: right;
font-weight: bold;
color: #ff6b6b;
}
.cart-summary {
margin-top: 20px;
padding: 15px;
background: #f8f9fa;
border-radius: 8px;
}
.summary-item {
display: block;
margin-bottom: 8px;
}
.summary-total {
display: block;
font-size: 18px;
font-weight: bold;
color: #ff6b6b;
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid #ddd;
}
</style>
侦听器
vue
<template>
<view class="watch-demo">
<view class="search-section">
<input
v-model="searchKeyword"
placeholder="搜索商品..."
class="search-input"
/>
<text class="search-tip">\{\{ searchTip \}\}</text>
</view>
<view class="user-section">
<text class="label">用户信息:</text>
<input v-model="user.name" placeholder="姓名" />
<input v-model="user.email" placeholder="邮箱" />
<text class="user-status">\{\{ userStatus \}\}</text>
</view>
<view class="counter-section">
<text class="label">计数器:\{\{ counter \}\}</text>
<button @click="counter++">增加</button>
<text class="counter-info">\{\{ counterInfo \}\}</text>
</view>
</view>
</template>
<script>
export default {
data() {
return {
searchKeyword: '',
searchTip: '请输入搜索关键词',
user: {
name: '',
email: ''
},
userStatus: '请填写用户信息',
counter: 0,
counterInfo: ''
}
},
watch: {
// 简单侦听
searchKeyword(newVal, oldVal) {
console.log('搜索关键词变化:', oldVal, '->', newVal)
if (newVal.length === 0) {
this.searchTip = '请输入搜索关键词'
} else if (newVal.length < 2) {
this.searchTip = '关键词太短,请输入至少2个字符'
} else {
this.searchTip = `正在搜索"\${newVal}"...`
this.performSearch(newVal)
}
},
// 深度侦听对象
user: {
handler(newVal, oldVal) {
console.log('用户信息变化:', newVal)
if (!newVal.name && !newVal.email) {
this.userStatus = '请填写用户信息'
} else if (!newVal.name) {
this.userStatus = '请填写姓名'
} else if (!newVal.email) {
this.userStatus = '请填写邮箱'
} else if (!this.isValidEmail(newVal.email)) {
this.userStatus = '邮箱格式不正确'
} else {
this.userStatus = '用户信息完整'
}
},
deep: true,
immediate: true
},
// 侦听计算属性
counter: {
handler(newVal, oldVal) {
if (newVal > oldVal) {
this.counterInfo = `计数器增加了 \${newVal - oldVal}`
} else if (newVal < oldVal) {
this.counterInfo = `计数器减少了 \${oldVal - newVal}`
}
// 特殊值提醒
if (newVal === 10) {
uni.showToast({
title: '恭喜达到10!',
icon: 'success'
})
}
},
immediate: true
}
},
methods: {
performSearch(keyword) {
// 模拟搜索延迟
setTimeout(() => {
if (this.searchKeyword === keyword) {
this.searchTip = `找到 \${Math.floor(Math.random() * 100)} 个相关结果`
}
}, 500)
},
isValidEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
return emailRegex.test(email)
}
}
}
</script>
<style>
.watch-demo {
padding: 20px;
}
.search-section,
.user-section,
.counter-section {
margin-bottom: 30px;
padding: 15px;
background: #f8f9fa;
border-radius: 8px;
}
.label {
display: block;
font-weight: bold;
margin-bottom: 10px;
}
.search-input {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
margin-bottom: 10px;
}
.search-tip {
color: #666;
font-size: 14px;
}
.user-section input {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
margin-bottom: 10px;
}
.user-status {
color: #007aff;
font-size: 14px;
}
.counter-info {
color: #666;
font-size: 14px;
margin-left: 10px;
}
</style>
小结
今天我们学习了:
- ✅ 数据绑定的各种方式
- ✅ 双向数据绑定在表单中的应用
- ✅ 事件处理和事件修饰符
- ✅ 计算属性的使用场景
- ✅ 侦听器的深度监听和即时触发
核心要点:
- 数据驱动视图更新
- 合理使用计算属性提高性能
- 侦听器用于响应数据变化
- 事件处理实现用户交互
下一篇预告
下一篇我们将学习《API调用详解 - 网络请求、本地存储、设备信息》,学习如何与后端交互和使用设备功能。
数据绑定和事件处理是现代前端开发的核心,掌握了这些,你就能构建出真正动态的应用!