Mobile wallpaper 1Mobile wallpaper 2Mobile wallpaper 3Mobile wallpaper 4Mobile wallpaper 5Mobile wallpaper 6
1065 字
5 分钟
8-文章篇-文章详情-Springboot Vue教程

后端实现#

文章详情Dto#

在dto下创建ArticleDetailDto类,代码如下:

package com.example.demo.dto;
import com.example.demo.entity.Article;
import lombok.Data;
import lombok.NoArgsConstructor; // 确保有无参构造函数,方便JSON反序列化
import lombok.AllArgsConstructor; // 如果你希望有一个包含所有字段的构造函数
import java.time.LocalDateTime;
/**
* 文章详情DTO,包含作者用户名
*/
@Data // 自动生成 Getter, Setter, toString, equals, hashCode 方法
@NoArgsConstructor // 自动生成无参构造函数
@AllArgsConstructor // 自动生成全参构造函数 (包括 authorUsername)
public class ArticleDetailDto {
private Integer id;
private String title;
private String content;
private Integer userId;
private String authorUsername; // 作者用户名
private LocalDateTime createTime;
private LocalDateTime updateTime;
private Integer viewCount;
private Integer status;
public ArticleDetailDto(Article article, String authorUsername) {
this.id = article.getId();
this.title = article.getTitle();
this.content = article.getContent();
this.userId = article.getUserId();
this.createTime = article.getCreateTime();
this.updateTime = article.getUpdateTime();
this.viewCount = article.getViewCount();
this.status = article.getStatus();
this.authorUsername = authorUsername; // 设置作者用户名
}
}

服务层#

ArticleService中增加如下代码:

/**
* 根据文章ID获取文章详情
* @param articleId 文章ID
* @return 包含文章详情的 Optional<Article> 对象,如果找不到则为 Optional.empty()
*/
Optional<ArticleDetailDto> getArticleById(Integer articleId);

在Impl中的ArticleServiceImpl中增加如下代码:

@Autowired
private UserMapper userMapper;
@Override
public Optional<ArticleDetailDto> getArticleById(Integer articleId) {
Optional<Article> optionalArticle = Optional.ofNullable(baseMapper.selectById(articleId));
if (optionalArticle.isPresent()) {
int userId = optionalArticle.get().getUserId();
String username = userMapper.selectById(userId).getUsername();
ArticleDetailDto articleDetailDto = new ArticleDetailDto(optionalArticle.get(), username);
return Optional.of(articleDetailDto);
}
return Optional.empty();
}

控制层#

ArticleController中添加:

/**
* 根据文章ID获取文章详情
* @param id 文章ID,通过路径变量传入
* @return SaResult 包含文章详情或错误信息
*/
@GetMapping("/{id}") // 定义一个 GET 接口,路径中包含文章ID
public SaResult getArticleDetail(@PathVariable("id") Integer id) {
Optional<ArticleDetailDto> articleOptional = articleService.getArticleById(id);
if (articleOptional.isPresent()) {
return SaResult.ok("文章详情获取成功").setData(articleOptional.get());
} else {
return SaResult.error("未找到指定ID的文章");
}
}

前端实现#

api对接#

在api文件夹中的Api.ts中添加:

/**
* 根据文章ID获取文章详情
* @param id 文章ID
* @returns Promise<Article> 文章详情
*/
export const getArticleDetail = (id: number): Promise<Article> => {
return api.get(`/article/${id}`); // 调用后端接口
};

在router文件夹中的index.ts,在const router = createRouter函数中增加:

{
path: '/article/:id', // **新增路由:文章详情页,:id 是动态参数**
name: 'articleDetail',
component: () => import('@/views/ArticleDetail.vue'),
props: true, // 允许组件通过 props 接收路由参数
},

文章详情接口#

在interface中的Article.ts中修改Article接口:

export interface Article {
id: number
title: string
content: string
userId: number
createTime: string | Date
updateTime?: string | Date // 可选
status?: 0 | 1 // 0: 草稿, 1: 已发布
viewCount?: number
authorUsername?: string
}

文章视图#

在views下创建ArticleDetail.vue,代码如下:

<template>
<div class="article-detail-container">
<el-card class="article-card" v-loading="loading">
<template #header>
<div class="card-header">
<h2>{{ article?.title }}</h2>
<el-text class="mx-1" type="info" v-if="article?.authorUsername">作者: {{ article?.authorUsername }}</el-text>
</div>
</template>
<div class="article-content" v-html="formattedContent"></div>
<el-divider />
<div class="article-meta">
<el-text class="mx-1" type="info" v-if="article?.createTime">发布时间: {{ formatDateTime(article?.createTime) }}</el-text>
<el-text class="mx-1" type="info" v-if="article?.updateTime">更新时间: {{ formatDateTime(article?.updateTime) }}</el-text>
</div>
</el-card>
<el-empty v-if="!article && !loading && !error" description="文章不存在或已删除"></el-empty>
<div v-if="error" class="error-message">
<el-alert :title="error" type="error" show-icon />
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue'
import { useRoute } from 'vue-router'
import { getArticleDetail } from '@/api/Api'
import type { Article } from '@/interface/Article'
import { ElCard, ElText, ElDivider, ElEmpty, ElAlert, ElMessage } from 'element-plus'
const route = useRoute()
const articleId = ref<number | null>(null)
const article = ref<Article | null>(null)
const loading = ref(true)
const error = ref<string | null>(null)
const formatDateTime = (dateString: string | Date | undefined) => {
if (!dateString) return ''
const date = new Date(dateString)
return date.toLocaleString()
}
const formattedContent = computed(() => {
if (article.value?.content) { // 使用可选链操作符确保 article.value 不是 null
return article.value.content.replace(/\\n/g, '<br>')
}
return ''
})
const fetchArticleDetail = async () => {
loading.value = true
error.value = null // 在每次请求前清除之前的错误
try {
const id = Number(route.params.id)
if (isNaN(id) || id <= 0) {
error.value = '无效的文章ID'
loading.value = false
return
}
articleId.value = id
const res = await getArticleDetail(id)
article.value = res
} catch (err: any) {
console.error('获取文章详情失败:', err)
// 修复 2: 确保 error.value 是一个字符串,如果 err.msg 不存在,提供一个默认值
error.value = err.msg ? String(err.msg) : '获取文章详情失败,请稍后再试。'
ElMessage.error(error.value) // ElMessage.error 期望一个 string
article.value = null // 请求失败时,清空文章数据
} finally {
loading.value = false
}
}
onMounted(() => {
fetchArticleDetail()
})
</script>
<style scoped>
.article-detail-container {
max-width: 800px;
margin: 20px auto;
padding: 20px;
background-color: #f9f9f9;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.article-card {
width: 100%;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.card-header h2 {
margin: 0;
font-size: 2em;
color: #333;
}
.article-content {
font-size: 1.1em;
line-height: 1.8;
color: #555;
white-space: pre-wrap;
}
.article-meta {
display: flex;
justify-content: flex-end;
gap: 15px;
margin-top: 20px;
font-size: 0.9em;
color: #888;
}
.error-message {
margin-top: 20px;
text-align: center;
}
</style>

到此,所有的核心功能差不多已经完成。做到这儿就可以结束了。

分享

如果这篇文章对你有帮助,欢迎分享给更多人!

8-文章篇-文章详情-Springboot Vue教程
https://blog.yumui.top/posts/8-springboot-vue/
作者
Yu Felix
发布于
2025-05-13
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时