Vue3仿卖座电影开发纪实(六):优化与BUG修复

555 阅读2分钟

源码地址 Vue3+Vant3仿卖座电影

样式优化

宽高溢出视口校正

  • 全局根布局与视口等宽等高,溢出区域使用滚动浏览;
  • 注意配合box-sizing值border-box,即width/height包含padding与margin;

src/assets/base.css

* {

  /* 
  默认值是content-box width/height为内容宽高 元素占据空间的宽高事实上为 内容宽高width/height + 内边距padding + 边框宽度border + 外边距/margin 
  修改为border-box(指定的宽高width/height)包含边框+内边距 但不包含margin
  */
  box-sizing: border-box;
  ...
}

src/App.vue

<template>
  <!-- 全局根布局 -->
  <div id="root">
      ...
  </div>
</template>

src/assets/main.scss

#root {
  // 与视口等宽
  width: 100vw;

  // 与视口等高
  height: 100vh;

  // 高度溢出区域使用滚动
  overflow: scroll;
}

可复用组件与钩子

自定义钩子useScroll

此hook实现以下功能:

  • 组件挂载时给目标元素绑定scroll监听器;
  • 在scroll监听器中实时获取目标元素的滚动高度,保存在一个响应式数据中;
  • 这个实时滚动高度返回给调用者;
  • 组件卸载时移除scroll事件监听器;

src/hooks/useScroll.js核心代码

import { onMounted, onUnmounted, ref } from "vue";

export default function (target = window, dataRef = null) {
  /* 响应式数据实时滚动距离 */
  const scrollTop = ref(0);

  /* 滚动事件监听器 */
  const scrollHandler = (e) => {
    // console.log("scroll");

    // 实时同步scrollTop
    scrollTop.value = target.scrollTop;
  };

  /* 组件挂载时添加scrollHandler */
  onMounted(() => {
    target.addEventListener("scroll", scrollHandler);
  });

  /* 组件卸载时移除scrollHandler */
  onUnmounted(() => {
    target.removeEventListener("scroll", scrollHandler);
  });

  /* 实时滚动距离返回给调用者 */
  return scrollTop;
}

MyNavBar实现背景可透明

src/components/common/MyNavBar.vue

定义props

  props: {
    ...
    /* 默认不使用透明标题 */
    titleTransparent: {
      type: Boolean,
      default: false,
    },
    ...
  },

根据props确定是否要加transparent样式类

<!-- 根据props确定是否加transparent样式 :class="{ transparent: titleTransparent }" -->
<van-nav-bar
  :title="title"
  @click-left="(onClickLeft || defaulOnClickLeft)()"
  class="mynavbar"
  :class="{ transparent: titleTransparent }"
>
  <template #left>...</template>

  <template #title>
    <span class="title">{{ title }}</span>
  </template>

  <template #right v-if="showRight">...</template>
</van-nav-bar>

定义transparent样式

.transparent {
  // 背景透明
  background-color: transparent;

  // border-bottom取消
  border-bottom: none;

  // 属性变化时加转换动画
  transition: all .5s ease;

  // 标题文字也跟着透明
  .title {
    color: transparent;
  }
}

覆盖一下 Vant的默认边框颜色

src/assets/main.scss

/* 覆盖vant的原始样式 */
:root {
  --van-border-color: transparent;
}

PS:这里是一个坑点,需要自己调试才能发现

详情页交互

摘要的折叠与展开

src/views/DetailView.vue

模板

  <!-- 影片摘要 -->
  <!-- :class="{ hidde: hideSynopsis }" 动态判断是否使用折叠  -->
  <!-- :style="{ height: synopsisHeight }" 动态计算一下展开高度 -->
  <div
    class="film-synopsis grey-text"
    :class="{ hidde: hideSynopsis }"
    :style="{ height: synopsisHeight }"
  >
    {{ film.synopsis }}
  </div>

  <!-- 点击时在折叠与展开之间来回切换 -->
  <div class="toggle" @click="hideSynopsis = !hideSynopsis">

    <!-- 折叠时使用↓否则使用↑ -->
    <van-icon
      :name="hideSynopsis ? 'arrow-down' : 'arrow-up'"
      size="12"
      color="#666"
    />
  </div>

样式定义

.film .film-detail .film-synopsis {
  // 当高度变化时使用动画
  transition: height 0.5s ease;

  // 这里不适合写死 而是根据摘要长度动态计算
  // height: 120px;

  // 加折叠时 使用固定的高度
  &.hidde {
    height: 50px !important;
  }
}

动态数据

// 摘要是否使用折叠
const hideSynopsis = ref(true);

// 根据摘要长度变化动态计算展开时的高度
const synopsisHeight = computed(() => {
  // 这里假设一行25个字 行高是16px
  return Math.ceil(film.value.synopsis.length / 25) * 16 + "px";
});

NavBar随着滚动透明

src/views/DetailView.vue

设置MyNavBar在导航栏位置

<!-- :titleTransparent="scrollTop < 50" 当纵向滚动不足50MyNavBar使用透明标题  -->
<MyNavBar
  :title="film.name"
  :showRight="false"
  :onClickLeft="$router.back"
  :titleTransparent="scrollTop < 50"
>

  <!-- 将一个设置好样式的返回按钮塞在myleft插槽 -->
  <template #myleft>
    <span
      style="
        background-color: rgba(255, 255, 255, 0.5);
        border-radius: 50%;
        padding: 1px;
        display: inline-box;
        line-height: 0;
      "
    >
      <van-icon name="arrow-left" size="24" color="#666" />
    </span>
  </template>

</MyNavBar>

使用自定义钩子useScroll实时获取滚动高度

// 导入自定义hook
import useScroll from "../hooks/useScroll";

const scrollTop = useScroll(
  // 现在滚动的不再是window而是根组件的根元素了
  document.querySelector("#root")
);

其它互动优化

NavBar吸顶与Tabs吸顶协调

  • 令Tabs吸顶拥有比NabBar更高的z-index优先级;
  • 即二者事实上同时吸顶,但Tabs拥有更高的z-index优先级,因此视觉上NabBar在Tabs吸顶时自动被隐藏;
  • 最终实现效果见文章末尾的动图;

src/components/common/MyNavBar.vue

<template>
  <!-- 自带吸顶效果的导航条 -->
  <!-- 注意这里的z-index="10"!!! -->
  <van-sticky v-if="sticky" z-index="10">
    <van-nav-bar
      :title="title"
      @click-left="(onClickLeft || defaulOnClickLeft)()"
      class="mynavbar"
      :class="{ transparent: titleTransparent }"
    >
      <template #left>
        <!-- 自带默认内容的插槽 如果用户传递了myleft插槽内容 则覆盖myleft -->
        <slot name="myleft">
          <!-- 如果用户没有传递myleft的内容 则默认使用如下内容 -->
          <span>广州</span>
          <van-icon name="arrow-down" size="12" color="#666" />
        </slot>
      </template>

      <template #title>
        <span class="title">{{ title }}</span>
      </template>

      <template #right v-if="showRight">
        <van-icon name="search" size="24" color="#666" />
      </template>
    </van-nav-bar>
  </van-sticky>
</template>

src/views/FilmView.vue

<template>
  <div>
    <!-- 吸顶但被下方同时吸顶的Tabs覆盖 -->
    <MyNavBar :showRight="false" title="电影"/>

    <!-- Tabs吸顶时拥有比MyNavBar更高的z-index优先级,视觉上覆盖同时吸顶MyNavBar -->
    <van-sticky z-index="100">
      <van-tabs
      ...
      >
        <van-tab title="正在热映"></van-tab>
        <van-tab title="即将上映"></van-tab>
      </van-tabs>
    </van-sticky>

    <main>
      <!-- 路由出口 router-link对应的内容会呈现在RouterView容器中 -->
      <RouterView />
    </main>
  </div>
</template>

在【即将上映】页刷新时校正高亮Tab

  • 此时要确保被激活的Tab同时更新为“即将上映”(1号而非0号Tab);
  • 我们给Tabs的active双向绑定一个计算属性activeTab;
  • 当路由变化时activeTab会在0/1之间动态切换;
  • 最终实现效果见文章末尾的动图;

src/views/FilmView.vue

  <!-- v-model:active="activeTab" 激活的Tab项动态关联数据activeTab -->
  <van-tabs
    v-model:active="activeTab"
    color="#ff5f16"
    line-height="1.5px"
    line-width="80px"
    @click-tab="onClickTab"
    :sticky="true"
  >
    <van-tab title="正在热映"></van-tab>
    <van-tab title="即将上映"></van-tab>
  </van-tabs>
  /* data */
  data() {
    return {
      activeTab: 0,
    };
  },
  /* 组件挂载时重新确定一下该激活哪个Tab */
  mounted() {
    console.log("fv mounted");
    switch (this.$route.path) {
      case "/films/nowPlaying":
        this.activeTab = 0;
        break;
      case "/films/comingSoon":
        this.activeTab = 1;
        break;
    }
  },

返回电影页时恢复原来的子Tab

  • 给RouterLink的to属性使用动态数据currentFilmPath;
  • 监听route.path的变化,实时记录当前显示的电影子Tab路径,并同步给currentFilmPath;
  • 这样当再次点击TabBar中的【电影】时,就会显示之前最后一次切换到的子Tab了;

src/App.vue

数据定义

let currentFilmPath = ref("/movie/nowPlaying");

RouterLink关联动态动态数据

  <RouterLink
    class="tab"
    :class="{ active: $route.path.startsWith('/films/') }"
    :to="currentFilmPath"
    active-class="active"
  >
    <p><i class="fa fa-youtube-play"></i></p>
    <p>电影</p>
  </RouterLink>

实时记录电影子Tab的位置

const route = useRoute()

/* 侦听route变化 */
watch(
  () => route.path,
  (nv, ov) => {
    // 实时记录电影子Tab位置(这样下次切换回电影页时 RouterLink就能找回上次的位置了)
    nv.startsWith("/films") && (currentFilmPath.value = nv);
  },
  {
    immediate: true,
  }
);
</s