feat(app): 添加多模板关于我们页面及相关路由和404页面

- 新增404页面,优化未找到页面体验,避免被搜索引擎索引
- 增加文件代理接口,隐藏真实文件服务器地址,支持文件请求代理
- 实现/article、/case、/product及/page动态路由兼容列表与详情展示
- 添加动态CMS页面兼容入口处理旧式路径,统一路由与SEO设置
- 新增模板1、模板7、模板2、模板3关于我们页面,实现多模板支持
- 模板增强支持CMS单页内容加载及SEO信息动态设置
- 配置环境变量及Git忽略文件规则辅助开发和构建环境管理
This commit is contained in:
2026-09-08 12:13:44 +08:00
commit 2b69686795
381 changed files with 59891 additions and 0 deletions
@@ -0,0 +1,155 @@
<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>