Files
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

397 lines
18 KiB
Vue

<template>
<div class="bg-white">
<HeroSection />
<!-- 公司简介 -->
<section class="py-14 lg:py-20 bg-white">
<div class="t10-container">
<h2 class="t10-section-title">{{ siteName || '深圳市创芯存储技术有限公司' }}</h2>
<div class="grid lg:grid-cols-2 gap-10 items-center">
<div class="space-y-6">
<div class="bg-[var(--t10-primary-faint)] border border-[var(--t10-primary-soft)] rounded-lg p-6">
<div class="inline-flex items-center px-3 py-1 rounded-full bg-[var(--t10-primary)] text-white text-sm font-medium mb-3">
企业定位
</div>
<p class="text-[var(--t10-text-secondary)] leading-relaxed">
{{ companyPosition || '专注DRAM与Flash存储芯片,为网络通讯、消费电子、AI、汽车电子、工业控制等高速增长领域提供高性能和高可靠性存储解决方案。' }}
</p>
</div>
<div class="bg-[var(--t10-primary-faint)] border border-[var(--t10-primary-soft)] rounded-lg p-6">
<div class="inline-flex items-center px-3 py-1 rounded-full bg-[var(--t10-primary)] text-white text-sm font-medium mb-3">
企业目标
</div>
<p class="text-[var(--t10-text-secondary)] leading-relaxed">
{{ companyGoal || '致力于通过稳定的产品、有竞争力的价格、持续的供应和超越客户期望的服务,为客户创造满意度、忠诚度和口碑的价值。' }}
</p>
</div>
</div>
<div class="relative rounded-xl overflow-hidden shadow-lg">
<img
:src="aboutImage"
alt="公司环境"
class="w-full h-[280px] lg:h-[360px] object-cover"
@error="onAboutImgError"
>
</div>
</div>
</div>
</section>
<!-- 产品中心 -->
<section class="py-14 lg:py-20 bg-[var(--t10-bg-soft)]">
<div class="t10-container">
<h2 class="t10-section-title t10-section-title-center">{{ productNav?.title || '产品中心' }}</h2>
<!-- 分类 Tab -->
<div v-if="productTabs.length" class="t10-tabs justify-center mb-10">
<button
v-for="tab in productTabs"
:key="tab.navigationId"
class="t10-tab"
:class="{ 't10-tab-active': activeProductTab === tab.navigationId }"
@click="activeProductTab = tab.navigationId"
>
{{ tab.title }}
</button>
</div>
<!-- 产品列表 -->
<div v-if="displayProducts.length" class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-5">
<NuxtLink
v-for="item in displayProducts"
:key="item.id ?? item.productId"
:to="`/product/${item.id ?? item.productId}`"
class="group bg-white rounded-lg border border-[var(--t10-border)] hover:border-[var(--t10-primary)] hover:shadow-md transition-all overflow-hidden"
>
<div class="aspect-square bg-[var(--t10-bg-gray)] overflow-hidden">
<img
:src="itemImage(item)"
:alt="item.productName"
class="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105"
loading="lazy"
@error="onImgError"
>
</div>
<div class="p-4">
<h3 class="text-sm font-medium text-[var(--t10-text)] t10-ellipsis group-hover:text-[var(--t10-primary)] transition-colors">
{{ item.productName }}
</h3>
<p class="mt-1 text-xs text-[var(--t10-muted-2)] t10-clamp-2">
{{ item.summary || item.description || '' }}
</p>
</div>
</NuxtLink>
</div>
<div v-else class="text-center py-16 text-[var(--t10-muted-2)]">
暂无产品内容
</div>
<div class="text-center mt-10">
<NuxtLink v-if="productNav" :to="getNavLink(productNav)" class="t10-btn-outline">
查看更多产品
</NuxtLink>
</div>
</div>
</section>
<!-- 核心优势 -->
<section class="py-14 lg:py-20 bg-white">
<div class="t10-container">
<h2 class="t10-section-title t10-section-title-center">核心优势</h2>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-5">
<div
v-for="(adv, i) in advantages"
:key="i"
class="relative bg-[var(--t10-bg-soft)] hover:bg-[var(--t10-primary)] rounded-lg p-6 transition-all group"
>
<div class="text-4xl font-bold text-[var(--t10-primary)] group-hover:text-white/90 mb-3">
{{ adv.num }}
</div>
<h3 class="text-lg font-semibold text-[var(--t10-text-strong)] group-hover:text-white mb-2">
{{ adv.title }}
</h3>
<p class="text-sm text-[var(--t10-text-secondary)] group-hover:text-white/85 leading-relaxed">
{{ adv.desc }}
</p>
</div>
</div>
</div>
</section>
<!-- 行业解决方案 -->
<section class="py-14 lg:py-20 bg-[var(--t10-bg-soft)]">
<div class="t10-container">
<h2 class="t10-section-title t10-section-title-center">{{ caseNav?.title || '行业解决方案' }}</h2>
<div v-if="caseList.length" class="grid md:grid-cols-2 gap-6">
<NuxtLink
v-for="item in caseList"
:key="item.id ?? item.articleId"
:to="detailLink(caseNav?.model, item)"
class="group relative overflow-hidden rounded-xl h-[220px] lg:h-[260px]"
>
<img
:src="itemImage(item)"
:alt="item.title"
class="absolute inset-0 w-full h-full object-cover transition-transform duration-500 group-hover:scale-105"
loading="lazy"
@error="onImgError"
>
<div class="absolute inset-0 bg-gradient-to-t from-black/75 via-black/30 to-transparent" />
<div class="absolute bottom-0 left-0 right-0 p-6 text-white">
<h3 class="text-lg font-semibold mb-1 group-hover:text-[var(--t10-primary-light)] transition-colors">
{{ item.title }}
</h3>
<p class="text-sm text-white/80 t10-clamp-2">
{{ summaryOf(item) }}
</p>
</div>
</NuxtLink>
</div>
<div v-else class="grid md:grid-cols-2 gap-6">
<div
v-for="(sol, i) in fallbackSolutions"
:key="i"
class="group relative overflow-hidden rounded-xl h-[220px] lg:h-[260px]"
>
<img :src="sol.image" :alt="sol.title" class="absolute inset-0 w-full h-full object-cover transition-transform duration-500 group-hover:scale-105">
<div class="absolute inset-0 bg-gradient-to-t from-black/75 via-black/30 to-transparent" />
<div class="absolute bottom-0 left-0 right-0 p-6 text-white">
<h3 class="text-lg font-semibold mb-1">{{ sol.title }}</h3>
<p class="text-sm text-white/80 t10-clamp-2">{{ sol.desc }}</p>
</div>
</div>
</div>
</div>
</section>
<!-- 为什么选择创芯 -->
<section
class="relative py-16 lg:py-24 bg-cover bg-center"
style="background-image: url('/images/template-10/why-choose-bg.jpg');"
>
<div class="absolute inset-0 bg-gradient-to-r from-[#0047BA]/92 to-[#0052D9]/75" />
<div class="t10-container relative z-10">
<div class="max-w-3xl mx-auto text-center text-white">
<h2 class="text-2xl lg:text-3xl font-bold mb-5">为什么选择创芯</h2>
<p class="text-white/90 leading-relaxed mb-8 bg-white/10 rounded-xl p-6 backdrop-blur-sm">
{{ whyChooseText || '多年存储行业积淀,掌握嵌入式存储核心器件与方案设计能力,从选型推荐、样品测试到批量交付,提供全生命周期技术支持,助力客户缩短开发周期、降低供应链风险。' }}
</p>
<div class="flex flex-wrap items-center justify-center gap-4">
<NuxtLink v-if="contactNav" :to="getNavLink(contactNav)" class="t10-btn-primary bg-white text-[var(--t10-primary)] hover:bg-white/90">
在线咨询
</NuxtLink>
<button type="button" class="t10-btn-outline border-white text-white hover:bg-white hover:text-[var(--t10-primary)]" @click="openConsult">
样品申请
</button>
<NuxtLink v-if="productNav" :to="getNavLink(productNav)" class="t10-btn-outline border-white text-white hover:bg-white hover:text-[var(--t10-primary)]">
资料下载
</NuxtLink>
</div>
</div>
</div>
</section>
<!-- 新闻中心 -->
<section v-if="newsList.length" class="py-14 lg:py-20 bg-white">
<div class="t10-container">
<h2 class="t10-section-title t10-section-title-center">{{ newsNav?.title || '企业资讯' }}</h2>
<div class="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
<NuxtLink
v-for="item in newsList.slice(0, 6)"
:key="item.id ?? item.articleId"
:to="detailLink(newsNav?.model, item)"
class="group bg-white rounded-xl border border-[var(--t10-border)] overflow-hidden 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-clamp-2 group-hover:text-[var(--t10-primary)] transition-colors mb-3">
{{ item.title }}
</h3>
<p class="text-sm text-[var(--t10-muted-2)] t10-clamp-2 mb-4">
{{ summaryOf(item) }}
</p>
<span class="text-xs text-[var(--t10-muted)]">{{ formatDate(item.publishTime || item.createTime) }}</span>
</div>
</NuxtLink>
</div>
<div v-if="newsNav" class="text-center mt-10">
<NuxtLink :to="getNavLink(newsNav)" class="t10-btn-outline">
查看更多资讯
</NuxtLink>
</div>
</div>
</section>
</div>
</template>
<script setup lang="ts">
/**
* 模板 10 - 首页(创芯存储科技蓝风格)
* 板块:Hero → 公司简介 → 产品中心 → 核心优势 → 行业解决方案 → 为什么选择创芯 → 新闻中心
*/
import dayjs from 'dayjs'
import HeroSection from '../components/HeroSection.vue'
import type { CmsNavigation, Article, Product, CaseItem, PageResult, ApiEnvelope } from '~/types'
import { collectDescendantNavIds } from '~/utils/nav-tree'
import { getNavLink, stripHtml } from '~/utils'
import { getCompressedImageUrl } from '~/utils/image'
type ContentItem = Article | Product | CaseItem
const { siteInfo, siteName, navigations, allNavigations, fetchSiteInfo } = useSite()
const { openConsult } = useConsult()
await fetchSiteInfo()
const topNavs = computed<CmsNavigation[]>(() => {
const primary = (navigations.value || []).filter((n) => (n.parentId || 0) === 0)
if (primary.length) return primary
return (allNavigations.value || []).filter((n) => (n.parentId || 0) === 0)
})
const productNav = computed<CmsNavigation | undefined>(() => topNavs.value.find((n) => n.model === 'product'))
const caseNav = computed<CmsNavigation | undefined>(() => topNavs.value.find((n) => n.model === 'case'))
const newsNav = computed<CmsNavigation | undefined>(() => topNavs.value.find((n) => n.model === 'article'))
const contactNav = computed<CmsNavigation | undefined>(() => topNavs.value.find((n) => n.model === 'contact' || n.title.includes('联系')))
async function fetchList(nav: CmsNavigation | undefined, limit: number): Promise<ContentItem[]> {
if (!nav) return []
const api = nav.model === 'product'
? '/api/product/list'
: nav.model === 'case'
? '/api/case/list'
: '/api/article/list'
const ids = collectDescendantNavIds(nav.navigationId, allNavigations.value || [])
const load = async (categoryIds?: string): Promise<ContentItem[]> => {
try {
const res = await $fetch<ApiEnvelope<PageResult<ContentItem>> | PageResult<ContentItem>>(api, {
query: { categoryIds, page: 1, limit }
})
return ((res as ApiEnvelope<PageResult<ContentItem>>)?.data?.list
?? (res as PageResult<ContentItem>)?.list
?? []) as ContentItem[]
} catch {
return []
}
}
const scoped = ids.length ? ids.join(',') : undefined
const list = await load(scoped)
if (list.length || !scoped) return list
return load(undefined)
}
const [products, cases, articles] = await Promise.all([
fetchList(productNav.value, 12),
fetchList(caseNav.value, 8),
fetchList(newsNav.value, 6)
])
// 产品分类 Tab
const productTabs = computed<CmsNavigation[]>(() => {
const children = productNav.value?.children || []
if (children.length) return children.slice(0, 6)
return productNav.value ? [productNav.value] : []
})
const activeProductTab = ref<number | undefined>(productTabs.value[0]?.navigationId)
const displayProducts = computed<Product[]>(() => {
if (!activeProductTab.value) return products.slice(0, 8) as Product[]
const ids = collectDescendantNavIds(activeProductTab.value, allNavigations.value || [])
const filterIds = ids.length ? ids : [activeProductTab.value]
return (products as Product[]).filter((p: Product) => {
const item = p as Record<string, unknown>
const navId = (item.navigationId || item.categoryId) as number | undefined
return navId ? filterIds.includes(navId) : false
}).slice(0, 8)
})
watch(productTabs, () => {
activeProductTab.value = productTabs.value[0]?.navigationId
}, { immediate: true })
const caseList = computed<CaseItem[]>(() => (cases as CaseItem[]).slice(0, 4))
const newsList = computed<Article[]>(() => (articles as Article[]).slice(0, 6))
const companyPosition = computed(() => {
const cfg = siteInfo.value?.config as Record<string, unknown> | undefined
return cfg?.position as string | undefined
})
const companyGoal = computed(() => {
const cfg = siteInfo.value?.config as Record<string, unknown> | undefined
return cfg?.goal as string | undefined
})
const whyChooseText = computed(() => {
const cfg = siteInfo.value?.config as Record<string, unknown> | undefined
return cfg?.whyChoose as string | undefined
})
const aboutImage = computed(() => {
const cfg = siteInfo.value?.config as Record<string, unknown> | undefined
const raw = cfg?.aboutImage as string | undefined
return raw || '/images/template-10/about-building.jpg'
})
const advantages = [
{ num: '04', title: '灵活供货,交期可控', desc: '多仓备货与柔性产能,快速响应交付需求。' },
{ num: '05', title: '全程技术贴身服务', desc: '从选型到量产全链路技术支持,快速响应。' },
{ num: '01', title: '全系主流品类覆盖', desc: 'DDR3/DDR4/LPDDR/eMMC 全系列存储产品。' },
{ num: '02', title: '平台高兼容,落地零门槛', desc: '主流平台完整验证,缩短客户开发周期。' },
{ num: '03', title: '严选品质,可稳定量产', desc: '严格测试与制程管控,保障批量一致性。' }
]
const fallbackSolutions = [
{ title: '工业控制设备', desc: '工控主板、工业网关、PLC 设备,主打宽温稳定、7×24 小时连续运行。', image: '/images/template-10/solution-industrial.jpg' },
{ title: '智能物联网终端', desc: '智能家电、网关、安防摄像头、可穿戴等轻量化物联网设备方案。', image: '/images/template-10/solution-iot.jpg' },
{ title: '安防监控设备', desc: '支持高清录像、大容量存储、长时间录像回放,保障数据安全可靠。', image: '/images/template-10/solution-security.jpg' },
{ title: '消费&车载智能设备', desc: '车载中控、行车记录仪、智能电视、机顶盒等稳定存储方案。', image: '/images/template-10/solution-vehicle.jpg' }
]
const FALLBACK_IMAGE = '/images/template-10/news-default.jpg'
function itemImage(item: ContentItem): string {
const anyItem = item as Record<string, unknown>
const raw = (anyItem.image || anyItem.cover || anyItem.photo
|| (Array.isArray(anyItem.images) ? (anyItem.images as string[])[0] : '')) as string
if (!raw) return FALLBACK_IMAGE
return getCompressedImageUrl(raw, { width: 800 })
}
function onImgError(e: Event) {
const el = e.target as HTMLImageElement
if (el && !el.src.endsWith(FALLBACK_IMAGE)) el.src = FALLBACK_IMAGE
}
function onAboutImgError(e: Event) {
const el = e.target as HTMLImageElement
el.src = '/images/template-10/about-building.jpg'
}
function summaryOf(item: ContentItem): string {
const anyItem = item as Record<string, unknown>
const s = (anyItem.summary || anyItem.description || anyItem.content || '') as string
return stripHtml(s).slice(0, 120)
}
function detailLink(model: string | undefined, item: ContentItem): string {
const anyItem = item as Record<string, unknown>
const id = anyItem.id || anyItem.articleId || anyItem.productId
if (model === 'product') return `/product/${id}`
if (model === 'case') return `/case/${id}`
return `/article/${id}`
}
function formatDate(date?: unknown) {
if (!date) return ''
return dayjs(String(date)).format('YYYY-MM-DD')
}
</script>