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

数据交互(今日精选、文章详情)与功能完善(Day 3)

前言

前两天的开发工作完成了项目的初始化、环境搭建、前端架构优化、路由系统配置和基础页面开发。第三天的开发工作将重点关注数据交互、后端集成、功能完善和性能优化,确保网站能够真实展示数据并提供良好的用户体验。

一、后端API对接与数据交互

1. 后端环境准备

启动JeecgBoot后端服务

确保JeecgBoot后端服务已启动并运行在 http://localhost:8080/jeecg-boot

检查API接口

确认以下API接口可用:

  • 导航接口:/yucms/yucmsCategory/getFullNavList

  • 文章列表:/yucms/yucmsArticle/list

  • 文章详情:/yucms/yucmsArticle/queryById

  • 分类详情:/yucms/yucmsCategory/queryById

2. 修复API路径配置

问题分析

第二天的API路径配置中存在错误,需要修正:

  • 后端实际路径为 /yucms/yucmsArticle/queryById

  • 前端配置为 /yucms/yucmsArticle/getById

解决方案

修改 src/api/article.ts 文件:

复制代码
/**
 * 文章相关API
 */
enum Api {
  GetArticleDetail = '/yucms/yucmsArticle/queryById',  // 修正路径
  GetArticleList = '/yucms/yucmsArticle/list',
  GetRelatedArticles = '/yucms/yucmsArticle/getRelatedArticles',
  GetHotArticles = '/yucms/yucmsArticle/getHotArticles',
  GetCategoryName = '/yucms/yucmsCategory/queryById'  // 新增分类查询接口
}

/**
 * 获取分类名称
 * @param id 分类ID
 * @returns Promise<any>
 */
export const getCategoryName = (id: string) => {
  return defHttp.get({ 
    url: Api.GetCategoryName,
    params: { id }
  });
};

3. 解决Shiro权限验证问题

问题分析

后端接口需要Shiro权限验证,导致前端无法直接访问。

解决方案

在后端控制器中添加 @IgnoreAuth 注解:

修改 YucmsArticleController.java

复制代码
import org.jeecg.config.shiro.IgnoreAuth;

// ...

@IgnoreAuth
@Operation(summary="文章管理-通过id查询")
@GetMapping(value = "/queryById")
public Result<YucmsArticle> queryById(@RequestParam(name="id",required=true) String id) {
    // 原有代码
}

修改 YucmsCategoryController.java

复制代码
import org.jeecg.config.shiro.IgnoreAuth;

// ...

@IgnoreAuth
@Operation(summary="yucms_category-通过id查询")
@GetMapping(value = "/queryById")
public Result<YucmsCategory> queryById(@RequestParam(name="id",required=true) String id) {
    // 原有代码
}

二、文章详情页功能完善

1. 实现真实数据获取

修改 src/pages/ArticleDetail.vue

复制代码
<script setup>
import { ref, onMounted } from 'vue';
import { useRoute } from 'vue-router';
import { getFullNavList } from '../api/nav';
import { getArticleDetail, getCategoryName } from '../api/article';

// 文章数据
const article = ref({
  id: '',
  title: '',
  author: '',
  publishTime: '',
  views: 0,
  comments: 0,
  likes: 0,
  category: '',
  content: '',
  coverImage: '',
  source: ''
});

// 文章加载状态
const articleLoading = ref(false);
const articleError = ref(null);

// 处理图片路径,将相对路径转换为完整URL
function getImageUrl(imagePath) {
  if (!imagePath) return null;
  
  if (imagePath.startsWith('http://') || imagePath.startsWith('https://')) {
    return imagePath;
  }
  
  if (imagePath.startsWith('/')) {
    return `/jeecg-boot${imagePath}`;
  } else {
    return `/jeecg-boot/${imagePath}`;
  }
}

// 格式化日期
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');
  const hours = String(date.getHours()).padStart(2, '0');
  const minutes = String(date.getMinutes()).padStart(2, '0');
  return `${year}-${month}-${day} ${hours}:${minutes}`;
}

// 获取文章详情
async function fetchArticleDetail(articleId) {
  articleLoading.value = true;
  articleError.value = null;
  
  try {
    const response = await getArticleDetail(articleId);
    
    if (response.success && response.result) {
      const data = response.result;
      
      // 处理分类名称
      let categoryName = '';
      if (data.categoryId) {
        try {
          const categoryResponse = await getCategoryName(data.categoryId);
          if (categoryResponse.success && categoryResponse.result) {
            categoryName = categoryResponse.result.name;
          }
        } catch (categoryError) {
          console.error('获取分类名称失败:', categoryError);
        }
      }
      
      // 更新文章数据
      article.value = {
        id: data.id || '',
        title: data.title || '',
        author: data.author || '',
        publishTime: formatDate(data.publishTime),
        views: data.clickCount || 0,
        comments: 0,
        likes: data.likeCount || 0,
        category: categoryName,
        content: data.content || '',
        coverImage: data.coverImage || '',
        source: data.source || ''
      };
    } else {
      articleError.value = '未找到文章数据';
    }
  } catch (error) {
    console.error('获取文章详情失败:', error);
    articleError.value = '加载文章失败,请稍后重试';
  } finally {
    articleLoading.value = false;
  }
}

// 组件挂载时执行
onMounted(async () => {
  const route = useRoute();
  fetchNavList();
  initLucideIcons();
  
  // 获取文章ID并加载文章详情
  const articleId = route.params.id;
  if (articleId) {
    await fetchArticleDetail(articleId);
  }
});
</script>

2. 添加加载状态和错误处理

修改 src/pages/ArticleDetail.vue 模板部分:

复制代码
<!-- Article Card -->
<article class="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
  <!-- Loading State -->
  <div v-if="articleLoading" class="p-12 text-center">
    <div class="inline-block w-8 h-8 border-4 border-bank-primary/30 border-t-bank-primary rounded-full animate-spin mb-4"></div>
    <p class="text-slate-500">加载中...</p>
  </div>
  
  <!-- Error State -->
  <div v-else-if="articleError" class="p-12 text-center">
    <div class="w-16 h-16 mx-auto mb-4 rounded-full bg-red-100 flex items-center justify-center">
      <i data-lucide="alert-circle" class="w-8 h-8 text-red-500"></i>
    </div>
    <p class="text-slate-600 mb-4">{{ articleError }}</p>
    <button @click="() => fetchArticleDetail(route.params.id)" class="px-4 py-2 bg-bank-primary text-white rounded-lg hover:bg-bank-primary/90 transition-colors">
      重试
    </button>
  </div>
  
  <!-- Article Content -->
  <div v-else>
    <!-- 原有文章内容 -->
  </div>
</article>

三、首页数据动态获取

1. 实现精选文章数据获取

修改 src/api/article.ts 添加精选文章接口:

复制代码
/**
 * 获取精选文章
 * @param limit 数量限制,默认6条
 * @returns Promise<any>
 */
export const getEssenceArticles = (limit: number = 6) => {
  return defHttp.get({ 
    url: Api.GetEssenceArticles,
    params: { 
      izEssence: '1',
      pageNo: 1,
      pageSize: limit,
      column: 'publishTime',
      order: 'desc'
    }
  });
};

2. 修改首页组件获取数据

修改 src/pages/Home.vue

复制代码
<script setup>
import { ref, onMounted } from 'vue';
import { getFullNavList } from '../api/nav';
import { getEssenceArticles } from '../api/article';

// 精选文章数据
const essenceArticles = ref([]);
const articlesLoading = ref(false);

// 处理图片路径
function getImageUrl(imagePath) {
  if (!imagePath) return null;
  
  if (imagePath.startsWith('http://') || imagePath.startsWith('https://')) {
    return imagePath;
  }
  
  if (imagePath.startsWith('/')) {
    return `/jeecg-boot${imagePath}`;
  } else {
    return `/jeecg-boot/${imagePath}`;
  }
}

// 获取精选文章
async function fetchEssenceArticles() {
  articlesLoading.value = true;
  try {
    const response = await getEssenceArticles(6);
    if (response.success && response.result) {
      essenceArticles.value = response.result.records || [];
    }
  } catch (error) {
    console.error('获取精选文章失败:', error);
  } finally {
    articlesLoading.value = false;
  }
}

// 组件挂载时执行
onMounted(() => {
  fetchNavList();
  fetchEssenceArticles(); // 新增
  initBannerSlider();
  initLucideIcons();
});
</script>

3. 更新首页模板展示动态数据

修改 src/pages/Home.vue 模板:

复制代码
<!-- 精选文章 -->
<section class="mt-12">
  <div class="flex items-center justify-between mb-6">
    <div>
      <h2 class="text-xl font-bold text-slate-800">今日精选</h2>
      <p class="text-sm text-slate-500 mt-0.5">科技专家推荐 · 热门文章</p>
    </div>
    <a href="#" class="text-sm text-bank-primary hover:underline flex items-center gap-1">
      查看更多
      <i data-lucide="chevron-right" class="w-4 h-4"></i>
    </a>
  </div>
  
  <!-- 加载状态 -->
  <div v-if="articlesLoading" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
    <div v-for="i in 3" :key="i" class="bg-white rounded-xl border border-slate-200 p-6 animate-pulse">
      <div class="h-40 bg-slate-100 rounded-lg mb-4"></div>
      <div class="h-4 bg-slate-100 rounded w-3/4 mb-2"></div>
      <div class="h-3 bg-slate-100 rounded w-1/2 mb-4"></div>
      <div class="h-3 bg-slate-100 rounded w-1/3"></div>
    </div>
  </div>
  
  <!-- 文章列表 -->
  <div v-else class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
    <a 
      v-for="article in essenceArticles" 
      :key="article.id" 
      :href="'/article/' + article.id"
      class="group block bg-white rounded-xl border border-slate-200 overflow-hidden bank-hover-lift"
    >
      <div class="relative h-48 overflow-hidden">
        <img 
          :src="getImageUrl(article.coverImage) || 'https://images.unsplash.com/photo-1551288049-bebda4e38f71?w=400&h=300&fit=crop'" 
          :alt="article.title"
          class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
          loading="lazy"
        >
      </div>
      <div class="p-4">
        <h3 class="text-lg font-semibold text-slate-800 mb-2 line-clamp-2 group-hover:text-bank-primary transition-colors">
          {{ article.title }}
        </h3>
        <p class="text-sm text-slate-500 mb-3 line-clamp-2">{{ article.summary }}</p>
        <div class="flex items-center justify-between text-xs text-slate-400">
          <span>{{ article.author }}</span>
          <span>{{ new Date(article.publishTime).toLocaleDateString() }}</span>
        </div>
      </div>
    </a>
  </div>
</section>

四、文件上传路径配置

1. 修改文件上传路径

修改 application-dev.yml 配置文件:

复制代码
path:
  #文件上传根目录 设置(相对于项目根目录)
  upload: ./opt/uploadFiles
  #webapp文件路径
  webapp: ./opt/webapp

2. 创建上传目录

复制代码
mkdir -p "D:\Users\workspace\trae\JeecgBoot\opt\uploadFiles\temp"
mkdir -p "D:\Users\workspace\trae\JeecgBoot\opt\webapp"

3. 图片路径处理

确保 getImageUrl 函数正确处理图片路径:

复制代码
function getImageUrl(imagePath) {
  if (!imagePath) return null;
  
  // 检查是否已经是完整URL
  if (imagePath.startsWith('http://') || imagePath.startsWith('https://')) {
    return imagePath;
  }
  
  // JeecgBoot静态资源配置:./opt/uploadFiles 映射到 /**
  // 实际文件路径:D:\Users\workspace\trae\JeecgBoot\opt\uploadFiles\temp\xxx.png
  // 访问URL:http://localhost:8080/jeecg-boot/temp/xxx.png
  
  if (imagePath.startsWith('/')) {
    return `/jeecg-boot${imagePath}`;
  } else {
    return `/jeecg-boot/${imagePath}`;
  }
}

五、性能优化

1. 图片懒加载

实现图片懒加载:

复制代码
<img 
  :src="getImageUrl(article.coverImage) || placeholderImage" 
  :alt="article.title"
  class="w-full h-full object-cover"
  loading="lazy"
>

2. 路由懒加载

修改 src/router/index.js

复制代码
import { createRouter, createWebHistory } from 'vue-router'

const routes = [
  {
    path: '/',
    name: 'Home',
    component: () => import('../pages/Home.vue')
  },
  {
    path: '/article/:id',
    name: 'ArticleDetail',
    component: () => import('../pages/ArticleDetail.vue'),
    props: true
  }
]

const router = createRouter({
  history: createWebHistory(),
  routes
})

export default router

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. 添加返回顶部功能

src/pages/Home.vuesrc/pages/ArticleDetail.vue 中添加:

复制代码
<!-- 返回顶部按钮 -->
<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. 后端接口测试

使用Postman测试API接口:

  • GET /yucms/yucmsCategory/getFullNavList

  • GET /yucms/yucmsArticle/list?izEssence=1&pageNo=1&pageSize=6

  • GET /yucms/yucmsArticle/queryById?id=2046448310010691586

  • GET /yucms/yucmsCategory/queryById?id=2045904451398049793

八、项目结构

最终项目结构

复制代码
fintech-vue3/
├── public/
├── src/
│   ├── api/              # API模块
│   │   ├── nav.ts        # 导航API
│   │   └── article.ts    # 文章API
│   ├── pages/            # 页面组件
│   │   ├── Home.vue      # 首页
│   │   └── ArticleDetail.vue  # 文章详情页
│   ├── router/           # 路由配置
│   │   └── index.js
│   ├── utils/            # 工具函数
│   │   └── http/
│   │       └── axios/
│   │           └── index.ts  # HTTP请求封装
│   ├── App.vue           # 根组件
│   ├── main.js           # 入口文件
│   └── style.css         # 全局样式
├── .env.development      # 开发环境配置
├── index.html
├── package.json
├── postcss.config.js
├── tailwind.config.js
└── vite.config.js

后端项目结构

复制代码
jeecg-boot/
├── jeecg-module-system/
│   └── jeecg-system-start/
│       └── src/main/resources/
│           └── application-dev.yml  # 配置文件
└── jeecg-boot-module/
    └── jeecg-module-demo/
        └── src/main/java/org/jeecg/modules/demo/yucms/
            ├── controller/  # 控制器
            ├── entity/      # 实体类
            ├── mapper/      # Mapper
            └── service/     # 服务层

九、开发成果

  • ✅ 后端API对接与数据交互

  • ✅ 文章详情页真实数据展示

  • ✅ 首页精选文章动态获取

  • ✅ 文件上传路径配置与图片处理

  • ✅ 性能优化(图片懒加载、路由懒加载、代码分割)

  • ✅ 用户体验优化(页面加载动画、返回顶部功能)

  • ✅ 完整的测试与调试

十、后续计划

1. 功能扩展

  • 实现文章评论功能

  • 添加文章搜索功能

  • 实现用户登录与注册

  • 添加文章收藏功能

  • 实现文章分享功能

2. 性能优化

  • 服务器端渲染(SSR)

  • 静态站点生成(SSG)

  • 缓存策略优化

  • CDN集成

3. 部署上线

  • 构建优化

  • 部署到生产服务器

  • 域名配置

  • HTTPS配置

  • 监控与日志

4. 维护与迭代

  • 代码规范与文档

  • 自动化测试

  • CI/CD集成

  • 功能迭代与 bug 修复

十一、总结

第三天的开发工作完成了数据交互、后端集成、功能完善和性能优化。通过对接真实的后端API,实现了文章详情页和首页的动态数据展示。同时,通过性能优化和用户体验改进,提升了网站的整体质量。

在开发过程中,我们遇到了以下挑战并成功解决:

  1. API路径配置错误:修正了前端API路径,确保与后端接口匹配

  2. Shiro权限验证 :在后端控制器中添加 @IgnoreAuth 注解,允许前端直接访问

  3. 图片路径处理 :实现了 getImageUrl 函数,正确处理相对路径和绝对路径

  4. 性能优化:通过图片懒加载、路由懒加载和代码分割,提升了网站性能

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

十二、技术栈总结

分类 技术 版本 用途
前端框架 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 - 数据存储

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

相关推荐
神の愛2 小时前
左连接查询数据 left join
java·服务器·前端
南境十里·墨染春水3 小时前
linux学习进展 线程同步——互斥锁
java·linux·学习
雨奔3 小时前
Kubernetes 联邦 Deployment 指南:跨集群统一管理 Pod
java·容器·kubernetes
杨凯凡3 小时前
【021】反射与注解:Spring 里背后的影子
java·后端·spring
lulu12165440783 小时前
Claude Code项目大了响应慢怎么办?Subagents、Agent Teams、Git Worktree、工作流编排四种方案深度解析
java·人工智能·python·ai编程
riNt PTIP3 小时前
SpringBoot创建动态定时任务的几种方式
java·spring boot·spring
老星*4 小时前
AI选股核心设计思路
java·ai·开源·软件开发
それども4 小时前
Comparator.comparing 和 拆箱问题
java·jvm
星晨羽5 小时前
西门子机床opc ua协议实现变量读写及NC文件上传下载
java·spring boot