项目目标
本篇用 Vant 搭建一个 移动端博客 App,覆盖以下页面:
- 首页(文章列表 + 下拉刷新 + 分类 Tab)
- 文章详情
- 个人中心
项目结构
src/
├── App.vue # 根组件
├── main.js # 入口
├── router/index.js # 路由配置
├── api/article.js # 文章接口
├── views/
│ ├── Home.vue # 首页
│ ├── ArticleDetail.vue # 文章详情
│ └── Profile.vue # 个人中心
└── components/
└── ArticleCard.vue # 文章卡片
步骤 1:路由配置
// router/index.js
import { createRouter, createWebHistory } from "vue-router";
const routes = [
{
path: "/",
component: () => import("@/views/Home.vue"),
meta: { title: "博客首页", showTab: true },
},
{
path: "/article/:id",
component: () => import("@/views/ArticleDetail.vue"),
meta: { title: "文章详情" },
},
{
path: "/profile",
component: () => import("@/views/Profile.vue"),
meta: { title: "我的", showTab: true },
},
];
export default createRouter({
history: createWebHistory(),
routes,
});
步骤 2:App.vue 骨架
<template>
<div class="app">
<van-nav-bar
:title="navTitle"
v-if="showNav"
fixed placeholder safe-area-inset-top
/>
<div class="content">
<router-view />
</div>
<van-tabbar v-model="tab" route safe-area-inset-bottom v-if="showTab">
<van-tabbar-item icon="home-o" to="/">首页</van-tabbar-item>
<van-tabbar-item icon="user-o" to="/profile">我的</van-tabbar-item>
</van-tabbar>
</div>
</template>
<script setup>
import { computed, ref } from "vue";
import { useRoute } from "vue-router";
const route = useRoute();
const tab = ref(0);
const navTitle = computed(() => route.meta.title || "");
const showNav = computed(() => !!route.meta.title);
const showTab = computed(() => route.meta.showTab);
</script>
<style>
.app { min-height: 100vh; background: #f7f8fa; }
.content { padding-bottom: env(safe-area-inset-bottom); }
</style>
步骤 3:首页 Home.vue
<template>
<van-tabs v-model:active="activeCat" sticky swipeable>
<van-tab v-for="cat in categories" :key="cat.id" :title="cat.name" :name="cat.id">
<van-pull-refresh v-model="refreshing" @refresh="onRefresh">
<van-list
v-model:loading="loading"
:finished="finished"
finished-text="没有更多了"
@load="onLoad"
>
<ArticleCard
v-for="item in list"
:key="item.id"
:article="item"
@click="goDetail(item.id)"
/>
</van-list>
</van-pull-refresh>
</van-tab>
</van-tabs>
</template>
<script setup>
import { ref, watch } from "vue";
import { useRouter } from "vue-router";
import ArticleCard from "@/components/ArticleCard.vue";
import { fetchCategories, fetchArticleList } from "@/api/article";
const router = useRouter();
const categories = ref([]);
const activeCat = ref(0);
const list = ref([]);
const loading = ref(false);
const finished = ref(false);
const refreshing = ref(false);
const page = ref(1);
fetchCategories().then((data) => {
categories.value = data;
activeCat.value = data[0].id;
});
watch(activeCat, () => {
list.value = [];
page.value = 1;
finished.value = false;
loading.value = true;
});
const onLoad = async () => {
const { data } = await fetchArticleList({
categoryId: activeCat.value,
page: page.value++,
pageSize: 10,
});
list.value.push(...data.list);
if (list.value.length >= data.total) finished.value = true;
loading.value = false;
};
const onRefresh = async () => {
page.value = 1;
const { data } = await fetchArticleList({
categoryId: activeCat.value, page: 1, pageSize: 10,
});
list.value = data.list;
finished.value = list.value.length >= data.total;
refreshing.value = false;
};
const goDetail = (id) => router.push(`/article/${id}`);
</script>
步骤 4:文章卡片组件
<template>
<van-cell :title="article.title" :label="article.summary" is-link @click="$emit('click')">
<template #icon>
<van-image
v-if="article.cover"
:src="article.cover"
width="80" height="60"
fit="cover" round
/>
</template>
<template #right-icon>
<span class="meta">
{{ article.views }} 阅读 · {{ formatDate(article.ctime) }}
</span>
</template>
</van-cell>
</template>
<script setup>
defineProps({ article: { type: Object, required: true } });
defineEmits(["click"]);
const formatDate = (d) => `${new Date(d).getMonth() + 1}-${new Date(d).getDate()}`;
</script>
<style scoped>
.meta { font-size: 12px; color: #969799; }
</style>
步骤 5:文章详情页
<template>
<div class="detail">
<van-image
v-if="article.cover"
:src="article.cover"
width="100%" aspect-ratio="2.5" fit="cover"
/>
<div class="content">
<h1 class="title">{{ article.title }}</h1>
<div class="meta">
<span>{{ formatDate(article.ctime) }}</span>
<span>{{ article.views }} 阅读</span>
</div>
<div class="body" v-html="article.content" />
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from "vue";
import { useRoute } from "vue-router";
import { fetchArticleDetail } from "@/api/article";
const route = useRoute();
const article = ref({});
onMounted(async () => {
article.value = await fetchArticleDetail(route.params.id);
});
const formatDate = (d) => new Date(d).toLocaleDateString("zh-CN");
</script>
<style scoped>
.detail { background: #fff; min-height: 100vh; }
.detail .content { padding: 16px; }
.detail .title { font-size: 20px; margin: 0 0 12px; }
.detail .meta { color: #969799; font-size: 13px; margin-bottom: 16px; }
.detail .meta span { margin-right: 12px; }
.detail .body { line-height: 1.8; font-size: 15px; }
.detail .body img { max-width: 100%; border-radius: 4px; }
.detail .body pre { background: #f7f8fa; padding: 12px; border-radius: 4px; overflow-x: auto; }
.detail .body code { background: #f7f8fa; padding: 2px 4px; border-radius: 2px; font-size: 13px; }
.detail .body pre code { background: none; padding: 0; }
</style>
启动运行
npm install
npm run dev
至此你已经用 Vant 搭建了一个完整的移动端博客 App。Vant 的组件远不止这些——Tabs、Swiper、Checkbox、Radio、Stepper、Uploader、Badge、Tag、NoticeBar、CountDown……可以按需查阅文档继续扩展。