功能(列表页面、相关文章推荐等)完善与组件化开发
前言
前三天的开发工作完成了项目的初始化、环境搭建、前端架构优化、路由系统配置、数据交互与后端集成。第四天的开发工作将重点关注功能完善、组件化开发、搜索功能实现和用户体验优化,进一步提升网站的整体质量和可维护性。
编辑
编辑
一、文章列表页面开发
1. 页面结构设计
文章列表页面采用掘金风格的三栏布局:
- 顶部导航:与首页保持一致
- 左侧目录区:显示栏目目录,支持栏目筛选
- 中间列表区:展示文章列表,支持分页
- 右侧信息区:显示作者信息、热门文章、广告等
2. 功能实现
路由配置
修改 src/router/index.js:
import { createRouter, createWebHistory } from 'vue-router'
import Home from '../pages/Home.vue'
import ArticleDetail from '../pages/ArticleDetail.vue'
import ArticleList from '../pages/ArticleList.vue'
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/list',
name: 'ArticleList',
component: ArticleList
},
{
path: '/article/:id',
name: 'ArticleDetail',
component: ArticleDetail,
props: true
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
文章列表组件
创建 src/pages/ArticleList.vue:
<template>
<div class="font-sans antialiased bg-slate-50 min-h-screen">
<!-- Header -->
<header class="sticky top-0 z-50 w-full border-b bg-white/95 backdrop-blur">
<!-- 导航内容与首页一致 -->
</header>
<!-- Main Content -->
<main class="container mx-auto px-4 py-8">
<div class="grid grid-cols-1 lg:grid-cols-4 gap-6">
<!-- Left Sidebar: Category Navigation -->
<div class="lg:col-span-1">
<div class="bg-white rounded-xl shadow-sm border border-slate-200 p-4 sticky top-24">
<h3 class="font-semibold text-slate-800 mb-4">栏目目录</h3>
<ul class="space-y-4">
<li>
<a href="#" @click.prevent="selectCategory('')"
:class="[selectedCategoryId === '' ? 'bg-blue-50 text-bank-primary font-medium' : 'text-slate-600 hover:text-bank-primary', 'flex items-center justify-between text-sm transition-colors p-2 rounded-lg']">
<span>全部</span>
<span :class="[selectedCategoryId === '' ? 'bg-bank-primary/10 text-bank-primary' : 'bg-slate-100 text-slate-500', 'px-2 py-0.5 rounded-full text-xs']">{{ totalArticleCount }}</span>
</a>
</li>
<template v-for="category in displayCategories" :key="category.id">
<li>
<a href="#" @click.prevent="selectCategory(category.id)"
:class="[selectedCategoryId === category.id ? 'bg-blue-50 text-bank-primary font-medium' : 'text-slate-600 hover:text-bank-primary', 'flex items-center justify-between text-sm transition-colors p-2 rounded-lg']">
<span>{{ category.name }}</span>
<span :class="[selectedCategoryId === category.id ? 'bg-bank-primary/10 text-bank-primary' : 'bg-slate-100 text-slate-500', 'px-2 py-0.5 rounded-full text-xs']">{{ category.articleCount || 0 }}</span>
</a>
</li>
</template>
</ul>
</div>
</div>
<!-- Middle Content: Article List -->
<div class="lg:col-span-2">
<!-- Article List -->
<div class="space-y-4">
<article v-for="article in articles" :key="article.id" class="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden bank-hover-lift">
<div class="p-6">
<div class="flex items-center gap-2 mb-3">
<span v-if="article.categoryId_dictText" class="px-2 py-1 bg-bank-primary/10 text-bank-primary text-xs font-medium rounded">{{ article.categoryId_dictText }}</span>
</div>
<h2 class="text-xl font-bold text-slate-800 mb-2 hover:text-bank-primary transition-colors">
<a :href="'/article/' + article.id">{{ article.title }}</a>
</h2>
<p class="text-slate-500 mb-4 line-clamp-3">{{ truncateContent(article.content, 150) }}</p>
<div class="flex items-center justify-between text-sm text-slate-500">
<div class="flex items-center gap-4">
<span>{{ article.author }}</span>
<span>{{ formatDate(article.publishTime) }}</span>
</div>
<div class="flex items-center gap-4">
<span>{{ article.clickCount || 0 }} 阅读</span>
<span>{{ article.likeCount || 0 }} 点赞</span>
</div>
</div>
</div>
</article>
</div>
<!-- Pagination -->
<div v-if="total > 0" class="mt-8 flex flex-col items-center gap-2">
<div class="text-sm text-slate-500">
共 {{ total }} 条记录
</div>
<nav class="flex items-center gap-1">
<button
@click="changePage(currentPage - 1)"
:disabled="currentPage === 1"
class="w-10 h-10 flex items-center justify-center rounded-lg border border-slate-300 bg-white text-slate-700 hover:border-bank-primary hover:text-bank-primary transition-colors disabled:cursor-not-allowed disabled:opacity-50"
>
<
</button>
<template v-for="page in totalPages" :key="page">
<button
@click="changePage(page)"
:class="[currentPage === page ? 'bg-bank-primary text-white' : 'border border-slate-300 bg-white text-slate-700 hover:border-bank-primary hover:text-bank-primary', 'w-10 h-10 flex items-center justify-center rounded-lg transition-colors']"
>{{ page }}</button>
</template>
<button
@click="changePage(currentPage + 1)"
:disabled="currentPage === totalPages"
class="w-10 h-10 flex items-center justify-center rounded-lg border border-slate-300 bg-white text-slate-700 hover:border-bank-primary hover:text-bank-primary transition-colors disabled:cursor-not-allowed disabled:opacity-50"
>
>
</button>
</nav>
</div>
</div>
<!-- Right Sidebar -->
<div class="lg:col-span-1 space-y-6">
<!-- Author Info -->
<div class="bg-white rounded-xl shadow-sm border border-slate-200 p-5">
<!-- 作者信息内容 -->
</div>
<!-- Hot Articles -->
<div class="bg-white rounded-xl shadow-sm border border-slate-200 p-5">
<!-- 热门文章内容 -->
</div>
<!-- Ad -->
<div class="bg-white rounded-xl shadow-sm border border-slate-200 p-5">
<!-- 广告内容 -->
</div>
</div>
</div>
</main>
<!-- Footer -->
<Footer />
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue';
import { getFullNavList } from '../api/nav';
import { getArticleList } from '../api/article';
import Footer from '../components/Footer.vue';
// 导航数据
const navList = ref([]);
const fullNavList = ref([]);
const mobileMenuOpen = ref(false);
// 文章数据
const articles = ref([]);
const loading = ref(false);
const currentPage = ref(1);
const pageSize = ref(10);
const total = ref(0);
const selectedCategoryId = ref('');
const categoryArticleCounts = ref({});
// 计算属性:需要显示的栏目(平铺方式)
const displayCategories = computed(() => {
const result = [];
// 遍历所有一级栏目
fullNavList.value.forEach(category => {
// 屏蔽首页栏目(ID为2045904451398049793)
if (category.id === '2045904451398049793') {
return;
}
if (category.level === 1) {
// 检查是否有二级栏目
const hasChildren = fullNavList.value.some(child => child.pid === category.id);
if (!hasChildren) {
// 没有二级栏目,显示一级栏目
result.push({
...category,
articleCount: categoryArticleCounts.value[category.id] || 0
});
}
} else if (category.level === 2) {
// 有二级栏目,显示二级栏目
result.push({
...category,
articleCount: categoryArticleCounts.value[category.id] || 0
});
}
});
// 按sort字段排序
return result.sort((a, b) => {
const sortA = a.sort || 0;
const sortB = b.sort || 0;
return sortA - sortB;
});
});
// 计算属性:总文章数
const totalArticleCount = computed(() => {
let count = 0;
displayCategories.value.forEach(category => {
count += category.articleCount || 0;
});
return count;
});
// 计算属性:总页数
const totalPages = computed(() => {
return Math.ceil(total.value / pageSize.value);
});
// 格式化日期
function formatDate(dateStr) {
if (!dateStr) return '';
const date = new Date(dateStr);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
// 截取文章内容
function truncateContent(content, length = 150) {
if (!content) return '';
const text = content.replace(/<[^>]+>/g, '');
if (text.length > length) {
return text.substring(0, length) + '...';
}
return text;
}
// 获取文章列表
async function fetchArticles() {
loading.value = true;
try {
const params = {
pageNo: currentPage.value,
pageSize: pageSize.value,
column: 'publishTime',
order: 'desc'
};
// 如果选择了分类,添加分类ID参数
if (selectedCategoryId.value) {
params.categoryId = selectedCategoryId.value;
}
const response = await getArticleList(params);
if (response.success && response.result) {
articles.value = response.result.records || [];
total.value = response.result.total || 0;
} else {
articles.value = [];
total.value = 0;
}
} catch (error) {
console.error('获取文章列表失败:', error);
articles.value = [];
total.value = 0;
} finally {
loading.value = false;
}
}
// 获取栏目文章数量
async function fetchCategoryArticleCounts() {
try {
const allCategories = fullNavList.value;
for (const category of allCategories) {
const response = await getArticleList({
categoryId: category.id,
pageNo: 1,
pageSize: 1
});
if (response.success && response.result) {
categoryArticleCounts.value[category.id] = response.result.total || 0;
}
}
} catch (error) {
console.error('获取栏目文章数量失败:', error);
}
}
// 选择栏目
function selectCategory(categoryId) {
selectedCategoryId.value = categoryId;
currentPage.value = 1; // 重置页码
fetchArticles();
}
// 切换页码
function changePage(page) {
currentPage.value = page;
fetchArticles();
}
// 获取完整导航数据
async function fetchNavList() {
try {
const data = await getFullNavList();
if (data.success) {
const sortedList = data.result.sort((a, b) => {
const sortA = a.sort || 0;
const sortB = b.sort || 0;
if (sortA !== sortB) {
return sortA - sortB;
}
return b.id.localeCompare(a.id);
});
const firstLevel = sortedList.filter(item => item.level === 1);
const secondLevel = sortedList.filter(item => item.level === 2);
firstLevel.forEach(parent => {
parent.children = secondLevel.filter(child => child.pid === parent.id);
});
navList.value = firstLevel;
fullNavList.value = sortedList;
// 获取栏目文章数量
await fetchCategoryArticleCounts();
}
} catch (error) {
console.error('获取导航数据失败:', error);
}
}
// 组件挂载时执行
onMounted(async () => {
await fetchNavList();
fetchArticles();
initLucideIcons();
});
</script>
二、公共组件抽离
1. 抽离Footer组件
创建 src/components/Footer.vue:
<template>
<footer class="bg-slate-800 text-white py-12">
<div class="container mx-auto px-4">
<div class="grid grid-cols-1 md:grid-cols-4 gap-8">
<div>
<div class="flex items-center gap-2 mb-4">
<div class="w-10 h-10 rounded-lg bank-gradient flex items-center justify-center">
<span class="text-white font-bold text-lg">BT</span>
</div>
<span class="font-bold text-lg">银科圈</span>
</div>
<p class="text-slate-400 text-sm">专注银行科技领域,分享技术干货与职场指南</p>
<div class="flex gap-4 mt-4">
<a href="#" class="text-slate-400 hover:text-white transition-colors">
<i data-lucide="mail" class="w-5 h-5"></i>
</a>
<a href="#" class="text-slate-400 hover:text-white transition-colors">
<i data-lucide="twitter" class="w-5 h-5"></i>
</a>
<a href="#" class="text-slate-400 hover:text-white transition-colors">
<i data-lucide="github" class="w-5 h-5"></i>
</a>
</div>
</div>
<div>
<h3 class="font-semibold mb-4">技术领域</h3>
<ul class="space-y-2 text-sm text-slate-400">
<li><a href="#" class="hover:text-white transition-colors">银行核心系统</a></li>
<li><a href="#" class="hover:text-white transition-colors">信创改造</a></li>
<li><a href="#" class="hover:text-white transition-colors">自动化测试</a></li>
<li><a href="#" class="hover:text-white transition-colors">运维与灾备</a></li>
<li><a href="#" class="hover:text-white transition-colors">分布式架构</a></li>
</ul>
</div>
<div>
<h3 class="font-semibold mb-4">职场指南</h3>
<ul class="space-y-2 text-sm text-slate-400">
<li><a href="#" class="hover:text-white transition-colors">晋升路线</a></li>
<li><a href="#" class="hover:text-white transition-colors">面试技巧</a></li>
<li><a href="#" class="hover:text-white transition-colors">薪资爆料</a></li>
<li><a href="#" class="hover:text-white transition-colors">职业发展</a></li>
<li><a href="#" class="hover:text-white transition-colors">项目管理</a></li>
</ul>
</div>
<div>
<h3 class="font-semibold mb-4">关于我们</h3>
<ul class="space-y-2 text-sm text-slate-400">
<li><a href="#" class="hover:text-white transition-colors">关于银科圈</a></li>
<li><a href="#" class="hover:text-white transition-colors">联系方式</a></li>
<li><a href="#" class="hover:text-white transition-colors">广告合作</a></li>
<li><a href="#" class="hover:text-white transition-colors">加入我们</a></li>
<li><a href="#" class="hover:text-white transition-colors">隐私政策</a></li>
</ul>
</div>
</div>
<div class="border-t border-slate-700 mt-8 pt-8 text-center text-sm text-slate-500">
<p>© 2024 银科圈 YinKeQuan. 保留所有权利。</p>
</div>
<!-- Stats Bar -->
<div class="mt-4 text-center text-sm text-slate-500">
<div class="flex flex-wrap items-center justify-center gap-6">
<span>文章总数:1,234 篇</span>
<span>|</span>
<span>注册用户:56,789 人</span>
<span>|</span>
<span>今日访问:12,345 次</span>
<span>|</span>
<span>
<i data-lucide="users" class="w-4 h-4 inline-block mr-1"></i>
在线人数:1,234
</span>
</div>
</div>
</div>
</footer>
</template>
<script setup>
import { onMounted } from 'vue';
onMounted(() => {
if (window.lucide) {
window.lucide.createIcons();
}
});
</script>
<style scoped>
.bank-gradient {
background: linear-gradient(135deg, #1a56db 0%, #0e7490 100%);
}
</style>
2. 在页面中使用Footer组件
修改 src/pages/Home.vue、src/pages/ArticleDetail.vue 和 src/pages/ArticleList.vue:
<!-- 在script部分导入 -->
import Footer from '../components/Footer.vue';
<!-- 在模板底部使用 -->
<Footer />
三、相关文章推荐功能完善
1. 功能优化
修改 src/pages/ArticleDetail.vue 中的相关文章推荐:
<!-- Related Articles -->
<section class="mt-8">
<h3 class="text-lg font-bold text-slate-800 mb-4">相关文章推荐</h3>
<div v-if="relatedArticles.length > 0" class="grid grid-cols-1 md:grid-cols-2 gap-4">
<a v-for="related in relatedArticles" :key="related.id" :href="'/article/' + related.id" class="group flex gap-4 p-4 rounded-xl bg-white bank-card-shadow bank-hover-lift hover:bg-slate-50/80">
<div class="relative w-28 h-20 sm:w-36 sm:h-24 shrink-0 rounded-lg overflow-hidden bg-slate-100">
<img
:src="getImageUrl(related.cover) || 'https://images.unsplash.com/photo-1551288049-bebda4e38f71?w=400&h=300&fit=crop'"
:alt="related.title"
class="w-full h-full object-cover group-hover:scale-105 transition-all duration-300"
loading="lazy"
>
<span class="absolute top-1.5 left-1.5 px-2 py-0.5 bg-bank-primary/90 text-white text-xs rounded">{{ related.category || '精选' }}</span>
</div>
<div class="flex-1 min-w-0">
<h4 class="font-semibold text-slate-800 group-hover:text-bank-primary line-clamp-2 leading-snug">{{ related.title }}</h4>
<p class="text-sm text-slate-500 mt-1 line-clamp-2">{{ truncateContent(related.content, 20) }}</p>
<div class="flex items-center gap-3 mt-2 text-xs text-slate-400">
<span class="flex items-center gap-1"><i data-lucide="eye" class="w-3.5 h-3.5"></i> {{ related.views || 0 }}</span>
<span class="flex items-center gap-1"><i data-lucide="clock" class="w-3.5 h-3.5"></i> {{ formatDate(related.time) }}</span>
</div>
</div>
</a>
</div>
<div v-else class="text-center py-8 text-slate-500">
暂无相关文章
</div>
</section>
2. 数据获取逻辑
修改 src/pages/ArticleDetail.vue 中的相关文章获取逻辑:
// 获取相关文章(同一栏目下的其他文章)
async function fetchRelatedArticles(categoryId, excludeArticleId) {
try {
const response = await getArticleList({
categoryId: categoryId,
pageNo: 1,
pageSize: 10,
column: 'publishTime',
order: 'desc'
});
if (response.success && response.result) {
// 过滤掉当前文章,并取前4条
relatedArticles.value = (response.result.records || [])
.filter(item => item.id !== excludeArticleId)
.slice(0, 4)
.map(item => ({
id: item.id,
title: item.title,
content: item.content || '',
cover: item.cover_image || item.coverImage || '',
category: item.categoryId_dictText || '',
views: item.clickCount || 0,
time: item.publishTime
}));
} else {
relatedArticles.value = [];
}
} catch (error) {
console.error('获取相关文章失败:', error);
relatedArticles.value = [];
}
}
四、首页栏目文章数量动态获取
1. 功能实现
修改 src/pages/Home.vue,添加栏目文章数量统计:
// 栏目文章数量统计
const categoryArticleCounts = ref({});
// 获取栏目文章数量(包含一级和二级栏目)
async function fetchCategoryArticleCounts() {
try {
// 获取所有栏目(一级和二级)
const allCategories = fullNavList.value;
for (const category of allCategories) {
const response = await getArticleList({
categoryId: category.id,
pageNo: 1,
pageSize: 1
});
if (response.success && response.result) {
categoryArticleCounts.value[category.id] = response.result.total || 0;
}
}
} catch (error) {
console.error('获取栏目文章数量失败:', error);
}
}
// 根据栏目名称获取文章数量(包含子栏目)
function getCategoryCountByName(categoryName) {
const category = fullNavList.value.find(item => item.name === categoryName && item.level === 1);
if (category) {
// 获取当前栏目及其所有子栏目的ID
const categoryIds = [category.id];
// 递归获取所有二级栏目
const childCategories = fullNavList.value.filter(item => item.pid === category.id);
childCategories.forEach(child => {
categoryIds.push(child.id);
});
// 计算总文章数
let totalCount = 0;
categoryIds.forEach(id => {
totalCount += categoryArticleCounts.value[id] || 0;
});
return totalCount;
}
return 0;
}
// 组件挂载时执行
onMounted(() => {
fetchNavList();
fetchEssenceArticles();
initBannerSlider();
initLucideIcons();
// 获取栏目文章数量(延迟执行,确保导航数据已加载)
setTimeout(() => {
fetchCategoryArticleCounts();
}, 500);
});
2. 模板更新
修改 src/pages/Home.vue 中的热门分类板块:
<!-- Tech Category -->
<div class="bg-white rounded-xl bank-card-shadow overflow-hidden">
<div class="p-4 flex items-center gap-3 bg-blue-500">
<div class="w-10 h-10 rounded-lg bg-white/20 flex items-center justify-center">
<i data-lucide="database" class="w-5 h-5 text-white"></i>
</div>
<div>
<h3 class="font-semibold text-white">技术实战</h3>
<p class="text-xs text-white/80">{{ getCategoryCountByName('技术实战') }} 篇文章</p>
</div>
</div>
<!-- 其他内容 -->
</div>
<!-- Career Category -->
<div class="bg-white rounded-xl bank-card-shadow overflow-hidden">
<div class="p-4 flex items-center gap-3 bg-emerald-500">
<div class="w-10 h-10 rounded-lg bg-white/20 flex items-center justify-center">
<i data-lucide="server" class="w-5 h-5 text-white"></i>
</div>
<div>
<h3 class="font-semibold text-white">职场热点</h3>
<p class="text-xs text-white/80">{{ getCategoryCountByName('职场热点') }} 篇文章</p>
</div>
</div>
<!-- 其他内容 -->
</div>
<!-- Job Category -->
<div class="bg-white rounded-xl bank-card-shadow overflow-hidden">
<div class="p-4 flex items-center gap-3 bg-purple-500">
<div class="w-10 h-10 rounded-lg bg-white/20 flex items-center justify-center">
<i data-lucide="test-tube" class="w-5 h-5 text-white"></i>
</div>
<div>
<h3 class="font-semibold text-white">求职招聘</h3>
<p class="text-xs text-white/80">{{ getCategoryCountByName('求职招聘') }} 篇文章</p>
</div>
</div>
<!-- 其他内容 -->
</div>
五、搜索功能实现
1. 搜索框组件
在 src/pages/Home.vue 和 src/pages/ArticleList.vue 中添加搜索框:
<!-- 在头部导航中添加搜索框 -->
<div class="hidden md:flex items-center flex-1 max-w-md mx-8">
<div class="relative w-full">
<input
v-model="searchKeyword"
@keyup.enter="handleSearch"
type="text"
placeholder="搜索文章、技术、职场..."
class="w-full pl-10 pr-4 py-2 rounded-full border border-slate-300 focus:outline-none focus:ring-2 focus:ring-bank-primary/50 focus:border-bank-primary transition-colors"
>
<button @click="handleSearch" class="absolute left-3 top-1/2 transform -translate-y-1/2 text-slate-400 hover:text-bank-primary transition-colors">
<i data-lucide="search" class="w-5 h-5"></i>
</button>
</div>
</div>
2. 搜索功能逻辑
添加搜索相关代码:
// 搜索相关
const searchKeyword = ref('');
// 处理搜索
function handleSearch() {
if (searchKeyword.value.trim()) {
// 跳转到搜索结果页面
router.push({
path: '/list',
query: { keyword: searchKeyword.value.trim() }
});
}
}
3. 搜索结果处理
修改 src/pages/ArticleList.vue 以支持搜索:
// 搜索相关
const route = useRoute();
const searchKeyword = ref('');
// 获取文章列表
async function fetchArticles() {
loading.value = true;
try {
const params = {
pageNo: currentPage.value,
pageSize: pageSize.value,
column: 'publishTime',
order: 'desc'
};
// 如果选择了分类,添加分类ID参数
if (selectedCategoryId.value) {
params.categoryId = selectedCategoryId.value;
}
// 如果有搜索关键词,添加搜索参数
if (searchKeyword.value) {
params.keyword = searchKeyword.value;
}
const response = await getArticleList(params);
// 其他代码不变
} catch (error) {
console.error('获取文章列表失败:', error);
articles.value = [];
total.value = 0;
} finally {
loading.value = false;
}
}
// 组件挂载时执行
onMounted(async () => {
// 获取搜索关键词
if (route.query.keyword) {
searchKeyword.value = route.query.keyword;
}
await fetchNavList();
fetchArticles();
initLucideIcons();
});
六、性能优化
1. 图片懒加载
确保所有图片都使用懒加载:
<img
:src="getImageUrl(article.coverImage) || placeholderImage"
:alt="article.title"
class="w-full h-full object-cover"
loading="lazy"
>
2. 路由懒加载
优化路由配置:
import { createRouter, createWebHistory } from 'vue-router'
const routes = [
{
path: '/',
name: 'Home',
component: () => import('../pages/Home.vue')
},
{
path: '/list',
name: 'ArticleList',
component: () => import('../pages/ArticleList.vue')
},
{
path: '/article/:id',
name: 'ArticleDetail',
component: () => import('../pages/ArticleDetail.vue'),
props: true
}
]
3. 代码分割
在 vite.config.js 中配置代码分割:
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd());
return {
// 其他配置...
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ['vue', 'vue-router'],
axios: ['axios']
}
}
}
}
};
});
七、用户体验优化
1. 添加页面加载动画
修改 src/App.vue:
<template>
<div class="app-container">
<!-- 页面加载动画 -->
<div v-if="isLoading" class="loading-container">
<div class="loading-spinner"></div>
<p class="loading-text">加载中...</p>
</div>
<router-view v-slot="{ Component }">
<transition name="fade" mode="out-in">
<component :is="Component" />
</transition>
</router-view>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
const isLoading = ref(true);
onMounted(() => {
// 模拟加载时间
setTimeout(() => {
isLoading.value = false;
}, 500);
});
</script>
<style>
.app-container {
min-height: 100vh;
}
.loading-container {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: white;
z-index: 9999;
}
.loading-spinner {
width: 48px;
height: 48px;
border: 4px solid #f3f3f3;
border-top: 4px solid #1a56db;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 16px;
}
.loading-text {
color: #666;
font-size: 14px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>
2. 添加返回顶部功能
在页面组件中添加返回顶部按钮:
<!-- 返回顶部按钮 -->
<button
v-if="showBackToTop"
@click="backToTop"
class="fixed bottom-8 right-8 w-12 h-12 bg-bank-primary text-white rounded-full shadow-lg flex items-center justify-center hover:bg-bank-primary/90 transition-all z-50"
>
<i data-lucide="arrow-up" class="w-6 h-6"></i>
</button>
// 返回顶部相关
const showBackToTop = ref(false);
// 监听滚动
function handleScroll() {
showBackToTop.value = window.scrollY > 300;
}
// 返回顶部
function backToTop() {
window.scrollTo({
top: 0,
behavior: 'smooth'
});
}
// 组件挂载时添加滚动监听
onMounted(() => {
window.addEventListener('scroll', handleScroll);
// 其他初始化代码...
});
// 组件卸载时移除滚动监听
onUnmounted(() => {
window.removeEventListener('scroll', handleScroll);
});
八、测试与调试
1. 功能测试
- ✅ 文章列表页面加载与导航
- ✅ 栏目筛选功能
- ✅ 分页功能
- ✅ 搜索功能
- ✅ 相关文章推荐
- ✅ 公共组件使用
- ✅ 首页栏目文章数量显示
2. 响应式测试
- ✅ 移动端布局
- ✅ 平板布局
- ✅ 桌面布局
3. 性能测试
- ✅ 页面加载速度
- ✅ 图片懒加载效果
- ✅ 路由懒加载效果
- ✅ 代码分割效果
九、项目结构
最终项目结构
fintech-vue3/
├── public/
├── src/
│ ├── api/ # API模块
│ │ ├── nav.ts # 导航API
│ │ └── article.ts # 文章API
│ ├── components/ # 公共组件
│ │ └── Footer.vue # 页脚组件
│ ├── pages/ # 页面组件
│ │ ├── Home.vue # 首页
│ │ ├── ArticleDetail.vue # 文章详情页
│ │ └── ArticleList.vue # 文章列表页
│ ├── router/ # 路由配置
│ │ └── index.js
│ ├── utils/ # 工具函数
│ │ └── http/ # HTTP请求封装
│ │ └── axios/
│ │ └── index.ts
│ ├── App.vue # 根组件
│ ├── main.js # 入口文件
│ └── style.css # 全局样式
├── .env.development # 开发环境配置
├── index.html
├── package.json
├── postcss.config.js
├── tailwind.config.js
└── vite.config.js
十、开发成果
- ✅ 文章列表页面开发(掘金风格三栏布局)
- ✅ 公共Footer组件抽离
- ✅ 相关文章推荐功能完善(添加内容截取)
- ✅ 首页栏目文章数量动态获取(包含子栏目)
- ✅ 搜索功能实现
- ✅ 性能优化(图片懒加载、路由懒加载、代码分割)
- ✅ 用户体验优化(页面加载动画、返回顶部功能)
- ✅ 完整的测试与调试
十一、后续计划
1. 功能扩展
- 实现文章评论功能
- 添加用户登录与注册
- 实现文章收藏功能
- 添加文章分享功能
- 实现用户个人中心
2. 性能优化
- 服务器端渲染(SSR)
- 静态站点生成(SSG)
- 缓存策略优化
- CDN集成
3. 部署上线
- 构建优化
- 部署到生产服务器
- 域名配置
- HTTPS配置
- 监控与日志
4. 维护与迭代
- 代码规范与文档
- 自动化测试
- CI/CD集成
- 功能迭代与 bug 修复
十二、总结
第四天的开发工作完成了功能完善、组件化开发、搜索功能实现和用户体验优化。通过文章列表页面的开发、公共组件的抽离、相关文章推荐功能的完善和首页栏目文章数量的动态获取,进一步提升了网站的功能完整性和用户体验。
在开发过程中,我们遵循了以下原则:
- 组件化设计:将重复的功能抽离为公共组件,提高代码复用率
- 模块化开发:将功能按模块划分,提高代码可维护性
- 性能优先:通过各种优化手段提升网站性能
- 用户体验至上:注重细节,提升用户体验
通过这四天的开发,我们已经搭建了一个功能完善、性能优异、用户体验良好的企业级门户网站。后续将继续扩展功能、优化性能、部署上线,打造一个成熟的生产级应用。
十三、技术栈总结
| 分类 | 技术 | 版本 | 用途 |
|---|---|---|---|
| 前端框架 | Vue | 3.x | 前端核心框架 |
| 构建工具 | Vite | 5.x | 项目构建和开发服务器 |
| CSS框架 | Tailwind CSS | 3.x | 响应式样式设计 |
| 路由 | Vue Router | 4.x | 页面路由管理 |
| HTTP客户端 | Axios | 1.x | API请求封装 |
| 图标库 | Lucide Icons | - | 图标展示 |
| 后端框架 | JeecgBoot | - | 后端API服务 |
| 数据库 | MySQL | - | 数据存储 |
通过这个项目,我们展示了如何使用现代前端技术栈构建一个企业级门户网站,从项目初始化到功能完善的完整开发流程。
如您是做金融科技相关工作的人,对系统感兴趣,欢迎+讨论交流:
编辑