Files
hjc-web/app/templates/template-10/pages/CaseDetail.vue
T
gxwebsoft 2b69686795 feat(app): 添加多模板关于我们页面及相关路由和404页面
- 新增404页面,优化未找到页面体验,避免被搜索引擎索引
- 增加文件代理接口,隐藏真实文件服务器地址,支持文件请求代理
- 实现/article、/case、/product及/page动态路由兼容列表与详情展示
- 添加动态CMS页面兼容入口处理旧式路径,统一路由与SEO设置
- 新增模板1、模板7、模板2、模板3关于我们页面,实现多模板支持
- 模板增强支持CMS单页内容加载及SEO信息动态设置
- 配置环境变量及Git忽略文件规则辅助开发和构建环境管理
2026-09-08 12:13:44 +08:00

145 lines
5.6 KiB
Vue

<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>