企业级门户网站设计与实现:基于SpringBoot + Vue3的全栈解决方案(Day 4)

0 阅读12分钟

 功能(列表页面、相关文章推荐等)完善与组件化开发

前言

前三天的开发工作完成了项目的初始化、环境搭建、前端架构优化、路由系统配置、数据交互与后端集成。第四天的开发工作将重点关注功能完善、组件化开发、搜索功能实现和用户体验优化,进一步提升网站的整体质量和可维护性。

​编辑

​编辑

一、文章列表页面开发

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"
              >
                &lt;
              </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"
              >
                &gt;
              </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.vuesrc/pages/ArticleDetail.vuesrc/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.vuesrc/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 修复

十二、总结

第四天的开发工作完成了功能完善、组件化开发、搜索功能实现和用户体验优化。通过文章列表页面的开发、公共组件的抽离、相关文章推荐功能的完善和首页栏目文章数量的动态获取,进一步提升了网站的功能完整性和用户体验。

在开发过程中,我们遵循了以下原则:

  1. 组件化设计:将重复的功能抽离为公共组件,提高代码复用率
  2. 模块化开发:将功能按模块划分,提高代码可维护性
  3. 性能优先:通过各种优化手段提升网站性能
  4. 用户体验至上:注重细节,提升用户体验

通过这四天的开发,我们已经搭建了一个功能完善、性能优异、用户体验良好的企业级门户网站。后续将继续扩展功能、优化性能、部署上线,打造一个成熟的生产级应用。

十三、技术栈总结

分类技术版本用途
前端框架Vue3.x前端核心框架
构建工具Vite5.x项目构建和开发服务器
CSS框架Tailwind CSS3.x响应式样式设计
路由Vue Router4.x页面路由管理
HTTP客户端Axios1.xAPI请求封装
图标库Lucide Icons-图标展示
后端框架JeecgBoot-后端API服务
数据库MySQL-数据存储

通过这个项目,我们展示了如何使用现代前端技术栈构建一个企业级门户网站,从项目初始化到功能完善的完整开发流程。

如您是做金融科技相关工作的人,对系统感兴趣,欢迎+讨论交流:

​编辑