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中的数据绑定与事件处理:

  1. 数据绑定:掌握了插值表达式、属性绑定和样式绑定的使用方法
  2. 双向绑定:学习了v-model的使用和自定义组件的双向绑定实现
  3. 事件处理:了解了各种事件的绑定方法和事件修饰符的使用
  4. 表单处理:实现了完整的表单验证和提交流程
  5. 计算属性:掌握了计算属性的定义和使用场景
  6. 侦听器:学习了watch的各种用法和最佳实践

这些知识是构建交互式UniApp应用的基础,下一章我们将学习生命周期与API调用。