2b69686795
- 新增404页面,优化未找到页面体验,避免被搜索引擎索引 - 增加文件代理接口,隐藏真实文件服务器地址,支持文件请求代理 - 实现/article、/case、/product及/page动态路由兼容列表与详情展示 - 添加动态CMS页面兼容入口处理旧式路径,统一路由与SEO设置 - 新增模板1、模板7、模板2、模板3关于我们页面,实现多模板支持 - 模板增强支持CMS单页内容加载及SEO信息动态设置 - 配置环境变量及Git忽略文件规则辅助开发和构建环境管理
145 lines
5.6 KiB
Vue
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>
|