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,193 @@
<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/about-banner.jpg'); min-height: 300px;"
>
<div class="t10-container t10-banner-inner">
<nav class="t10-breadcrumb mb-3">
<NuxtLink to="/">首页</NuxtLink>
<span class="mx-2">/</span>
<span class="text-white">{{ newsNav?.title || '企业资讯' }}</span>
</nav>
<h1 class="text-3xl sm:text-4xl font-bold text-white mb-2">{{ pageTitle }}</h1>
<p class="text-lg text-white/80 tracking-[0.2em] uppercase">News</p>
</div>
</section>
<!-- 子导航 -->
<div class="border-b border-[var(--t10-border)] bg-white">
<div class="t10-container">
<div class="flex flex-wrap items-center justify-between py-4 gap-4">
<nav class="flex flex-wrap gap-6 text-sm lg:pl-[460px]">
<NuxtLink
v-for="link in subNavs"
:key="link.path"
:to="link.path"
class="text-[var(--t10-text-secondary)] hover:text-[var(--t10-primary)] transition-colors"
:class="{ '!text-[var(--t10-primary)] font-medium': route.path === link.path }"
>
{{ link.title }}
</NuxtLink>
</nav>
<nav class="text-xs text-[var(--t10-muted-2)]">
<NuxtLink to="/" class="hover:text-[var(--t10-primary)]">首页</NuxtLink>
<span class="mx-2">/</span>
<span class="text-[var(--t10-text)]">{{ pageTitle }}</span>
</nav>
</div>
</div>
</div>
<div class="t10-container py-10">
<h2 class="t10-section-title t10-section-title-center">{{ pageTitle }}</h2>
<div v-if="pending" class="flex justify-center py-12">
<SiteLoading />
</div>
<SiteError v-else-if="error" message="获取资讯列表失败" />
<div v-else class="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
<NuxtLink
v-for="item in newsList"
: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 mb-3 min-h-[48px]">
{{ item.title }}
</h3>
<p class="text-sm text-[var(--t10-muted-2)] t10-clamp-2 mb-4 min-h-[40px]">
{{ summaryOf(item) }}
</p>
<span class="text-xs text-[var(--t10-muted)]">{{ formatDate(item.publishTime || item.createTime) }}</span>
</div>
</NuxtLink>
</div>
<!-- 空状态 -->
<div v-if="!pending && !error && newsList.length === 0" class="text-center py-20 text-[var(--t10-muted-2)]">
暂无资讯内容
</div>
<!-- 分页 -->
<div v-if="totalPages > 1" class="flex justify-center mt-12">
<nav class="flex items-center gap-1.5 flex-wrap justify-center">
<button
v-for="p in totalPages"
:key="p"
class="min-w-[36px] h-9 px-2 text-sm border rounded transition-colors"
:class="p === currentPage
? 'bg-[var(--t10-primary)] border-[var(--t10-primary)] text-white'
: 'bg-white border-[var(--t10-border)] text-[var(--t10-text)] hover:border-[var(--t10-primary)] hover:text-[var(--t10-primary)]'"
@click="currentPage = p"
>
{{ p }}
</button>
</nav>
</div>
</div>
</div>
</template>
<script setup lang="ts">
/**
* 模板 10 - 新闻资讯列表页(创芯存储科技蓝风格)
*/
import dayjs from 'dayjs'
import type { Article, PageResult, ApiEnvelope, CmsNavigation } from '~/types'
import { getCompressedImageUrl } from '~/utils/image'
import { stripHtml } from '~/utils'
const { allNavigations, fetchSiteInfo } = useSite()
const route = useRoute()
await fetchSiteInfo()
const navigationId = computed<number | undefined>(() => {
const qid = route.query.navId as string
if (qid) {
const n = Number(qid)
if (!Number.isNaN(n)) return n
}
const pid = route.params.id as string
if (pid) {
const n = Number(pid)
if (!Number.isNaN(n)) return n
}
return undefined
})
const currentPage = ref(1)
const limit = 9
const { data, pending, error } = await useFetch<
ApiEnvelope<PageResult<Article>> | PageResult<Article>
>('/api/article/list', {
key: `article-list-${navigationId.value ?? 'all'}-p${currentPage.value}`,
query: {
page: currentPage,
limit,
...(navigationId.value ? { navigationId: navigationId.value } : {})
},
watch: [currentPage, navigationId]
})
const newsList = computed<Article[]>(() => {
const envelope = data.value as ApiEnvelope<PageResult<Article>>
const direct = data.value as PageResult<Article>
return envelope?.data?.list || direct?.list || []
})
const totalCount = computed(() => {
const envelope = data.value as ApiEnvelope<PageResult<Article>>
const direct = data.value as PageResult<Article>
return envelope?.data?.count || envelope?.data?.total || direct?.count || direct?.total || 0
})
const totalPages = computed(() => Math.max(1, Math.ceil(totalCount.value / limit)))
const newsNav = computed<CmsNavigation | undefined>(() => {
const navs = allNavigations.value || []
return navs.find((n) => n.navigationId === navigationId.value) || navs.find((n) => n.model === 'article')
})
const pageTitle = computed(() => newsNav.value?.title || '企业资讯')
const subNavs = [
{ title: '关于创芯', path: '/about' },
{ title: '新闻资讯', path: '/article' },
{ title: '联系我们', path: '/contact' }
]
const FALLBACK_IMAGE = '/images/template-10/news-default.jpg'
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
}
function onImgError(e: Event) {
const el = e.target as HTMLImageElement
if (el && !el.src.endsWith(FALLBACK_IMAGE)) el.src = FALLBACK_IMAGE
}
function summaryOf(item: Article): string {
const anyItem = item as Record<string, unknown>
const s = (anyItem.summary || anyItem.description || anyItem.content || '') as string
return stripHtml(s).slice(0, 100)
}
function formatDate(date?: unknown) {
if (!date) return ''
return dayjs(String(date)).format('YYYY-MM-DD')
}
</script>