WikiWiki
首页
Java开发
Java面试
Linux手册
  • AI相关
  • Python Flask
  • Pytorch
  • youlo8
SEO
uniapp小程序
Vue前端
work
数据库
软件设计师
入门指南
首页
Java开发
Java面试
Linux手册
  • AI相关
  • Python Flask
  • Pytorch
  • youlo8
SEO
uniapp小程序
Vue前端
work
数据库
软件设计师
入门指南
  • uni-app-api
  • uni-app开发交友小程序
  • uni-app开发慕课博客
  • uni-app框架实战_基础
  • uni-app框架实战_项目
  • 微信小程序原生开发

uniapp开发ImoocBlog

一、准备阶段

1.项目目录介绍

├─pages			    // 页面存放文件夹,等同于 微信小程序中的 pages
│  └─index			// 默认生成的页面
├─static			// 静态资源存放文件夹
└─uni_modules		// uni-app组件目录
│  └─uni-xxx		// uni-app 所提供的业务组件,等同于 微信小程序中的组件
├─App.vue			// 应用配置文件,用来配置全局样式、生命周期函数等,等同于 微信小程序中的app.js
└─main.js			// 项目入口文件
├─mainfest.json		 // 配置应用名称、appid、logo、版本等打包信息,
└─pages.json		// 配置页面路径、窗口样式、tabBar 等页面类信息,等同于 微信小程序中的app.json
└─uni.scss			// uni-app内置的常用样式变量

1.使用 VSCode 开发 uniapp(当你想要使用 VSCode 来开发 uniapp 时,可以查看本小节)

虽说 HBuilder X 开发体验还算不错,但是有时候金窝银窝不如自己的狗窝,当我们习惯了 VSCode 之后,有时候不太愿意换开发工具。

那么怎么使用 VSCode 来开发 uniapp 呢? 其实是有办法的。

  1. 使用 HBuilder X 运行项目(运行方式,参考上一小节)
  2. 使用 VSCode 打开项目
  3. 在 VSCode 中安装插件:
    1. uni-helper - 让开发者在 VSCode 中开发 uni-* 的体验尽可能好。
    2. uni-app-snippets - 支持 uni-app 基本能力的代码片段,包括组件和 API
    3. uni-app-schemas - 支持 uni-app pages.json 和 manifest.json 简单的格式校验
    4. uni-ui-snippets - 支持 uni-ui 组件代码片段
  4. 在 VSCode 中修改代码,运行结果自动发生变化

1. tabbar

样式

image-20240529223501481

hot、hot-video、my 三个页面的创建

配置 pages.json

  "tabBar": {
    "selectedColor": "#f94d2a",
    "list": [
      {
        "pagePath": "pages/hot/hot",
        "text": "热榜",
        "iconPath": "static/tab-icons/hot.png",
        "selectedIconPath": "static/tab-icons/hot-active.png"
      },
      {
        "pagePath": "pages/hot-video/hot-video",
        "text": "热播",
        "iconPath": "static/tab-icons/hot-video.png",
        "selectedIconPath": "static/tab-icons/hot-video-active.png"
      },
      {
        "pagePath": "pages/my/my",
        "text": "我的",
        "iconPath": "static/tab-icons/my.png",
        "selectedIconPath": "static/tab-icons/my-active.png"
      }
    ]
  }

热搜页面分析

image-20210517195254481

2.logo 图片展示

效果

image-20240529225858130

<template>
  <view class="hot-container">
    <!-- logo -->
    <image class="logo" mode="aspectFit" src="@/static/images/logo.png" />
  </view>
</template>
<style lang="scss" scoped>
.hot-container {
  background-color: $uni-bg-color;
  .logo {
    width: 100%;
    height: 80px;
  }
}
</style>

uni.scss

$uni-bg-color:#FFFFFF;

3.搜索框组件

my-search.vue

<template>
  <view class="my-search-container">
    <!-- 搜索按钮 -->
    <view class="my-search-box">
      <image class="icon" src="@/static/images/search.png" />
      <text
        class="placeholder"
        :style="{
          color: config.textColor
        }"
        >{{ placeholderText }}</text
      >
    </view>
  </view>
</template>

<script>
export default {
  name: 'my-search',
  props: {
    placeholderText: {
      type: String,
      default: '搜索'
    }
  },
  data() {
    return {};
  }
};
</script>

<style lang="scss">
.my-search-container {
  display: flex;
  align-items: center;
  .my-search-box {
    height: 36px;
    background-color: #ffffff;
    border-radius: 15px;
    border: 1px solid #c9c9c9;
    width: 100%;
    display: flex;
    align-items: center;
    padding: 0 $uni-spacing-row-base;

    .icon {
      width: $uni-img-size-sm;
      height: $uni-img-size-sm;
    }

    .placeholder {
      font-size: $uni-font-size-sm;
      margin-left: $uni-spacing-row-sm;
      color: #454545;
    }
  }
}
</style>

hot.vue

<view class="search-box">
    <!-- 搜索模块 -->
    <my-search placeholderText="uni-app 自定义组件" />
</view>

<style lang="scss" scoped>    
.hot-container {	
  ...
  .search-box {
    padding: 0 16px;
    margin-bottom: $uni-spacing-col-base;
  }
}
</style>

4.tabs组件

<script>
export default {
  name: 'my-tabs',
  props: {
    // 父组件传入的 tabs 数据
    tabData: {
      type: Array,
      default: () => []
    },
    // 默认激活项
    defaultIndex: {
      type: Number,
      default: 0
    },
    // 配置对象
    config: {
      type: Object,
      default: () => {
        return {};
      }
    }
  }
};
</script>

5-13:tabs组件 - 封装网络请求

  1. 创建 utils 文件夹

  2. 创建 request.js ,封装请求对象

    const BASE_URL = 'https://api.imooc-blog.lgdsunday.club/api';
    function request({ url, data, method }) {
      return new Promise((resolve, reject) => {
        uni.request({
          url: BASE_URL + url,
          data,
          method,
          success: ({ data }) => {
            if (data.success) {
              resolve(data);
            } else {
              uni.showToast({
                title: data.message,
                icon: 'none',
                mask: true,
                duration: 3000
              });
              reject(data.message);
            }
          },
          fail: (error) => {
            reject(error);
          }
        });
      });
    }
    
    export default request;
    
    
  3. 创建 api 文件夹

  4. 创建 hot 文件,封装 hot 相关的请求方法

    import request from '../utils/request';
    
    export function getHotTabs() {
      return request({
        url: '/hot/tabs'
      });
    }
    
    

5-14:tabs组件 - 进行基本的数据展示

  1. 在 hot 中使用

    <template>
      <view class="hot-container">
      	... 
        <!-- tabs -->
        <my-tabs :tabData="tabData" :defaultIndex="currentIndex"></my-tabs>
      </view>
    </template>
    
    <script>
    import { getHotTabs } from 'api/hot';
    export default {
      data() {
        return {
          // tabs 数据源
          tabData: [],
          // 当前的切换 index
          currentIndex: 0
        };
      },
      // 在实例创建完成后被立即调用
      created() {
        this.getHotTabs();
      },
      // 定义方法
      methods: {
        /**
         * 获取热搜标题数据
         */
        async getHotTabs() {
          // uniapp 支持 async await
          const { data: res } = await getHotTabs();
          this.tabData = res.list;
        }
      }
    };
    </script>
    
  2. 在 tabs 中展示

    <template>
      <view class="tab-container">
        <view class="tab-box">
          <scroll-view id="_scroll" scroll-x class="scroll-view" scroll-with-animation>
            <view class="scroll-content">
              <view class="tab-item-box">
                <block v-for="(item, index) in tabData" :key="index">
                  <view class="tab-item">{{ item.label || item }}</view>
                </block>
              </view>
            </view>
          </scroll-view>
        </view>
      </view>
    </template>
    

5-15:tabs组件 - 美化样式

my-tabs

<style lang="scss" scoped>
.tab-container {
  font-size: $uni-font-size-base;
  height: 45px;
  line-height: 45px;
  background-color: $uni-bg-color;
  .tab-box {
    width: 100%;
    height: 45px;
    display: flex;
    position: relative;
    .scroll-view {
      white-space: nowrap;
      width: 100%;
      height: 100%;
      box-sizing: border-box;
      .scroll-content {
        width: 100%;
        height: 100%;
        position: relative;

        .tab-item-box {
          height: 100%;
          .tab-item {
            height: 100%;
            display: inline-block;
            text-align: center;
            padding: 0 15px;
            position: relative;
            text-align: center;
            color: $uni-text-color;
          }
        }
      }
    }
  }
}
</style>

5-16:tabs组件 - 设置激活项

  1. 因为根据 子组件不可以直接修改父组件传递过来的数据 特性,所以可以通过定义一个 data-> activeIndex 来跟随 defaultIndex 的变化

    <script>
    export default {  
        data: () => {
            return {
              // 当前激活项的 index
              activeIndex: -1
            };
          },
            // 侦听器
          watch: {
            // 监听激活项目的变化
            defaultIndex: {
              handler(val) {
                this.activeIndex = val;
              },
              // 该回调将会在侦听开始之后被立即调用
              immediate: true
            }
          },
    	}
    </script>
    
  2. 找到 tab-item 的 view ,判断 active 的状态。并添加点击事件,修改 activeIndex 的值

    <view
          class="tab-item"
          :class="{ 'tab-item-active': activeIndex === index }"
          @click="tabClick(index)"
          >{{ item.label || item }}</view
        >
    
      methods: {
        /**
         * tab 的点击事件处理
         */
        tabClick(index) {
          this.activeIndex = index;
          // 发送通知
          this.$emit('tabClick', index);
        }
      }
    
    &-active {
        color: $uni-text-color-hot;
        font-weight: bold;
    }
    
    $uni-text-color-hot: #f94d2a; // 热点颜色
    

5-17:tabs组件 - 定义滑块

<!-- 滑块 -->
<view
      class="underLine"
      :style="{
              transform: 'translateX(' + slider.left + 'px)'
              }"
      />
data: () => {
    return {
      // 滑块
      slider: {
        // 距离左侧的距离
        left: 0
      }
    };
  },
.underLine {
    height: 2px;
    width: 25px;
    background-color: #f01414;
    border-radius: 3px;
    transition: 0.5s;
    position: absolute;
    bottom: 0;
}

5-18:tabs组件 - 实现滑块的滚动 - 01

实现滑块滚动的功能

  1. 确定滚动的时机
  2. 计算滚动的距离

确定滚动的时机

  1. 监听激活项目的变化
  2. tab 的点击事件处理

确定滚动时机后,执行滚动的方法

/**
     * 根据当前的 activeIndex 下标,计算 【滑块】 滚动位置
     */
tabToIndex() {
    // 获取当前的 activeIndex
    const activeIndex = this.activeIndex;
    // 滑块的宽度
    const underLineWidth = this.defaultConfig.underLineWidth;
    // 配置 滚动条 的数据
    this.slider = {
        // TODO:left 如何定义呢?
        left: 0
    };
    console.log('TODO:left 如何定义呢?');
}    

data: () => {
    return {
      // 默认配置
      defaultConfig: {
        // 下划线宽度 px
        underLineWidth: 24,
        // 下划线高度 px
        underLineHeight: 2,
        // 下划线颜色
        underLineColor: '#f94d2a'
      }
    };

5-19:tabs组件 - 实现滑块的滚动 - 02

实现滑块滚动的功能

  1. 确定滚动的时机
  2. 计算滚动的距离
    1. 维护一个单独的数据对象 tabList
    2. 在 tabList 的 item 中为一个 _slider 属性
    3. 该属性保存了 【当前 item 下 的滑块位置】
      1. 计算公式:滑块左侧位置 = item.left + (item.width - slider.width) / 2
data: () => {
    return {
      // 内部维护的数据对象,为每个 item 单独额外维护一个 slider 的滑块对象
      tabList: []
    };
  },
      
 // 侦听器
  watch: {
    // 侦听数据的变化
    tabData: {
      handler(val) {
        this.tabList = val;
        setTimeout(() => {
          this.updateTabWidth();
        }, 0);
      },
      // 该回调将会在侦听开始之后被立即调用
      immediate: true
    },
  },
      
      /**
     * 更新 tab item 的宽度
     */
      updateTabWidth() {
          /**
       * 为 tabList 的每个 item 单独额外维护一个 slider 的滑块对象
       */
          let data = this.tabList;
          if (data.length == 0) return false;

          // 获取 dom 的固定写法
          const query = uni.createSelectorQuery().in(this);
          // 循环数据源
          data.forEach((item, index) => {
              // 获取 dom 的固定写法
              query
                  .select('#_tab_' + index)
                  .boundingClientRect((res) => {
                  // 为数据对象中每一个 item 都维护一个 _slider(滑动条) 对象
                  item._slider = {
                      // 当前的 tab 距离左侧的距离
                      left: res.left + (res.width - this.defaultConfig.underLineWidth) / 2
                  };
                  // 运算完成之后,执行一次 【滑块】位置运算
                  if (data.length - 1 === index) {
                      this.tabToIndex();
                  }
              })
                  .exec();
          });
      },
    /**
     * 根据当前的 activeIndex 下标,计算 【滑块】 滚动位置
     */
    tabToIndex() {
      if (this.tabList.length === 0) return;
      // 获取当前的 activeIndex
      const activeIndex = this.activeIndex;
      // 滑块的宽度
      const underLineWidth = this.defaultConfig.underLineWidth;
      // 配置 滚动条 的数据
      this.slider = {
        // TODO:left 如何定义呢?
        // 1. 维护一个单独的数据对象 `tabList`
        // 2. 在 `tabList`  的 `item` 中为一个 `_slider` 属性
        // 3. 该属性保存了 【当前 `item` 下 的滑块位置】
        //    3.1. 计算公式:`滑块左侧位置 = item.left + (item.width - slider.width) / 2`
        left: this.tabList[activeIndex]._slider.left
      };
    }
  

5-20:tabs组件 - scrollView 的点击位移

当 【选中项】发生变化时,希望 scrollView 也进行对应的位移。

  <scroll-view
        :scroll-left="scrollLeft"
      ></scroll-view>

// scrollView 的横向滚动条位置
scrollLeft: 0,

简单的算法:

this.scrollLeft = this.activeIndex * this.defaultConfig.underLineWidth;

5-21:tabs组件 - 增加可配置项

<view
      class="tab-item"
      :id="'_tab_' + index"
      :class="{ 'tab-item-active': activeIndex === index }"
      @click="tabClick(index)"
      :style="{
              color:
              activeIndex === index ? defaultConfig.activeTextColor : defaultConfig.textColor
              }"
      >{{ item.label || item }}</view>
  props: {
    // 配置对象
    config: {
      type: Object,
      default: () => {
        return {};
      }
    }
  },
  data: () => {
    return {
      // 默认配置
      defaultConfig: {
        // 默认的字体颜色
        textColor: '#333333',
        // 高亮字体颜色
        activeTextColor: '#f94d2a',
        // 下划线宽度 px
        underLineWidth: 24,
        // 下划线高度 px
        underLineHeight: 2,
        // 下划线颜色
        underLineColor: '#f94d2a'
      }
    };
  },
  // 侦听器
  watch: {
    // 监听 config
    config: {
      handler(val) {
        this.defaultConfig = { ...this.defaultConfig, ...val };
      },
      // 该回调将会在侦听开始之后被立即调用
      immediate: true
    }
  },

5-22:List 组件 - 分析 List 组件

  1. 使用 mock 数据,构建 List 的基本结构
  2. 美化 item 样式
  3. 根据 tab 的切换,获取真实数据
  4. 渲染真实数据
  5. 通过 swiper 改造List
  6. 完成 swiper 和 tabs 的联动效果

5-23: List 组件 - 使用 mock 数据,构建 List 的基本结构

hot.vue

<template>
 	...
    <!-- list -->
    <view>
      <hot-list-item v-for="(item, index) in 50" :key="index"></hot-list-item>
    </view>
  </view>
</template>

hot-list-item.vue

<template>
  <view class="item-container">
    <view class="item-box">
      <view class="item-box-left">
        <hot-ranking />
      </view>
      <view class="item-box-right">
        <!-- 标题 -->
        <view class="item-title line-clamp-2">标题</view>
        <!-- 简介 -->
        <view class="item-desc line-clamp-2">简介</view>
        <view class="item-bottom-box">
          <!-- 作者 -->
          <text class="item-author">作者</text>
          <!-- 热度 -->
          <view class="hot-box">
            <image class="hot-icon" src="@/static/images/hot-icon.png" />
            <text class="hot-text">1000 热度</text>
          </view>
        </view>
      </view>
    </view>
  </view>
</template>

<script>
export default {
  name: 'hot-list-item',
  data() {
    return {};
  }
};
</script>

<style lang="scss" scoped></style>

hot-ranking.vue

<template>
  <view class="ranking">
    <image class="ranking-bg" :src="getRankingBg" />
    <text class="ranking-text">1</text>
  </view>
</template>

<script>
export default {
  name: 'hot-ranking'
};
</script>

<style lang="scss" scoped></style>

5-24: List 组件 - 美化 item 样式

hot-list-item.vue

<style lang="scss" scoped>
.item-container {
  padding-bottom: $uni-spacing-col-lg;
  .item-box {
    display: flex;
    margin: 0 $uni-spacing-col-base;
    padding: $uni-spacing-row-lg $uni-spacing-col-base;
    background-color: $uni-bg-color;
    border-radius: $uni-border-radius-lg;
    box-shadow: 2px 2px 5px 1px rgba(143, 143, 143, 0.1);
    .item-box-left {
      margin-right: $uni-spacing-row-sm;
    }
    .item-box-right {
      width: 100%;
      .item-title {
        font-size: $uni-font-size-lg;
        font-weight: bold;
        color: $uni-text-color-title;
      }
      .item-desc {
        padding-top: $uni-spacing-row-sm;
        font-size: $uni-font-size-base;
        color: $uni-text-color;
      }
      .item-bottom-box {
        margin-top: $uni-spacing-col-sm;
        display: flex;
        justify-content: space-between;
        align-items: center;
        .item-author {
          font-size: $uni-font-size-sm;
          color: $uni-text-color-grey;
        }
        .hot-box {
          .hot-icon {
            width: $uni-img-size-sm;
            height: $uni-img-size-sm;
            vertical-align: top;
          }
          .hot-text {
            margin-left: $uni-spacing-row-sm;
            font-size: $uni-font-size-sm;
            color: $uni-text-color-hot;
          }
        }
      }
    }
  }
}
</style>

hot-ranking.vue

<style lang="scss" scoped>
.ranking {
  position: relative;
  text-align: center;
  width: 18px;
  height: 20px;
  .ranking-bg {
    width: 100%;
    height: 100%;
  }
  .ranking-text {
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-55%, -55%);
    font-size: $uni-font-size-sm;
    font-weight: bold;
    color: $uni-text-color;
  }
  .text-white {
    color: white;
  }
}
</style>

5-25: List 组件 - 根据 tab 的切换,获取真实数据

hot.js

/**
 * 热搜文章列表
 */
export function getHotListFromTabType(type) {
  return request({
    url: '/hot/list',
    data: {
      type
    }
  });
}

hot.vue

<template>
    <!-- tabs -->
    <my-tabs
      ...
      @tabClick="tabClick"
    ></my-tabs>
	<!-- list -->
    <view>
      <!-- 加载动画 -->
      <uni-load-more status="loading" v-if="isLoading"></uni-load-more>
      <!-- 列表 -->
      <block v-else>
        <hot-list-item v-for="(item, index) in 50" :key="index"></hot-list-item>
      </block>
    </view>
  </view>
</template>

<script>
import { getHotTabs, getHotListFromTabType } from 'api/hot';
export default {
  data() {
    return {
      ...
      // list 列表数据加载过程
      isLoading: true,
      // 以 index 为 key,对应的 list 为 val
      listData: {}
    };
  },
  // 定义方法
  methods: {
    /**
     * 获取热搜标题数据
     */
    async getHotTabs() {
      ...
      // 获取列表数据
      this.getHotListFromTab();
    },
    /**
     * list 列表数据
     */
    async getHotListFromTab() {
      // 展示 loading
      this.isLoading = true;
      // 判断缓存是否有数据,不存在则重新获取数据
      if (!this.listData[this.currentIndex]) {
        // 获取列表数据
        const id = this.tabData[this.currentIndex].id;
        const { data: res } = await getHotListFromTabType(id);
        // 放入数据缓存
        this.listData[this.currentIndex] = res.list;
      }

      // 隐藏 loading
      this.isLoading = false;
    },
    /**
     * tab item 的点击事件
     */
    tabClick(index) {
      this.currentIndex = index;
      // 获取列表数据
      this.getHotListFromTab();
    }
  }
};
</script>

5-26:List 组件 - 渲染真实数据

hot.vue

<hot-list-item
    v-for="(item, index) in listData[currentIndex]"
    :key="index"
    :data="item"
    :ranking="index + 1"
></hot-list-item>

hot-list-item.vue

<template>
  <view class="item-container" @click="$emit('click')">
    <view class="item-box">
      <view class="item-box-left">
        <hot-ranking :ranking="ranking" />
      </view>
      <view class="item-box-right">
        <!-- 标题 -->
        <view class="item-title line-clamp-2">{{ data.title }}</view>
        <!-- 简介 -->
        <view class="item-desc line-clamp-2">{{ data.desc }}</view>
        <view class="item-bottom-box">
          <!-- 作者 -->
          <text class="item-author">{{ data.nickname }}</text>
          <!-- 热度 -->
          <view class="hot-box">
            <image class="hot-icon" src="@/static/images/hot-icon.png" />
            <text class="hot-text">{{ data.views }} 热度</text>
          </view>
        </view>
      </view>
    </view>
  </view>
</template>

<script>
export default {
  name: 'hot-list-item',
  props: {
    data: {
      type: Object,
      required: true
    },
    ranking: {
      type: Number,
      required: true
    }
  },
  data() {
    return {};
  }
};
</script>

hot-ranking.vue

<template>
  <view class="ranking">
    <image class="ranking-bg" :src="getRankingBg" />
    <text class="ranking-text" :class="{ 'text-white': ranking <= 3 }">{{ ranking }}</text>
  </view>
</template>

<script>
export default {
  name: 'hot-ranking',
  props: {
    ranking: {
      type: Number,
      required: true
    }
  },
  /**
   * 当依赖值发生变化时,会重新计算
   */
  computed: {
    getRankingBg() {
      if (this.ranking <= 3) {
        return require(`@/static/images/ranking-${this.ranking}.png`);
      }
      return require('@/static/images/ranking-other.png');
    }
  }
};
</script>

style/global.scss

/**
 * 这里是共用样式的定义位置
 */

.line-clamp-2 {
  overflow: hidden;
  text-overflow: ellipsis;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
}

main.js

// 通用样式
import './styles/global.scss';

5-27:List 组件 - 通过 swiper 改造List

想要让 list 具备【横向翻页】的效果,那么可以使用 swiper 对其进行改造!

 <!-- 基于 swiper 的 list 列表 -->
    <swiper class="swiper" :current="currentIndex">
      <swiper-item class="swiper-item" v-for="(tabItem, tabIndex) in tabData" :key="tabIndex">
        <view>
          <!-- 加载动画 -->
          <uni-load-more status="loading" v-if="isLoading"></uni-load-more>
          <!-- 列表 -->
          <block v-else>
            <!-- 列表循环数据更改为 listData[tabIndex] -->
            <hot-list-item
              v-for="(item, index) in listData[tabIndex]"
              :key="index"
              :data="item"
              :ranking="index + 1"
            ></hot-list-item>
          </block>
        </view>
      </swiper-item>
    </swiper>

当前的问题:

  1. 列表高度展示错误
  2. 切换 tab 时的 list 的卡顿问题

5-28:List 组件 - 解决列表高度展示错误的问题

原因:

没有给 swiper 指定高度。

解决方案:

指定高度即可。

<template>
  <view class="hot-container">
    <!-- 基于 swiper 的 list 列表 -->
    <swiper class="swiper" :current="currentIndex" :style="{ height: currentSwiperHeight + 'px' }">
     ...
            <hot-list-item
              :class="'hot-list-item-' + tabIndex"
              v-for="(item, index) in listData[tabIndex]"
              :key="index"
              :data="item"
              :ranking="index + 1"
            ></hot-list-item>
         ...
    </swiper>
  </view>
</template>

<script>
export default {
  data() {
    return {
      // 当前 swiper 的高度
      currentSwiperHeight: 0,
      // 以 index 为 key,对应的 swiper 的高度 为 val
      swiperHeightData: {}
    };
  },
  // 定义方法
  methods: {
    /**
     * list 列表数据
     */
    async getHotListFromTab() {
      ...
      // 因为 this.$nextTick 存在一定的兼容性问题,所以更加推荐使用 setTimeout
      setTimeout(async () => {
        // 获取当前 swiper 的高度
        this.currentSwiperHeight = await this.getCurrentSwiperHeight();
        // 放入缓存
        this.swiperHeightData[this.currentIndex] = this.currentSwiperHeight;
      }, 0);
    },
    /**
     * 计算当前 swiper 的高度
     */
    getCurrentSwiperHeight() {
      return new Promise((resolve, reject) => {
        let sum = 0;
        const query = uni.createSelectorQuery().in(this);
        query
          .selectAll(`.hot-list-item-${this.currentIndex}`)
          .boundingClientRect((res) => {
            res.forEach((item) => {
              sum += item.height;
            });
            resolve(sum);
          })
          .exec();
      });
    }
  }
};
</script>

5-29:List 组件 - 解决 切换 tab 时的 list 的卡顿问题

原因:

动画未执行完成,DOM 未渲染完成,即获取数据,执行了新的渲染逻辑。

解决方案:

等待 动画执行完成, DOM 渲染完成。之后再获取数据,渲染列表。

<template>
 <swiper
      class="swiper"
      :current="currentIndex"
      :style="{ height: currentSwiperHeight + 'px' }"
      @animationfinish="onSwiperEnd"
    >
</template>

<script>
import { getHotTabs, getHotListFromTabType } from 'api/hot';
export default {
  // 定义方法
  methods: 
    /**
     * list 列表数据
     */
    async getHotListFromTab() {
      // 在 onSwiperEnd 中进行判断。【判断缓存是否有数据,不存在则重新获取数据】
    },
    /**
     * tab item 的点击事件
     */
    tabClick(index) {
      this.currentIndex = index;
      // 获取列表数据
      // this.getHotListFromTab();
    },
    /**
     * 解决卡顿问题;等待 swiper 动画完成之后,获取数据
     */
    onSwiperEnd() {
      // 判断缓存是否有数据,不存在则重新获取数据
      if (!this.listData[this.currentIndex]) {
        this.getHotListFromTab();
        return;
      }
      // 未 return ,则证明存在缓存数据,即同时存在 height 的缓存数据
      this.currentSwiperHeight = this.swiperHeightData[this.currentIndex];
    }
};
</script>

问题: swiper 滚动时,tabs 无法产生联动

5-30:List 组件 - swiper 和 tabs 联动

目前状态:

tabs 切换时,swiper 可以联动。

swiper 切换时,tabs 无法联动。

解决:

让swiper 切换时,tabs 进行联动。

<template>
	<swiper
      class="swiper"
      :current="currentIndex"
      :style="{ height: currentSwiperHeight + 'px' }"
      @animationfinish="onSwiperEnd"
      @change="onSwiperChange"
    >
</template>
<script>
    export default {
    	methods: {
            // 监听 swiper 的切换事件
            onSwiperChange(e) {
              this.currentIndex = e.detail.current;
            },
        }
    }
</script>

5-31:List 组件 - tabs 中滑块跟随滚动

watch: {
        // 监听激活项目的变化
    defaultIndex: {
      handler(val) {
        this.activeIndex = val;
        // 定义滑块的位置
        this.tabToIndex();
      },
      // 该回调将会在侦听开始之后被立即调用
      immediate: true
    },
    
}

tabToIndex() {
      if (this.tabList.length === 0) return;
}

问题: tabs 吸顶

5-32:完成 tabs 的吸顶效果

<template>
	<!-- tabs -->
    <view class="tab-sticky">
      <my-tabs
        :tabData="tabData"
        :defaultIndex="currentIndex"
        :config="{ textColor: '#333333' }"
        @tabClick="tabClick"
      ></my-tabs>
    </view>
</template>

<style lang="scss" scoped>
  .tab-sticky {
    position: -webkit-sticky;
    position: sticky;
    z-index: 99;
    top: 0;
  }
}
</style>

5-33:控制列表滚动位置

// 当前的滚动距离
currentPageScrollTop: 0 

/**
   * 监听页面的滚动
   */
  onPageScroll(res) {
    this.currentPageScrollTop = res.scrollTop;
  },   

	// 监听 swiper 的切换事件
    onSwiperChange(e) {
      if (this.currentPageScrollTop > 130) {
        // 控制列表滚动位置
        uni.pageScrollTo({
          scrollTop: 130
        });
      }	
      this.currentIndex = e.detail.current;
    },

5-34:List 组件 - 处理热度的显示

通过 filters 过滤器 进行处理:创建 filters 文件夹,创建 index.js 文件:

filters/index.js

/**
 * 将字符转化为以千为单位的字符
 * @param {*} val 待转换字符
 * @returns
 */
export function hotNumber(val) {
  const num = parseInt(val);
  if (num < 1000) return val;

  // 将 val 转为字符串
  val = val + '';
  // 获取以 千 为单位的值
  return val.substring(0, val.length - 3) + 'k';
}

main.js

import * as filters from './filters';

// 注册过滤器
Object.keys(filters).forEach((key) => {
  Vue.filter(key, filters[key]);
});

hot-list-item.vue

 <text class="hot-text">{{ data.views | hotNumber }} 热度</text>

5-35:总结

  1. 首页内容完成
    1. 对 uniapp 进行了基础的了解
    2. 创建 imooc-blog 的项目
    3. 完成了 tabbar 的搭建
    4. 了解了 .vue 的单文件组件
    5. 分析了首页的模块组成
    6. 封装了 request API 请求模块
    7. 复杂组件 tabs
    8. tabs 和 基于 swiper 的列表联动
最近更新:: 2025/8/22 15:05
Contributors: yanpeng_
Prev
uni-app开发交友小程序
Next
uni-app框架实战_基础