feat(app): 添加多模板关于我们页面及相关路由和404页面
- 新增404页面,优化未找到页面体验,避免被搜索引擎索引 - 增加文件代理接口,隐藏真实文件服务器地址,支持文件请求代理 - 实现/article、/case、/product及/page动态路由兼容列表与详情展示 - 添加动态CMS页面兼容入口处理旧式路径,统一路由与SEO设置 - 新增模板1、模板7、模板2、模板3关于我们页面,实现多模板支持 - 模板增强支持CMS单页内容加载及SEO信息动态设置 - 配置环境变量及Git忽略文件规则辅助开发和构建环境管理
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-white pb-16">
|
||||
<!-- Banner -->
|
||||
<section
|
||||
class="t10-page-banner"
|
||||
style="background-image: url('/images/template-10/solution-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="caseNav?.path || '/case'">{{ caseNav?.title || '行业解决方案' }}</NuxtLink>
|
||||
<span class="mx-2">/</span>
|
||||
<span class="text-white">{{ caseItem?.title || '方案详情' }}</span>
|
||||
</nav>
|
||||
<h1 class="text-3xl sm:text-4xl font-bold text-white">{{ caseNav?.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="caseItem">
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<div class="rounded-2xl overflow-hidden mb-10">
|
||||
<img
|
||||
:src="coverImage"
|
||||
:alt="caseItem.title"
|
||||
class="w-full h-[260px] lg:h-[400px] object-cover"
|
||||
@error="onImgError"
|
||||
>
|
||||
</div>
|
||||
<h1 class="text-2xl lg:text-3xl font-bold text-[var(--t10-text-strong)] mb-6">
|
||||
{{ caseItem.title }}
|
||||
</h1>
|
||||
<div v-if="caseItem.summary" class="text-[var(--t10-text-secondary)] leading-relaxed mb-8 p-6 bg-[var(--t10-bg-soft)] rounded-xl border-l-4 border-[var(--t10-primary)]">
|
||||
{{ caseItem.summary }}
|
||||
</div>
|
||||
<div class="prose max-w-none">
|
||||
<RichText :content="caseItem.content" />
|
||||
</div>
|
||||
<PageAttachments
|
||||
v-if="caseItem.attachments?.length"
|
||||
:attachments="caseItem.attachments"
|
||||
class="mt-10"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 相关方案 -->
|
||||
<div v-if="relatedCases.length" class="mt-16">
|
||||
<h2 class="t10-section-title t10-section-title-center">相关方案</h2>
|
||||
<div class="grid md:grid-cols-3 gap-6">
|
||||
<NuxtLink
|
||||
v-for="item in relatedCases"
|
||||
:key="item.id ?? item.articleId"
|
||||
:to="`/case/${item.id ?? item.articleId}`"
|
||||
class="group rounded-xl overflow-hidden border border-[var(--t10-border)] hover:border-[var(--t10-primary)] hover:shadow-md 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-ellipsis 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 type { CaseItem, PageResult, ApiEnvelope } from '~/types'
|
||||
import { getCompressedImageUrl } from '~/utils/image'
|
||||
|
||||
const { allNavigations, fetchSiteInfo } = useSite()
|
||||
const route = useRoute()
|
||||
|
||||
await fetchSiteInfo()
|
||||
|
||||
const caseId = computed(() => route.params.id as string)
|
||||
|
||||
const { data: caseRes, pending, error } = await useFetch<{ data?: CaseItem }>('/api/case/detail', {
|
||||
key: `case-detail-${caseId.value}`,
|
||||
query: { id: caseId.value }
|
||||
})
|
||||
|
||||
const caseItem = computed<CaseItem | undefined>(() => caseRes.value?.data)
|
||||
|
||||
const caseNav = computed(() => {
|
||||
const navs = allNavigations.value || []
|
||||
return navs.find((n) => n.model === 'case')
|
||||
})
|
||||
|
||||
const FALLBACK_IMAGE = '/images/template-10/solution-industrial.jpg'
|
||||
const coverImage = computed(() => {
|
||||
const c = caseItem.value as Record<string, unknown> | undefined
|
||||
const raw = (c?.cover || c?.image || c?.photo || (Array.isArray(c?.images) ? (c.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: CaseItem): string {
|
||||
const anyItem = item as Record<string, unknown>
|
||||
const raw = (anyItem.cover || anyItem.image || 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<CaseItem>> | PageResult<CaseItem>>('/api/case/list', {
|
||||
key: `case-related-${caseId.value}`,
|
||||
query: { page: 1, limit: 3 },
|
||||
server: false
|
||||
})
|
||||
const relatedCases = computed<CaseItem[]>(() => {
|
||||
const d = relatedRes.value as (ApiEnvelope<PageResult<CaseItem>> | PageResult<CaseItem>) | null
|
||||
if (!d) return []
|
||||
const list = ((d as ApiEnvelope<PageResult<CaseItem>>)?.data?.list
|
||||
?? (d as PageResult<CaseItem>)?.list
|
||||
?? []) as CaseItem[]
|
||||
return list.filter((c) => String((c as Record<string, unknown>).id ?? (c as Record<string, unknown>).articleId) !== caseId.value).slice(0, 3)
|
||||
})
|
||||
</script>
|
||||
Reference in New Issue
Block a user