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 组件设计原则
- 单一职责:每个组件只负责一个功能
- 可复用性:组件应该易于在不同场景中复用
- 可配置性:通过props提供灵活的配置选项
- 可扩展性:通过slot提供内容扩展能力
- 一致性:保持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中页面和组件的开发:
- 页面开发:掌握了页面结构、生命周期和页面间通信
- 组件开发:学习了组件的创建、注册和使用方法
- 内置组件:了解了常用内置组件的使用方法
- 样式动画:掌握了样式绑定和CSS动画的实现
- 最佳实践:学习了组件设计原则和开发规范
良好的页面和组件设计是构建高质量UniApp应用的关键,下一章我们将学习路由与导航的实现。