frontend/src/views/components/articleDetail.vue
2026-01-27 18:02:04 +08:00

619 lines
15 KiB
Vue

<template>
<Header />
<div class="detail">
<div class="backimage"></div>
<div class="content">
<div class="content-left">
<div class="detail-header">
<h1>{{ article.title }}</h1>
<el-divider></el-divider>
<div class="detail-meta">
<div>
<i class="fa-regular fa-calendar"></i>
{{ article.publish_date }}
</div>
<div>
<i class="fa-solid fa-user-pen"></i>
{{ article.author }}
</div>
<div>
<i class="fa-solid fa-list"></i>
{{ article.catename }}
</div>
<div>
<i class="fa-regular fa-thumbs-up"></i>
{{ article.likes }}
</div>
<div>
<i class="fa-regular fa-eye"></i>
{{ article.views }}
</div>
</div>
</div>
<div class="detail-content" ref="contentRef">
<div v-html="article.content" class="content-html"></div>
</div>
<div class="theend">
<el-divider></el-divider>
<div class="the-end">The End</div>
<el-divider></el-divider>
</div>
<div class="endtool">
<div
style="
margin-bottom: 65px;
display: flex;
justify-content: center;
align-items: center;
gap: 40px;
"
>
<div class="tool-item" @click="handleShareClick">
<div class="tool-icon">
<i class="fa-solid fa-share-nodes"></i>
</div>
<span class="tool-text">分享</span>
</div>
<div class="tool-item" @click="handleLikeClick">
<div class="tool-icon">
<i
class="fa-regular fa-thumbs-up"
:class="{ liked: article.liked }"
></i>
</div>
<span class="tool-text">
{{ article.liked ? '取消点赞' : '点赞' }}
</span>
</div>
<div class="tool-item">
<div class="tool-icon">
<i class="fa-regular fa-comment"></i>
</div>
<span class="tool-text">评论</span>
</div>
</div>
</div>
<div class="article-nav">
<div
class="nav-item"
v-if="nextPreviousArticles.previous"
@click="goToArticle(nextPreviousArticles.previous.id)"
>
<div class="nav-label">
<i class="fa-solid fa-arrow-left"></i>
上一篇
</div>
<div class="nav-title">
{{ nextPreviousArticles.previous.title }}
</div>
</div>
<div
class="nav-item"
v-if="nextPreviousArticles.next"
@click="goToArticle(nextPreviousArticles.next.id)"
>
<div class="nav-label">
下一篇
<i class="fa-solid fa-arrow-right"></i>
</div>
<div class="nav-title">{{ nextPreviousArticles.next.title }}</div>
</div>
</div>
</div>
<div class="content-right">
<div class="card-item">
<div class="card-header">相关文章</div>
<el-divider></el-divider>
<div class="card-content">
<div
class="article-item"
v-for="item in relatedArticles"
:key="item.id"
>
<div class="thumb-img">
<img :src="imagePath(item.image)" alt="" />
</div>
<div class="article-title">{{ item.title }}</div>
</div>
</div>
</div>
</div>
</div>
</div>
<Footer />
</template>
<script lang="ts" setup>
import { ref, onMounted, nextTick, getCurrentInstance } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { addLike, reduceLike } from '@/api/article'
import Header from '@/views/components/header.vue'
import Footer from '@/views/components/footer.vue'
import { getKingdeeNewsDetail, getCompanyNewsDetail } from '@/api/newscenter'
const route = useRoute()
const article = ref<any>({})
const relatedArticles = ref<any[]>([])
const nextPreviousArticles = ref<{
previous?: { id: number; title: string }
next?: { id: number; title: string }
}>({})
const loading = ref(true)
const instance = getCurrentInstance()
const proxy = instance?.proxy as any
const router = useRouter()
const contentRef = ref<HTMLElement | null>(null)
const VITE_APP_API_URL = import.meta.env.VITE_APP_API_URL
// 处理代码块复制功能
const handleCopyCode = async (e: Event) => {
const target = e.target as HTMLElement
const preElement = target.closest('.code-block-wrapper')
if (!preElement) return
const codeElement = preElement.querySelector('code')
if (!codeElement) return
const code = codeElement.innerText
try {
await navigator.clipboard.writeText(code)
proxy.$message.success('复制成功')
} catch (err) {
proxy.$message.error('复制失败,请手动复制')
}
}
// 为代码块添加复制按钮
const initCodeBlocks = () => {
if (!contentRef.value) return
const preElements = contentRef.value.querySelectorAll('pre')
preElements.forEach((pre) => {
if (pre.parentElement?.classList.contains('code-block-wrapper')) return
const wrapper = document.createElement('div')
wrapper.className = 'code-block-wrapper'
pre.parentNode?.insertBefore(wrapper, pre)
wrapper.appendChild(pre)
const copyBtn = document.createElement('button')
copyBtn.className = 'copy-code-btn'
copyBtn.innerHTML = '<i class="fa-regular fa-copy"></i>'
copyBtn.title = '复制代码'
copyBtn.onclick = handleCopyCode
wrapper.appendChild(copyBtn)
})
}
//分享功能
const handleShareClick = async () => {
const url = window.location.href
try {
await navigator.clipboard.writeText(url)
proxy.$message.success('复制地址成功')
} catch (err) {
proxy.$message.error('复制失败,请手动复制地址')
}
}
//点赞增加
const handleLikeClick = async () => {
if (article.value.liked) {
await reduceLike(article.value.id)
article.value.likes--
article.value.liked = false
} else {
await addLike(article.value.id)
article.value.likes++
article.value.liked = true
}
}
//拼接接口图片路径
const imagePath = (path: string) => {
if (!path) {
return new URL('@/assets/images/noimage.png', import.meta.url).href
}
return VITE_APP_API_URL + path
}
const goToArticle = (id: number) => {
if (route.path.includes('companyNews')) {
router.push(`/companyNews/detail/${id}`)
} else if (route.path.includes('kingdeeNews')) {
router.push(`/kingdeeNews/detail/${id}`)
} else {
router.push(`/kingdeeNews/detail/${id}`)
}
}
onMounted(async () => {
const id = route.params.id
loading.value = true
try {
let res
const path = route.path
if (path.includes('companyNews')) {
res = await getCompanyNewsDetail(id as string)
} else if (path.includes('kingdeeNews')) {
res = await getKingdeeNewsDetail(id as string)
} else {
res = await getKingdeeNewsDetail(id as string)
}
if (res.code === 200) {
article.value = res.data
nextTick(() => {
initCodeBlocks()
})
if (res.data.relatedArticles && res.data.relatedArticles.list) {
relatedArticles.value = res.data.relatedArticles.list.map(
(item: any) => ({
...item,
catename: item.cate,
}),
)
}
if (res.data.nextPreviousArticles) {
nextPreviousArticles.value = res.data.nextPreviousArticles
}
}
} catch (err) {
console.error('获取文章详情失败:', err)
} finally {
loading.value = false
}
})
</script>
<style lang="scss" scoped>
.detail {
padding-top: 120px;
background-color: #f8f8f8;
padding-bottom: 120px;
.content {
width: 1200px;
margin: 0 auto;
display: flex;
gap: 20px;
position: relative;
z-index: 1;
.content-left {
width: 70%;
border-radius: 10px;
background-color: #fff;
opacity: 0.9;
.detail-header {
margin-bottom: 20px;
padding: 50px 50px 0 50px;
.detail-meta {
display: flex;
align-items: center;
gap: 20px;
color: #707070;
font-size: 14px;
padding-bottom: 20px;
}
}
.detail-content {
padding: 0 50px;
.content-html {
:deep(pre) {
position: relative;
padding-right: 50px;
}
}
/* 代码块包装器 */
:deep(.code-block-wrapper) {
position: relative;
border-radius: 12px;
overflow: hidden;
margin: 20px 0;
pre {
margin: 0;
}
}
/* 复制按钮 */
:deep(.copy-code-btn) {
position: absolute;
top: 10px;
right: 10px;
width: 25px;
height: 25px;
border: none;
border-radius: 6px;
background: rgba(255, 255, 255, 0.1);
color: rgba(255, 255, 255, 0.7);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
transition: all 0.2s ease;
z-index: 10;
&:hover {
background: rgba(255, 255, 255, 0.2);
color: #fff;
}
&:active {
transform: scale(0.95);
}
}
/* 代码样式 */
:deep(code) {
border-radius: 4px;
padding: 2px 6px;
font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
font-size: 13px;
color: #f06b6b;
}
/* 代码块样式 */
:deep(pre) {
background: linear-gradient(135deg, #1e1e2e 0%, #2d2d3f 100%);
border-radius: 12px;
padding: 10px;
overflow-x: auto;
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
code {
background-color: transparent;
padding: 0;
padding-left: 10px;
color: #cdd6f4;
font-size: 13px;
line-height: 1.7;
}
}
:deep(img) {
width: 100%;
height: auto;
max-width: 100%;
display: block;
}
:deep(p) {
font-size: 14px;
line-height: 25px;
color: #606266;
}
}
.theend {
display: flex;
justify-content: center;
align-items: center;
gap: 20px;
margin: 60px;
.the-end {
width: 200px;
text-align: center;
color: #dcdfe6;
}
}
.endtool {
padding: 65px;
.tool-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
cursor: pointer;
transition: transform 0.3s ease;
&:hover {
transform: translateY(-5px);
.tool-icon {
background-color: #ff6b6b;
box-shadow: 0 8px 20px rgba(64, 158, 255, 0.4);
}
}
&:active {
transform: translateY(-2px);
.tool-icon {
background-color: #337ecc;
box-shadow: 0 4px 10px rgba(64, 158, 255, 0.3);
transform: scale(0.95);
}
}
&:has(.liked) {
.tool-icon {
background-color: #ff6b6b;
box-shadow: 0 8px 20px rgba(255, 107, 107, 0.4);
}
.tool-text {
color: #ff6b6b;
}
}
.tool-icon {
width: 50px;
height: 50px;
border-radius: 50%;
background-color: #3973ff;
color: #fff;
display: flex;
justify-content: center;
align-items: center;
font-size: 20px;
box-shadow: 0 4px 15px rgba(64, 158, 255, 0.3);
transition: all 0.3s ease;
.fa-thumbs-up.liked {
color: #fff;
animation: likeAnimation 0.4s ease;
}
}
.tool-text {
font-size: 14px;
color: #606266;
font-weight: 500;
transition: color 0.3s ease;
}
&:hover .tool-text {
color: #3973ff;
}
}
}
.article-nav {
display: flex;
justify-content: space-between;
padding: 30px 65px;
border-top: 1px solid #f0f0f0;
.nav-item {
max-width: 45%;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
.nav-label {
color: #3973ff;
}
.nav-title {
color: #3973ff;
}
}
.nav-label {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
color: #909399;
margin-bottom: 10px;
transition: color 0.3s ease;
}
.nav-title {
font-size: 14px;
color: #303133;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
transition: color 0.3s ease;
}
}
}
}
.content-right {
width: 30%;
opacity: 0.9;
.card-item {
// position: sticky;
top: 100px;
padding: 30px;
border-radius: 10px;
background-color: #fff;
.card-header {
font-size: 18px;
font-weight: 500;
}
.card-content {
.article-item {
padding-bottom: 20px;
border-bottom: 1px solid #f0f0f0;
margin-bottom: 20px;
&:last-child {
padding-bottom: 0;
border-bottom: none;
margin-bottom: 0;
}
.thumb-img {
width: 100%;
overflow: hidden;
img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 10px;
}
}
.article-title {
font-size: 16px;
font-weight: 500;
color: #303133;
margin-top: 10px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
cursor: pointer;
&:hover {
color: #3973ff;
transition: color 0.3s ease;
}
}
}
}
}
}
}
}
.backimage {
width: 800px;
height: 400px;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
position: fixed;
right: 100px;
bottom: 100px;
pointer-events: none;
background-image: url('@/assets/images/sologen.png');
opacity: 0.2;
}
@keyframes likeAnimation {
0% {
transform: scale(1);
}
25% {
transform: scale(1.3);
}
50% {
transform: scale(0.9);
}
100% {
transform: scale(1);
}
}
</style>