13.3 开发首页页面
首页页面主要展示的是文章数据,包含左侧的文章分类和中间的文章列表,以及右侧的其他信息区域。所以首页是典型的左中右布局。
13.3.1 开发左侧文章分类组件
文章分类组件需要获取文章分类数据,因此要先定义文章 Store 存储该数据,再创建组件并关联数据做渲染,详细步骤如下。
(1)创建首页左侧的文章分类组件 views/home/nav.vue,模版代码如下:
<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 和自定义事件,并通过路由地址获取当前选中的分类,代码如下:
<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)添加组件基本样式如下:
.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,编写模版代码如下:
<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),代码如下:
<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 组件,模版代码如下:
<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 以及函数等,代码如下:
<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)添加文章列表组件样式,代码如下:
.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,添加文章分类状态、文章列表状态,以及获取状态的方法。代码如下:
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,定义文章和文章分类的类型如下:
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:
export { default as articleStore } from "./article";
13.3.4 开发首页入口组件,组合各个子组件
在首页入口组件中获取文章 Store 中的数据,并将其传递给文章分类组件、文章列表组件,且定义页面布局和数据筛选的方法,代码如下:
(1)创建文件 views/home/index.vue 表示首页入口组件,编写 JS 代码如下:
<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)编写组件模版文件如下:
<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)编写组件样式代码如下:
<style lang="less">
.main-box {
display: flex;
align-items: flex-start;
.main-ctx {
flex: 1;
display: flex;
align-items: flex-start;
}
}
</style>
现在访问首页地址就可以看到页面了,页面效果如图所示: