1. 数据绑定基础
1.1 插值表达式
<template>
<view class="container">
<!-- 文本插值 -->
<text>{{ message }}</text>
<text>{{ user.name }}</text>
<!-- 表达式 -->
<text>{{ count + 1 }}</text>
<text>{{ message.split('').reverse().join('') }}</text>
<text>{{ isActive ? '激活' : '未激活' }}</text>
<!-- 函数调用 -->
<text>{{ formatDate(date) }}</text>
<text>{{ getMessage() }}</text>
</view>
</template>
<script>
export default {
data() {
return {
message: 'Hello UniApp',
count: 0,
isActive: true,
date: new Date(),
user: {
name: 'John Doe',
age: 25
}
}
},
methods: {
formatDate(date) {
return date.toLocaleDateString()
},
getMessage() {
return `当前时间:${new Date().toLocaleTimeString()}`
}
}
}
</script>
1.2 属性绑定
<template>
<view class="container">
<!-- 基础属性绑定 -->
<image :src="imageSrc" :alt="imageAlt" />
<input :value="inputValue" :placeholder="placeholder" />
<!-- 布尔属性绑定 -->
<button :disabled="isDisabled">按钮</button>
<input :readonly="isReadonly" />
<!-- 动态属性名 -->
<view :[attributeName]="attributeValue">动态属性</view>
<!-- 多个属性绑定 -->
<view v-bind="objectProps">对象属性绑定</view>
</view>
</template>
<script>
export default {
data() {
return {
imageSrc: '/static/logo.png',
imageAlt: 'Logo',
inputValue: 'Hello',
placeholder: '请输入内容',
isDisabled: false,
isReadonly: true,
attributeName: 'title',
attributeValue: '这是一个标题',
objectProps: {
id: 'my-view',
class: 'highlight',
'data-type': 'container'
}
}
}
}
</script>
1.3 样式绑定
<template>
<view class="container">
<!-- 类名绑定 -->
<view :class="{ active: isActive, disabled: isDisabled }">条件类名</view>
<view :class="[baseClass, { active: isActive }]">数组类名</view>
<view :class="computedClass">计算类名</view>
<!-- 内联样式绑定 -->
<view :style="{ color: textColor, fontSize: fontSize + 'rpx' }">内联样式</view>
<view :style="[baseStyle, activeStyle]">数组样式</view>
<view :style="computedStyle">计算样式</view>
<!-- 动态样式 -->
<view
class="box"
:style="{
backgroundColor: bgColor,
transform: `translateX(${translateX}rpx) scale(${scale})`,
transition: 'all 0.3s ease'
}"
>
动态变换
</view>
</view>
</template>
<script>
export default {
data() {
return {
isActive: true,
isDisabled: false,
baseClass: 'base',
textColor: '#333',
fontSize: 28,
bgColor: '#007aff',
translateX: 0,
scale: 1,
baseStyle: {
padding: '20rpx',
margin: '10rpx'
},
activeStyle: {
border: '2rpx solid #007aff'
}
}
},
computed: {
computedClass() {
return {
'theme-dark': this.isDarkMode,
'size-large': this.isLargeSize
}
},
computedStyle() {
return {
width: this.boxWidth + 'rpx',
height: this.boxHeight + 'rpx',
borderRadius: this.borderRadius + 'rpx'
}
}
}
}
</script>
2. 双向数据绑定
2.1 v-model基础用法
<template>
<view class="form">
<!-- 文本输入 -->
<input v-model="username" placeholder="用户名" />
<textarea v-model="description" placeholder="描述" />
<!-- 数字输入 -->
<input v-model.number="age" type="number" placeholder="年龄" />
<!-- 复选框 -->
<checkbox v-model="agreed">同意协议</checkbox>
<!-- 单选框 -->
<radio-group v-model="gender">
<radio value="male">男</radio>
<radio value="female">女</radio>
</radio-group>
<!-- 选择器 -->
<picker v-model="selectedCity" :range="cities">
<view class="picker-text">{{ cities[selectedCity] || '请选择城市' }}</view>
</picker>
<!-- 开关 -->
<switch v-model="isEnabled" />
<!-- 滑块 -->
<slider v-model="volume" min="0" max="100" />
<!-- 显示绑定的数据 -->
<view class="result">
<text>用户名:{{ username }}</text>
<text>年龄:{{ age }}</text>
<text>描述:{{ description }}</text>
<text>同意协议:{{ agreed }}</text>
<text>性别:{{ gender }}</text>
<text>城市:{{ cities[selectedCity] }}</text>
<text>启用状态:{{ isEnabled }}</text>
<text>音量:{{ volume }}</text>
</view>
</view>
</template>
<script>
export default {
data() {
return {
username: '',
age: null,
description: '',
agreed: false,
gender: '',
selectedCity: 0,
cities: ['北京', '上海', '广州', '深圳'],
isEnabled: false,
volume: 50
}
}
}
</script>
2.2 自定义组件的v-model
<!-- 自定义输入组件 -->
<!-- components/custom-input/custom-input.vue -->
<template>
<view class="custom-input">
<text class="label" v-if="label">{{ label }}</text>
<input
class="input"
:value="value"
:placeholder="placeholder"
:type="type"
@input="onInput"
@focus="onFocus"
@blur="onBlur"
/>
<view class="error" v-if="error">{{ error }}</view>
</view>
</template>
<script>
export default {
name: 'CustomInput',
props: {
value: {
type: [String, Number],
default: ''
},
label: String,
placeholder: String,
type: {
type: String,
default: 'text'
},
error: String
},
methods: {
onInput(e) {
this.$emit('input', e.detail.value)
},
onFocus(e) {
this.$emit('focus', e)
},
onBlur(e) {
this.$emit('blur', e)
}
}
}
</script>
<!-- 使用自定义组件 -->
<template>
<view>
<custom-input
v-model="formData.username"
label="用户名"
placeholder="请输入用户名"
:error="errors.username"
/>
<custom-input
v-model="formData.email"
label="邮箱"
type="email"
placeholder="请输入邮箱"
:error="errors.email"
/>
</view>
</template>
<script>
import CustomInput from '@/components/custom-input/custom-input.vue'
export default {
components: {
CustomInput
},
data() {
return {
formData: {
username: '',
email: ''
},
errors: {}
}
}
}
</script>
2.3 v-model修饰符
<template>
<view class="form">
<!-- .lazy - 在change事件后同步 -->
<input v-model.lazy="lazyValue" placeholder="懒加载绑定" />
<!-- .number - 自动转换为数字 -->
<input v-model.number="numberValue" type="number" placeholder="数字" />
<!-- .trim - 自动过滤首尾空格 -->
<input v-model.trim="trimValue" placeholder="自动去除空格" />
<!-- 组合使用修饰符 -->
<input v-model.lazy.trim="combinedValue" placeholder="组合修饰符" />
<!-- 显示结果 -->
<view class="result">
<text>懒加载值:{{ lazyValue }}</text>
<text>数字值:{{ numberValue }} (类型:{{ typeof numberValue }})</text>
<text>去空格值:'{{ trimValue }}'</text>
<text>组合值:'{{ combinedValue }}'</text>
</view>
</view>
</template>
<script>
export default {
data() {
return {
lazyValue: '',
numberValue: 0,
trimValue: '',
combinedValue: ''
}
}
}
</script>
3. 事件处理
3.1 基础事件绑定
<template>
<view class="container">
<!-- 基础事件绑定 -->
<button @click="handleClick">点击事件</button>
<button @tap="handleTap">轻触事件</button>
<!-- 传递参数 -->
<button @click="handleClickWithParams('hello', 123)">传递参数</button>
<!-- 传递事件对象 -->
<button @click="handleClickWithEvent($event)">传递事件对象</button>
<!-- 传递参数和事件对象 -->
<button @click="handleClickWithBoth('data', $event)">传递参数和事件</button>
<!-- 内联处理器 -->
<button @click="count++">计数:{{ count }}</button>
<!-- 多个事件处理器 -->
<button @click="handleClick1(); handleClick2()">多个处理器</button>
</view>
</template>
<script>
export default {
data() {
return {
count: 0
}
},
methods: {
handleClick() {
console.log('按钮被点击了')
uni.showToast({
title: '点击成功',
icon: 'success'
})
},
handleTap() {
console.log('按钮被轻触了')
},
handleClickWithParams(message, number) {
console.log('接收到参数:', message, number)
},
handleClickWithEvent(event) {
console.log('事件对象:', event)
console.log('事件类型:', event.type)
console.log('目标元素:', event.target)
},
handleClickWithBoth(data, event) {
console.log('数据:', data)
console.log('事件:', event)
},
handleClick1() {
console.log('处理器1')
},
handleClick2() {
console.log('处理器2')
}
}
}
</script>
3.2 事件修饰符
<template>
<view class="container">
<!-- .stop - 阻止事件冒泡 -->
<view class="outer" @click="handleOuterClick">
<view class="inner" @click.stop="handleInnerClick">
阻止冒泡
</view>
</view>
<!-- .prevent - 阻止默认行为 -->
<form @submit.prevent="handleSubmit">
<button form-type="submit">提交表单</button>
</form>
<!-- .capture - 使用事件捕获模式 -->
<view class="capture-outer" @click.capture="handleCaptureOuter">
<view class="capture-inner" @click="handleCaptureInner">
事件捕获
</view>
</view>
<!-- .self - 只在事件目标是元素本身时触发 -->
<view class="self-container" @click.self="handleSelfClick">
<view class="self-child">子元素</view>
点击容器本身才触发
</view>
<!-- .once - 事件只触发一次 -->
<button @click.once="handleOnceClick">只能点击一次</button>
<!-- 组合修饰符 -->
<view @click.stop.prevent="handleCombined">组合修饰符</view>
</view>
</template>
<script>
export default {
methods: {
handleOuterClick() {
console.log('外层点击')
},
handleInnerClick() {
console.log('内层点击(阻止冒泡)')
},
handleSubmit() {
console.log('表单提交(阻止默认行为)')
},
handleCaptureOuter() {
console.log('外层捕获')
},
handleCaptureInner() {
console.log('内层点击')
},
handleSelfClick() {
console.log('只有点击容器本身才触发')
},
handleOnceClick() {
console.log('这个事件只会触发一次')
uni.showToast({
title: '只能点击一次',
icon: 'none'
})
},
handleCombined() {
console.log('组合修饰符:阻止冒泡和默认行为')
}
}
}
</script>
<style>
.outer {
padding: 40rpx;
background-color: #f0f0f0;
margin: 20rpx 0;
}
.inner {
padding: 20rpx;
background-color: #007aff;
color: white;
text-align: center;
}
.capture-outer {
padding: 40rpx;
background-color: #ff6b6b;
margin: 20rpx 0;
}
.capture-inner {
padding: 20rpx;
background-color: #4ecdc4;
color: white;
text-align: center;
}
.self-container {
padding: 40rpx;
background-color: #ffe66d;
margin: 20rpx 0;
text-align: center;
}
.self-child {
padding: 20rpx;
background-color: #ff6b6b;
color: white;
display: inline-block;
}
</style>
3.3 触摸事件
<template>
<view class="container">
<!-- 触摸事件 -->
<view
class="touch-area"
@touchstart="handleTouchStart"
@touchmove="handleTouchMove"
@touchend="handleTouchEnd"
@touchcancel="handleTouchCancel"
>
<text>触摸区域</text>
<text>起始位置:{{ startPosition.x }}, {{ startPosition.y }}</text>
<text>当前位置:{{ currentPosition.x }}, {{ currentPosition.y }}</text>
<text>移动距离:{{ moveDistance.x }}, {{ moveDistance.y }}</text>
</view>
<!-- 长按事件 -->
<view
class="longpress-area"
@longpress="handleLongPress"
@longtap="handleLongTap"
>
长按区域
</view>
<!-- 手势事件 -->
<view
class="gesture-area"
@touchstart="handleGestureStart"
@touchmove="handleGestureMove"
@touchend="handleGestureEnd"
>
<text>手势识别区域</text>
<text>手势:{{ gesture }}</text>
</view>
</view>
</template>
<script>
export default {
data() {
return {
startPosition: { x: 0, y: 0 },
currentPosition: { x: 0, y: 0 },
moveDistance: { x: 0, y: 0 },
gesture: '无',
gestureStartTime: 0,
gestureStartPos: { x: 0, y: 0 }
}
},
methods: {
handleTouchStart(e) {
const touch = e.touches[0]
this.startPosition = {
x: Math.round(touch.clientX),
y: Math.round(touch.clientY)
}
this.currentPosition = { ...this.startPosition }
this.moveDistance = { x: 0, y: 0 }
console.log('触摸开始:', this.startPosition)
},
handleTouchMove(e) {
const touch = e.touches[0]
this.currentPosition = {
x: Math.round(touch.clientX),
y: Math.round(touch.clientY)
}
this.moveDistance = {
x: this.currentPosition.x - this.startPosition.x,
y: this.currentPosition.y - this.startPosition.y
}
},
handleTouchEnd(e) {
console.log('触摸结束,总移动距离:', this.moveDistance)
},
handleTouchCancel(e) {
console.log('触摸取消')
},
handleLongPress(e) {
console.log('长按事件')
uni.showToast({
title: '长按触发',
icon: 'none'
})
},
handleLongTap(e) {
console.log('长按轻触事件')
},
// 手势识别
handleGestureStart(e) {
const touch = e.touches[0]
this.gestureStartTime = Date.now()
this.gestureStartPos = {
x: touch.clientX,
y: touch.clientY
}
this.gesture = '开始'
},
handleGestureMove(e) {
const touch = e.touches[0]
const deltaX = touch.clientX - this.gestureStartPos.x
const deltaY = touch.clientY - this.gestureStartPos.y
if (Math.abs(deltaX) > Math.abs(deltaY)) {
this.gesture = deltaX > 0 ? '右滑' : '左滑'
} else {
this.gesture = deltaY > 0 ? '下滑' : '上滑'
}
},
handleGestureEnd(e) {
const duration = Date.now() - this.gestureStartTime
if (duration < 200) {
this.gesture = '快速' + this.gesture
}
setTimeout(() => {
this.gesture = '无'
}, 1000)
}
}
}
</script>
<style>
.touch-area {
width: 600rpx;
height: 400rpx;
background-color: #e3f2fd;
border: 2rpx solid #2196f3;
margin: 20rpx auto;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
border-radius: 10rpx;
}
.longpress-area {
width: 400rpx;
height: 200rpx;
background-color: #fff3e0;
border: 2rpx solid #ff9800;
margin: 20rpx auto;
display: flex;
justify-content: center;
align-items: center;
border-radius: 10rpx;
font-size: 32rpx;
color: #ff9800;
}
.gesture-area {
width: 600rpx;
height: 300rpx;
background-color: #f3e5f5;
border: 2rpx solid #9c27b0;
margin: 20rpx auto;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
border-radius: 10rpx;
}
</style>
4. 表单处理
4.1 表单验证
<template>
<view class="form-container">
<form @submit="handleSubmit">
<!-- 用户名 -->
<view class="form-item">
<text class="label">用户名 *</text>
<input
v-model="form.username"
class="input"
:class="{ error: errors.username }"
placeholder="请输入用户名"
@blur="validateUsername"
/>
<text class="error-text" v-if="errors.username">{{ errors.username }}</text>
</view>
<!-- 邮箱 -->
<view class="form-item">
<text class="label">邮箱 *</text>
<input
v-model="form.email"
class="input"
:class="{ error: errors.email }"
type="email"
placeholder="请输入邮箱"
@blur="validateEmail"
/>
<text class="error-text" v-if="errors.email">{{ errors.email }}</text>
</view>
<!-- 密码 -->
<view class="form-item">
<text class="label">密码 *</text>
<input
v-model="form.password"
class="input"
:class="{ error: errors.password }"
type="password"
placeholder="请输入密码"
@blur="validatePassword"
/>
<text class="error-text" v-if="errors.password">{{ errors.password }}</text>
</view>
<!-- 确认密码 -->
<view class="form-item">
<text class="label">确认密码 *</text>
<input
v-model="form.confirmPassword"
class="input"
:class="{ error: errors.confirmPassword }"
type="password"
placeholder="请确认密码"
@blur="validateConfirmPassword"
/>
<text class="error-text" v-if="errors.confirmPassword">{{ errors.confirmPassword }}</text>
</view>
<!-- 手机号 -->
<view class="form-item">
<text class="label">手机号</text>
<input
v-model="form.phone"
class="input"
:class="{ error: errors.phone }"
type="number"
placeholder="请输入手机号"
maxlength="11"
@blur="validatePhone"
/>
<text class="error-text" v-if="errors.phone">{{ errors.phone }}</text>
</view>
<!-- 年龄 -->
<view class="form-item">
<text class="label">年龄</text>
<input
v-model.number="form.age"
class="input"
:class="{ error: errors.age }"
type="number"
placeholder="请输入年龄"
@blur="validateAge"
/>
<text class="error-text" v-if="errors.age">{{ errors.age }}</text>
</view>
<!-- 性别 -->
<view class="form-item">
<text class="label">性别</text>
<radio-group v-model="form.gender" @change="handleGenderChange">
<label class="radio-item">
<radio value="male" :checked="form.gender === 'male'" />男
</label>
<label class="radio-item">
<radio value="female" :checked="form.gender === 'female'" />女
</label>
</radio-group>
</view>
<!-- 兴趣爱好 -->
<view class="form-item">
<text class="label">兴趣爱好</text>
<checkbox-group @change="handleHobbiesChange">
<label class="checkbox-item" v-for="hobby in hobbies" :key="hobby.value">
<checkbox :value="hobby.value" :checked="form.hobbies.includes(hobby.value)" />
{{ hobby.label }}
</label>
</checkbox-group>
</view>
<!-- 同意协议 -->
<view class="form-item">
<label class="checkbox-item">
<checkbox v-model="form.agreed" :checked="form.agreed" />
我已阅读并同意<text class="link" @click="showAgreement">用户协议</text>
</label>
<text class="error-text" v-if="errors.agreed">{{ errors.agreed }}</text>
</view>
<!-- 提交按钮 -->
<button
class="submit-btn"
:class="{ disabled: !isFormValid }"
:disabled="!isFormValid"
form-type="submit"
>
注册
</button>
</form>
</view>
</template>
<script>
export default {
data() {
return {
form: {
username: '',
email: '',
password: '',
confirmPassword: '',
phone: '',
age: null,
gender: '',
hobbies: [],
agreed: false
},
errors: {},
hobbies: [
{ label: '阅读', value: 'reading' },
{ label: '运动', value: 'sports' },
{ label: '音乐', value: 'music' },
{ label: '旅行', value: 'travel' }
]
}
},
computed: {
isFormValid() {
return Object.keys(this.errors).length === 0 &&
this.form.username &&
this.form.email &&
this.form.password &&
this.form.confirmPassword &&
this.form.agreed
}
},
methods: {
validateUsername() {
if (!this.form.username) {
this.$set(this.errors, 'username', '用户名不能为空')
} else if (this.form.username.length < 3) {
this.$set(this.errors, 'username', '用户名至少3个字符')
} else {
this.$delete(this.errors, 'username')
}
},
validateEmail() {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!this.form.email) {
this.$set(this.errors, 'email', '邮箱不能为空')
} else if (!emailRegex.test(this.form.email)) {
this.$set(this.errors, 'email', '邮箱格式不正确')
} else {
this.$delete(this.errors, 'email')
}
},
validatePassword() {
if (!this.form.password) {
this.$set(this.errors, 'password', '密码不能为空')
} else if (this.form.password.length < 6) {
this.$set(this.errors, 'password', '密码至少6个字符')
} else {
this.$delete(this.errors, 'password')
// 如果确认密码已输入,重新验证确认密码
if (this.form.confirmPassword) {
this.validateConfirmPassword()
}
}
},
validateConfirmPassword() {
if (!this.form.confirmPassword) {
this.$set(this.errors, 'confirmPassword', '请确认密码')
} else if (this.form.password !== this.form.confirmPassword) {
this.$set(this.errors, 'confirmPassword', '两次密码输入不一致')
} else {
this.$delete(this.errors, 'confirmPassword')
}
},
validatePhone() {
const phoneRegex = /^1[3-9]\d{9}$/
if (this.form.phone && !phoneRegex.test(this.form.phone)) {
this.$set(this.errors, 'phone', '手机号格式不正确')
} else {
this.$delete(this.errors, 'phone')
}
},
validateAge() {
if (this.form.age !== null && (this.form.age < 1 || this.form.age > 120)) {
this.$set(this.errors, 'age', '年龄必须在1-120之间')
} else {
this.$delete(this.errors, 'age')
}
},
handleGenderChange(e) {
this.form.gender = e.detail.value
},
handleHobbiesChange(e) {
this.form.hobbies = e.detail.value
},
validateForm() {
this.validateUsername()
this.validateEmail()
this.validatePassword()
this.validateConfirmPassword()
this.validatePhone()
this.validateAge()
if (!this.form.agreed) {
this.$set(this.errors, 'agreed', '请同意用户协议')
} else {
this.$delete(this.errors, 'agreed')
}
},
handleSubmit(e) {
e.preventDefault()
this.validateForm()
if (this.isFormValid) {
console.log('表单提交:', this.form)
uni.showToast({
title: '注册成功',
icon: 'success'
})
// 提交表单数据
this.submitForm()
} else {
uni.showToast({
title: '请检查表单信息',
icon: 'none'
})
}
},
async submitForm() {
try {
// 模拟API调用
const response = await this.registerUser(this.form)
console.log('注册成功:', response)
// 跳转到登录页或首页
uni.navigateTo({
url: '/pages/login/login'
})
} catch (error) {
console.error('注册失败:', error)
uni.showToast({
title: '注册失败,请重试',
icon: 'none'
})
}
},
registerUser(userData) {
// 模拟API调用
return new Promise((resolve, reject) => {
setTimeout(() => {
if (Math.random() > 0.1) {
resolve({ success: true, message: '注册成功' })
} else {
reject(new Error('网络错误'))
}
}, 1000)
})
},
showAgreement() {
uni.navigateTo({
url: '/pages/agreement/agreement'
})
}
}
}
</script>
<style>
.form-container {
padding: 40rpx;
}
.form-item {
margin-bottom: 40rpx;
}
.label {
display: block;
font-size: 28rpx;
color: #333;
margin-bottom: 10rpx;
}
.input {
width: 100%;
height: 80rpx;
padding: 0 20rpx;
border: 2rpx solid #e5e5e5;
border-radius: 8rpx;
font-size: 28rpx;
box-sizing: border-box;
}
.input.error {
border-color: #ff4757;
}
.error-text {
color: #ff4757;
font-size: 24rpx;
margin-top: 10rpx;
display: block;
}
.radio-item,
.checkbox-item {
display: inline-flex;
align-items: center;
margin-right: 40rpx;
margin-bottom: 20rpx;
font-size: 28rpx;
}
.link {
color: #007aff;
text-decoration: underline;
}
.submit-btn {
width: 100%;
height: 80rpx;
background-color: #007aff;
color: white;
border: none;
border-radius: 8rpx;
font-size: 32rpx;
margin-top: 40rpx;
}
.submit-btn.disabled {
background-color: #ccc;
color: #999;
}
</style>
5. 计算属性与侦听器
5.1 计算属性
<template>
<view class="container">
<!-- 基础计算属性 -->
<view class="section">
<text>原始消息:{{ message }}</text>
<text>反转消息:{{ reversedMessage }}</text>
<text>消息长度:{{ messageLength }}</text>
</view>
<!-- 购物车示例 -->
<view class="cart-section">
<text class="title">购物车</text>
<view class="cart-item" v-for="item in cartItems" :key="item.id">
<text>{{ item.name }}</text>
<text>数量:{{ item.quantity }}</text>
<text>单价:¥{{ item.price }}</text>
<text>小计:¥{{ item.quantity * item.price }}</text>
</view>
<view class="cart-summary">
<text>总数量:{{ totalQuantity }}</text>
<text>总金额:¥{{ totalPrice }}</text>
<text>平均单价:¥{{ averagePrice }}</text>
</view>
</view>
<!-- 用户信息示例 -->
<view class="user-section">
<input v-model="user.firstName" placeholder="名" />
<input v-model="user.lastName" placeholder="姓" />
<text>全名:{{ fullName }}</text>
<text>显示名:{{ displayName }}</text>
</view>
</view>
</template>
<script>
export default {
data() {
return {
message: 'Hello UniApp',
cartItems: [
{ id: 1, name: '商品A', quantity: 2, price: 100 },
{ id: 2, name: '商品B', quantity: 1, price: 200 },
{ id: 3, name: '商品C', quantity: 3, price: 50 }
],
user: {
firstName: '',
lastName: ''
}
}
},
computed: {
// 基础计算属性
reversedMessage() {
return this.message.split('').reverse().join('')
},
messageLength() {
return this.message.length
},
// 购物车计算属性
totalQuantity() {
return this.cartItems.reduce((total, item) => total + item.quantity, 0)
},
totalPrice() {
return this.cartItems.reduce((total, item) => total + (item.quantity * item.price), 0)
},
averagePrice() {
if (this.totalQuantity === 0) return 0
return (this.totalPrice / this.totalQuantity).toFixed(2)
},
// 用户信息计算属性
fullName() {
return `${this.user.firstName} ${this.user.lastName}`.trim()
},
displayName() {
if (this.fullName) {
return this.fullName
}
return '未设置姓名'
},
// 带getter和setter的计算属性
fullNameWithSetter: {
get() {
return `${this.user.firstName} ${this.user.lastName}`.trim()
},
set(value) {
const names = value.split(' ')
this.user.firstName = names[0] || ''
this.user.lastName = names[1] || ''
}
}
}
}
</script>
5.2 侦听器
<template>
<view class="container">
<view class="section">
<input v-model="searchKeyword" placeholder="搜索关键词" />
<text>搜索结果数量:{{ searchResults.length }}</text>
</view>
<view class="section">
<input v-model.number="count" type="number" placeholder="计数器" />
<text>当前值:{{ count }}</text>
<text>变化次数:{{ changeCount }}</text>
</view>
<view class="section">
<input v-model="user.name" placeholder="用户名" />
<input v-model="user.email" placeholder="邮箱" />
<text>用户信息变化次数:{{ userChangeCount }}</text>
</view>
</view>
</template>
<script>
export default {
data() {
return {
searchKeyword: '',
searchResults: [],
count: 0,
changeCount: 0,
user: {
name: '',
email: ''
},
userChangeCount: 0,
searchTimer: null
}
},
watch: {
// 基础侦听器
count(newVal, oldVal) {
console.log(`count changed from ${oldVal} to ${newVal}`)
this.changeCount++
},
// 深度侦听对象
user: {
handler(newVal, oldVal) {
console.log('user changed:', newVal)
this.userChangeCount++
// 保存用户信息到本地存储
this.saveUserInfo(newVal)
},
deep: true
},
// 立即执行的侦听器
searchKeyword: {
handler(newVal) {
// 防抖搜索
if (this.searchTimer) {
clearTimeout(this.searchTimer)
}
this.searchTimer = setTimeout(() => {
this.performSearch(newVal)
}, 500)
},
immediate: true
},
// 侦听对象的特定属性
'user.name'(newVal, oldVal) {
console.log(`user name changed from ${oldVal} to ${newVal}`)
if (newVal) {
this.validateUserName(newVal)
}
},
'user.email'(newVal, oldVal) {
console.log(`user email changed from ${oldVal} to ${newVal}`)
if (newVal) {
this.validateEmail(newVal)
}
}
},
methods: {
performSearch(keyword) {
if (!keyword) {
this.searchResults = []
return
}
console.log('执行搜索:', keyword)
// 模拟搜索API调用
this.searchResults = [
{ id: 1, title: `搜索结果1 - ${keyword}` },
{ id: 2, title: `搜索结果2 - ${keyword}` },
{ id: 3, title: `搜索结果3 - ${keyword}` }
]
},
saveUserInfo(userInfo) {
try {
uni.setStorageSync('userInfo', userInfo)
console.log('用户信息已保存')
} catch (error) {
console.error('保存用户信息失败:', error)
}
},
validateUserName(name) {
if (name.length < 2) {
uni.showToast({
title: '用户名至少2个字符',
icon: 'none'
})
}
},
validateEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!emailRegex.test(email)) {
uni.showToast({
title: '邮箱格式不正确',
icon: 'none'
})
}
}
},
// 使用$watch动态添加侦听器
mounted() {
// 动态侦听器
this.$watch('count', (newVal, oldVal) => {
if (newVal > 10) {
uni.showToast({
title: '计数超过10了!',
icon: 'none'
})
}
})
// 可以取消的侦听器
const unwatch = this.$watch('searchKeyword', (newVal) => {
console.log('动态侦听器:', newVal)
})
// 在某个条件下取消侦听
setTimeout(() => {
unwatch() // 取消侦听
}, 10000)
},
beforeDestroy() {
// 清理定时器
if (this.searchTimer) {
clearTimeout(this.searchTimer)
}
}
}
</script>
6. 总结
本章详细介绍了UniApp中的数据绑定与事件处理:
- 数据绑定:掌握了插值表达式、属性绑定和样式绑定的使用方法
- 双向绑定:学习了v-model的使用和自定义组件的双向绑定实现
- 事件处理:了解了各种事件的绑定方法和事件修饰符的使用
- 表单处理:实现了完整的表单验证和提交流程
- 计算属性:掌握了计算属性的定义和使用场景
- 侦听器:学习了watch的各种用法和最佳实践
这些知识是构建交互式UniApp应用的基础,下一章我们将学习生命周期与API调用。