1. 页面开发基础

1.1 页面结构

UniApp页面采用Vue单文件组件格式,包含template、script、style三个部分:

<template>
    <view class="container">
        <text class="title">{{ title }}</text>
        <button @click="handleClick">点击我</button>
    </view>
</template>

<script>
export default {
    data() {
        return {
            title: 'Hello UniApp'
        }
    },
    onLoad() {
        console.log('页面加载完成')
    },
    methods: {
        handleClick() {
            uni.showToast({
                title: '按钮被点击了',
                icon: 'success'
            })
        }
    }
}
</script>

<style scoped>
.container {
    padding: 20rpx;
    display: flex;
    flex-direction: column;
    align-items: center;
}

.title {
    font-size: 32rpx;
    color: #333;
    margin-bottom: 20rpx;
}
</style>

1.2 页面生命周期

export default {
    // 页面加载时触发,只会调用一次
    onLoad(options) {
        console.log('页面加载', options)
        // 获取页面参数
        const { id, name } = options
    },
    
    // 页面显示时触发,每次显示都会调用
    onShow() {
        console.log('页面显示')
        // 刷新数据
        this.refreshData()
    },
    
    // 页面准备就绪时触发
    onReady() {
        console.log('页面准备就绪')
        // 可以进行DOM操作
    },
    
    // 页面隐藏时触发
    onHide() {
        console.log('页面隐藏')
        // 暂停定时器等
    },
    
    // 页面卸载时触发
    onUnload() {
        console.log('页面卸载')
        // 清理资源
    },
    
    // 下拉刷新
    onPullDownRefresh() {
        console.log('下拉刷新')
        // 刷新数据
        this.loadData().then(() => {
            uni.stopPullDownRefresh()
        })
    },
    
    // 上拉加载更多
    onReachBottom() {
        console.log('上拉加载更多')
        // 加载更多数据
        this.loadMoreData()
    },
    
    // 页面滚动
    onPageScroll(e) {
        console.log('页面滚动', e.scrollTop)
    },
    
    // 分享
    onShareAppMessage() {
        return {
            title: '分享标题',
            path: '/pages/index/index'
        }
    }
}

1.3 页面间通信

页面跳转传参

// 跳转并传递参数
uni.navigateTo({
    url: '/pages/detail/detail?id=123&name=test'
})

// 接收参数
export default {
    onLoad(options) {
        console.log(options.id)    // 123
        console.log(options.name)  // test
    }
}

页面间数据传递

// 使用uni.$emit发送事件
uni.$emit('dataUpdate', { data: 'new data' })

// 使用uni.$on监听事件
uni.$on('dataUpdate', (data) => {
    console.log('接收到数据', data)
})

// 页面卸载时移除监听
onUnload() {
    uni.$off('dataUpdate')
}

2. 组件开发

2.1 组件基础结构

<!-- components/custom-button/custom-button.vue -->
<template>
    <button 
        class="custom-button" 
        :class="buttonClass"
        :disabled="disabled"
        @click="handleClick"
    >
        <text class="button-text">{{ text }}</text>
    </button>
</template>

<script>
export default {
    name: 'CustomButton',
    props: {
        text: {
            type: String,
            default: '按钮'
        },
        type: {
            type: String,
            default: 'default',
            validator: (value) => {
                return ['default', 'primary', 'success', 'warning', 'danger'].includes(value)
            }
        },
        size: {
            type: String,
            default: 'medium'
        },
        disabled: {
            type: Boolean,
            default: false
        }
    },
    computed: {
        buttonClass() {
            return [
                `button-${this.type}`,
                `button-${this.size}`,
                {
                    'button-disabled': this.disabled
                }
            ]
        }
    },
    methods: {
        handleClick(e) {
            if (!this.disabled) {
                this.$emit('click', e)
            }
        }
    }
}
</script>

<style scoped>
.custom-button {
    border: none;
    border-radius: 8rpx;
    padding: 0;
    margin: 0;
    background: none;
    display: flex;
    align-items: center;
    justify-content: center;
    transition: all 0.3s;
}

.button-text {
    font-size: 28rpx;
}

/* 按钮类型样式 */
.button-default {
    background-color: #f8f8f8;
    border: 1rpx solid #e5e5e5;
}

.button-primary {
    background-color: #007aff;
    color: white;
}

.button-success {
    background-color: #4cd964;
    color: white;
}

.button-warning {
    background-color: #f0ad4e;
    color: white;
}

.button-danger {
    background-color: #dd524d;
    color: white;
}

/* 按钮尺寸样式 */
.button-small {
    height: 60rpx;
    padding: 0 20rpx;
}

.button-medium {
    height: 80rpx;
    padding: 0 30rpx;
}

.button-large {
    height: 100rpx;
    padding: 0 40rpx;
}

/* 禁用状态 */
.button-disabled {
    opacity: 0.6;
    cursor: not-allowed;
}
</style>

2.2 组件注册与使用

全局注册

// main.js
import Vue from 'vue'
import CustomButton from '@/components/custom-button/custom-button.vue'

// 全局注册组件
Vue.component('CustomButton', CustomButton)

局部注册

<template>
    <view>
        <custom-button 
            text="点击我" 
            type="primary" 
            size="large"
            @click="handleButtonClick"
        />
    </view>
</template>

<script>
import CustomButton from '@/components/custom-button/custom-button.vue'

export default {
    components: {
        CustomButton
    },
    methods: {
        handleButtonClick() {
            console.log('按钮被点击了')
        }
    }
}
</script>

2.3 高级组件开发

表单组件

<!-- components/form-item/form-item.vue -->
<template>
    <view class="form-item" :class="{ 'form-item-error': hasError }">
        <view class="form-label" v-if="label">
            <text class="label-text">{{ label }}</text>
            <text class="required-mark" v-if="required">*</text>
        </view>
        <view class="form-content">
            <slot></slot>
        </view>
        <view class="form-error" v-if="hasError">
            <text class="error-text">{{ errorMessage }}</text>
        </view>
    </view>
</template>

<script>
export default {
    name: 'FormItem',
    props: {
        label: String,
        required: Boolean,
        prop: String
    },
    inject: ['form'],
    computed: {
        hasError() {
            return this.form && this.form.errors[this.prop]
        },
        errorMessage() {
            return this.hasError ? this.form.errors[this.prop] : ''
        }
    }
}
</script>

<style scoped>
.form-item {
    margin-bottom: 30rpx;
}

.form-label {
    display: flex;
    align-items: center;
    margin-bottom: 10rpx;
}

.label-text {
    font-size: 28rpx;
    color: #333;
}

.required-mark {
    color: #ff4757;
    margin-left: 4rpx;
}

.form-content {
    position: relative;
}

.form-error {
    margin-top: 10rpx;
}

.error-text {
    font-size: 24rpx;
    color: #ff4757;
}

.form-item-error .form-content {
    border-color: #ff4757;
}
</style>

列表组件

<!-- components/list-view/list-view.vue -->
<template>
    <scroll-view 
        class="list-view"
        scroll-y
        :refresher-enabled="enableRefresh"
        :refresher-triggered="refreshing"
        @refresherrefresh="onRefresh"
        @scrolltolower="onLoadMore"
    >
        <view class="list-content">
            <slot name="header"></slot>
            
            <view class="list-items">
                <view 
                    class="list-item" 
                    v-for="(item, index) in list" 
                    :key="getItemKey(item, index)"
                    @click="onItemClick(item, index)"
                >
                    <slot :item="item" :index="index">
                        <text>{{ item.title || item.name || item }}</text>
                    </slot>
                </view>
            </view>
            
            <view class="list-loading" v-if="loading">
                <text>加载中...</text>
            </view>
            
            <view class="list-no-more" v-if="noMore">
                <text>没有更多数据了</text>
            </view>
            
            <slot name="footer"></slot>
        </view>
    </scroll-view>
</template>

<script>
export default {
    name: 'ListView',
    props: {
        list: {
            type: Array,
            default: () => []
        },
        loading: {
            type: Boolean,
            default: false
        },
        noMore: {
            type: Boolean,
            default: false
        },
        enableRefresh: {
            type: Boolean,
            default: true
        },
        itemKey: {
            type: String,
            default: 'id'
        }
    },
    data() {
        return {
            refreshing: false
        }
    },
    methods: {
        getItemKey(item, index) {
            return item[this.itemKey] || index
        },
        onItemClick(item, index) {
            this.$emit('item-click', { item, index })
        },
        async onRefresh() {
            this.refreshing = true
            try {
                await this.$emit('refresh')
            } finally {
                this.refreshing = false
            }
        },
        onLoadMore() {
            if (!this.loading && !this.noMore) {
                this.$emit('load-more')
            }
        }
    }
}
</script>

<style scoped>
.list-view {
    height: 100%;
}

.list-content {
    min-height: 100%;
}

.list-item {
    padding: 20rpx;
    border-bottom: 1rpx solid #f0f0f0;
}

.list-item:active {
    background-color: #f8f8f8;
}

.list-loading,
.list-no-more {
    padding: 20rpx;
    text-align: center;
    color: #999;
    font-size: 24rpx;
}
</style>

2.4 组件通信

父子组件通信

<!-- 父组件 -->
<template>
    <view>
        <child-component 
            :message="parentMessage"
            @child-event="handleChildEvent"
        />
    </view>
</template>

<script>
import ChildComponent from './child-component.vue'

export default {
    components: {
        ChildComponent
    },
    data() {
        return {
            parentMessage: 'Hello from parent'
        }
    },
    methods: {
        handleChildEvent(data) {
            console.log('接收到子组件事件', data)
        }
    }
}
</script>
<!-- 子组件 -->
<template>
    <view>
        <text>{{ message }}</text>
        <button @click="sendToParent">发送给父组件</button>
    </view>
</template>

<script>
export default {
    props: {
        message: String
    },
    methods: {
        sendToParent() {
            this.$emit('child-event', { data: 'Hello from child' })
        }
    }
}
</script>

跨组件通信

// 事件总线
// utils/event-bus.js
import Vue from 'vue'
export default new Vue()

// 发送事件
import EventBus from '@/utils/event-bus.js'
EventBus.$emit('custom-event', data)

// 监听事件
import EventBus from '@/utils/event-bus.js'
EventBus.$on('custom-event', (data) => {
    console.log(data)
})

// 移除监听
EventBus.$off('custom-event')

3. 内置组件使用

3.1 视图容器组件

view组件

<template>
    <view class="container">
        <view class="header">头部内容</view>
        <view class="content">主要内容</view>
        <view class="footer">底部内容</view>
    </view>
</template>

scroll-view组件

<template>
    <scroll-view 
        class="scroll-area"
        scroll-y
        :scroll-top="scrollTop"
        @scroll="onScroll"
        @scrolltoupper="onScrollToUpper"
        @scrolltolower="onScrollToLower"
    >
        <view class="scroll-content">
            <view v-for="item in list" :key="item.id">
                {{ item.title }}
            </view>
        </view>
    </scroll-view>
</template>

<script>
export default {
    data() {
        return {
            scrollTop: 0,
            list: []
        }
    },
    methods: {
        onScroll(e) {
            this.scrollTop = e.detail.scrollTop
        },
        onScrollToUpper() {
            console.log('滚动到顶部')
        },
        onScrollToLower() {
            console.log('滚动到底部')
        }
    }
}
</script>

swiper组件

<template>
    <swiper 
        class="swiper"
        :indicator-dots="true"
        :autoplay="true"
        :interval="3000"
        :duration="500"
        @change="onSwiperChange"
    >
        <swiper-item v-for="(item, index) in banners" :key="index">
            <image :src="item.image" class="swiper-image" />
        </swiper-item>
    </swiper>
</template>

<script>
export default {
    data() {
        return {
            banners: [
                { image: '/static/banner1.jpg' },
                { image: '/static/banner2.jpg' },
                { image: '/static/banner3.jpg' }
            ]
        }
    },
    methods: {
        onSwiperChange(e) {
            console.log('当前轮播图索引', e.detail.current)
        }
    }
}
</script>

<style>
.swiper {
    height: 300rpx;
}

.swiper-image {
    width: 100%;
    height: 100%;
}
</style>

3.2 基础内容组件

text组件

<template>
    <view>
        <text class="title">标题文本</text>
        <text class="content" selectable>可选择的文本内容</text>
        <text class="phone" @click="makeCall">电话:138****8888</text>
    </view>
</template>

<script>
export default {
    methods: {
        makeCall() {
            uni.makePhoneCall({
                phoneNumber: '13888888888'
            })
        }
    }
}
</script>

image组件

<template>
    <view>
        <image 
            :src="imageSrc"
            class="image"
            mode="aspectFill"
            :lazy-load="true"
            @load="onImageLoad"
            @error="onImageError"
        />
    </view>
</template>

<script>
export default {
    data() {
        return {
            imageSrc: '/static/placeholder.jpg'
        }
    },
    methods: {
        onImageLoad(e) {
            console.log('图片加载成功', e.detail)
        },
        onImageError(e) {
            console.log('图片加载失败', e.detail)
            // 设置默认图片
            this.imageSrc = '/static/default.jpg'
        }
    }
}
</script>

<style>
.image {
    width: 200rpx;
    height: 200rpx;
    border-radius: 10rpx;
}
</style>

3.3 表单组件

input组件

<template>
    <view class="form">
        <input 
            class="input"
            type="text"
            placeholder="请输入用户名"
            v-model="username"
            @input="onInput"
            @focus="onFocus"
            @blur="onBlur"
        />
        
        <input 
            class="input"
            type="password"
            placeholder="请输入密码"
            v-model="password"
            :password="true"
        />
        
        <input 
            class="input"
            type="number"
            placeholder="请输入手机号"
            v-model="phone"
            maxlength="11"
        />
    </view>
</template>

<script>
export default {
    data() {
        return {
            username: '',
            password: '',
            phone: ''
        }
    },
    methods: {
        onInput(e) {
            console.log('输入内容', e.detail.value)
        },
        onFocus() {
            console.log('获得焦点')
        },
        onBlur() {
            console.log('失去焦点')
        }
    }
}
</script>

picker组件

<template>
    <view>
        <picker 
            mode="selector"
            :range="cities"
            :value="cityIndex"
            @change="onCityChange"
        >
            <view class="picker-item">
                选择城市:{{ cities[cityIndex] }}
            </view>
        </picker>
        
        <picker 
            mode="date"
            :value="date"
            @change="onDateChange"
        >
            <view class="picker-item">
                选择日期:{{ date }}
            </view>
        </picker>
        
        <picker 
            mode="time"
            :value="time"
            @change="onTimeChange"
        >
            <view class="picker-item">
                选择时间:{{ time }}
            </view>
        </picker>
    </view>
</template>

<script>
export default {
    data() {
        return {
            cities: ['北京', '上海', '广州', '深圳'],
            cityIndex: 0,
            date: '2023-01-01',
            time: '12:00'
        }
    },
    methods: {
        onCityChange(e) {
            this.cityIndex = e.detail.value
        },
        onDateChange(e) {
            this.date = e.detail.value
        },
        onTimeChange(e) {
            this.time = e.detail.value
        }
    }
}
</script>

4. 组件样式与动画

4.1 样式绑定

<template>
    <view>
        <!-- 类名绑定 -->
        <view :class="{ active: isActive, disabled: isDisabled }">动态类名</view>
        <view :class="[baseClass, { active: isActive }]">数组类名</view>
        
        <!-- 内联样式绑定 -->
        <view :style="{ color: textColor, fontSize: fontSize + 'rpx' }">动态样式</view>
        <view :style="[baseStyle, activeStyle]">数组样式</view>
    </view>
</template>

<script>
export default {
    data() {
        return {
            isActive: true,
            isDisabled: false,
            baseClass: 'base',
            textColor: '#333',
            fontSize: 28,
            baseStyle: {
                padding: '20rpx'
            },
            activeStyle: {
                backgroundColor: '#007aff'
            }
        }
    }
}
</script>

4.2 CSS动画

<template>
    <view>
        <view class="fade-item" :class="{ show: showItem }">淡入淡出</view>
        <view class="slide-item" :class="{ active: slideActive }">滑动效果</view>
        <view class="rotate-item" @click="rotate">旋转动画</view>
    </view>
</template>

<script>
export default {
    data() {
        return {
            showItem: false,
            slideActive: false,
            rotateAngle: 0
        }
    },
    methods: {
        rotate() {
            this.rotateAngle += 180
        }
    }
}
</script>

<style>
/* 淡入淡出动画 */
.fade-item {
    opacity: 0;
    transition: opacity 0.3s ease;
}

.fade-item.show {
    opacity: 1;
}

/* 滑动动画 */
.slide-item {
    transform: translateX(-100%);
    transition: transform 0.3s ease;
}

.slide-item.active {
    transform: translateX(0);
}

/* 旋转动画 */
.rotate-item {
    transition: transform 0.3s ease;
    cursor: pointer;
}

.rotate-item:active {
    transform: rotate(180deg);
}

/* 关键帧动画 */
@keyframes bounce {
    0%, 20%, 50%, 80%, 100% {
        transform: translateY(0);
    }
    40% {
        transform: translateY(-30rpx);
    }
    60% {
        transform: translateY(-15rpx);
    }
}

.bounce {
    animation: bounce 1s infinite;
}
</style>

5. 组件最佳实践

5.1 组件设计原则

  1. 单一职责:每个组件只负责一个功能
  2. 可复用性:组件应该易于在不同场景中复用
  3. 可配置性:通过props提供灵活的配置选项
  4. 可扩展性:通过slot提供内容扩展能力
  5. 一致性:保持API和样式的一致性

5.2 组件命名规范

// 组件文件命名:kebab-case
// custom-button.vue
// user-profile.vue
// product-list.vue

// 组件名称:PascalCase
export default {
    name: 'CustomButton'
}

// 组件使用:kebab-case
// <custom-button></custom-button>

5.3 组件文档化

<!--
组件名称: CustomButton
组件描述: 自定义按钮组件

Props:
- text: String - 按钮文本
- type: String - 按钮类型 (default|primary|success|warning|danger)
- size: String - 按钮尺寸 (small|medium|large)
- disabled: Boolean - 是否禁用

Events:
- click: 点击事件

Slots:
- default: 默认插槽

示例:
<custom-button 
    text="点击我" 
    type="primary" 
    size="large"
    @click="handleClick"
/>
-->

<template>
    <!-- 组件模板 -->
</template>

6. 总结

本章详细介绍了UniApp中页面和组件的开发:

  1. 页面开发:掌握了页面结构、生命周期和页面间通信
  2. 组件开发:学习了组件的创建、注册和使用方法
  3. 内置组件:了解了常用内置组件的使用方法
  4. 样式动画:掌握了样式绑定和CSS动画的实现
  5. 最佳实践:学习了组件设计原则和开发规范

良好的页面和组件设计是构建高质量UniApp应用的关键,下一章我们将学习路由与导航的实现。