2b69686795
- 新增404页面,优化未找到页面体验,避免被搜索引擎索引 - 增加文件代理接口,隐藏真实文件服务器地址,支持文件请求代理 - 实现/article、/case、/product及/page动态路由兼容列表与详情展示 - 添加动态CMS页面兼容入口处理旧式路径,统一路由与SEO设置 - 新增模板1、模板7、模板2、模板3关于我们页面,实现多模板支持 - 模板增强支持CMS单页内容加载及SEO信息动态设置 - 配置环境变量及Git忽略文件规则辅助开发和构建环境管理
156 lines
6.1 KiB
Vue
156 lines
6.1 KiB
Vue
<template>
|
||
<div class="min-h-screen bg-[var(--t10-bg-soft)] pb-16">
|
||
<!-- Banner -->
|
||
<section
|
||
class="t10-page-banner"
|
||
style="background-image: url('/images/template-10/news-banner.jpg'); min-height: 280px;"
|
||
>
|
||
<div class="t10-container t10-banner-inner">
|
||
<nav class="t10-breadcrumb mb-3">
|
||
<NuxtLink to="/">首页</NuxtLink>
|
||
<span class="mx-2">/</span>
|
||
<NuxtLink :to="newsNav?.path || '/article'">{{ newsNav?.title || '企业资讯' }}</NuxtLink>
|
||
<span class="mx-2">/</span>
|
||
<span class="text-white">{{ article?.title || '资讯详情' }}</span>
|
||
</nav>
|
||
<h1 class="text-3xl sm:text-4xl font-bold text-white">{{ newsNav?.title || '企业资讯' }}</h1>
|
||
</div>
|
||
</section>
|
||
|
||
<div class="t10-container py-10">
|
||
<div v-if="pending" class="flex justify-center py-12">
|
||
<SiteLoading />
|
||
</div>
|
||
<SiteError v-else-if="error" message="获取资讯详情失败" />
|
||
|
||
<template v-else-if="article">
|
||
<div class="max-w-4xl mx-auto bg-white rounded-xl border border-[var(--t10-border)] overflow-hidden">
|
||
<img
|
||
v-if="coverImage"
|
||
:src="coverImage"
|
||
:alt="article.title"
|
||
class="w-full h-[240px] lg:h-[360px] object-cover"
|
||
@error="onImgError"
|
||
>
|
||
<div class="p-6 lg:p-10">
|
||
<h1 class="text-2xl lg:text-3xl font-bold text-[var(--t10-text-strong)] mb-4">
|
||
{{ article.title }}
|
||
</h1>
|
||
<div class="flex flex-wrap items-center gap-4 text-sm text-[var(--t10-muted-2)] mb-8 pb-6 border-b border-[var(--t10-border)]">
|
||
<span v-if="article.publishTime || article.createTime">
|
||
发布时间:{{ formatDate(article.publishTime || article.createTime) }}
|
||
</span>
|
||
<span v-if="article.author">作者:{{ article.author }}</span>
|
||
<span v-if="article.source">来源:{{ article.source }}</span>
|
||
</div>
|
||
<div class="prose max-w-none">
|
||
<RichText :content="article.content" />
|
||
</div>
|
||
<PageAttachments
|
||
v-if="article.attachments?.length"
|
||
:attachments="article.attachments"
|
||
class="mt-10"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 相关资讯 -->
|
||
<div v-if="relatedNews.length" class="mt-12">
|
||
<h2 class="t10-section-title t10-section-title-center">相关资讯</h2>
|
||
<div class="grid md:grid-cols-3 gap-6">
|
||
<NuxtLink
|
||
v-for="item in relatedNews"
|
||
:key="item.id ?? item.articleId"
|
||
:to="`/article/${item.id ?? item.articleId}`"
|
||
class="group bg-white rounded-xl border border-[var(--t10-border)] overflow-hidden hover:shadow-md hover:border-[var(--t10-primary)] transition-all"
|
||
>
|
||
<div class="aspect-[16/10] overflow-hidden bg-[var(--t10-bg-gray)]">
|
||
<img
|
||
:src="itemImage(item)"
|
||
:alt="item.title"
|
||
class="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105"
|
||
loading="lazy"
|
||
@error="onImgError"
|
||
>
|
||
</div>
|
||
<div class="p-5">
|
||
<h3 class="font-medium text-[var(--t10-text)] t10-clamp-2 group-hover:text-[var(--t10-primary)] transition-colors">
|
||
{{ item.title }}
|
||
</h3>
|
||
</div>
|
||
</NuxtLink>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<div v-else class="text-center py-20 text-[var(--t10-muted-2)]">
|
||
资讯不存在或已下架
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
/**
|
||
* 模板 10 - 新闻资讯详情页(创芯存储科技蓝风格)
|
||
*/
|
||
import dayjs from 'dayjs'
|
||
import type { Article, PageResult, ApiEnvelope } from '~/types'
|
||
import { getCompressedImageUrl } from '~/utils/image'
|
||
|
||
const { allNavigations, fetchSiteInfo } = useSite()
|
||
const route = useRoute()
|
||
|
||
await fetchSiteInfo()
|
||
|
||
const articleId = computed(() => route.params.id as string)
|
||
|
||
const { data: articleRes, pending, error } = await useFetch<{ data?: Article }>('/api/article/detail', {
|
||
key: `article-detail-${articleId.value}`,
|
||
query: { id: articleId.value }
|
||
})
|
||
|
||
const article = computed<Article | undefined>(() => articleRes.value?.data)
|
||
|
||
const newsNav = computed(() => {
|
||
const navs = allNavigations.value || []
|
||
return navs.find((n) => n.model === 'article')
|
||
})
|
||
|
||
const FALLBACK_IMAGE = '/images/template-10/news-default.jpg'
|
||
const coverImage = computed(() => {
|
||
const a = article.value as Record<string, unknown> | undefined
|
||
const raw = (a?.cover || a?.image || a?.photo || (Array.isArray(a?.images) ? (a.images as string[])[0] : '')) as string
|
||
return raw ? getCompressedImageUrl(raw, { width: 1200 }) : FALLBACK_IMAGE
|
||
})
|
||
function onImgError(e: Event) {
|
||
const el = e.target as HTMLImageElement
|
||
if (el && !el.src.endsWith(FALLBACK_IMAGE)) el.src = FALLBACK_IMAGE
|
||
}
|
||
function itemImage(item: Article): string {
|
||
const anyItem = item as Record<string, unknown>
|
||
const raw = (anyItem.image || anyItem.cover || anyItem.photo
|
||
|| (Array.isArray(anyItem.images) ? (anyItem.images as string[])[0] : '')) as string
|
||
return raw ? getCompressedImageUrl(raw, { width: 600 }) : FALLBACK_IMAGE
|
||
}
|
||
|
||
const { data: relatedRes } = await useFetch<ApiEnvelope<PageResult<Article>> | PageResult<Article>>('/api/article/list', {
|
||
key: `article-related-${articleId.value}`,
|
||
query: { page: 1, limit: 3 },
|
||
server: false
|
||
})
|
||
const relatedNews = computed<Article[]>(() => {
|
||
const d = relatedRes.value as (ApiEnvelope<PageResult<Article>> | PageResult<Article>) | null
|
||
if (!d) return []
|
||
const list = ((d as ApiEnvelope<PageResult<Article>>)?.data?.list
|
||
?? (d as PageResult<Article>)?.list
|
||
?? []) as Article[]
|
||
return list.filter((a) => String((a as Record<string, unknown>).id ?? (a as Record<string, unknown>).articleId) !== articleId.value).slice(0, 3)
|
||
})
|
||
|
||
function formatDate(date?: unknown) {
|
||
if (!date) return ''
|
||
return dayjs(String(date)).format('YYYY-MM-DD HH:mm')
|
||
}
|
||
</script>
|