3.1 Vue 3基础语法
3.1.1 响应式数据系统
1. Composition API响应式数据
<template>
<view class="reactive-demo">
<view class="counter-section">
<text class="counter-text">计数器: {{ count }}</text>
<button @click="increment">增加</button>
<button @click="decrement">减少</button>
</view>
<view class="user-section">
<text class="user-info">用户: {{ user.name }} ({{ user.age }}岁)</text>
<button @click="updateUser">更新用户信息</button>
</view>
<view class="list-section">
<text class="list-title">待办事项 ({{ todos.length }})</text>
<view v-for="todo in todos" :key="todo.id" class="todo-item">
<text :class="{ completed: todo.completed }">{{ todo.text }}</text>
<button @click="toggleTodo(todo.id)">切换状态</button>
</view>
<button @click="addTodo">添加待办</button>
</view>
</view>
</template>
<script setup>
import { ref, reactive, computed, watch, onMounted } from 'vue'
// ref 创建基本类型响应式数据
const count = ref(0)
// reactive 创建对象响应式数据
const user = reactive({
name: '张三',
age: 25,
email: 'zhangsan@example.com'
})
// reactive 创建数组响应式数据
const todos = reactive([
{ id: 1, text: '学习UniApp', completed: false },
{ id: 2, text: '完成项目', completed: true }
])
// 计算属性
const completedTodos = computed(() => {
return todos.filter(todo => todo.completed)
})
const completedCount = computed(() => completedTodos.value.length)
// 方法定义
const increment = () => {
count.value++
}
const decrement = () => {
count.value--
}
const updateUser = () => {
user.name = '李四'
user.age = 30
}
const toggleTodo = (id) => {
const todo = todos.find(item => item.id === id)
if (todo) {
todo.completed = !todo.completed
}
}
const addTodo = () => {
const newId = Math.max(...todos.map(t => t.id)) + 1
todos.push({
id: newId,
text: `新待办事项 ${newId}`,
completed: false
})
}
// 监听器
watch(count, (newValue, oldValue) => {
console.log(`计数器从 ${oldValue} 变为 ${newValue}`)
if (newValue > 10) {
uni.showToast({
title: '计数器超过10了!',
icon: 'none'
})
}
})
watch(user, (newUser) => {
console.log('用户信息更新:', newUser)
}, { deep: true })
watch(todos, (newTodos) => {
console.log('待办事项更新,总数:', newTodos.length)
// 保存到本地存储
uni.setStorageSync('todos', newTodos)
}, { deep: true })
// 生命周期
onMounted(() => {
console.log('组件已挂载')
// 从本地存储加载数据
const savedTodos = uni.getStorageSync('todos')
if (savedTodos && savedTodos.length > 0) {
todos.splice(0, todos.length, ...savedTodos)
}
})
</script>
<style lang="scss" scoped>
.reactive-demo {
padding: 20rpx;
.counter-section,
.user-section,
.list-section {
margin-bottom: 40rpx;
padding: 20rpx;
border: 1px solid #eee;
border-radius: 8rpx;
}
.counter-text,
.user-info,
.list-title {
display: block;
margin-bottom: 20rpx;
font-size: 32rpx;
font-weight: bold;
}
.todo-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10rpx 0;
border-bottom: 1px solid #f0f0f0;
&:last-child {
border-bottom: none;
}
.completed {
text-decoration: line-through;
color: #999;
}
}
button {
margin: 10rpx;
padding: 10rpx 20rpx;
background-color: #007aff;
color: white;
border: none;
border-radius: 4rpx;
font-size: 28rpx;
}
}
</style>
3. swiper组件 - 轮播容器
<template>
<view class="swiper-demo">
<text class="title">Swiper组件演示</text>
<!-- 基础轮播 -->
<view class="section">
<text class="section-title">基础轮播</text>
<swiper
class="basic-swiper"
:indicator-dots="true"
:autoplay="true"
:interval="3000"
:duration="500"
@change="onSwiperChange"
>
<swiper-item v-for="(item, index) in bannerList" :key="index">
<view class="swiper-item" :style="{ backgroundColor: item.color }">
<text>{{ item.title }}</text>
</view>
</swiper-item>
</swiper>
</view>
<!-- 垂直轮播 -->
<view class="section">
<text class="section-title">垂直轮播</text>
<swiper
class="vertical-swiper"
:vertical="true"
:indicator-dots="true"
indicator-color="rgba(255,255,255,0.5)"
indicator-active-color="#fff"
>
<swiper-item v-for="(item, index) in verticalList" :key="index">
<view class="vertical-item">
<image :src="item.image" class="item-image" />
<view class="item-content">
<text class="item-title">{{ item.title }}</text>
<text class="item-desc">{{ item.description }}</text>
</view>
</view>
</swiper-item>
</swiper>
</view>
<!-- 自定义指示器 -->
<view class="section">
<text class="section-title">自定义指示器</text>
<swiper
class="custom-swiper"
:current="currentIndex"
@change="onCustomSwiperChange"
>
<swiper-item v-for="(item, index) in customList" :key="index">
<view class="custom-item">
<text>{{ item.content }}</text>
</view>
</swiper-item>
</swiper>
<view class="custom-indicators">
<view
v-for="(item, index) in customList"
:key="index"
class="indicator"
:class="{ active: index === currentIndex }"
@click="switchToSlide(index)"
>
{{ index + 1 }}
</view>
</view>
</view>
</view>
</template>
<script>
export default {
name: 'SwiperDemo',
data() {
return {
currentIndex: 0,
bannerList: [
{ title: '轮播图1', color: '#ff6b6b' },
{ title: '轮播图2', color: '#4ecdc4' },
{ title: '轮播图3', color: '#45b7d1' },
{ title: '轮播图4', color: '#96ceb4' }
],
verticalList: [
{
title: '新闻标题1',
description: '这是新闻1的详细描述内容',
image: '/static/images/news1.jpg'
},
{
title: '新闻标题2',
description: '这是新闻2的详细描述内容',
image: '/static/images/news2.jpg'
},
{
title: '新闻标题3',
description: '这是新闻3的详细描述内容',
image: '/static/images/news3.jpg'
}
],
customList: [
{ content: '自定义内容1' },
{ content: '自定义内容2' },
{ content: '自定义内容3' },
{ content: '自定义内容4' },
{ content: '自定义内容5' }
]
}
},
methods: {
onSwiperChange(e) {
console.log('轮播切换:', e.detail.current)
},
onCustomSwiperChange(e) {
this.currentIndex = e.detail.current
},
switchToSlide(index) {
this.currentIndex = index
}
}
}
</script>
<style lang="scss" scoped>
.swiper-demo {
padding: 20rpx;
.title {
display: block;
font-size: 32rpx;
font-weight: bold;
text-align: center;
margin-bottom: 30rpx;
}
.section {
margin-bottom: 40rpx;
.section-title {
display: block;
font-size: 28rpx;
font-weight: bold;
margin-bottom: 20rpx;
color: #333;
}
}
.basic-swiper {
height: 300rpx;
border-radius: 8rpx;
overflow: hidden;
.swiper-item {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
text {
color: white;
font-size: 32rpx;
font-weight: bold;
}
}
}
.vertical-swiper {
height: 400rpx;
border-radius: 8rpx;
overflow: hidden;
.vertical-item {
height: 100%;
display: flex;
padding: 20rpx;
background-color: #f8f8f8;
.item-image {
width: 200rpx;
height: 150rpx;
border-radius: 8rpx;
margin-right: 20rpx;
}
.item-content {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
.item-title {
font-size: 28rpx;
font-weight: bold;
margin-bottom: 10rpx;
color: #333;
}
.item-desc {
font-size: 24rpx;
color: #666;
line-height: 1.5;
}
}
}
}
.custom-swiper {
height: 250rpx;
border-radius: 8rpx;
overflow: hidden;
.custom-item {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(45deg, #667eea 0%, #764ba2 100%);
text {
color: white;
font-size: 28rpx;
font-weight: bold;
}
}
}
.custom-indicators {
display: flex;
justify-content: center;
margin-top: 20rpx;
.indicator {
width: 60rpx;
height: 60rpx;
margin: 0 10rpx;
border-radius: 50%;
background-color: #ddd;
display: flex;
align-items: center;
justify-content: center;
font-size: 24rpx;
color: #666;
transition: all 0.3s;
&.active {
background-color: #007aff;
color: white;
transform: scale(1.2);
}
}
}
}
</style>
3.2.2 基础内容组件
1. text组件 - 文本显示
<template>
<view class="text-demo">
<text class="title">Text组件演示</text>
<!-- 基础文本 -->
<view class="section">
<text class="section-title">基础文本</text>
<text class="basic-text">这是一段基础文本内容</text>
</view>
<!-- 可选择文本 -->
<view class="section">
<text class="section-title">可选择文本</text>
<text class="selectable-text" selectable>这段文本可以被选择和复制</text>
</view>
<!-- 富文本样式 -->
<view class="section">
<text class="section-title">富文本样式</text>
<text class="rich-text bold">粗体文本</text>
<text class="rich-text italic">斜体文本</text>
<text class="rich-text underline">下划线文本</text>
<text class="rich-text highlight">高亮文本</text>
</view>
<!-- 不同大小文本 -->
<view class="section">
<text class="section-title">不同大小文本</text>
<text class="size-text small">小号文本</text>
<text class="size-text normal">正常文本</text>
<text class="size-text large">大号文本</text>
<text class="size-text xlarge">超大文本</text>
</view>
<!-- 颜色文本 -->
<view class="section">
<text class="section-title">颜色文本</text>
<text class="color-text primary">主色调文本</text>
<text class="color-text success">成功文本</text>
<text class="color-text warning">警告文本</text>
<text class="color-text danger">危险文本</text>
</view>
<!-- 文本溢出处理 -->
<view class="section">
<text class="section-title">文本溢出处理</text>
<text class="overflow-text ellipsis">
这是一段很长的文本内容,用来演示文本溢出时的省略号处理效果
</text>
<text class="overflow-text break-word">
这是一段很长的文本内容用来演示文本换行处理效果当文本超出容器宽度时会自动换行显示
</text>
</view>
<!-- 文本对齐 -->
<view class="section">
<text class="section-title">文本对齐</text>
<text class="align-text left">左对齐文本</text>
<text class="align-text center">居中对齐文本</text>
<text class="align-text right">右对齐文本</text>
</view>
</view>
</template>
<script>
export default {
name: 'TextDemo'
}
</script>
<style lang="scss" scoped>
.text-demo {
padding: 20rpx;
.title {
display: block;
font-size: 32rpx;
font-weight: bold;
text-align: center;
margin-bottom: 30rpx;
}
.section {
margin-bottom: 30rpx;
.section-title {
display: block;
font-size: 28rpx;
font-weight: bold;
margin-bottom: 15rpx;
color: #333;
}
}
.basic-text {
display: block;
font-size: 26rpx;
line-height: 1.5;
color: #666;
}
.selectable-text {
display: block;
font-size: 26rpx;
line-height: 1.5;
color: #333;
background-color: #f0f8ff;
padding: 20rpx;
border-radius: 8rpx;
}
.rich-text {
display: block;
font-size: 26rpx;
margin-bottom: 10rpx;
&.bold {
font-weight: bold;
}
&.italic {
font-style: italic;
}
&.underline {
text-decoration: underline;
}
&.highlight {
background-color: #ffeb3b;
padding: 4rpx 8rpx;
border-radius: 4rpx;
}
}
.size-text {
display: block;
margin-bottom: 10rpx;
&.small {
font-size: 20rpx;
}
&.normal {
font-size: 26rpx;
}
&.large {
font-size: 32rpx;
}
&.xlarge {
font-size: 40rpx;
}
}
.color-text {
display: block;
margin-bottom: 10rpx;
font-size: 26rpx;
&.primary {
color: #007aff;
}
&.success {
color: #09bb07;
}
&.warning {
color: #ff9500;
}
&.danger {
color: #ff3b30;
}
}
.overflow-text {
display: block;
width: 100%;
margin-bottom: 15rpx;
font-size: 26rpx;
&.ellipsis {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
&.break-word {
word-wrap: break-word;
word-break: break-all;
line-height: 1.5;
}
}
.align-text {
display: block;
width: 100%;
margin-bottom: 10rpx;
font-size: 26rpx;
&.left {
text-align: left;
}
&.center {
text-align: center;
}
&.right {
text-align: right;
}
}
}
</style>
2. image组件 - 图片显示
<template>
<view class="image-demo">
<text class="title">Image组件演示</text>
<!-- 基础图片 -->
<view class="section">
<text class="section-title">基础图片</text>
<image
class="basic-image"
src="/static/images/demo.jpg"
@load="onImageLoad"
@error="onImageError"
/>
</view>
<!-- 不同缩放模式 -->
<view class="section">
<text class="section-title">缩放模式</text>
<view class="mode-grid">
<view v-for="mode in scaleModes" :key="mode.value" class="mode-item">
<text class="mode-label">{{ mode.label }}</text>
<image
class="mode-image"
src="/static/images/demo.jpg"
:mode="mode.value"
/>
</view>
</view>
</view>
<!-- 懒加载图片 -->
<view class="section">
<text class="section-title">懒加载图片</text>
<scroll-view class="lazy-scroll" scroll-y>
<view v-for="n in 20" :key="n" class="lazy-item">
<image
class="lazy-image"
:src="`/static/images/lazy${n % 5 + 1}.jpg`"
lazy-load
@load="onLazyImageLoad"
/>
<text>懒加载图片 {{ n }}</text>
</view>
</scroll-view>
</view>
<!-- 图片预览 -->
<view class="section">
<text class="section-title">图片预览</text>
<view class="preview-grid">
<image
v-for="(img, index) in previewImages"
:key="index"
class="preview-image"
:src="img.thumb"
@click="previewImage(index)"
/>
</view>
</view>
<!-- 图片上传预览 -->
<view class="section">
<text class="section-title">图片上传预览</text>
<view class="upload-container">
<view v-for="(img, index) in uploadedImages" :key="index" class="upload-item">
<image class="upload-image" :src="img" />
<view class="delete-btn" @click="deleteImage(index)">
<text>×</text>
</view>
</view>
<view class="upload-btn" @click="chooseImage">
<text>+</text>
</view>
</view>
</view>
</view>
</template>
<script>
export default {
name: 'ImageDemo',
data() {
return {
scaleModes: [
{ label: 'scaleToFill', value: 'scaleToFill' },
{ label: 'aspectFit', value: 'aspectFit' },
{ label: 'aspectFill', value: 'aspectFill' },
{ label: 'widthFix', value: 'widthFix' },
{ label: 'heightFix', value: 'heightFix' },
{ label: 'top', value: 'top' },
{ label: 'bottom', value: 'bottom' },
{ label: 'center', value: 'center' },
{ label: 'left', value: 'left' },
{ label: 'right', value: 'right' }
],
previewImages: [
{
thumb: '/static/images/thumb1.jpg',
full: '/static/images/full1.jpg'
},
{
thumb: '/static/images/thumb2.jpg',
full: '/static/images/full2.jpg'
},
{
thumb: '/static/images/thumb3.jpg',
full: '/static/images/full3.jpg'
},
{
thumb: '/static/images/thumb4.jpg',
full: '/static/images/full4.jpg'
}
],
uploadedImages: []
}
},
methods: {
onImageLoad(e) {
console.log('图片加载成功:', e.detail)
},
onImageError(e) {
console.log('图片加载失败:', e.detail)
uni.showToast({
title: '图片加载失败',
icon: 'none'
})
},
onLazyImageLoad(e) {
console.log('懒加载图片加载成功')
},
previewImage(index) {
const urls = this.previewImages.map(img => img.full)
uni.previewImage({
current: index,
urls: urls
})
},
chooseImage() {
uni.chooseImage({
count: 9 - this.uploadedImages.length,
sizeType: ['original', 'compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
this.uploadedImages.push(...res.tempFilePaths)
},
fail: (err) => {
console.log('选择图片失败:', err)
}
})
},
deleteImage(index) {
uni.showModal({
title: '确认删除',
content: '确定要删除这张图片吗?',
success: (res) => {
if (res.confirm) {
this.uploadedImages.splice(index, 1)
}
}
})
}
}
}
</script>
<style lang="scss" scoped>
.image-demo {
padding: 20rpx;
.title {
display: block;
font-size: 32rpx;
font-weight: bold;
text-align: center;
margin-bottom: 30rpx;
}
.section {
margin-bottom: 40rpx;
.section-title {
display: block;
font-size: 28rpx;
font-weight: bold;
margin-bottom: 20rpx;
color: #333;
}
}
.basic-image {
width: 100%;
height: 400rpx;
border-radius: 8rpx;
}
.mode-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20rpx;
.mode-item {
text-align: center;
.mode-label {
display: block;
font-size: 22rpx;
margin-bottom: 10rpx;
color: #666;
}
.mode-image {
width: 100%;
height: 200rpx;
border: 1px solid #ddd;
border-radius: 4rpx;
}
}
}
.lazy-scroll {
height: 600rpx;
border: 1px solid #ddd;
border-radius: 8rpx;
.lazy-item {
display: flex;
align-items: center;
padding: 20rpx;
border-bottom: 1px solid #f0f0f0;
.lazy-image {
width: 120rpx;
height: 120rpx;
margin-right: 20rpx;
border-radius: 8rpx;
}
text {
font-size: 26rpx;
color: #333;
}
}
}
.preview-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20rpx;
.preview-image {
width: 100%;
height: 300rpx;
border-radius: 8rpx;
cursor: pointer;
}
}
.upload-container {
display: flex;
flex-wrap: wrap;
gap: 20rpx;
.upload-item {
position: relative;
width: 200rpx;
height: 200rpx;
.upload-image {
width: 100%;
height: 100%;
border-radius: 8rpx;
}
.delete-btn {
position: absolute;
top: -10rpx;
right: -10rpx;
width: 40rpx;
height: 40rpx;
background-color: #ff3b30;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
text {
color: white;
font-size: 24rpx;
font-weight: bold;
}
}
}
.upload-btn {
width: 200rpx;
height: 200rpx;
border: 2px dashed #ddd;
border-radius: 8rpx;
display: flex;
align-items: center;
justify-content: center;
background-color: #fafafa;
text {
font-size: 60rpx;
color: #999;
}
}
}
}
</style>
3.2.3 表单组件
1. input组件 - 输入框
<template>
<view class="input-demo">
<text class="title">Input组件演示</text>
<!-- 基础输入框 -->
<view class="section">
<text class="section-title">基础输入框</text>
<input
class="basic-input"
v-model="basicValue"
placeholder="请输入内容"
@input="onInput"
@focus="onFocus"
@blur="onBlur"
/>
<text class="input-value">输入值:{{ basicValue }}</text>
</view>
<!-- 不同类型输入框 -->
<view class="section">
<text class="section-title">不同类型输入框</text>
<view class="input-group">
<text class="input-label">文本输入:</text>
<input
class="form-input"
type="text"
v-model="formData.text"
placeholder="请输入文本"
/>
</view>
<view class="input-group">
<text class="input-label">数字输入:</text>
<input
class="form-input"
type="number"
v-model="formData.number"
placeholder="请输入数字"
/>
</view>
<view class="input-group">
<text class="input-label">身份证输入:</text>
<input
class="form-input"
type="idcard"
v-model="formData.idcard"
placeholder="请输入身份证号"
/>
</view>
<view class="input-group">
<text class="input-label">小数输入:</text>
<input
class="form-input"
type="digit"
v-model="formData.digit"
placeholder="请输入小数"
/>
</view>
</view>
<!-- 密码输入框 -->
<view class="section">
<text class="section-title">密码输入框</text>
<view class="password-container">
<input
class="password-input"
:type="showPassword ? 'text' : 'password'"
v-model="password"
placeholder="请输入密码"
:password="!showPassword"
/>
<view class="password-toggle" @click="togglePassword">
<text>{{ showPassword ? '隐藏' : '显示' }}</text>
</view>
</view>
</view>
<!-- 搜索输入框 -->
<view class="section">
<text class="section-title">搜索输入框</text>
<view class="search-container">
<input
class="search-input"
v-model="searchValue"
placeholder="搜索内容"
confirm-type="search"
@confirm="onSearch"
/>
<view class="search-btn" @click="onSearch">
<text>搜索</text>
</view>
</view>
</view>
<!-- 限制输入 -->
<view class="section">
<text class="section-title">限制输入</text>
<view class="input-group">
<text class="input-label">最大长度限制(10字符):</text>
<input
class="form-input"
v-model="limitedValue"
placeholder="最多输入10个字符"
:maxlength="10"
/>
<text class="char-count">{{ limitedValue.length }}/10</text>
</view>
<view class="input-group">
<text class="input-label">禁用输入框:</text>
<input
class="form-input disabled"
value="禁用状态"
disabled
/>
</view>
</view>
<!-- 表单验证 -->
<view class="section">
<text class="section-title">表单验证</text>
<view class="input-group">
<text class="input-label">邮箱:</text>
<input
class="form-input"
:class="{ error: emailError }"
v-model="email"
placeholder="请输入邮箱"
@blur="validateEmail"
/>
<text v-if="emailError" class="error-text">{{ emailError }}</text>
</view>
<view class="input-group">
<text class="input-label">手机号:</text>
<input
class="form-input"
:class="{ error: phoneError }"
v-model="phone"
placeholder="请输入手机号"
type="number"
@blur="validatePhone"
/>
<text v-if="phoneError" class="error-text">{{ phoneError }}</text>
</view>
</view>
</view>
</template>
<script>
export default {
name: 'InputDemo',
data() {
return {
basicValue: '',
formData: {
text: '',
number: '',
idcard: '',
digit: ''
},
password: '',
showPassword: false,
searchValue: '',
limitedValue: '',
email: '',
emailError: '',
phone: '',
phoneError: ''
}
},
methods: {
onInput(e) {
console.log('输入事件:', e.detail.value)
},
onFocus(e) {
console.log('获得焦点:', e.detail)
},
onBlur(e) {
console.log('失去焦点:', e.detail)
},
togglePassword() {
this.showPassword = !this.showPassword
},
onSearch() {
if (this.searchValue.trim()) {
uni.showToast({
title: `搜索:${this.searchValue}`,
icon: 'none'
})
}
},
validateEmail() {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!this.email) {
this.emailError = '邮箱不能为空'
} else if (!emailRegex.test(this.email)) {
this.emailError = '邮箱格式不正确'
} else {
this.emailError = ''
}
},
validatePhone() {
const phoneRegex = /^1[3-9]\d{9}$/
if (!this.phone) {
this.phoneError = '手机号不能为空'
} else if (!phoneRegex.test(this.phone)) {
this.phoneError = '手机号格式不正确'
} else {
this.phoneError = ''
}
}
}
}
</script>
<style lang="scss" scoped>
.input-demo {
padding: 20rpx;
.title {
display: block;
font-size: 32rpx;
font-weight: bold;
text-align: center;
margin-bottom: 30rpx;
}
.section {
margin-bottom: 40rpx;
.section-title {
display: block;
font-size: 28rpx;
font-weight: bold;
margin-bottom: 20rpx;
color: #333;
}
}
.basic-input {
width: 100%;
padding: 25rpx;
border: 1px solid #ddd;
border-radius: 8rpx;
font-size: 28rpx;
margin-bottom: 15rpx;
&:focus {
border-color: #007aff;
}
}
.input-value {
display: block;
font-size: 24rpx;
color: #666;
}
.input-group {
margin-bottom: 25rpx;
.input-label {
display: block;
font-size: 26rpx;
margin-bottom: 10rpx;
color: #333;
}
.form-input {
width: 100%;
padding: 20rpx;
border: 1px solid #ddd;
border-radius: 6rpx;
font-size: 26rpx;
&:focus {
border-color: #007aff;
}
&.error {
border-color: #ff3b30;
}
&.disabled {
background-color: #f5f5f5;
color: #999;
}
}
.char-count {
display: block;
text-align: right;
font-size: 22rpx;
color: #999;
margin-top: 8rpx;
}
.error-text {
display: block;
font-size: 22rpx;
color: #ff3b30;
margin-top: 8rpx;
}
}
.password-container {
display: flex;
align-items: center;
border: 1px solid #ddd;
border-radius: 6rpx;
overflow: hidden;
.password-input {
flex: 1;
padding: 20rpx;
border: none;
font-size: 26rpx;
}
.password-toggle {
padding: 20rpx;
background-color: #f8f8f8;
border-left: 1px solid #ddd;
text {
font-size: 24rpx;
color: #007aff;
}
}
}
.search-container {
display: flex;
align-items: center;
border: 1px solid #ddd;
border-radius: 6rpx;
overflow: hidden;
.search-input {
flex: 1;
padding: 20rpx;
border: none;
font-size: 26rpx;
}
.search-btn {
padding: 20rpx 30rpx;
background-color: #007aff;
text {
color: white;
font-size: 26rpx;
}
}
}
}
</style>
2. textarea组件 - 多行文本输入
<template>
<view class="textarea-demo">
<text class="title">Textarea组件演示</text>
<!-- 基础多行输入 -->
<view class="section">
<text class="section-title">基础多行输入</text>
<textarea
class="basic-textarea"
v-model="basicContent"
placeholder="请输入多行文本内容"
@input="onTextareaInput"
@focus="onTextareaFocus"
@blur="onTextareaBlur"
/>
<text class="content-length">字符数:{{ basicContent.length }}</text>
</view>
<!-- 自动调整高度 -->
<view class="section">
<text class="section-title">自动调整高度</text>
<textarea
class="auto-textarea"
v-model="autoContent"
placeholder="输入内容时高度会自动调整"
auto-height
:maxlength="200"
/>
<text class="content-length">{{ autoContent.length }}/200</text>
</view>
<!-- 固定行数 -->
<view class="section">
<text class="section-title">固定行数</text>
<textarea
class="fixed-textarea"
v-model="fixedContent"
placeholder="固定显示5行"
:show-count="true"
:maxlength="500"
/>
</view>
<!-- 评论输入框 -->
<view class="section">
<text class="section-title">评论输入框</text>
<view class="comment-container">
<textarea
class="comment-textarea"
v-model="commentContent"
placeholder="写下你的评论..."
:maxlength="300"
@input="onCommentInput"
/>
<view class="comment-actions">
<text class="char-count">{{ commentContent.length }}/300</text>
<button
class="submit-btn"
:disabled="!commentContent.trim()"
@click="submitComment"
>
发布
</button>
</view>
</view>
</view>
<!-- 富文本编辑器样式 -->
<view class="section">
<text class="section-title">富文本编辑器样式</text>
<view class="editor-container">
<view class="editor-toolbar">
<view class="toolbar-item" @click="insertText('**粗体**')">
<text>B</text>
</view>
<view class="toolbar-item" @click="insertText('*斜体*')">
<text>I</text>
</view>
<view class="toolbar-item" @click="insertText('~~删除线~~')">
<text>S</text>
</view>
<view class="toolbar-item" @click="insertText('[链接](url)')">
<text>链接</text>
</view>
</view>
<textarea
class="editor-textarea"
v-model="editorContent"
placeholder="支持Markdown语法"
auto-height
/>
</view>
</view>
</view>
</template>
<script>
export default {
name: 'TextareaDemo',
data() {
return {
basicContent: '',
autoContent: '',
fixedContent: '',
commentContent: '',
editorContent: ''
}
},
methods: {
onTextareaInput(e) {
console.log('多行输入:', e.detail.value)
},
onTextareaFocus(e) {
console.log('多行输入获得焦点')
},
onTextareaBlur(e) {
console.log('多行输入失去焦点')
},
onCommentInput(e) {
// 限制输入长度
if (e.detail.value.length > 300) {
this.commentContent = e.detail.value.substring(0, 300)
}
},
submitComment() {
if (this.commentContent.trim()) {
uni.showToast({
title: '评论发布成功',
icon: 'success'
})
// 清空评论内容
this.commentContent = ''
}
},
insertText(text) {
this.editorContent += text
}
}
}
</script>
<style lang="scss" scoped>
.textarea-demo {
padding: 20rpx;
.title {
display: block;
font-size: 32rpx;
font-weight: bold;
text-align: center;
margin-bottom: 30rpx;
}
.section {
margin-bottom: 40rpx;
.section-title {
display: block;
font-size: 28rpx;
font-weight: bold;
margin-bottom: 20rpx;
color: #333;
}
}
.basic-textarea {
width: 100%;
min-height: 200rpx;
padding: 20rpx;
border: 1px solid #ddd;
border-radius: 8rpx;
font-size: 26rpx;
line-height: 1.5;
&:focus {
border-color: #007aff;
}
}
.auto-textarea {
width: 100%;
min-height: 120rpx;
padding: 20rpx;
border: 1px solid #ddd;
border-radius: 8rpx;
font-size: 26rpx;
line-height: 1.5;
}
.fixed-textarea {
width: 100%;
height: 250rpx;
padding: 20rpx;
border: 1px solid #ddd;
border-radius: 8rpx;
font-size: 26rpx;
line-height: 1.5;
}
.content-length {
display: block;
text-align: right;
font-size: 22rpx;
color: #999;
margin-top: 10rpx;
}
.comment-container {
border: 1px solid #ddd;
border-radius: 8rpx;
overflow: hidden;
.comment-textarea {
width: 100%;
min-height: 150rpx;
padding: 20rpx;
border: none;
font-size: 26rpx;
line-height: 1.5;
}
.comment-actions {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15rpx 20rpx;
background-color: #f8f8f8;
border-top: 1px solid #eee;
.char-count {
font-size: 22rpx;
color: #999;
}
.submit-btn {
padding: 10rpx 30rpx;
background-color: #007aff;
color: white;
border: none;
border-radius: 20rpx;
font-size: 24rpx;
&:disabled {
background-color: #ccc;
}
}
}
}
.editor-container {
border: 1px solid #ddd;
border-radius: 8rpx;
overflow: hidden;
.editor-toolbar {
display: flex;
padding: 15rpx;
background-color: #f8f8f8;
border-bottom: 1px solid #eee;
.toolbar-item {
padding: 10rpx 15rpx;
margin-right: 10rpx;
background-color: white;
border: 1px solid #ddd;
border-radius: 4rpx;
text {
font-size: 22rpx;
font-weight: bold;
}
&:active {
background-color: #e6e6e6;
}
}
}
.editor-textarea {
width: 100%;
min-height: 200rpx;
padding: 20rpx;
border: none;
font-size: 26rpx;
line-height: 1.5;
}
}
}
</style>
3. button组件 - 按钮
<template>
<view class="button-demo">
<text class="title">Button组件演示</text>
<!-- 基础按钮 -->
<view class="section">
<text class="section-title">基础按钮</text>
<view class="button-group">
<button class="demo-btn" @click="onButtonClick('默认按钮')">默认按钮</button>
<button class="demo-btn" type="primary" @click="onButtonClick('主要按钮')">主要按钮</button>
<button class="demo-btn" type="warn" @click="onButtonClick('警告按钮')">警告按钮</button>
</view>
</view>
<!-- 按钮尺寸 -->
<view class="section">
<text class="section-title">按钮尺寸</text>
<view class="button-group">
<button class="demo-btn" size="mini" type="primary">迷你按钮</button>
<button class="demo-btn" size="default" type="primary">默认按钮</button>
</view>
<button class="demo-btn full-btn" type="primary">全宽按钮</button>
</view>
<!-- 按钮状态 -->
<view class="section">
<text class="section-title">按钮状态</text>
<view class="button-group">
<button class="demo-btn" type="primary" :disabled="false">正常状态</button>
<button class="demo-btn" type="primary" :disabled="true">禁用状态</button>
<button class="demo-btn" type="primary" :loading="isLoading" @click="toggleLoading">
{{ isLoading ? '加载中...' : '点击加载' }}
</button>
</view>
</view>
<!-- 镂空按钮 -->
<view class="section">
<text class="section-title">镂空按钮</text>
<view class="button-group">
<button class="demo-btn" plain type="primary">镂空主要</button>
<button class="demo-btn" plain type="warn">镂空警告</button>
<button class="demo-btn" plain>镂空默认</button>
</view>
</view>
<!-- 自定义样式按钮 -->
<view class="section">
<text class="section-title">自定义样式按钮</text>
<view class="button-group">
<button class="custom-btn success-btn" @click="onButtonClick('成功按钮')">成功按钮</button>
<button class="custom-btn info-btn" @click="onButtonClick('信息按钮')">信息按钮</button>
<button class="custom-btn danger-btn" @click="onButtonClick('危险按钮')">危险按钮</button>
</view>
</view>
<!-- 圆形按钮 -->
<view class="section">
<text class="section-title">圆形按钮</text>
<view class="button-group">
<button class="round-btn primary-round">+</button>
<button class="round-btn success-round">✓</button>
<button class="round-btn danger-round">×</button>
<button class="round-btn info-round">?</button>
</view>
</view>
<!-- 图标按钮 -->
<view class="section">
<text class="section-title">图标按钮</text>
<view class="button-group">
<button class="icon-btn" @click="onButtonClick('分享')">
<text class="icon">📤</text>
<text>分享</text>
</button>
<button class="icon-btn" @click="onButtonClick('收藏')">
<text class="icon">⭐</text>
<text>收藏</text>
</button>
<button class="icon-btn" @click="onButtonClick('下载')">
<text class="icon">⬇️</text>
<text>下载</text>
</button>
</view>
</view>
<!-- 按钮组 -->
<view class="section">
<text class="section-title">按钮组</text>
<view class="btn-group">
<button
v-for="(item, index) in tabList"
:key="index"
class="group-btn"
:class="{ active: activeTab === index }"
@click="switchTab(index)"
>
{{ item.name }}
</button>
</view>
</view>
<!-- 浮动按钮 -->
<view class="section">
<text class="section-title">浮动按钮</text>
<view class="float-container">
<text>页面内容区域</text>
<button class="float-btn" @click="onFloatClick">
<text>+</text>
</button>
</view>
</view>
</view>
</template>
<script>
export default {
name: 'ButtonDemo',
data() {
return {
isLoading: false,
activeTab: 0,
tabList: [
{ name: '首页' },
{ name: '分类' },
{ name: '购物车' },
{ name: '我的' }
]
}
},
methods: {
onButtonClick(type) {
uni.showToast({
title: `点击了${type}`,
icon: 'none'
})
},
toggleLoading() {
this.isLoading = true
setTimeout(() => {
this.isLoading = false
uni.showToast({
title: '加载完成',
icon: 'success'
})
}, 2000)
},
switchTab(index) {
this.activeTab = index
uni.showToast({
title: `切换到${this.tabList[index].name}`,
icon: 'none'
})
},
onFloatClick() {
uni.showActionSheet({
itemList: ['新建文档', '新建文件夹', '上传文件'],
success: (res) => {
console.log('选择了:', res.tapIndex)
}
})
}
}
}
</script>
<style lang="scss" scoped>
.button-demo {
padding: 20rpx;
.title {
display: block;
font-size: 32rpx;
font-weight: bold;
text-align: center;
margin-bottom: 30rpx;
}
.section {
margin-bottom: 40rpx;
.section-title {
display: block;
font-size: 28rpx;
font-weight: bold;
margin-bottom: 20rpx;
color: #333;
}
}
.button-group {
display: flex;
flex-wrap: wrap;
gap: 20rpx;
margin-bottom: 20rpx;
}
.demo-btn {
flex: 1;
min-width: 200rpx;
height: 80rpx;
line-height: 80rpx;
font-size: 26rpx;
border-radius: 8rpx;
}
.full-btn {
width: 100%;
height: 80rpx;
line-height: 80rpx;
font-size: 26rpx;
border-radius: 8rpx;
}
.custom-btn {
flex: 1;
min-width: 200rpx;
height: 80rpx;
line-height: 80rpx;
font-size: 26rpx;
border-radius: 8rpx;
border: none;
color: white;
&.success-btn {
background-color: #09bb07;
}
&.info-btn {
background-color: #10aeff;
}
&.danger-btn {
background-color: #ff3b30;
}
&:active {
opacity: 0.8;
}
}
.round-btn {
width: 100rpx;
height: 100rpx;
border-radius: 50%;
border: none;
color: white;
font-size: 32rpx;
font-weight: bold;
&.primary-round {
background-color: #007aff;
}
&.success-round {
background-color: #09bb07;
}
&.danger-round {
background-color: #ff3b30;
}
&.info-round {
background-color: #10aeff;
}
&:active {
opacity: 0.8;
}
}
.icon-btn {
flex: 1;
min-width: 200rpx;
height: 80rpx;
display: flex;
align-items: center;
justify-content: center;
background-color: #f8f8f8;
border: 1px solid #ddd;
border-radius: 8rpx;
.icon {
margin-right: 10rpx;
font-size: 28rpx;
}
text {
font-size: 26rpx;
color: #333;
}
&:active {
background-color: #e6e6e6;
}
}
.btn-group {
display: flex;
border: 1px solid #ddd;
border-radius: 8rpx;
overflow: hidden;
.group-btn {
flex: 1;
height: 80rpx;
line-height: 80rpx;
background-color: white;
border: none;
border-right: 1px solid #ddd;
font-size: 26rpx;
color: #666;
&:last-child {
border-right: none;
}
&.active {
background-color: #007aff;
color: white;
}
&:active {
background-color: #e6e6e6;
}
&.active:active {
background-color: #0056cc;
}
}
}
.float-container {
position: relative;
height: 400rpx;
background-color: #f8f8f8;
border-radius: 8rpx;
display: flex;
align-items: center;
justify-content: center;
text {
font-size: 26rpx;
color: #666;
}
.float-btn {
position: absolute;
bottom: 30rpx;
right: 30rpx;
width: 100rpx;
height: 100rpx;
border-radius: 50%;
background-color: #007aff;
border: none;
color: white;
font-size: 40rpx;
font-weight: bold;
box-shadow: 0 4rpx 12rpx rgba(0, 122, 255, 0.3);
&:active {
opacity: 0.8;
}
}
}
}
</style>
3.3 自定义组件开发
3.3.1 组件创建与注册
1. 创建自定义组件
在 components
目录下创建组件文件:
<!-- components/custom-card/custom-card.vue -->
<template>
<view class="custom-card" :class="cardClass" @click="onCardClick">
<!-- 卡片头部 -->
<view v-if="title || $slots.header" class="card-header">
<slot name="header">
<text class="card-title">{{ title }}</text>
<text v-if="subtitle" class="card-subtitle">{{ subtitle }}</text>
</slot>
</view>
<!-- 卡片内容 -->
<view class="card-content">
<slot></slot>
</view>
<!-- 卡片底部 -->
<view v-if="$slots.footer" class="card-footer">
<slot name="footer"></slot>
</view>
</view>
</template>
<script>
export default {
name: 'CustomCard',
props: {
// 卡片标题
title: {
type: String,
default: ''
},
// 卡片副标题
subtitle: {
type: String,
default: ''
},
// 卡片类型
type: {
type: String,
default: 'default',
validator: (value) => {
return ['default', 'primary', 'success', 'warning', 'danger'].includes(value)
}
},
// 是否显示阴影
shadow: {
type: Boolean,
default: true
},
// 是否可点击
clickable: {
type: Boolean,
default: false
}
},
computed: {
cardClass() {
return {
[`card-${this.type}`]: this.type !== 'default',
'card-shadow': this.shadow,
'card-clickable': this.clickable
}
}
},
methods: {
onCardClick() {
if (this.clickable) {
this.$emit('click')
}
}
}
}
</script>
<style lang="scss" scoped>
.custom-card {
background-color: white;
border-radius: 12rpx;
overflow: hidden;
margin-bottom: 20rpx;
&.card-shadow {
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
}
&.card-clickable {
cursor: pointer;
transition: transform 0.2s;
&:active {
transform: scale(0.98);
}
}
&.card-primary {
border-left: 8rpx solid #007aff;
}
&.card-success {
border-left: 8rpx solid #09bb07;
}
&.card-warning {
border-left: 8rpx solid #ff9500;
}
&.card-danger {
border-left: 8rpx solid #ff3b30;
}
.card-header {
padding: 30rpx 30rpx 20rpx;
border-bottom: 1px solid #f0f0f0;
.card-title {
display: block;
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 8rpx;
}
.card-subtitle {
display: block;
font-size: 24rpx;
color: #999;
}
}
.card-content {
padding: 30rpx;
}
.card-footer {
padding: 20rpx 30rpx 30rpx;
border-top: 1px solid #f0f0f0;
}
}
</style>
2. 组件注册与使用
<!-- pages/index/index.vue -->
<template>
<view class="page">
<text class="title">自定义组件演示</text>
<!-- 基础卡片 -->
<custom-card title="基础卡片" subtitle="这是一个基础卡片组件">
<text>这里是卡片的主要内容区域,可以放置任何内容。</text>
</custom-card>
<!-- 不同类型卡片 -->
<custom-card title="主要卡片" type="primary" :clickable="true" @click="onCardClick('primary')">
<text>这是一个主要类型的可点击卡片。</text>
</custom-card>
<custom-card title="成功卡片" type="success">
<text>这是一个成功类型的卡片。</text>
</custom-card>
<custom-card title="警告卡片" type="warning">
<text>这是一个警告类型的卡片。</text>
</custom-card>
<!-- 自定义插槽 -->
<custom-card type="danger">
<template #header>
<view class="custom-header">
<text class="header-icon">⚠️</text>
<view class="header-content">
<text class="header-title">自定义头部</text>
<text class="header-desc">使用插槽自定义头部内容</text>
</view>
</view>
</template>
<view class="card-body">
<text>这里是卡片主体内容。</text>
</view>
<template #footer>
<view class="custom-footer">
<button class="footer-btn" size="mini">取消</button>
<button class="footer-btn primary" size="mini" type="primary">确认</button>
</view>
</template>
</custom-card>
</view>
</template>
<script>
import CustomCard from '@/components/custom-card/custom-card.vue'
export default {
name: 'Index',
components: {
CustomCard
},
methods: {
onCardClick(type) {
uni.showToast({
title: `点击了${type}卡片`,
icon: 'none'
})
}
}
}
</script>
<style lang="scss" scoped>
.page {
padding: 20rpx;
.title {
display: block;
font-size: 32rpx;
font-weight: bold;
text-align: center;
margin-bottom: 30rpx;
}
.custom-header {
display: flex;
align-items: center;
.header-icon {
font-size: 40rpx;
margin-right: 20rpx;
}
.header-content {
flex: 1;
.header-title {
display: block;
font-size: 28rpx;
font-weight: bold;
color: #333;
margin-bottom: 8rpx;
}
.header-desc {
display: block;
font-size: 22rpx;
color: #999;
}
}
}
.card-body {
text {
font-size: 26rpx;
line-height: 1.5;
color: #666;
}
}
.custom-footer {
display: flex;
justify-content: flex-end;
gap: 20rpx;
.footer-btn {
padding: 10rpx 30rpx;
font-size: 24rpx;
border-radius: 20rpx;
&.primary {
background-color: #007aff;
color: white;
}
}
}
}
</style>
3.3.2 组件通信进阶
1. 父子组件双向绑定
<!-- components/custom-input/custom-input.vue -->
<template>
<view class="custom-input" :class="inputClass">
<text v-if="label" class="input-label">{{ label }}</text>
<view class="input-container">
<text v-if="prefixIcon" class="prefix-icon">{{ prefixIcon }}</text>
<input
class="input-field"
:type="type"
:value="modelValue"
:placeholder="placeholder"
:disabled="disabled"
:maxlength="maxlength"
@input="onInput"
@focus="onFocus"
@blur="onBlur"
/>
<text v-if="suffixIcon" class="suffix-icon" @click="onSuffixClick">{{ suffixIcon }}</text>
<view v-if="clearable && modelValue" class="clear-icon" @click="onClear">
<text>×</text>
</view>
</view>
<text v-if="errorMessage" class="error-message">{{ errorMessage }}</text>
<text v-if="showCount && maxlength" class="char-count">
{{ modelValue ? modelValue.length : 0 }}/{{ maxlength }}
</text>
</view>
</template>
<script>
export default {
name: 'CustomInput',
props: {
// v-model绑定值
modelValue: {
type: String,
default: ''
},
// 输入框类型
type: {
type: String,
default: 'text'
},
// 标签文本
label: {
type: String,
default: ''
},
// 占位符
placeholder: {
type: String,
default: ''
},
// 是否禁用
disabled: {
type: Boolean,
default: false
},
// 最大长度
maxlength: {
type: Number,
default: -1
},
// 前缀图标
prefixIcon: {
type: String,
default: ''
},
// 后缀图标
suffixIcon: {
type: String,
default: ''
},
// 是否可清空
clearable: {
type: Boolean,
default: false
},
// 是否显示字符计数
showCount: {
type: Boolean,
default: false
},
// 错误信息
errorMessage: {
type: String,
default: ''
},
// 输入框尺寸
size: {
type: String,
default: 'default',
validator: (value) => {
return ['small', 'default', 'large'].includes(value)
}
}
},
emits: ['update:modelValue', 'focus', 'blur', 'suffix-click', 'clear'],
computed: {
inputClass() {
return {
[`input-${this.size}`]: this.size !== 'default',
'input-disabled': this.disabled,
'input-error': this.errorMessage
}
}
},
methods: {
onInput(e) {
this.$emit('update:modelValue', e.detail.value)
},
onFocus(e) {
this.$emit('focus', e)
},
onBlur(e) {
this.$emit('blur', e)
},
onSuffixClick() {
this.$emit('suffix-click')
},
onClear() {
this.$emit('update:modelValue', '')
this.$emit('clear')
}
}
}
</script>
<style lang="scss" scoped>
.custom-input {
margin-bottom: 30rpx;
.input-label {
display: block;
font-size: 26rpx;
color: #333;
margin-bottom: 12rpx;
}
.input-container {
position: relative;
display: flex;
align-items: center;
border: 1px solid #ddd;
border-radius: 8rpx;
background-color: white;
.prefix-icon {
padding: 0 15rpx;
font-size: 28rpx;
color: #999;
}
.input-field {
flex: 1;
padding: 20rpx 15rpx;
border: none;
font-size: 26rpx;
background-color: transparent;
}
.suffix-icon {
padding: 0 15rpx;
font-size: 28rpx;
color: #999;
}
.clear-icon {
padding: 0 15rpx;
font-size: 32rpx;
color: #ccc;
&:active {
color: #999;
}
}
}
.error-message {
display: block;
font-size: 22rpx;
color: #ff3b30;
margin-top: 8rpx;
}
.char-count {
display: block;
text-align: right;
font-size: 22rpx;
color: #999;
margin-top: 8rpx;
}
&.input-small {
.input-container {
.input-field {
padding: 15rpx;
font-size: 24rpx;
}
}
}
&.input-large {
.input-container {
.input-field {
padding: 25rpx 15rpx;
font-size: 28rpx;
}
}
}
&.input-disabled {
.input-container {
background-color: #f5f5f5;
.input-field {
color: #999;
}
}
}
&.input-error {
.input-container {
border-color: #ff3b30;
}
}
}
</style>
2. 使用自定义输入组件
<template>
<view class="form-demo">
<text class="title">自定义表单组件</text>
<!-- 基础输入 -->
<custom-input
v-model="formData.username"
label="用户名"
placeholder="请输入用户名"
prefix-icon="👤"
:clearable="true"
@focus="onInputFocus"
/>
<!-- 密码输入 -->
<custom-input
v-model="formData.password"
label="密码"
:type="showPassword ? 'text' : 'password'"
placeholder="请输入密码"
prefix-icon="🔒"
:suffix-icon="showPassword ? '👁️' : '👁️🗨️'"
@suffix-click="togglePassword"
/>
<!-- 邮箱输入 -->
<custom-input
v-model="formData.email"
label="邮箱"
placeholder="请输入邮箱地址"
prefix-icon="📧"
:error-message="emailError"
@blur="validateEmail"
/>
<!-- 手机号输入 -->
<custom-input
v-model="formData.phone"
label="手机号"
type="number"
placeholder="请输入手机号"
prefix-icon="📱"
:maxlength="11"
:show-count="true"
:error-message="phoneError"
@blur="validatePhone"
/>
<!-- 备注输入 -->
<custom-input
v-model="formData.remark"
label="备注"
placeholder="请输入备注信息"
:maxlength="100"
:show-count="true"
size="large"
/>
<!-- 提交按钮 -->
<button
class="submit-btn"
type="primary"
:disabled="!isFormValid"
@click="submitForm"
>
提交表单
</button>
<!-- 表单数据显示 -->
<view class="form-data">
<text class="data-title">表单数据:</text>
<text class="data-content">{{ JSON.stringify(formData, null, 2) }}</text>
</view>
</view>
</template>
<script>
import CustomInput from '@/components/custom-input/custom-input.vue'
export default {
name: 'FormDemo',
components: {
CustomInput
},
data() {
return {
showPassword: false,
formData: {
username: '',
password: '',
email: '',
phone: '',
remark: ''
},
emailError: '',
phoneError: ''
}
},
computed: {
isFormValid() {
return this.formData.username &&
this.formData.password &&
this.formData.email &&
this.formData.phone &&
!this.emailError &&
!this.phoneError
}
},
methods: {
onInputFocus() {
console.log('用户名输入框获得焦点')
},
togglePassword() {
this.showPassword = !this.showPassword
},
validateEmail() {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!this.formData.email) {
this.emailError = '邮箱不能为空'
} else if (!emailRegex.test(this.formData.email)) {
this.emailError = '邮箱格式不正确'
} else {
this.emailError = ''
}
},
validatePhone() {
const phoneRegex = /^1[3-9]\d{9}$/
if (!this.formData.phone) {
this.phoneError = '手机号不能为空'
} else if (!phoneRegex.test(this.formData.phone)) {
this.phoneError = '手机号格式不正确'
} else {
this.phoneError = ''
}
},
submitForm() {
if (this.isFormValid) {
uni.showToast({
title: '表单提交成功',
icon: 'success'
})
console.log('提交的表单数据:', this.formData)
}
}
}
}
</script>
<style lang="scss" scoped>
.form-demo {
padding: 20rpx;
.title {
display: block;
font-size: 32rpx;
font-weight: bold;
text-align: center;
margin-bottom: 40rpx;
}
.submit-btn {
width: 100%;
height: 80rpx;
line-height: 80rpx;
font-size: 28rpx;
border-radius: 8rpx;
margin: 40rpx 0;
&:disabled {
background-color: #ccc;
}
}
.form-data {
padding: 20rpx;
background-color: #f8f8f8;
border-radius: 8rpx;
.data-title {
display: block;
font-size: 26rpx;
font-weight: bold;
margin-bottom: 15rpx;
color: #333;
}
.data-content {
display: block;
font-size: 22rpx;
line-height: 1.5;
color: #666;
white-space: pre-wrap;
}
}
}
</style>
2. Options API响应式数据
<template>
<view class="options-demo">
<text class="title">Options API演示</text>
<!-- 基础数据绑定 -->
<view class="section">
<text class="section-title">基础数据绑定</text>
<view class="data-display">
<text>用户名:{{ userInfo.name }}</text>
<text>年龄:{{ userInfo.age }}</text>
<text>邮箱:{{ userInfo.email }}</text>
</view>
<button @click="updateUserInfo">更新用户信息</button>
</view>
<!-- 计算属性 -->
<view class="section">
<text class="section-title">计算属性</text>
<view class="computed-display">
<text>全名:{{ fullName }}</text>
<text>年龄分组:{{ ageGroup }}</text>
<text>购物车总价:¥{{ totalPrice }}</text>
</view>
</view>
<!-- 侦听器 -->
<view class="section">
<text class="section-title">侦听器</text>
<input
class="search-input"
v-model="searchKeyword"
placeholder="输入搜索关键词"
/>
<text class="search-result">搜索结果:{{ searchResult }}</text>
</view>
<!-- 方法调用 -->
<view class="section">
<text class="section-title">方法调用</text>
<view class="counter-display">
<text>计数器:{{ counter }}</text>
<view class="button-group">
<button @click="increment">+1</button>
<button @click="decrement">-1</button>
<button @click="reset">重置</button>
</view>
</view>
</view>
<!-- 购物车示例 -->
<view class="section">
<text class="section-title">购物车示例</text>
<view class="cart-list">
<view
v-for="(item, index) in cartItems"
:key="item.id"
class="cart-item"
>
<text class="item-name">{{ item.name }}</text>
<text class="item-price">¥{{ item.price }}</text>
<view class="quantity-control">
<button @click="decreaseQuantity(index)">-</button>
<text class="quantity">{{ item.quantity }}</text>
<button @click="increaseQuantity(index)">+</button>
</view>
<button class="remove-btn" @click="removeItem(index)">删除</button>
</view>
</view>
<button @click="addRandomItem">添加商品</button>
</view>
</view>
</template>
<script>
export default {
name: 'OptionsDemo',
data() {
return {
// 用户信息
userInfo: {
name: '张三',
age: 25,
email: 'zhangsan@example.com'
},
// 搜索关键词
searchKeyword: '',
searchResult: '',
// 计数器
counter: 0,
// 购物车商品
cartItems: [
{ id: 1, name: 'iPhone 15', price: 5999, quantity: 1 },
{ id: 2, name: 'MacBook Pro', price: 12999, quantity: 1 },
{ id: 3, name: 'AirPods Pro', price: 1999, quantity: 2 }
],
// 商品库存
productPool: [
{ name: 'iPad Air', price: 4599 },
{ name: 'Apple Watch', price: 2999 },
{ name: 'Magic Mouse', price: 699 },
{ name: 'Magic Keyboard', price: 1299 }
]
}
},
computed: {
// 计算全名
fullName() {
return `${this.userInfo.name}(${this.userInfo.age}岁)`
},
// 年龄分组
ageGroup() {
const age = this.userInfo.age
if (age < 18) return '未成年'
if (age < 30) return '青年'
if (age < 50) return '中年'
return '老年'
},
// 购物车总价
totalPrice() {
return this.cartItems.reduce((total, item) => {
return total + (item.price * item.quantity)
}, 0)
},
// 购物车商品总数
totalItems() {
return this.cartItems.reduce((total, item) => {
return total + item.quantity
}, 0)
}
},
watch: {
// 监听搜索关键词变化
searchKeyword: {
handler(newVal, oldVal) {
console.log(`搜索关键词从 "${oldVal}" 变为 "${newVal}"`)
this.performSearch(newVal)
},
immediate: true // 立即执行一次
},
// 监听用户年龄变化
'userInfo.age': {
handler(newAge, oldAge) {
console.log(`年龄从 ${oldAge} 变为 ${newAge}`)
if (newAge >= 18 && oldAge < 18) {
uni.showToast({
title: '恭喜成年!',
icon: 'success'
})
}
}
},
// 深度监听购物车变化
cartItems: {
handler(newItems) {
console.log('购物车发生变化:', newItems)
// 可以在这里保存到本地存储
uni.setStorageSync('cartItems', newItems)
},
deep: true // 深度监听
}
},
methods: {
// 更新用户信息
updateUserInfo() {
this.userInfo.name = '李四'
this.userInfo.age = Math.floor(Math.random() * 50) + 18
this.userInfo.email = 'lisi@example.com'
uni.showToast({
title: '用户信息已更新',
icon: 'success'
})
},
// 执行搜索
performSearch(keyword) {
if (!keyword.trim()) {
this.searchResult = ''
return
}
// 模拟搜索延迟
setTimeout(() => {
this.searchResult = `找到 ${Math.floor(Math.random() * 100)} 条关于 "${keyword}" 的结果`
}, 500)
},
// 计数器增加
increment() {
this.counter++
},
// 计数器减少
decrement() {
if (this.counter > 0) {
this.counter--
}
},
// 重置计数器
reset() {
this.counter = 0
},
// 增加商品数量
increaseQuantity(index) {
this.cartItems[index].quantity++
},
// 减少商品数量
decreaseQuantity(index) {
if (this.cartItems[index].quantity > 1) {
this.cartItems[index].quantity--
}
},
// 删除商品
removeItem(index) {
uni.showModal({
title: '确认删除',
content: '确定要删除这个商品吗?',
success: (res) => {
if (res.confirm) {
this.cartItems.splice(index, 1)
uni.showToast({
title: '删除成功',
icon: 'success'
})
}
}
})
},
// 添加随机商品
addRandomItem() {
const randomProduct = this.productPool[Math.floor(Math.random() * this.productPool.length)]
const newItem = {
id: Date.now(),
name: randomProduct.name,
price: randomProduct.price,
quantity: 1
}
this.cartItems.push(newItem)
uni.showToast({
title: `已添加 ${newItem.name}`,
icon: 'success'
})
}
},
// 生命周期钩子
created() {
console.log('组件创建完成')
// 从本地存储恢复购物车数据
const savedCart = uni.getStorageSync('cartItems')
if (savedCart && savedCart.length > 0) {
this.cartItems = savedCart
}
},
mounted() {
console.log('组件挂载完成')
},
beforeDestroy() {
console.log('组件即将销毁')
}
}
</script>
<style lang="scss" scoped>
.options-demo {
padding: 20rpx;
.title {
display: block;
font-size: 32rpx;
font-weight: bold;
text-align: center;
margin-bottom: 30rpx;
}
.section {
margin-bottom: 40rpx;
padding: 20rpx;
background-color: #f8f8f8;
border-radius: 8rpx;
.section-title {
display: block;
font-size: 28rpx;
font-weight: bold;
margin-bottom: 20rpx;
color: #333;
}
}
.data-display {
margin-bottom: 20rpx;
text {
display: block;
font-size: 26rpx;
margin-bottom: 10rpx;
color: #666;
}
}
.computed-display {
text {
display: block;
font-size: 26rpx;
margin-bottom: 10rpx;
color: #666;
}
}
.search-input {
width: 100%;
padding: 15rpx;
border: 1px solid #ddd;
border-radius: 8rpx;
font-size: 26rpx;
margin-bottom: 15rpx;
}
.search-result {
display: block;
font-size: 24rpx;
color: #999;
}
.counter-display {
text-align: center;
text {
display: block;
font-size: 32rpx;
font-weight: bold;
margin-bottom: 20rpx;
color: #333;
}
}
.button-group {
display: flex;
justify-content: center;
gap: 20rpx;
button {
padding: 10rpx 30rpx;
font-size: 26rpx;
border-radius: 20rpx;
}
}
.cart-list {
margin-bottom: 20rpx;
}
.cart-item {
display: flex;
align-items: center;
padding: 20rpx;
background-color: white;
border-radius: 8rpx;
margin-bottom: 15rpx;
.item-name {
flex: 1;
font-size: 26rpx;
color: #333;
}
.item-price {
font-size: 24rpx;
color: #ff6b35;
margin-right: 20rpx;
}
.quantity-control {
display: flex;
align-items: center;
margin-right: 20rpx;
button {
width: 60rpx;
height: 60rpx;
border: 1px solid #ddd;
background-color: white;
font-size: 24rpx;
}
.quantity {
width: 80rpx;
text-align: center;
font-size: 26rpx;
border-top: 1px solid #ddd;
border-bottom: 1px solid #ddd;
line-height: 60rpx;
}
}
.remove-btn {
padding: 8rpx 20rpx;
background-color: #ff3b30;
color: white;
border: none;
border-radius: 20rpx;
font-size: 22rpx;
}
}
}
</style>
3.4 样式和布局
3.4.1 CSS样式基础
1. 基础样式语法
<template>
<view class="style-demo">
<text class="title">CSS样式演示</text>
<!-- 文本样式 -->
<view class="section">
<text class="section-title">文本样式</text>
<text class="text-large">大号文本</text>
<text class="text-medium">中号文本</text>
<text class="text-small">小号文本</text>
<text class="text-bold">粗体文本</text>
<text class="text-italic">斜体文本</text>
<text class="text-underline">下划线文本</text>
<text class="text-color-primary">主色调文本</text>
<text class="text-color-success">成功色文本</text>
<text class="text-color-warning">警告色文本</text>
<text class="text-color-danger">危险色文本</text>
</view>
<!-- 背景样式 -->
<view class="section">
<text class="section-title">背景样式</text>
<view class="bg-demo bg-color">纯色背景</view>
<view class="bg-demo bg-gradient">渐变背景</view>
<view class="bg-demo bg-image">图片背景</view>
</view>
<!-- 边框样式 -->
<view class="section">
<text class="section-title">边框样式</text>
<view class="border-demo border-solid">实线边框</view>
<view class="border-demo border-dashed">虚线边框</view>
<view class="border-demo border-dotted">点线边框</view>
<view class="border-demo border-radius">圆角边框</view>
<view class="border-demo border-circle">圆形边框</view>
</view>
<!-- 阴影效果 -->
<view class="section">
<text class="section-title">阴影效果</text>
<view class="shadow-demo shadow-small">小阴影</view>
<view class="shadow-demo shadow-medium">中阴影</view>
<view class="shadow-demo shadow-large">大阴影</view>
<view class="shadow-demo shadow-colored">彩色阴影</view>
</view>
<!-- 变换效果 -->
<view class="section">
<text class="section-title">变换效果</text>
<view class="transform-demo transform-rotate">旋转</view>
<view class="transform-demo transform-scale">缩放</view>
<view class="transform-demo transform-translate">平移</view>
<view class="transform-demo transform-skew">倾斜</view>
</view>
<!-- 动画效果 -->
<view class="section">
<text class="section-title">动画效果</text>
<view class="animation-demo animation-bounce">弹跳动画</view>
<view class="animation-demo animation-pulse">脉冲动画</view>
<view class="animation-demo animation-shake">摇摆动画</view>
<view class="animation-demo animation-fade">淡入淡出</view>
</view>
</view>
</template>
<script>
export default {
name: 'StyleDemo'
}
</script>
<style lang="scss" scoped>
// 颜色变量
$primary-color: #007aff;
$success-color: #09bb07;
$warning-color: #ff9500;
$danger-color: #ff3b30;
$text-color: #333;
$border-color: #ddd;
$bg-color: #f8f8f8;
.style-demo {
padding: 20rpx;
.title {
display: block;
font-size: 32rpx;
font-weight: bold;
text-align: center;
margin-bottom: 30rpx;
color: $text-color;
}
.section {
margin-bottom: 40rpx;
.section-title {
display: block;
font-size: 28rpx;
font-weight: bold;
margin-bottom: 20rpx;
color: $text-color;
}
}
// 文本样式
.text-large {
display: block;
font-size: 36rpx;
margin-bottom: 10rpx;
}
.text-medium {
display: block;
font-size: 28rpx;
margin-bottom: 10rpx;
}
.text-small {
display: block;
font-size: 22rpx;
margin-bottom: 10rpx;
}
.text-bold {
display: block;
font-weight: bold;
margin-bottom: 10rpx;
}
.text-italic {
display: block;
font-style: italic;
margin-bottom: 10rpx;
}
.text-underline {
display: block;
text-decoration: underline;
margin-bottom: 10rpx;
}
.text-color-primary {
display: block;
color: $primary-color;
margin-bottom: 10rpx;
}
.text-color-success {
display: block;
color: $success-color;
margin-bottom: 10rpx;
}
.text-color-warning {
display: block;
color: $warning-color;
margin-bottom: 10rpx;
}
.text-color-danger {
display: block;
color: $danger-color;
margin-bottom: 10rpx;
}
// 背景样式
.bg-demo {
height: 120rpx;
margin-bottom: 20rpx;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 26rpx;
border-radius: 8rpx;
}
.bg-color {
background-color: $primary-color;
}
.bg-gradient {
background: linear-gradient(45deg, $primary-color, $success-color);
}
.bg-image {
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><pattern id="grain" width="100" height="100" patternUnits="userSpaceOnUse"><circle cx="50" cy="50" r="2" fill="%23ffffff" opacity="0.1"/></pattern></defs><rect width="100" height="100" fill="%23007aff"/><rect width="100" height="100" fill="url(%23grain)"/></svg>');
background-size: 50rpx 50rpx;
}
// 边框样式
.border-demo {
height: 80rpx;
margin-bottom: 20rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 26rpx;
color: $text-color;
}
.border-solid {
border: 2rpx solid $primary-color;
}
.border-dashed {
border: 2rpx dashed $success-color;
}
.border-dotted {
border: 2rpx dotted $warning-color;
}
.border-radius {
border: 2rpx solid $danger-color;
border-radius: 20rpx;
}
.border-circle {
width: 120rpx;
height: 120rpx;
border: 2rpx solid $primary-color;
border-radius: 50%;
margin: 0 auto 20rpx;
}
// 阴影效果
.shadow-demo {
height: 80rpx;
margin-bottom: 20rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 26rpx;
color: $text-color;
background-color: white;
border-radius: 8rpx;
}
.shadow-small {
box-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.1);
}
.shadow-medium {
box-shadow: 0 4rpx 8rpx rgba(0, 0, 0, 0.15);
}
.shadow-large {
box-shadow: 0 8rpx 16rpx rgba(0, 0, 0, 0.2);
}
.shadow-colored {
box-shadow: 0 4rpx 12rpx rgba(0, 122, 255, 0.3);
}
// 变换效果
.transform-demo {
width: 120rpx;
height: 80rpx;
margin: 20rpx auto;
display: flex;
align-items: center;
justify-content: center;
font-size: 24rpx;
color: white;
background-color: $primary-color;
border-radius: 8rpx;
transition: transform 0.3s ease;
&:active {
opacity: 0.8;
}
}
.transform-rotate {
&:active {
transform: rotate(15deg);
}
}
.transform-scale {
&:active {
transform: scale(1.1);
}
}
.transform-translate {
&:active {
transform: translateX(20rpx);
}
}
.transform-skew {
&:active {
transform: skew(10deg, 5deg);
}
}
// 动画效果
.animation-demo {
width: 120rpx;
height: 80rpx;
margin: 20rpx auto;
display: flex;
align-items: center;
justify-content: center;
font-size: 24rpx;
color: white;
background-color: $success-color;
border-radius: 8rpx;
}
.animation-bounce {
animation: bounce 2s infinite;
}
.animation-pulse {
animation: pulse 2s infinite;
}
.animation-shake {
animation: shake 2s infinite;
}
.animation-fade {
animation: fade 2s infinite;
}
}
// 动画关键帧
@keyframes bounce {
0%, 20%, 50%, 80%, 100% {
transform: translateY(0);
}
40% {
transform: translateY(-20rpx);
}
60% {
transform: translateY(-10rpx);
}
}
@keyframes pulse {
0% {
transform: scale(1);
}
50% {
transform: scale(1.1);
}
100% {
transform: scale(1);
}
}
@keyframes shake {
0%, 100% {
transform: translateX(0);
}
10%, 30%, 50%, 70%, 90% {
transform: translateX(-10rpx);
}
20%, 40%, 60%, 80% {
transform: translateX(10rpx);
}
}
@keyframes fade {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.3;
}
}
</style>
2. 布局系统
<template>
<view class="layout-demo">
<text class="title">布局系统演示</text>
<!-- Flex布局 -->
<view class="section">
<text class="section-title">Flex布局</text>
<!-- 水平排列 -->
<view class="flex-demo">
<text class="demo-title">水平排列</text>
<view class="flex-container flex-row">
<view class="flex-item">1</view>
<view class="flex-item">2</view>
<view class="flex-item">3</view>
</view>
</view>
<!-- 垂直排列 -->
<view class="flex-demo">
<text class="demo-title">垂直排列</text>
<view class="flex-container flex-column">
<view class="flex-item">1</view>
<view class="flex-item">2</view>
<view class="flex-item">3</view>
</view>
</view>
<!-- 居中对齐 -->
<view class="flex-demo">
<text class="demo-title">居中对齐</text>
<view class="flex-container flex-center">
<view class="flex-item">居中</view>
</view>
</view>
<!-- 两端对齐 -->
<view class="flex-demo">
<text class="demo-title">两端对齐</text>
<view class="flex-container flex-between">
<view class="flex-item">左</view>
<view class="flex-item">中</view>
<view class="flex-item">右</view>
</view>
</view>
<!-- 环绕对齐 -->
<view class="flex-demo">
<text class="demo-title">环绕对齐</text>
<view class="flex-container flex-around">
<view class="flex-item">1</view>
<view class="flex-item">2</view>
<view class="flex-item">3</view>
</view>
</view>
<!-- 弹性布局 -->
<view class="flex-demo">
<text class="demo-title">弹性布局</text>
<view class="flex-container flex-elastic">
<view class="flex-item flex-1">弹性1</view>
<view class="flex-item flex-2">弹性2</view>
<view class="flex-item flex-1">弹性1</view>
</view>
</view>
</view>
<!-- Grid布局 -->
<view class="section">
<text class="section-title">Grid布局</text>
<!-- 基础网格 -->
<view class="grid-demo">
<text class="demo-title">基础网格 (2x2)</text>
<view class="grid-container grid-2x2">
<view class="grid-item">1</view>
<view class="grid-item">2</view>
<view class="grid-item">3</view>
<view class="grid-item">4</view>
</view>
</view>
<!-- 三列网格 -->
<view class="grid-demo">
<text class="demo-title">三列网格</text>
<view class="grid-container grid-3-cols">
<view class="grid-item">1</view>
<view class="grid-item">2</view>
<view class="grid-item">3</view>
<view class="grid-item">4</view>
<view class="grid-item">5</view>
<view class="grid-item">6</view>
</view>
</view>
<!-- 不等宽网格 -->
<view class="grid-demo">
<text class="demo-title">不等宽网格</text>
<view class="grid-container grid-unequal">
<view class="grid-item grid-span-2">跨两列</view>
<view class="grid-item">1</view>
<view class="grid-item">2</view>
<view class="grid-item">3</view>
<view class="grid-item">4</view>
</view>
</view>
</view>
<!-- 定位布局 -->
<view class="section">
<text class="section-title">定位布局</text>
<!-- 相对定位 -->
<view class="position-demo">
<text class="demo-title">相对定位</text>
<view class="position-container">
<view class="position-item position-relative">相对定位</view>
<view class="position-item">普通元素</view>
</view>
</view>
<!-- 绝对定位 -->
<view class="position-demo">
<text class="demo-title">绝对定位</text>
<view class="position-container position-relative-container">
<view class="position-item position-absolute-tl">左上</view>
<view class="position-item position-absolute-tr">右上</view>
<view class="position-item position-absolute-bl">左下</view>
<view class="position-item position-absolute-br">右下</view>
<view class="position-item position-absolute-center">居中</view>
</view>
</view>
<!-- 固定定位 -->
<view class="position-demo">
<text class="demo-title">固定定位</text>
<view class="position-container">
<text class="position-note">固定定位元素会相对于视口定位</text>
<view class="position-item position-fixed">固定位置</view>
</view>
</view>
</view>
<!-- 响应式布局 -->
<view class="section">
<text class="section-title">响应式布局</text>
<!-- 媒体查询 -->
<view class="responsive-demo">
<text class="demo-title">媒体查询响应式</text>
<view class="responsive-container">
<view class="responsive-item">响应式1</view>
<view class="responsive-item">响应式2</view>
<view class="responsive-item">响应式3</view>
<view class="responsive-item">响应式4</view>
</view>
</view>
<!-- 百分比布局 -->
<view class="responsive-demo">
<text class="demo-title">百分比布局</text>
<view class="percentage-container">
<view class="percentage-item percentage-25">25%</view>
<view class="percentage-item percentage-50">50%</view>
<view class="percentage-item percentage-25">25%</view>
</view>
</view>
<!-- 视口单位布局 -->
<view class="responsive-demo">
<text class="demo-title">视口单位布局</text>
<view class="viewport-container">
<view class="viewport-item viewport-width">50vw宽度</view>
<view class="viewport-item viewport-height">30vh高度</view>
</view>
</view>
</view>
<!-- 实际应用示例 -->
<view class="section">
<text class="section-title">实际应用示例</text>
<!-- 卡片布局 -->
<view class="example-demo">
<text class="demo-title">卡片布局</text>
<view class="card-layout">
<view class="card">
<view class="card-header">
<text class="card-title">卡片标题1</text>
<text class="card-subtitle">副标题</text>
</view>
<view class="card-content">
<text class="card-text">这是卡片的内容区域,可以放置各种信息。</text>
</view>
<view class="card-footer">
<button class="card-btn">操作</button>
</view>
</view>
<view class="card">
<view class="card-header">
<text class="card-title">卡片标题2</text>
<text class="card-subtitle">副标题</text>
</view>
<view class="card-content">
<text class="card-text">这是另一个卡片的内容区域。</text>
</view>
<view class="card-footer">
<button class="card-btn">操作</button>
</view>
</view>
</view>
</view>
<!-- 导航布局 -->
<view class="example-demo">
<text class="demo-title">导航布局</text>
<view class="nav-layout">
<view class="nav-header">
<text class="nav-title">页面标题</text>
<view class="nav-actions">
<button class="nav-btn">搜索</button>
<button class="nav-btn">更多</button>
</view>
</view>
<view class="nav-content">
<text class="content-text">主要内容区域</text>
</view>
<view class="nav-footer">
<view class="nav-tab">
<text class="tab-text">首页</text>
</view>
<view class="nav-tab">
<text class="tab-text">分类</text>
</view>
<view class="nav-tab">
<text class="tab-text">购物车</text>
</view>
<view class="nav-tab">
<text class="tab-text">我的</text>
</view>
</view>
</view>
</view>
<!-- 列表布局 -->
<view class="example-demo">
<text class="demo-title">列表布局</text>
<view class="list-layout">
<view class="list-item" v-for="(item, index) in listData" :key="index">
<view class="item-avatar">
<text class="avatar-text">{{ item.name.charAt(0) }}</text>
</view>
<view class="item-content">
<text class="item-title">{{ item.name }}</text>
<text class="item-desc">{{ item.description }}</text>
</view>
<view class="item-action">
<text class="item-time">{{ item.time }}</text>
</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
export default {
name: 'LayoutDemo',
data() {
return {
listData: [
{
name: '张三',
description: '这是一条消息内容,展示列表项的基本信息。',
time: '10:30'
},
{
name: '李四',
description: '另一条消息内容,用于演示列表布局效果。',
time: '09:15'
},
{
name: '王五',
description: '第三条消息,展示更多的列表项样式。',
time: '昨天'
}
]
}
}
}
</script>
<style lang="scss" scoped>
// 颜色变量
$primary-color: #007aff;
$success-color: #09bb07;
$warning-color: #ff9500;
$danger-color: #ff3b30;
$text-color: #333;
$border-color: #ddd;
$bg-color: #f8f8f8;
.layout-demo {
padding: 20rpx;
.title {
display: block;
font-size: 32rpx;
font-weight: bold;
text-align: center;
margin-bottom: 30rpx;
color: $text-color;
}
.section {
margin-bottom: 40rpx;
.section-title {
display: block;
font-size: 28rpx;
font-weight: bold;
margin-bottom: 20rpx;
color: $text-color;
}
}
.demo-title {
display: block;
font-size: 24rpx;
color: #666;
margin-bottom: 10rpx;
}
// Flex布局样式
.flex-demo {
margin-bottom: 30rpx;
}
.flex-container {
border: 1rpx solid $border-color;
border-radius: 8rpx;
padding: 20rpx;
min-height: 120rpx;
}
.flex-row {
display: flex;
flex-direction: row;
}
.flex-column {
display: flex;
flex-direction: column;
}
.flex-center {
display: flex;
justify-content: center;
align-items: center;
}
.flex-between {
display: flex;
justify-content: space-between;
align-items: center;
}
.flex-around {
display: flex;
justify-content: space-around;
align-items: center;
}
.flex-elastic {
display: flex;
}
.flex-item {
background-color: $primary-color;
color: white;
padding: 20rpx;
margin: 5rpx;
border-radius: 8rpx;
text-align: center;
font-size: 24rpx;
}
.flex-1 {
flex: 1;
}
.flex-2 {
flex: 2;
}
// Grid布局样式
.grid-demo {
margin-bottom: 30rpx;
}
.grid-container {
border: 1rpx solid $border-color;
border-radius: 8rpx;
padding: 20rpx;
}
.grid-2x2 {
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr;
gap: 10rpx;
}
.grid-3-cols {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10rpx;
}
.grid-unequal {
display: grid;
grid-template-columns: 2fr 1fr 1fr;
gap: 10rpx;
}
.grid-item {
background-color: $success-color;
color: white;
padding: 20rpx;
border-radius: 8rpx;
text-align: center;
font-size: 24rpx;
min-height: 80rpx;
display: flex;
align-items: center;
justify-content: center;
}
.grid-span-2 {
grid-column: span 2;
}
// 定位布局样式
.position-demo {
margin-bottom: 30rpx;
}
.position-container {
border: 1rpx solid $border-color;
border-radius: 8rpx;
padding: 20rpx;
min-height: 200rpx;
}
.position-relative-container {
position: relative;
}
.position-item {
background-color: $warning-color;
color: white;
padding: 15rpx;
border-radius: 8rpx;
text-align: center;
font-size: 22rpx;
margin: 5rpx;
}
.position-relative {
position: relative;
left: 20rpx;
top: 10rpx;
}
.position-absolute-tl {
position: absolute;
top: 20rpx;
left: 20rpx;
}
.position-absolute-tr {
position: absolute;
top: 20rpx;
right: 20rpx;
}
.position-absolute-bl {
position: absolute;
bottom: 20rpx;
left: 20rpx;
}
.position-absolute-br {
position: absolute;
bottom: 20rpx;
right: 20rpx;
}
.position-absolute-center {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.position-fixed {
position: fixed;
bottom: 100rpx;
right: 30rpx;
z-index: 999;
}
.position-note {
display: block;
font-size: 22rpx;
color: #666;
margin-bottom: 20rpx;
}
// 响应式布局样式
.responsive-demo {
margin-bottom: 30rpx;
}
.responsive-container {
display: flex;
flex-wrap: wrap;
gap: 10rpx;
border: 1rpx solid $border-color;
border-radius: 8rpx;
padding: 20rpx;
}
.responsive-item {
background-color: $danger-color;
color: white;
padding: 20rpx;
border-radius: 8rpx;
text-align: center;
font-size: 22rpx;
flex: 1;
min-width: 200rpx;
}
.percentage-container {
display: flex;
border: 1rpx solid $border-color;
border-radius: 8rpx;
overflow: hidden;
}
.percentage-item {
background-color: $primary-color;
color: white;
padding: 30rpx 0;
text-align: center;
font-size: 22rpx;
}
.percentage-25 {
width: 25%;
}
.percentage-50 {
width: 50%;
background-color: $success-color;
}
.viewport-container {
border: 1rpx solid $border-color;
border-radius: 8rpx;
padding: 20rpx;
}
.viewport-item {
background-color: $warning-color;
color: white;
margin-bottom: 10rpx;
border-radius: 8rpx;
text-align: center;
font-size: 22rpx;
display: flex;
align-items: center;
justify-content: center;
}
.viewport-width {
width: 50vw;
height: 80rpx;
}
.viewport-height {
width: 100%;
height: 30vh;
}
// 实际应用示例样式
.example-demo {
margin-bottom: 30rpx;
}
// 卡片布局
.card-layout {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.card {
background-color: white;
border-radius: 12rpx;
box-shadow: 0 4rpx 8rpx rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.card-header {
padding: 20rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.card-title {
display: block;
font-size: 28rpx;
font-weight: bold;
color: $text-color;
margin-bottom: 5rpx;
}
.card-subtitle {
display: block;
font-size: 22rpx;
color: #999;
}
.card-content {
padding: 20rpx;
}
.card-text {
font-size: 26rpx;
color: #666;
line-height: 1.5;
}
.card-footer {
padding: 20rpx;
border-top: 1rpx solid #f0f0f0;
text-align: right;
}
.card-btn {
background-color: $primary-color;
color: white;
border: none;
padding: 12rpx 30rpx;
border-radius: 20rpx;
font-size: 24rpx;
}
// 导航布局
.nav-layout {
background-color: white;
border-radius: 12rpx;
overflow: hidden;
box-shadow: 0 4rpx 8rpx rgba(0, 0, 0, 0.1);
}
.nav-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx;
background-color: $primary-color;
color: white;
}
.nav-title {
font-size: 28rpx;
font-weight: bold;
}
.nav-actions {
display: flex;
gap: 10rpx;
}
.nav-btn {
background-color: rgba(255, 255, 255, 0.2);
color: white;
border: none;
padding: 8rpx 20rpx;
border-radius: 15rpx;
font-size: 22rpx;
}
.nav-content {
padding: 40rpx 20rpx;
text-align: center;
min-height: 200rpx;
display: flex;
align-items: center;
justify-content: center;
}
.content-text {
font-size: 26rpx;
color: #666;
}
.nav-footer {
display: flex;
border-top: 1rpx solid #f0f0f0;
}
.nav-tab {
flex: 1;
padding: 20rpx;
text-align: center;
border-right: 1rpx solid #f0f0f0;
&:last-child {
border-right: none;
}
}
.tab-text {
font-size: 24rpx;
color: #666;
}
// 列表布局
.list-layout {
background-color: white;
border-radius: 12rpx;
overflow: hidden;
box-shadow: 0 4rpx 8rpx rgba(0, 0, 0, 0.1);
}
.list-item {
display: flex;
align-items: center;
padding: 20rpx;
border-bottom: 1rpx solid #f0f0f0;
&:last-child {
border-bottom: none;
}
}
.item-avatar {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
background-color: $primary-color;
display: flex;
align-items: center;
justify-content: center;
margin-right: 20rpx;
}
.avatar-text {
color: white;
font-size: 28rpx;
font-weight: bold;
}
.item-content {
flex: 1;
}
.item-title {
display: block;
font-size: 26rpx;
font-weight: bold;
color: $text-color;
margin-bottom: 5rpx;
}
.item-desc {
display: block;
font-size: 22rpx;
color: #999;
line-height: 1.4;
}
.item-action {
text-align: right;
}
.item-time {
font-size: 20rpx;
color: #ccc;
}
}
// 媒体查询
@media screen and (max-width: 600rpx) {
.responsive-item {
flex-basis: 100%;
}
.card-layout {
.card {
margin-bottom: 15rpx;
}
}
}
@media screen and (min-width: 601rpx) and (max-width: 900rpx) {
.responsive-item {
flex-basis: calc(50% - 5rpx);
}
}
@media screen and (min-width: 901rpx) {
.responsive-item {
flex-basis: calc(25% - 7.5rpx);
}
.card-layout {
flex-direction: row;
.card {
flex: 1;
}
}
}
</style>
3.4.2 rpx单位与适配
1. rpx单位说明
rpx(responsive pixel)是UniApp中的响应式像素单位,可以根据屏幕宽度进行自适应。
- 规定屏幕宽为750rpx
- 如在iPhone6上,屏幕宽度为375px,共有750个物理像素,则750rpx = 375px = 750物理像素,1rpx = 0.5px = 1物理像素
2. 单位换算示例
<template>
<view class="unit-demo">
<text class="title">单位换算演示</text>
<!-- rpx单位 -->
<view class="section">
<text class="section-title">rpx单位</text>
<view class="unit-item rpx-100">100rpx</view>
<view class="unit-item rpx-200">200rpx</view>
<view class="unit-item rpx-300">300rpx</view>
<view class="unit-item rpx-400">400rpx</view>
</view>
<!-- px单位 -->
<view class="section">
<text class="section-title">px单位</text>
<view class="unit-item px-50">50px</view>
<view class="unit-item px-100">100px</view>
<view class="unit-item px-150">150px</view>
<view class="unit-item px-200">200px</view>
</view>
<!-- 百分比单位 -->
<view class="section">
<text class="section-title">百分比单位</text>
<view class="unit-container">
<view class="unit-item percent-25">25%</view>
<view class="unit-item percent-50">50%</view>
<view class="unit-item percent-75">75%</view>
<view class="unit-item percent-100">100%</view>
</view>
</view>
<!-- 视口单位 -->
<view class="section">
<text class="section-title">视口单位</text>
<view class="unit-item vw-25">25vw</view>
<view class="unit-item vw-50">50vw</view>
<view class="unit-item vh-20">20vh</view>
<view class="unit-item vh-30">30vh</view>
</view>
<!-- 混合使用 -->
<view class="section">
<text class="section-title">混合使用</text>
<view class="mixed-container">
<view class="mixed-item mixed-1">rpx + %</view>
<view class="mixed-item mixed-2">px + vw</view>
<view class="mixed-item mixed-3">rpx + vh</view>
</view>
</view>
</view>
</template>
<script>
export default {
name: 'UnitDemo'
}
</script>
<style lang="scss" scoped>
.unit-demo {
padding: 20rpx;
.title {
display: block;
font-size: 32rpx;
font-weight: bold;
text-align: center;
margin-bottom: 30rpx;
color: #333;
}
.section {
margin-bottom: 40rpx;
.section-title {
display: block;
font-size: 28rpx;
font-weight: bold;
margin-bottom: 20rpx;
color: #333;
}
}
.unit-item {
background-color: #007aff;
color: white;
margin-bottom: 15rpx;
border-radius: 8rpx;
text-align: center;
font-size: 24rpx;
display: flex;
align-items: center;
justify-content: center;
min-height: 80rpx;
}
// rpx单位样式
.rpx-100 {
width: 100rpx;
}
.rpx-200 {
width: 200rpx;
}
.rpx-300 {
width: 300rpx;
}
.rpx-400 {
width: 400rpx;
}
// px单位样式
.px-50 {
width: 50px;
background-color: #09bb07;
}
.px-100 {
width: 100px;
background-color: #09bb07;
}
.px-150 {
width: 150px;
background-color: #09bb07;
}
.px-200 {
width: 200px;
background-color: #09bb07;
}
// 百分比单位样式
.unit-container {
display: flex;
flex-direction: column;
gap: 15rpx;
}
.percent-25 {
width: 25%;
background-color: #ff9500;
}
.percent-50 {
width: 50%;
background-color: #ff9500;
}
.percent-75 {
width: 75%;
background-color: #ff9500;
}
.percent-100 {
width: 100%;
background-color: #ff9500;
}
// 视口单位样式
.vw-25 {
width: 25vw;
background-color: #ff3b30;
}
.vw-50 {
width: 50vw;
background-color: #ff3b30;
}
.vh-20 {
width: 100%;
height: 20vh;
background-color: #ff3b30;
}
.vh-30 {
width: 100%;
height: 30vh;
background-color: #ff3b30;
}
// 混合使用样式
.mixed-container {
display: flex;
flex-direction: column;
gap: 15rpx;
}
.mixed-item {
background-color: #5856d6;
}
.mixed-1 {
width: 80%;
height: 120rpx;
}
.mixed-2 {
width: 60vw;
height: 100px;
}
.mixed-3 {
width: 500rpx;
height: 15vh;
}
}
</style>
3.5 本章总结
3.5.1 学习要点回顾
Vue 3基础语法
- Composition API:ref、reactive、computed、watch等
- Options API:data、computed、watch、methods等
- 生命周期钩子的使用
UniApp生命周期
- 应用生命周期:onLaunch、onShow、onHide等
- 页面生命周期:onLoad、onShow、onReady等
组件通信
- 父子组件通信:props、$emit
- 兄弟组件通信:事件总线
- 自定义组件开发
内置组件使用
- 视图容器:view、scroll-view、swiper
- 基础内容:text、image
- 表单组件:input、textarea、button、form等
样式和布局
- CSS基础样式:文本、背景、边框、阴影等
- 布局系统:Flex、Grid、定位布局
- 响应式设计:媒体查询、百分比、视口单位
- rpx单位与适配
3.5.2 实践练习
创建一个用户信息管理页面
- 使用Composition API管理用户数据
- 实现用户信息的增删改查
- 使用表单组件收集用户输入
开发一个商品列表组件
- 使用自定义组件展示商品信息
- 实现商品的筛选和排序功能
- 使用Flex布局实现响应式设计
制作一个导航页面
- 使用swiper组件实现轮播图
- 使用Grid布局实现功能入口
- 添加动画效果和交互反馈
3.5.3 常见问题解答
Q: Composition API和Options API有什么区别? A: Composition API提供了更好的逻辑复用和类型推导,适合复杂组件;Options API更直观易懂,适合简单组件。
Q: 什么时候使用rpx,什么时候使用px? A: rpx用于需要适配不同屏幕尺寸的元素;px用于固定尺寸的元素,如边框宽度。
Q: 如何选择合适的布局方式? A: Flex布局适合一维布局,Grid布局适合二维布局,定位布局适合特殊位置需求。
Q: 组件通信有哪些方式? A: 父子组件使用props和$emit,兄弟组件使用事件总线,复杂状态使用Vuex或Pinia。
3.5.4 下一章预告
下一章我们将学习页面路由与导航,包括:
路由配置
- pages.json配置详解
- 路由参数传递
- 路由守卫
页面跳转
- 编程式导航
- 声明式导航
- 页面栈管理
导航组件
- 导航栏配置
- 底部导航
- 自定义导航
页面传值
- URL参数传递
- 页面间数据传递
- 全局数据管理
通过下一章的学习,你将掌握UniApp中页面间的导航和数据传递,为构建完整的应用打下基础。
本章重点: - 掌握Vue 3基础语法在UniApp中的应用 - 熟练使用UniApp内置组件 - 理解组件通信机制 - 掌握样式和布局技巧 - 了解rpx单位的使用场景
下章重点:
- 页面路由配置和管理
- 页面间导航和数据传递
- 导航组件的使用和自定义
<view class="controls">
<input v-model="message" placeholder="输入消息" />
<button @click="addItem">添加项目</button>
<button @click="clearItems">清空列表</button>
</view>
<view class="item-list">
<view v-for="(item, index) in items" :key="index" class="item">
{{ item }}
</view>
</view>
### 3.1.2 生命周期钩子
**1. 应用生命周期**
```javascript
// App.vue - 应用级生命周期
export default {
// 应用启动时触发
onLaunch(options) {
console.log('应用启动', options)
// 初始化应用
this.initApp()
// 检查更新
this.checkUpdate()
// 设置全局数据
this.setGlobalData()
},
// 应用显示时触发
onShow(options) {
console.log('应用显示', options)
// 刷新用户信息
this.refreshUserInfo()
// 检查网络状态
this.checkNetworkStatus()
},
// 应用隐藏时触发
onHide() {
console.log('应用隐藏')
// 保存应用状态
this.saveAppState()
// 暂停定时器
this.pauseTimers()
},
// 应用错误时触发
onError(error) {
console.error('应用错误:', error)
// 错误上报
this.reportError(error)
// 显示错误提示
uni.showToast({
title: '应用出现错误',
icon: 'none'
})
},
// 页面不存在时触发
onPageNotFound(res) {
console.log('页面不存在:', res)
// 重定向到首页
uni.redirectTo({
url: '/pages/index/index'
})
},
methods: {
initApp() {
// 初始化应用配置
this.loadConfig()
// 初始化第三方SDK
this.initSDK()
// 设置状态栏
this.setStatusBar()
},
checkUpdate() {
// #ifdef APP-PLUS
plus.runtime.getProperty(plus.runtime.appid, (widgetInfo) => {
console.log('当前版本:', widgetInfo.version)
// 检查是否有新版本
})
// #endif
},
setGlobalData() {
// 设置全局变量
getApp().globalData = {
userInfo: null,
systemInfo: uni.getSystemInfoSync(),
config: {}
}
}
}
}
2. 页面生命周期
<template>
<view class="lifecycle-demo">
<text class="title">页面生命周期演示</text>
<view class="lifecycle-log">
<view v-for="(log, index) in lifecycleLogs" :key="index" class="log-item">
{{ log }}
</view>
</view>
<button @click="clearLogs">清空日志</button>
</view>
</template>
<script>
export default {
name: 'LifecycleDemo',
data() {
return {
lifecycleLogs: []
}
},
// 页面加载时触发
onLoad(options) {
this.addLog('onLoad - 页面加载', options)
// 获取页面参数
if (options.id) {
this.loadData(options.id)
}
// 设置页面标题
uni.setNavigationBarTitle({
title: '生命周期演示'
})
},
// 页面显示时触发
onShow() {
this.addLog('onShow - 页面显示')
// 刷新数据
this.refreshData()
// 开始定时器
this.startTimer()
},
// 页面初次渲染完成时触发
onReady() {
this.addLog('onReady - 页面初次渲染完成')
// 获取节点信息
this.getNodeInfo()
// 初始化组件
this.initComponents()
},
// 页面隐藏时触发
onHide() {
this.addLog('onHide - 页面隐藏')
// 停止定时器
this.stopTimer()
// 保存页面状态
this.savePageState()
},
// 页面卸载时触发
onUnload() {
this.addLog('onUnload - 页面卸载')
// 清理资源
this.cleanup()
// 取消网络请求
this.cancelRequests()
},
// 下拉刷新时触发
onPullDownRefresh() {
this.addLog('onPullDownRefresh - 下拉刷新')
// 刷新数据
setTimeout(() => {
this.refreshData()
uni.stopPullDownRefresh()
}, 1000)
},
// 上拉加载时触发
onReachBottom() {
this.addLog('onReachBottom - 上拉加载')
// 加载更多数据
this.loadMoreData()
},
// 页面滚动时触发
onPageScroll(e) {
// 节流处理,避免频繁触发
if (this.scrollTimer) return
this.scrollTimer = setTimeout(() => {
this.addLog(`onPageScroll - 页面滚动: ${e.scrollTop}px`)
this.scrollTimer = null
}, 100)
},
// 分享时触发
onShareAppMessage() {
this.addLog('onShareAppMessage - 分享')
return {
title: '生命周期演示页面',
path: '/pages/lifecycle/lifecycle'
}
},
methods: {
addLog(message, data = null) {
const timestamp = new Date().toLocaleTimeString()
const logMessage = `[${timestamp}] ${message}`
if (data) {
console.log(logMessage, data)
} else {
console.log(logMessage)
}
this.lifecycleLogs.push(logMessage)
// 限制日志数量
if (this.lifecycleLogs.length > 20) {
this.lifecycleLogs.shift()
}
},
clearLogs() {
this.lifecycleLogs = []
},
loadData(id) {
// 模拟数据加载
console.log('加载数据,ID:', id)
},
refreshData() {
// 模拟数据刷新
console.log('刷新数据')
},
loadMoreData() {
// 模拟加载更多数据
console.log('加载更多数据')
},
startTimer() {
// 启动定时器
this.timer = setInterval(() => {
console.log('定时器执行')
}, 5000)
},
stopTimer() {
// 停止定时器
if (this.timer) {
clearInterval(this.timer)
this.timer = null
}
},
getNodeInfo() {
// 获取节点信息
const query = uni.createSelectorQuery().in(this)
query.select('.title').boundingClientRect(data => {
console.log('标题节点信息:', data)
}).exec()
},
initComponents() {
// 初始化组件
console.log('初始化组件')
},
savePageState() {
// 保存页面状态
const pageState = {
logs: this.lifecycleLogs,
timestamp: Date.now()
}
uni.setStorageSync('pageState', pageState)
},
cleanup() {
// 清理资源
this.stopTimer()
if (this.scrollTimer) {
clearTimeout(this.scrollTimer)
}
},
cancelRequests() {
// 取消网络请求
console.log('取消网络请求')
}
}
}
</script>
<style lang="scss" scoped>
.lifecycle-demo {
padding: 20rpx;
.title {
display: block;
font-size: 36rpx;
font-weight: bold;
text-align: center;
margin-bottom: 30rpx;
}
.lifecycle-log {
max-height: 600rpx;
overflow-y: auto;
border: 1px solid #eee;
border-radius: 8rpx;
margin-bottom: 30rpx;
.log-item {
padding: 10rpx 15rpx;
border-bottom: 1px solid #f0f0f0;
font-size: 24rpx;
color: #666;
&:last-child {
border-bottom: none;
}
}
}
button {
width: 100%;
padding: 20rpx;
background-color: #ff6b6b;
color: white;
border: none;
border-radius: 8rpx;
font-size: 28rpx;
}
}
</style>
3.1.3 组件通信
1. 父子组件通信
<!-- 父组件 ParentComponent.vue -->
<template>
<view class="parent-component">
<text class="title">父子组件通信演示</text>
<!-- 传递props给子组件 -->
<child-component
:message="parentMessage"
:user-info="userInfo"
:items="itemList"
@update-message="handleUpdateMessage"
@add-item="handleAddItem"
@custom-event="handleCustomEvent"
/>
<view class="parent-controls">
<input v-model="parentMessage" placeholder="父组件消息" />
<button @click="updateUserInfo">更新用户信息</button>
<button @click="addParentItem">父组件添加项目</button>
</view>
<view class="event-log">
<text class="log-title">事件日志:</text>
<view v-for="(log, index) in eventLogs" :key="index" class="log-item">
{{ log }}
</view>
</view>
</view>
</template>
<script>
import ChildComponent from './ChildComponent.vue'
export default {
name: 'ParentComponent',
components: {
ChildComponent
},
data() {
return {
parentMessage: '来自父组件的消息',
userInfo: {
name: '张三',
age: 25,
email: 'zhangsan@example.com'
},
itemList: ['项目1', '项目2', '项目3'],
eventLogs: []
}
},
methods: {
// 处理子组件发出的事件
handleUpdateMessage(newMessage) {
this.parentMessage = newMessage
this.addLog(`子组件更新消息: ${newMessage}`)
},
handleAddItem(item) {
this.itemList.push(item)
this.addLog(`子组件添加项目: ${item}`)
},
handleCustomEvent(data) {
this.addLog(`收到自定义事件: ${JSON.stringify(data)}`)
},
// 父组件方法
updateUserInfo() {
this.userInfo.name = '李四'
this.userInfo.age = 30
this.addLog('父组件更新用户信息')
},
addParentItem() {
const newItem = `父组件项目${this.itemList.length + 1}`
this.itemList.push(newItem)
this.addLog(`父组件添加项目: ${newItem}`)
},
addLog(message) {
const timestamp = new Date().toLocaleTimeString()
this.eventLogs.push(`[${timestamp}] ${message}`)
if (this.eventLogs.length > 10) {
this.eventLogs.shift()
}
}
}
}
</script>
<style lang="scss" scoped>
.parent-component {
padding: 20rpx;
.title {
display: block;
font-size: 32rpx;
font-weight: bold;
text-align: center;
margin-bottom: 30rpx;
}
.parent-controls {
margin: 30rpx 0;
input {
width: 100%;
padding: 20rpx;
margin-bottom: 20rpx;
border: 1px solid #ddd;
border-radius: 4rpx;
font-size: 28rpx;
}
button {
margin-right: 20rpx;
margin-bottom: 10rpx;
padding: 15rpx 25rpx;
background-color: #007aff;
color: white;
border: none;
border-radius: 4rpx;
font-size: 26rpx;
}
}
.event-log {
margin-top: 30rpx;
padding: 20rpx;
background-color: #f8f8f8;
border-radius: 8rpx;
.log-title {
display: block;
font-size: 28rpx;
font-weight: bold;
margin-bottom: 15rpx;
}
.log-item {
font-size: 24rpx;
color: #666;
margin-bottom: 8rpx;
&:last-child {
margin-bottom: 0;
}
}
}
}
</style>
<!-- 子组件 ChildComponent.vue -->
<template>
<view class="child-component">
<view class="props-display">
<text class="section-title">接收到的Props:</text>
<text>消息: {{ message }}</text>
<text>用户: {{ userInfo.name }} ({{ userInfo.age }}岁)</text>
<text>项目数量: {{ items.length }}</text>
</view>
<view class="items-list">
<text class="section-title">项目列表:</text>
<view v-for="(item, index) in items" :key="index" class="item">
{{ item }}
</view>
</view>
<view class="child-controls">
<input v-model="childMessage" placeholder="子组件消息" />
<button @click="updateParentMessage">更新父组件消息</button>
<button @click="addItemToParent">向父组件添加项目</button>
<button @click="emitCustomEvent">发送自定义事件</button>
</view>
</view>
</template>
<script>
export default {
name: 'ChildComponent',
// 定义props
props: {
message: {
type: String,
default: '默认消息'
},
userInfo: {
type: Object,
default: () => ({})
},
items: {
type: Array,
default: () => []
}
},
data() {
return {
childMessage: '来自子组件的消息'
}
},
// 监听props变化
watch: {
message(newValue, oldValue) {
console.log(`子组件接收到新消息: ${newValue}`)
},
userInfo: {
handler(newValue) {
console.log('子组件接收到用户信息更新:', newValue)
},
deep: true
},
items(newValue) {
console.log('子组件接收到项目列表更新:', newValue)
}
},
methods: {
// 向父组件发送事件
updateParentMessage() {
this.$emit('update-message', this.childMessage)
},
addItemToParent() {
const newItem = `子组件项目${Date.now()}`
this.$emit('add-item', newItem)
},
emitCustomEvent() {
const eventData = {
type: 'custom',
timestamp: Date.now(),
data: {
childMessage: this.childMessage,
randomNumber: Math.random()
}
}
this.$emit('custom-event', eventData)
}
}
}
</script>
<style lang="scss" scoped>
.child-component {
margin: 20rpx 0;
padding: 20rpx;
border: 2px solid #e0e0e0;
border-radius: 8rpx;
background-color: #fafafa;
.section-title {
display: block;
font-size: 28rpx;
font-weight: bold;
margin-bottom: 15rpx;
color: #333;
}
.props-display {
margin-bottom: 25rpx;
text {
display: block;
font-size: 26rpx;
margin-bottom: 8rpx;
color: #666;
&:not(.section-title) {
padding-left: 20rpx;
}
}
}
.items-list {
margin-bottom: 25rpx;
.item {
padding: 8rpx 20rpx;
margin-bottom: 5rpx;
background-color: #fff;
border-radius: 4rpx;
font-size: 24rpx;
color: #555;
}
}
.child-controls {
input {
width: 100%;
padding: 15rpx;
margin-bottom: 15rpx;
border: 1px solid #ccc;
border-radius: 4rpx;
font-size: 26rpx;
}
button {
margin-right: 15rpx;
margin-bottom: 10rpx;
padding: 12rpx 20rpx;
background-color: #09bb07;
color: white;
border: none;
border-radius: 4rpx;
font-size: 24rpx;
}
}
}
</style>
2. 兄弟组件通信
<!-- 事件总线方式 -->
<!-- EventBus.js -->
<script>
// 创建事件总线
import { createApp } from 'vue'
const eventBus = createApp({})
export default eventBus
</script>
<!-- 兄弟组件A SiblingA.vue -->
<template>
<view class="sibling-a">
<text class="title">兄弟组件A</text>
<input v-model="messageToB" placeholder="发送给组件B的消息" />
<button @click="sendMessageToB">发送消息给B</button>
<view class="received-messages">
<text class="subtitle">收到来自B的消息:</text>
<view v-for="(msg, index) in messagesFromB" :key="index" class="message">
{{ msg }}
</view>
</view>
</view>
</template>
<script>
import eventBus from './EventBus.js'
export default {
name: 'SiblingA',
data() {
return {
messageToB: '',
messagesFromB: []
}
},
mounted() {
// 监听来自组件B的消息
eventBus.$on('message-from-b', this.handleMessageFromB)
},
beforeUnmount() {
// 移除事件监听
eventBus.$off('message-from-b', this.handleMessageFromB)
},
methods: {
sendMessageToB() {
if (this.messageToB.trim()) {
eventBus.$emit('message-from-a', this.messageToB)
this.messageToB = ''
}
},
handleMessageFromB(message) {
this.messagesFromB.push(message)
if (this.messagesFromB.length > 5) {
this.messagesFromB.shift()
}
}
}
}
</script>
<style lang="scss" scoped>
.sibling-a {
padding: 20rpx;
margin-bottom: 20rpx;
border: 2px solid #007aff;
border-radius: 8rpx;
.title {
display: block;
font-size: 28rpx;
font-weight: bold;
margin-bottom: 20rpx;
color: #007aff;
}
input {
width: 100%;
padding: 15rpx;
margin-bottom: 15rpx;
border: 1px solid #ddd;
border-radius: 4rpx;
font-size: 26rpx;
}
button {
padding: 15rpx 25rpx;
background-color: #007aff;
color: white;
border: none;
border-radius: 4rpx;
font-size: 26rpx;
}
.received-messages {
margin-top: 20rpx;
.subtitle {
display: block;
font-size: 24rpx;
font-weight: bold;
margin-bottom: 10rpx;
}
.message {
padding: 10rpx;
margin-bottom: 8rpx;
background-color: #f0f8ff;
border-radius: 4rpx;
font-size: 24rpx;
}
}
}
</style>
3.2 UniApp内置组件
3.2.1 视图容器组件
1. view组件 - 基础容器
<template>
<view class="view-demo">
<text class="title">View组件演示</text>
<!-- 基础view -->
<view class="basic-view">
<text>基础view容器</text>
</view>
<!-- 可点击view -->
<view class="clickable-view" @click="handleViewClick">
<text>可点击的view</text>
</view>
<!-- 长按view -->
<view class="longpress-view" @longpress="handleLongPress">
<text>长按这个view</text>
</view>
<!-- 触摸事件view -->
<view
class="touch-view"
@touchstart="handleTouchStart"
@touchmove="handleTouchMove"
@touchend="handleTouchEnd"
>
<text>触摸事件view</text>
<text class="touch-info">{{ touchInfo }}</text>
</view>
<!-- 嵌套view -->
<view class="nested-container">
<view class="nested-header">
<text>嵌套容器头部</text>
</view>
<view class="nested-content">
<view class="nested-item" v-for="n in 3" :key="n">
<text>嵌套项目 {{ n }}</text>
</view>
</view>
<view class="nested-footer">
<text>嵌套容器底部</text>
</view>
</view>
</view>
</template>
<script>
export default {
name: 'ViewDemo',
data() {
return {
touchInfo: '等待触摸...'
}
},
methods: {
handleViewClick() {
uni.showToast({
title: 'View被点击',
icon: 'none'
})
},
handleLongPress() {
uni.showModal({
title: '长按事件',
content: 'View被长按了',
showCancel: false
})
},
handleTouchStart(e) {
const touch = e.touches[0]
this.touchInfo = `触摸开始: (${Math.round(touch.clientX)}, ${Math.round(touch.clientY)})`
},
handleTouchMove(e) {
const touch = e.touches[0]
this.touchInfo = `触摸移动: (${Math.round(touch.clientX)}, ${Math.round(touch.clientY)})`
},
handleTouchEnd() {
this.touchInfo = '触摸结束'
setTimeout(() => {
this.touchInfo = '等待触摸...'
}, 1000)
}
}
}
</script>
<style lang="scss" scoped>
.view-demo {
padding: 20rpx;
.title {
display: block;
font-size: 32rpx;
font-weight: bold;
text-align: center;
margin-bottom: 30rpx;
}
.basic-view {
padding: 30rpx;
margin-bottom: 20rpx;
background-color: #f0f0f0;
border-radius: 8rpx;
text-align: center;
}
.clickable-view {
padding: 30rpx;
margin-bottom: 20rpx;
background-color: #007aff;
border-radius: 8rpx;
text-align: center;
text {
color: white;
}
&:active {
background-color: #0056cc;
}
}
.longpress-view {
padding: 30rpx;
margin-bottom: 20rpx;
background-color: #ff6b6b;
border-radius: 8rpx;
text-align: center;
text {
color: white;
}
}
.touch-view {
padding: 30rpx;
margin-bottom: 20rpx;
background-color: #4ecdc4;
border-radius: 8rpx;
text-align: center;
text {
color: white;
display: block;
&.touch-info {
margin-top: 10rpx;
font-size: 24rpx;
}
}
}
.nested-container {
border: 2px solid #ddd;
border-radius: 8rpx;
overflow: hidden;
.nested-header {
padding: 20rpx;
background-color: #333;
text-align: center;
text {
color: white;
font-weight: bold;
}
}
.nested-content {
padding: 20rpx;
.nested-item {
padding: 15rpx;
margin-bottom: 10rpx;
background-color: #f8f8f8;
border-radius: 4rpx;
&:last-child {
margin-bottom: 0;
}
}
}
.nested-footer {
padding: 20rpx;
background-color: #666;
text-align: center;
text {
color: white;
}
}
}
}
</style>
2. scroll-view组件 - 滚动容器
<template>
<view class="scroll-demo">
<text class="title">ScrollView组件演示</text>
<!-- 垂直滚动 -->
<view class="section">
<text class="section-title">垂直滚动</text>
<scroll-view
class="vertical-scroll"
scroll-y
:scroll-top="scrollTop"
@scroll="onVerticalScroll"
@scrolltoupper="onScrollToUpper"
@scrolltolower="onScrollToLower"
>
<view v-for="n in 20" :key="n" class="scroll-item vertical-item">
<text>垂直滚动项目 {{ n }}</text>
</view>
</scroll-view>
<view class="scroll-controls">
<button @click="scrollToTop">滚动到顶部</button>
<button @click="scrollToBottom">滚动到底部</button>
</view>
</view>
<!-- 水平滚动 -->
<view class="section">
<text class="section-title">水平滚动</text>
<scroll-view
class="horizontal-scroll"
scroll-x
:scroll-left="scrollLeft"
@scroll="onHorizontalScroll"
>
<view class="horizontal-container">
<view v-for="n in 10" :key="n" class="scroll-item horizontal-item">
<text>{{ n }}</text>
</view>
</view>
</scroll-view>
<view class="scroll-controls">
<button @click="scrollToLeft">滚动到左侧</button>
<button @click="scrollToRight">滚动到右侧</button>
</view>
</view>
<!-- 下拉刷新和上拉加载 -->
<view class="section">
<text class="section-title">下拉刷新和上拉加载</text>
<scroll-view
class="refresh-scroll"
scroll-y
refresher-enabled
:refresher-triggered="refresherTriggered"
@refresherrefresh="onRefresh"
@scrolltolower="onLoadMore"
>
<view v-for="(item, index) in dataList" :key="index" class="data-item">
<text>{{ item.title }}</text>
<text class="item-desc">{{ item.description }}</text>
</view>
<view v-if="loading" class="loading-more">
<text>加载中...</text>
</view>
</scroll-view>
</view>
</view>
</template>
<script>
export default {
name: 'ScrollDemo',
data() {
return {
scrollTop: 0,
scrollLeft: 0,
refresherTriggered: false,
loading: false,
dataList: [
{ title: '数据项目1', description: '这是第一个数据项目的描述' },
{ title: '数据项目2', description: '这是第二个数据项目的描述' },
{ title: '数据项目3', description: '这是第三个数据项目的描述' },
{ title: '数据项目4', description: '这是第四个数据项目的描述' },
{ title: '数据项目5', description: '这是第五个数据项目的描述' }
]
}
},
methods: {
// 垂直滚动事件
onVerticalScroll(e) {
console.log('垂直滚动:', e.detail.scrollTop)
},
onScrollToUpper() {
console.log('滚动到顶部')
uni.showToast({
title: '已到顶部',
icon: 'none'
})
},
onScrollToLower() {
console.log('滚动到底部')
uni.showToast({
title: '已到底部',
icon: 'none'
})
},
scrollToTop() {
this.scrollTop = 0
},
scrollToBottom() {
this.scrollTop = 9999
},
// 水平滚动事件
onHorizontalScroll(e) {
console.log('水平滚动:', e.detail.scrollLeft)
},
scrollToLeft() {
this.scrollLeft = 0
},
scrollToRight() {
this.scrollLeft = 9999
},
// 下拉刷新
onRefresh() {
console.log('下拉刷新')
this.refresherTriggered = true
// 模拟刷新数据
setTimeout(() => {
this.dataList = [
{ title: '新数据1', description: '刷新后的新数据1' },
{ title: '新数据2', description: '刷新后的新数据2' },
{ title: '新数据3', description: '刷新后的新数据3' }
]
this.refresherTriggered = false
uni.showToast({
title: '刷新完成',
icon: 'success'
})
}, 2000)
},
// 上拉加载更多
onLoadMore() {
if (this.loading) return
console.log('上拉加载更多')
this.loading = true
// 模拟加载更多数据
setTimeout(() => {
const newItems = []
const startIndex = this.dataList.length + 1
for (let i = 0; i < 3; i++) {
newItems.push({
title: `数据项目${startIndex + i}`,
description: `这是第${startIndex + i}个数据项目的描述`
})
}
this.dataList.push(...newItems)
this.loading = false
uni.showToast({
title: '加载完成',
icon: 'success'
})
}, 1500)
}
}
}
</script>
<style lang="scss" scoped>
.scroll-demo {
padding: 20rpx;
.title {
display: block;
font-size: 32rpx;
font-weight: bold;
text-align: center;
margin-bottom: 30rpx;
}
.section {
margin-bottom: 40rpx;
.section-title {
display: block;
font-size: 28rpx;
font-weight: bold;
margin-bottom: 20rpx;
color: #333;
}
}
.vertical-scroll {
height: 400rpx;
border: 1px solid #ddd;
border-radius: 8rpx;
.vertical-item {
height: 100rpx;
display: flex;
align-items: center;
justify-content: center;
border-bottom: 1px solid #f0f0f0;
&:nth-child(odd) {
background-color: #f8f8f8;
}
}
}
.horizontal-scroll {
border: 1px solid #ddd;
border-radius: 8rpx;
.horizontal-container {
display: flex;
width: max-content;
}
.horizontal-item {
width: 150rpx;
height: 150rpx;
margin-right: 20rpx;
background-color: #007aff;
display: flex;
align-items: center;
justify-content: center;
border-radius: 8rpx;
text {
color: white;
font-size: 32rpx;
font-weight: bold;
}
&:last-child {
margin-right: 0;
}
}
}
.refresh-scroll {
height: 500rpx;
border: 1px solid #ddd;
border-radius: 8rpx;
.data-item {
padding: 30rpx 20rpx;
border-bottom: 1px solid #f0f0f0;
text {
display: block;
&:first-child {
font-size: 28rpx;
font-weight: bold;
margin-bottom: 10rpx;
}
&.item-desc {
font-size: 24rpx;
color: #666;
}
}
}
.loading-more {
padding: 30rpx;
text-align: center;
text {
color: #999;
font-size: 26rpx;
}
}
}
.scroll-controls {
margin-top: 20rpx;
display: flex;
justify-content: space-around;
button {
padding: 15rpx 30rpx;
background-color: #09bb07;
color: white;
border: none;
border-radius: 4rpx;
font-size: 26rpx;
}
}
}
</style>