添加文章细节显示
This commit is contained in:
448
web/src/views/detail/detailView.vue
Normal file
448
web/src/views/detail/detailView.vue
Normal file
@@ -0,0 +1,448 @@
|
|||||||
|
<template>
|
||||||
|
<div class="detail-page">
|
||||||
|
<!-- 加载状态 -->
|
||||||
|
<div class="loading" v-if="isLoading">
|
||||||
|
<el-icon class="loading-icon"><Loading /></el-icon>
|
||||||
|
<p>正在加载详情...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 错误状态 -->
|
||||||
|
<div class="error" v-else-if="errorMsg">
|
||||||
|
<el-icon class="error-icon"><WarningFilled /></el-icon>
|
||||||
|
<p>{{ errorMsg }}</p>
|
||||||
|
<el-button type="text" @click="fetchDetail()">重试</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 案例详情(type=case) -->
|
||||||
|
<div class="content case-content" v-else-if="detailData && currentType === 'case'">
|
||||||
|
<h1 class="title">{{ detailData.title }}</h1>
|
||||||
|
|
||||||
|
<div class="meta">
|
||||||
|
<div class="meta-item">
|
||||||
|
<span class="meta-label">指导教师:</span>
|
||||||
|
<span class="meta-value">{{ detailData.tutor_name }}({{ detailData.tutor_title }})</span>
|
||||||
|
</div>
|
||||||
|
<div class="meta-item">
|
||||||
|
<span class="meta-label">参与学生:</span>
|
||||||
|
<span class="meta-value">{{ detailData.student_names }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="meta-item">
|
||||||
|
<span class="meta-label">更新时间:</span>
|
||||||
|
<span class="meta-value">{{ detailData.update_time }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="cover" v-if="detailData.cover_url">
|
||||||
|
<img :src="detailData.cover_url" alt="封面图" class="cover-img">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="body" v-html="detailData.content"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 社区服务详情(type=service) -->
|
||||||
|
<div class="content service-content" v-else-if="detailData && currentType === 'service'">
|
||||||
|
<h1 class="title">{{ detailData.title }}</h1>
|
||||||
|
<p class="subtitle">{{ detailData.subtitle }}</p>
|
||||||
|
|
||||||
|
<div class="meta">
|
||||||
|
<div class="meta-item">
|
||||||
|
<span class="meta-label">发布时间:</span>
|
||||||
|
<span class="meta-value">{{ detailData.publish_time }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="meta-item">
|
||||||
|
<span class="meta-label">更新时间:</span>
|
||||||
|
<span class="meta-value">{{ detailData.update_time }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="meta-item" v-if="detailData.chief_editor">
|
||||||
|
<span class="meta-label">主编:</span>
|
||||||
|
<span class="meta-value">{{ detailData.chief_editor }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="cover" v-if="detailData.cover_url">
|
||||||
|
<img :src="detailData.cover_url" alt="封面图" class="cover-img">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="intro" v-if="detailData.intro">
|
||||||
|
<h3 class="section-title">简介</h3>
|
||||||
|
<p>{{ detailData.intro }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="body" v-html="detailData.content"></div>
|
||||||
|
|
||||||
|
<!-- 编辑信息(如有) -->
|
||||||
|
<div class="editors" v-if="hasEditorInfo">
|
||||||
|
<h3 class="section-title">编辑信息</h3>
|
||||||
|
<div class="editor-item">
|
||||||
|
<span class="editor-label">图文编辑:</span>
|
||||||
|
<span class="editor-value">{{ detailData.image_editors || '无' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="editor-item">
|
||||||
|
<span class="editor-label">文字编辑:</span>
|
||||||
|
<span class="editor-value">{{ detailData.text_editors || '无' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="editor-item">
|
||||||
|
<span class="editor-label">校对:</span>
|
||||||
|
<span class="editor-value">{{ detailData.proofreaders || '无' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="editor-item">
|
||||||
|
<span class="editor-label">审核:</span>
|
||||||
|
<span class="editor-value">{{ detailData.reviewers || '无' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 新闻详情(type=news) -->
|
||||||
|
<div class="content news-content" v-else-if="detailData && currentType === 'news'">
|
||||||
|
<h1 class="title">{{ detailData.title }}</h1>
|
||||||
|
|
||||||
|
<div class="meta">
|
||||||
|
<div class="meta-item">
|
||||||
|
<span class="meta-label">专题:</span>
|
||||||
|
<span class="meta-value">{{ detailData.topic || '未分类' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="meta-item">
|
||||||
|
<span class="meta-label">发布时间:</span>
|
||||||
|
<span class="meta-value">{{ detailData.create_at }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="meta-item">
|
||||||
|
<span class="meta-label">最后更新:</span>
|
||||||
|
<span class="meta-value">{{ detailData.update_at }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="cover" v-if="detailData.cover">
|
||||||
|
<img :src="detailData.cover" alt="新闻封面" class="cover-img">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 摘要(如有) -->
|
||||||
|
<div class="excerpt" v-if="detailData.excerpt">
|
||||||
|
<h3 class="section-title">摘要</h3>
|
||||||
|
<div v-html="detailData.excerpt" class="excerpt-content"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="body" v-html="detailData.content"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, ref, computed } from 'vue';
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { ElIcon, ElButton } from 'element-plus';
|
||||||
|
import { Loading, WarningFilled } from '@element-plus/icons-vue';
|
||||||
|
|
||||||
|
// 状态管理
|
||||||
|
const route = useRoute();
|
||||||
|
const isLoading = ref(false); // 加载状态
|
||||||
|
const errorMsg = ref(''); // 错误信息
|
||||||
|
const detailData = ref<any>(null); // 详情数据
|
||||||
|
const currentType = ref<string>(''); // 当前类型(case/service/news)
|
||||||
|
|
||||||
|
// 判断是否显示编辑信息(service类型专用)
|
||||||
|
const hasEditorInfo = computed(() => {
|
||||||
|
if (!detailData.value) return false;
|
||||||
|
const { image_editors, text_editors, proofreaders, reviewers } = detailData.value;
|
||||||
|
return !!image_editors || !!text_editors || !!proofreaders || !!reviewers;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 通用请求函数:根据type请求不同接口
|
||||||
|
const fetchDetail = async () => {
|
||||||
|
isLoading.value = true;
|
||||||
|
errorMsg.value = '';
|
||||||
|
detailData.value = null;
|
||||||
|
currentType.value = '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. 提取URL参数
|
||||||
|
const id = route.query.id;
|
||||||
|
const type = route.query.type;
|
||||||
|
|
||||||
|
// 2. 验证参数(支持case/service/news)
|
||||||
|
if (!id || !['case', 'service', 'news'].includes(type as string)) {
|
||||||
|
throw new Error('参数错误:缺少ID或类型不支持(仅支持case/service/news)');
|
||||||
|
}
|
||||||
|
currentType.value = type as string;
|
||||||
|
|
||||||
|
// 3. 根据类型请求对应接口
|
||||||
|
let apiUrl = '';
|
||||||
|
switch (type) {
|
||||||
|
case 'case':
|
||||||
|
apiUrl = `http://localhost:8080/api/teaching-cases/${id}`;
|
||||||
|
break;
|
||||||
|
case 'service':
|
||||||
|
apiUrl = `http://localhost:8080/api/social-service/${id}`;
|
||||||
|
break;
|
||||||
|
case 'news':
|
||||||
|
apiUrl = `http://localhost:8080/api/articles/${id}`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await axios.get(apiUrl, {
|
||||||
|
withCredentials: true // 修正:使用axios正确的跨域凭证配置
|
||||||
|
});
|
||||||
|
|
||||||
|
// 4. 处理不同接口的返回结构
|
||||||
|
switch (type) {
|
||||||
|
case 'case':
|
||||||
|
// case接口:数据直接在根节点
|
||||||
|
if (response.data && response.data.id) {
|
||||||
|
detailData.value = response.data;
|
||||||
|
} else {
|
||||||
|
throw new Error('案例数据格式异常');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'service':
|
||||||
|
// service接口:数据在data字段,且code=0
|
||||||
|
if (response.data && response.data.code === 0 && response.data.data) {
|
||||||
|
detailData.value = response.data.data;
|
||||||
|
} else {
|
||||||
|
throw new Error(`服务数据获取失败:${response.data?.message || '未知错误'}`);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'news':
|
||||||
|
// news接口:数据在article字段,且success=true
|
||||||
|
if (response.data && response.data.success && response.data.article) {
|
||||||
|
detailData.value = response.data.article;
|
||||||
|
} else {
|
||||||
|
throw new Error(`新闻数据获取失败:${response.data?.message || '接口返回异常'}`);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
errorMsg.value = err instanceof Error ? err.message : '加载失败,请稍后重试';
|
||||||
|
console.error('详情请求失败:', err);
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 页面挂载时加载数据
|
||||||
|
onMounted(() => {
|
||||||
|
fetchDetail();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* 基础样式 */
|
||||||
|
.detail-page {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 30px 20px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 加载状态 */
|
||||||
|
.loading {
|
||||||
|
text-align: center;
|
||||||
|
padding: 80px 0;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading .loading-icon {
|
||||||
|
font-size: 36px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
animation: spin 1.5s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading p {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 错误状态 */
|
||||||
|
.error {
|
||||||
|
text-align: center;
|
||||||
|
padding: 80px 0;
|
||||||
|
color: #ff4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error .error-icon {
|
||||||
|
font-size: 36px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error p {
|
||||||
|
font-size: 16px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error .el-button {
|
||||||
|
color: #165dff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 通用内容样式 */
|
||||||
|
.content {
|
||||||
|
line-height: 1.8;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
color: #1d2129;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
font-size: 18px;
|
||||||
|
color: #666;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 20px;
|
||||||
|
padding: 16px 0;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-item .meta-label {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover {
|
||||||
|
margin-bottom: 32px;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-img {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
max-height: 600px;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body p {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin: 20px auto;
|
||||||
|
display: block;
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.body a {
|
||||||
|
color: #165dff;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body a:hover {
|
||||||
|
color: #0e4bd8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 20px;
|
||||||
|
color: #1d2129;
|
||||||
|
margin: 32px 0 16px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
border-bottom: 2px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* service类型专用样式 */
|
||||||
|
.intro {
|
||||||
|
padding: 16px;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editors {
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-top: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-item {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-label {
|
||||||
|
display: inline-block;
|
||||||
|
width: 80px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* news类型专用样式 */
|
||||||
|
.excerpt {
|
||||||
|
padding: 16px;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
border-left: 4px solid #165dff;
|
||||||
|
border-radius: 0 8px 8px 0;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.excerpt-content {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式适配 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.detail-page {
|
||||||
|
padding: 20px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body {
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-label {
|
||||||
|
width: 70px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 加载动画 */
|
||||||
|
@keyframes spin {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user