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