Files
hjc-web/app/templates/template-10/pages/CaseList.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

239 lines
9.8 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: 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">{{ pageTitle }}</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">Solution</p>
</div>
</section>
<!-- 分类 Tab -->
<div class="border-b border-[var(--t10-border)] bg-white sticky top-[72px] z-30">
<div class="t10-container">
<div class="t10-tabs">
<button
v-for="tab in caseTabs"
:key="tab.navigationId"
class="t10-tab"
:class="{ 't10-tab-active': activeTab === tab.navigationId }"
@click="activeTab = tab.navigationId"
>
{{ tab.title }}
</button>
</div>
</div>
</div>
<div class="t10-container py-10">
<div v-if="pending" class="flex justify-center py-12">
<SiteLoading />
</div>
<SiteError v-else-if="error" message="获取方案列表失败" />
<!-- 方案交替列表 -->
<div v-else-if="displayCases.length" class="space-y-12">
<NuxtLink
v-for="(item, i) in displayCases"
:key="item.id ?? item.articleId"
:to="`/case/${item.id ?? item.articleId}`"
class="group grid md:grid-cols-2 gap-0 rounded-2xl overflow-hidden border border-[var(--t10-border)] hover:shadow-lg transition-all"
:class="i % 2 === 1 ? 'md:flex-row-reverse' : ''"
>
<div :class="['h-[240px] lg:h-[320px] overflow-hidden bg-[var(--t10-bg-gray)]', i % 2 === 1 ? 'md:order-2' : '']">
<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-8 lg:p-12 flex flex-col justify-center bg-white', i % 2 === 1 ? 'md:order-1' : '']">
<h3 class="text-xl lg:text-2xl font-bold text-[var(--t10-text-strong)] mb-4 group-hover:text-[var(--t10-primary)] transition-colors">
{{ item.title }}
</h3>
<p class="text-[var(--t10-text-secondary)] leading-relaxed mb-6">
{{ summaryOf(item) }}
</p>
<span class="inline-flex items-center text-[var(--t10-primary)] font-medium text-sm">
了解详情
<svg class="w-4 h-4 ml-1 transition-transform group-hover:translate-x-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</span>
</div>
</NuxtLink>
</div>
<!-- 兜底展示 -->
<div v-else class="space-y-12">
<div
v-for="(sol, i) in fallbackSolutions"
:key="i"
class="group grid md:grid-cols-2 gap-0 rounded-2xl overflow-hidden border border-[var(--t10-border)] hover:shadow-lg transition-all"
>
<div :class="['h-[240px] lg:h-[320px] overflow-hidden bg-[var(--t10-bg-gray)]', i % 2 === 1 ? 'md:order-2' : '']">
<img :src="sol.image" :alt="sol.title" class="w-full h-full object-cover">
</div>
<div :class="['p-8 lg:p-12 flex flex-col justify-center bg-white', i % 2 === 1 ? 'md:order-1' : '']">
<h3 class="text-xl lg:text-2xl font-bold text-[var(--t10-text-strong)] mb-4">{{ sol.title }}</h3>
<p class="text-[var(--t10-text-secondary)] leading-relaxed mb-6">{{ sol.desc }}</p>
<NuxtLink to="/contact" class="inline-flex items-center text-[var(--t10-primary)] font-medium text-sm">
了解详情
<svg class="w-4 h-4 ml-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</NuxtLink>
</div>
</div>
</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 type { CaseItem, PageResult, ApiEnvelope, CmsNavigation } from '~/types'
import { collectDescendantNavIds } from '~/utils/nav-tree'
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 categoryIds = computed<string | undefined>(() => {
const ids = collectDescendantNavIds(navigationId.value, allNavigations.value || [])
return ids.length ? ids.join(',') : undefined
})
const currentPage = ref(1)
const limit = 10
const { data, pending, error } = await useFetch<
ApiEnvelope<PageResult<CaseItem>> | PageResult<CaseItem>
>('/api/case/list', {
key: `case-list-${categoryIds.value ?? navigationId.value ?? 'all'}-p${currentPage.value}`,
query: {
page: currentPage,
limit,
...(categoryIds.value
? { categoryIds: categoryIds.value }
: (navigationId.value ? { navigationId: navigationId.value } : {}))
},
watch: [currentPage, categoryIds, navigationId]
})
const cases = computed<CaseItem[]>(() => {
const envelope = data.value as ApiEnvelope<PageResult<CaseItem>>
const direct = data.value as PageResult<CaseItem>
return envelope?.data?.list || direct?.list || []
})
const totalCount = computed(() => {
const envelope = data.value as ApiEnvelope<PageResult<CaseItem>>
const direct = data.value as PageResult<CaseItem>
return envelope?.data?.count || envelope?.data?.total || direct?.count || direct?.total || 0
})
const totalPages = computed(() => Math.max(1, Math.ceil(totalCount.value / limit)))
watch([categoryIds, navigationId], () => {
currentPage.value = 1
})
const caseNav = computed<CmsNavigation | undefined>(() => {
const navs = allNavigations.value || []
return navs.find((n) => n.navigationId === navigationId.value) || navs.find((n) => n.model === 'case')
})
const caseTabs = computed<CmsNavigation[]>(() => {
const parent = caseNav.value
if (parent?.children?.length) return parent.children.slice(0, 8)
return parent ? [parent] : []
})
const activeTab = ref<number | undefined>(caseTabs.value[0]?.navigationId)
watch(caseTabs, () => {
activeTab.value = caseTabs.value[0]?.navigationId
}, { immediate: true })
const displayCases = computed(() => {
if (!activeTab.value) return cases.value.slice(0, limit)
const ids = collectDescendantNavIds(activeTab.value, allNavigations.value || [])
const filterIds = ids.length ? ids : [activeTab.value]
return cases.value.filter((c) => {
const item = c as Record<string, unknown>
return filterIds.includes((item.navigationId || item.categoryId) as number)
}).slice(0, limit)
})
const pageTitle = computed(() => caseNav.value?.title || '行业解决方案')
const FALLBACK_IMAGE = '/images/template-10/solution-industrial.jpg'
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: 1000 }) : 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: CaseItem): string {
const anyItem = item as Record<string, unknown>
const s = (anyItem.summary || anyItem.description || anyItem.content || '') as string
return stripHtml(s).slice(0, 160)
}
const fallbackSolutions = [
{ title: '工业控制设备', desc: '工控主板、工业网关、PLC 设备,主打宽温稳定、7×24 小时连续运行、抗干扰,适配 DDR4/LPDDR/eMMC 稳定方案。', image: '/images/template-10/solution-industrial.jpg' },
{ title: '智能物联网终端', desc: '智能音箱、网关、安防摄像头、小体积、低功耗,提供轻量化物联网设备稳定可靠的存储配套。', image: '/images/template-10/solution-iot.jpg' },
{ title: '安防监控设备', desc: '支持高清录像、大容量存储、长时间录像回放,以 DDR4+eMMC 组合满足严苛场景。', image: '/images/template-10/solution-security.jpg' },
{ title: '消费&车载智能设备', desc: '车载中控、行车记录仪、智能电视、机顶盒,提供车规级和消费级稳定存储方案。', image: '/images/template-10/solution-vehicle.jpg' }
]
</script>