Skip to content

布局组件使用 - 让页面排版更美观

📐 好的布局是用户体验的基础,今天我们来学习如何用布局组件打造美观的页面

1. scroll-view - 滚动视图

scroll-view 是最常用的布局组件,用于创建可滚动的区域:

垂直滚动

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

<script>
export default {
  data() {
    return {
      list: []
    }
  },
  onLoad() {
    this.loadData()
  },
  methods: {
    loadData() {
      // 模拟数据
      for (let i = 1; i <= 50; i++) {
        this.list.push({
          id: i,
          title: `列表项 \${i}`
        })
      }
    },
    onScrollToUpper() {
      console.log('滚动到顶部')
      // 下拉刷新
      this.refreshData()
    },
    onScrollToLower() {
      console.log('滚动到底部')
      // 加载更多
      this.loadMore()
    },
    onScroll(e) {
      console.log('滚动中:', e.detail.scrollTop)
    },
    refreshData() {
      // 刷新数据
      uni.showToast({
        title: '刷新成功',
        icon: 'success'
      })
    },
    loadMore() {
      // 加载更多数据
      const currentLength = this.list.length
      for (let i = 1; i <= 10; i++) {
        this.list.push({
          id: currentLength + i,
          title: `新加载项 \${currentLength + i}`
        })
      }
    }
  }
}
</script>

<style>
.container {
  height: 100vh;
}
.scroll-area {
  height: 100%;
  padding: 20px;
}
.list-item {
  padding: 15px;
  margin-bottom: 10px;
  background: white;
  border-radius: 8px;
  box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
</style>

水平滚动

vue
<template>
  <view class="horizontal-scroll-demo">
    <scroll-view scroll-x class="scroll-x">
      <view class="scroll-content">
        <view 
          v-for="item in categories" 
          :key="item.id"
          class="category-item"
          :class="{ active: selectedCategory === item.id }"
          @click="selectCategory(item.id)"
        >
          <text>\{\{ item.name \}\}</text>
        </view>
      </view>
    </scroll-view>
  </view>
</template>

<script>
export default {
  data() {
    return {
      selectedCategory: 1,
      categories: [
        { id: 1, name: '推荐' },
        { id: 2, name: '热门' },
        { id: 3, name: '科技' },
        { id: 4, name: '娱乐' },
        { id: 5, name: '体育' },
        { id: 6, name: '财经' },
        { id: 7, name: '军事' },
        { id: 8, name: '国际' }
      ]
    }
  },
  methods: {
    selectCategory(id) {
      this.selectedCategory = id
      console.log('选择分类:', id)
    }
  }
}
</script>

<style>
.scroll-x {
  white-space: nowrap;
  padding: 10px 0;
}
.scroll-content {
  display: flex;
  padding: 0 15px;
}
.category-item {
  flex-shrink: 0;
  padding: 8px 16px;
  margin-right: 15px;
  background: #f5f5f5;
  border-radius: 20px;
  text-align: center;
}
.category-item.active {
  background: #007aff;
  color: white;
}
</style>

2. swiper - 轮播组件

swiper 用于创建轮播图和滑动切换效果:

基础轮播图

vue
<template>
  <view class="swiper-demo">
    <swiper 
      class="swiper"
      indicator-dots
      autoplay
      interval="3000"
      duration="500"
      circular
      @change="onSwiperChange"
    >
      <swiper-item 
        v-for="(banner, index) in banners" 
        :key="index"
      >
        <image 
          :src="banner.image" 
          class="swiper-image"
          mode="aspectFill"
          @click="onBannerClick(banner)"
        />
        <view class="banner-content">
          <text class="banner-title">\{\{ banner.title \}\}</text>
          <text class="banner-desc">\{\{ banner.description \}\}</text>
        </view>
      </swiper-item>
    </swiper>
    
    <!-- 自定义指示器 -->
    <view class="custom-dots">
      <view 
        v-for="(banner, index) in banners"
        :key="index"
        class="dot"
        :class="{ active: currentIndex === index }"
      ></view>
    </view>
  </view>
</template>

<script>
export default {
  data() {
    return {
      currentIndex: 0,
      banners: [
        {
          id: 1,
          title: '春季新品上市',
          description: '全场8折优惠',
          image: '/static/banner1.jpg',
          url: '/pages/promotion/promotion'
        },
        {
          id: 2,
          title: '限时秒杀',
          description: '每日10点开抢',
          image: '/static/banner2.jpg',
          url: '/pages/seckill/seckill'
        },
        {
          id: 3,
          title: '会员专享',
          description: '专属福利等你来',
          image: '/static/banner3.jpg',
          url: '/pages/member/member'
        }
      ]
    }
  },
  methods: {
    onSwiperChange(e) {
      this.currentIndex = e.detail.current
    },
    onBannerClick(banner) {
      uni.navigateTo({
        url: banner.url
      })
    }
  }
}
</script>

<style>
.swiper {
  height: 200px;
  border-radius: 12px;
  overflow: hidden;
}
.swiper-image {
  width: 100%;
  height: 100%;
}
.banner-content {
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  background: linear-gradient(transparent, rgba(0,0,0,0.6));
  padding: 20px;
  color: white;
}
.banner-title {
  font-size: 18px;
  font-weight: bold;
  display: block;
  margin-bottom: 5px;
}
.banner-desc {
  font-size: 14px;
  opacity: 0.9;
}
.custom-dots {
  display: flex;
  justify-content: center;
  margin-top: 10px;
}
.dot {
  width: 8px;
  height: 8px;
  border-radius: 4px;
  background: #ddd;
  margin: 0 4px;
  transition: all 0.3s;
}
.dot.active {
  background: #007aff;
  width: 20px;
}
</style>

3. Flex布局实战

常用布局模式

vue
<template>
  <view class="layout-demo">
    <!-- 水平居中 -->
    <view class="section">
      <text class="section-title">水平居中</text>
      <view class="flex-center-h">
        <view class="box">居中内容</view>
      </view>
    </view>
    
    <!-- 垂直居中 -->
    <view class="section">
      <text class="section-title">垂直居中</text>
      <view class="flex-center-v">
        <view class="box">居中内容</view>
      </view>
    </view>
    
    <!-- 完全居中 -->
    <view class="section">
      <text class="section-title">完全居中</text>
      <view class="flex-center">
        <view class="box">居中内容</view>
      </view>
    </view>
    
    <!-- 两端对齐 -->
    <view class="section">
      <text class="section-title">两端对齐</text>
      <view class="flex-between">
        <view class="box">左侧</view>
        <view class="box">右侧</view>
      </view>
    </view>
    
    <!-- 等分布局 -->
    <view class="section">
      <text class="section-title">等分布局</text>
      <view class="flex-equal">
        <view class="box flex-1">1</view>
        <view class="box flex-1">2</view>
        <view class="box flex-1">3</view>
      </view>
    </view>
    
    <!-- 网格布局 -->
    <view class="section">
      <text class="section-title">网格布局</text>
      <view class="grid">
        <view 
          v-for="i in 9" 
          :key="i"
          class="grid-item"
        >
          <text>\{\{ i \}\}</text>
        </view>
      </view>
    </view>
  </view>
</template>

<style>
.layout-demo {
  padding: 20px;
}
.section {
  margin-bottom: 30px;
}
.section-title {
  font-size: 16px;
  font-weight: bold;
  margin-bottom: 10px;
  display: block;
}

/* 水平居中 */
.flex-center-h {
  display: flex;
  justify-content: center;
  height: 60px;
  background: #f5f5f5;
}

/* 垂直居中 */
.flex-center-v {
  display: flex;
  align-items: center;
  height: 60px;
  background: #f5f5f5;
}

/* 完全居中 */
.flex-center {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 80px;
  background: #f5f5f5;
}

/* 两端对齐 */
.flex-between {
  display: flex;
  justify-content: space-between;
  align-items: center;
  height: 60px;
  background: #f5f5f5;
  padding: 0 15px;
}

/* 等分布局 */
.flex-equal {
  display: flex;
  gap: 10px;
}
.flex-1 {
  flex: 1;
}

/* 网格布局 */
.grid {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
}
.grid-item {
  width: calc(33.333% - 7px);
  height: 60px;
  background: #007aff;
  color: white;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 8px;
}

.box {
  background: #007aff;
  color: white;
  padding: 10px 15px;
  border-radius: 6px;
  text-align: center;
}
</style>

4. 实战案例:商品卡片布局

vue
<template>
  <view class="product-layout">
    <!-- 轮播图 -->
    <swiper class="banner-swiper" indicator-dots autoplay>
      <swiper-item v-for="banner in banners" :key="banner.id">
        <image :src="banner.image" class="banner-image" mode="aspectFill" />
      </swiper-item>
    </swiper>
    
    <!-- 分类导航 -->
    <scroll-view scroll-x class="category-nav">
      <view class="category-list">
        <view 
          v-for="category in categories"
          :key="category.id"
          class="category-item"
          @click="selectCategory(category.id)"
        >
          <image :src="category.icon" class="category-icon" />
          <text class="category-name">\{\{ category.name \}\}</text>
        </view>
      </view>
    </scroll-view>
    
    <!-- 商品列表 -->
    <scroll-view scroll-y class="product-list" @scrolltolower="loadMore">
      <view class="product-grid">
        <view 
          v-for="product in products"
          :key="product.id"
          class="product-card"
          @click="goToDetail(product.id)"
        >
          <image :src="product.image" class="product-image" mode="aspectFill" />
          <view class="product-info">
            <text class="product-name">\{\{ product.name \}\}</text>
            <view class="product-price-row">
              <text class="product-price">¥\{\{ product.price \}\}</text>
              <text class="product-sales">已售\{\{ product.sales \}\}</text>
            </view>
          </view>
        </view>
      </view>
      
      <!-- 加载更多 -->
      <view v-if="loading" class="loading">
        <text>加载中...</text>
      </view>
    </scroll-view>
  </view>
</template>

<script>
export default {
  data() {
    return {
      loading: false,
      banners: [
        { id: 1, image: '/static/banner1.jpg' },
        { id: 2, image: '/static/banner2.jpg' },
        { id: 3, image: '/static/banner3.jpg' }
      ],
      categories: [
        { id: 1, name: '手机', icon: '/static/phone.png' },
        { id: 2, name: '电脑', icon: '/static/computer.png' },
        { id: 3, name: '家电', icon: '/static/appliance.png' },
        { id: 4, name: '服装', icon: '/static/clothes.png' },
        { id: 5, name: '美妆', icon: '/static/beauty.png' }
      ],
      products: []
    }
  },
  onLoad() {
    this.loadProducts()
  },
  methods: {
    loadProducts() {
      // 模拟商品数据
      for (let i = 1; i <= 20; i++) {
        this.products.push({
          id: i,
          name: `商品名称 \${i}`,
          price: (Math.random() * 1000 + 100).toFixed(2),
          sales: Math.floor(Math.random() * 1000),
          image: `/static/product\${i % 5 + 1}.jpg`
        })
      }
    },
    selectCategory(id) {
      console.log('选择分类:', id)
      // 根据分类筛选商品
    },
    goToDetail(id) {
      uni.navigateTo({
        url: `/pages/product-detail/product-detail?id=\${id}`
      })
    },
    loadMore() {
      if (this.loading) return
      
      this.loading = true
      
      // 模拟加载更多
      setTimeout(() => {
        const currentLength = this.products.length
        for (let i = 1; i <= 10; i++) {
          this.products.push({
            id: currentLength + i,
            name: `商品名称 \${currentLength + i}`,
            price: (Math.random() * 1000 + 100).toFixed(2),
            sales: Math.floor(Math.random() * 1000),
            image: `/static/product\${(currentLength + i) % 5 + 1}.jpg`
          })
        }
        this.loading = false
      }, 1000)
    }
  }
}
</script>

<style>
.product-layout {
  height: 100vh;
  display: flex;
  flex-direction: column;
}

.banner-swiper {
  height: 180px;
  flex-shrink: 0;
}
.banner-image {
  width: 100%;
  height: 100%;
}

.category-nav {
  flex-shrink: 0;
  background: white;
  padding: 15px 0;
}
.category-list {
  display: flex;
  padding: 0 15px;
}
.category-item {
  flex-shrink: 0;
  margin-right: 25px;
  text-align: center;
}
.category-icon {
  width: 40px;
  height: 40px;
  margin-bottom: 5px;
}
.category-name {
  font-size: 12px;
  color: #666;
}

.product-list {
  flex: 1;
  background: #f5f5f5;
}
.product-grid {
  display: flex;
  flex-wrap: wrap;
  padding: 10px;
  gap: 10px;
}
.product-card {
  width: calc(50% - 5px);
  background: white;
  border-radius: 8px;
  overflow: hidden;
}
.product-image {
  width: 100%;
  height: 120px;
}
.product-info {
  padding: 10px;
}
.product-name {
  font-size: 14px;
  color: #333;
  display: block;
  margin-bottom: 8px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.product-price-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.product-price {
  color: #ff6b6b;
  font-size: 16px;
  font-weight: bold;
}
.product-sales {
  font-size: 12px;
  color: #999;
}

.loading {
  text-align: center;
  padding: 20px;
  color: #999;
}
</style>

小结

今天我们学习了:

  • scroll-view - 创建可滚动区域
  • swiper - 实现轮播效果
  • ✅ Flex布局的各种应用场景
  • ✅ 实战案例:商品页面布局

布局要点

  • 合理使用滚动组件提升用户体验
  • 灵活运用Flex布局实现各种排版
  • 注意性能优化,避免过度嵌套
  • 保持布局的一致性和美观性

下一篇预告

下一篇我们将学习《样式和CSS - 让你的小程序更好看》,深入学习UniApp的样式系统。


好的布局是用户体验的基础,掌握了这些布局技巧,你就能创造出美观实用的页面!