Skip to content

13.3 开发首页页面

首页页面主要展示的是文章数据,包含左侧的文章分类和中间的文章列表,以及右侧的其他信息区域。所以首页是典型的左中右布局。

13.3.1 开发左侧文章分类组件

文章分类组件需要获取文章分类数据,因此要先定义文章 Store 存储该数据,再创建组件并关联数据做渲染,详细步骤如下。

(1)创建首页左侧的文章分类组件 views/home/nav.vue,模版代码如下:

html
<div class="main-nav">
  <div
    :class="['cato-item', { active: active == item.key }]"
    v-for="item in props.category"
    @click="onClick(item)"
  >
    <el-icon :size="18"><Opportunity /></el-icon>
    <span class="text">{{ item.label }}</span>
  </div>
</div>

上方代码中,接收传入的分类数据 category,并处理点击选中时添加一个 active 类名。

(2)添加文章分类组件的 JS 代码,定义所需的 Props 和自定义事件,并通过路由地址获取当前选中的分类,代码如下:

vue
<script setup lang="ts">
import { Opportunity } from "@element-plus/icons-vue";
import { onMounted, ref } from "vue";
import { useRoute } from "vue-router";
const props = defineProps<{
  category: any[];
}>();
const emit = defineEmits<{
  (e: "onFilter", json: Record<string, string>): void;
}>();
const route = useRoute();
const active = ref("all");
const onClick = (item: any) => {
  active.value = item.key;
  emit("onFilter", { category: item.key });
};
onMounted(() => {
  active.value = (route.query["category"] as string) || "all";
});
</script>

(4)添加组件基本样式如下:

less
.main-nav {
  background: #fff;
  width: 180px;
  position: sticky;
  top: 80px;
  padding: 8px;
  .cato-item {
    position: relative;
    padding: 10px 17px;
    cursor: pointer;
    border-radius: 4px;
    color: var(--font-color2);
    &.active {
      color: var(--el-color-primary);
      font-weight: 500;
      background: #eaf2ff;
      .el-icon {
        color: var(--el-color-primary);
      }
    }
  }
}

13.3.2 开发中间文章列表组件

文章列表组件包括顶部的排序方式切换和下方的文章列表,该组件需要从文章 Store 中获取文章数据,并且切换排序方式时重新请求数据。

(1)创建首页的文章组件 views/home/articles.vue,编写模版代码如下:

html
<div class="main-articles">
  <div class="cus-tabs-header">
    <ul @click="onFilter">
      <li data-val="hot" :class="{ active: orderby == 'hot' }">最热</li>
      <li data-val="new" :class="{ active: orderby == 'new' }">最新</li>
    </ul>
  </div>
  <Articles :articles="props.articles" />
</div>

模版代码很简单,只定义了排序方式,文章列表的内容引入了另一个 Articles 组件。这是因为文章列表部分在个人中心也会用到,因此将它提取出来,只传入列表数据渲染即可。

(2)创建组件 JS 代码,这里要引入 Articles 组件、定义父组件传递的 Props(articles)和自定义事件(onFilter),代码如下:

vue
<script setup lang="ts">
import { onMounted, ref } from "vue";
import { useRoute } from "vue-router";
import Articles from "@/views/article/lists.vue";
const route = useRoute();
const orderby = ref("hot");
const props = defineProps<{
  articles: any[];
}>();
const emit = defineEmits<{
  (e: "onFilter", json: Record<string, string>): void;
}>();
const onFilter = (e: MouseEvent) => {
  let dom: any = e.target;
  orderby.value = dom.dataset.val;
  emit("onFilter", { orderby: orderby.value });
};
onMounted(() => {
  orderby.value = (route.query["orderby"] as string) || "hot";
});
</script>

(3)创建文件 views/article/lists.vue 表示上一步导入的 Articles 组件,模版代码如下:

html
<div class="article-lists">
  <div class="arts-item" v-for="item in props.articles" @click="toDetail(item)">
    <div class="art-meta">
      {{ item.user.username }} <i /> {{ getTimer(item.created_at) }}<i />{{
      store.getCateLabel(item.category) }}
    </div>
    <div class="artctx-wrapper">
      <div class="info-wrap">
        <h3>{{ item.title }}</h3>
        <p>{{ item.intro }}</p>
        <div class="handle fx">
          <span class="row">
            <span class="iconfont icon-liulan"></span>
            {{ item.page_view }}
          </span>
          <span class="row zan">
            <span class="iconfont icon-zan"></span>
            {{ item.praises || '点赞' }}
          </span>
          <span class="row">
            <span class="iconfont icon-wenda"></span>
            {{ item.comments || '评论' }}
          </span>
        </div>
      </div>
      <div class="img-wrap"></div>
    </div>
  </div>
</div>

上方代码中,接收父组件传递的文章列表数据并循环渲染,展示出每条文章的数据。其中 getCateLabel() 方法从文章 Store 中导入,用于获取文章分类名称。 getTimer() 方法则用于处理时间格式。

(4)添加组件 JS 代码并导入 Store 以及函数等,代码如下:

vue
<script setup lang="ts">
import { articleStore } from "@/stores";
import { getTimer } from "@/utils";
const store = articleStore();
const props = defineProps<{
  articles: any[];
}>();
const toDetail = (item: any) => {
  window.open("/article/" + item._id);
};
</script>

(5)添加文章列表组件样式,代码如下:

less
.article-lists {
  .arts-item {
    padding: 12px 20px 0;
    cursor: pointer;
    &:hover {
      background: var(--bg-color1);
    }
    .art-meta {
      color: var(--font-color3);
      font-size: 13px;
      line-height: 22px;
      display: flex;
      align-items: center;
    }
    .artctx-wrapper {
      padding: 10px 0 12px 0;
      border-bottom: 1px solid var(--border-color);
      h3 {
        font-size: 16px;
        line-height: 24px;
        margin-bottom: 8px;
      }
      p {
        font-size: 13px;
        margin: 8px 0;
      }
      .handle {
        .row {
          display: inline-flex;
          align-items: center;
          color: var(--font-color3);
          font-size: 13px;
          .iconfont {
            font-size: 17px;
            margin-right: 2px;
          }
          &:hover {
            color: var(--el-color-primary);
          }
        }
      }
    }
  }
}

最终文章列表的界面效果如图所示。

13.3.3 创建文章 Store,定义相关状态和方法

文章 Store 主要存储文章数据和文章分类数据,并定义从接口中获取数据的方法。此外还要定义文章操作相关的接口,包括点赞、收藏、修改文章等,我们先定义需要的获取列表方法。

(1)创建文件 store/article/index.ts 表示文章 Store,添加文章分类状态、文章列表状态,以及获取状态的方法。代码如下:

js
import request from '@/request'
const artiStore = defineStore('article', {
  state: () => ({
    articles: [] as ArticleType[],
    categories: [] as CategoryType[],
    meta: {
      page: 1,
      per_page: 10,
      total: 0,
    },
  }),
  actions: {
    // 获取文章分类
    async getCategory() {
      try {
        let res: any = await request.get('/arts/category')
        this.categories = res
      } catch (error) {
        console.log(error)
      }
    },
    // 获取文章列表
    async getArticles(
      params: Record<string, string> = {},
      fun?: (data: any) => void
    ) {
      try {
        if (params.category == 'all') {
          // 综合分类表示不进行分类过滤,因此置空
          params.category = null
        }
        let res: any = await request.get('/arts/lists', { params })
        if (res && !fun) {
          this.articles = res.data
          this.meta = res.meta
        }
        if (fun) fun(res)
      } catch (error) {
        console.log(error)
      }
    },
  }
})
export default artiStore

上方代码中,文章列表数据包含分页,所以要存储分页状态。当提供回调函数时,我们将列表数据传给回调函数,以供数据在组件中使用。

(2)创建文章类型文件 store/article/type.d.ts,定义文章和文章分类的类型如下:

js
interface CategoryType {
  key: string
  label: string
}
interface ArticleType {
  _id: string
  category: string
  comments: number
  content: string
  created_at: string
  created_by: string
  intro: string
  is_praise: boolean
  is_start?: boolean
  stars?: number
  page_view: number
  praises: number
  status: 0 | 1
  tags: string[]
  title: string
  updated_at: string
  user: UserType
}

上方代码中的文章类型,与文章接口返回的字段名称对应。其中 UserType 表示用户类型,定义在用户 Store 的类型文件中,后面会编写该类型。

(3)在 store/index.ts 文件中导出文章 Store:

js
export { default as articleStore } from "./article";

13.3.4 开发首页入口组件,组合各个子组件

在首页入口组件中获取文章 Store 中的数据,并将其传递给文章分类组件、文章列表组件,且定义页面布局和数据筛选的方法,代码如下:

(1)创建文件 views/home/index.vue 表示首页入口组件,编写 JS 代码如下:

vue
<script setup lang="ts">
import { articleStore } from "@/stores";
import { onMounted, ref } from "vue";
import { useRoute, useRouter } from "vue-router";
import NavComp from "./nav.vue";
import Articles from "./articles.vue";
import Others from "./other.vue";
const store = articleStore();
const router = useRouter();
const route = useRoute();
const filter = ref({});
const onFilter = (json: Record<string, string>) => {
  filter.value = {
    ...filter.value,
    ...json,
  };
  router.push({
    query: filter.value,
  });
  store.getArticles(filter.value);
};
onMounted(() => {
  filter.value = route.query;
  store.getCategory();
  store.getArticles(filter.value);
});
</script>

上方代码中,导入了文章分类、文章列表子组件,导入了文章 Store,并在组件初始化的时候获取地址栏中的参数,然后获取文章分类和列表数据。

其中 onFilter() 方法由子组件的自定义事件触发,表示修改了文章的过滤参数。该方法触发时用新的参数请求文章列表数据。

(2)编写组件模版文件如下:

html
<main class="main-box">
  <NavComp :category="store.categories" @on-filter="onFilter" />
  <div class="main-ctx">
    <Articles :articles="store.articles" @on-filter="onFilter" />
    <Others />
  </div>
</main>

因为大部分代码都写在了子组件中,所以上方的模版代码比较干净,只有基本结构。

(3)编写组件样式代码如下:

vue
<style lang="less">
.main-box {
  display: flex;
  align-items: flex-start;
  .main-ctx {
    flex: 1;
    display: flex;
    align-items: flex-start;
  }
}
</style>

现在访问首页地址就可以看到页面了,页面效果如图所示: