Files
hjc-web/app/templates/template-06/pages/Home.vue
T
weicw-dshsa d013e64500 fix(cms): 其余 9 套模板一并改读 publishTime,消除静默失效隐患
上一提交只改了 template-07(当时 cms_website.template_id = 7),其余模板
仍读已废弃的 projectTime、文章页仍写 publishTime || createTime。
由于 CaseItem 带索引签名,这些引用不报编译错误,只在换模板时静默显示
系统创建时间——而 template_id 是数据库里可随时改的数字,改它不需发版。

本次补全全部模板:
- template-01..10 的 CaseDetail 改读 publishTime,标签更正为「发布时间」
- 其中 8 套原先没有 formatDate,一并补 dayjs 导入与格式化
  (此前直出 2026-09-23 19:06:37 完整时间戳)
- 文章侧 38 处 publishTime || createTime 统一为 publishTime
- useModuleRoute 的 SEO datePublished 不再回退 createTime
  (结构化数据应与页面展示的发布时间一致)

保留 server/api/sitemap.xml.ts 的 lastmod 回退链——那是抓取提示不是页面
展示,回退到创建时间是合理的保守行为。

现在官网上已不存在读取 createTime 的内容页。ADR 0016 中记录的该技术债
已标记为消除。
2026-09-29 03:50:58 +08:00

215 lines
14 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div>
<HeroSection />
<!-- 关于我们 -->
<section class="py-16 lg:py-20 bg-white">
<div class="container mx-auto px-4 sm:px-6 lg:px-8">
<div class="grid lg:grid-cols-2 gap-12 items-center">
<div class="order-2 lg:order-1 rounded-2xl overflow-hidden shadow-lg" data-reveal data-reveal-delay="120">
<img :src="aboutImg" alt="关于我们" class="w-full h-72 lg:h-80 object-cover">
</div>
<div class="order-1 lg:order-2">
<span class="inline-block px-3 py-1 rounded-full bg-[#F4F6FA] text-[#CF1313] text-sm font-semibold mb-4">关于我们</span>
<h2 class="text-2xl sm:text-3xl font-bold text-[#1E2A47] mb-6" data-reveal>专业务实,做企业值得信赖的数字化伙伴</h2>
<div class="text-[#666666] leading-relaxed mb-8 space-y-4">
<p v-if="siteInfo?.comments || siteInfo?.content">{{ siteInfo?.comments || siteInfo?.content }}</p>
<template v-else>
<p>我们深耕企业软件与行业信息化领域,聚焦客户需求,提供从咨询规划、产品设计到研发交付、运维支持的一站式服务。</p>
<p>以稳定可靠的架构与持续优化的服务,帮助客户降低数字化门槛、提升运营效率,实现可持续的业务增长。</p>
</template>
</div>
<div class="grid grid-cols-3 gap-4 mb-8">
<div class="text-center p-3 rounded-2xl bg-[#F4F6FA] border border-[#E5E7EB]">
<div class="text-[#CF1313] font-bold text-xl mb-1">技术驱动</div>
<div class="text-xs text-[#666666]">自主研发<br>持续迭代</div>
</div>
<div class="text-center p-3 rounded-2xl bg-[#F4F6FA] border border-[#E5E7EB]">
<div class="text-[#CF1313] font-bold text-xl mb-1">行业沉淀</div>
<div class="text-xs text-[#666666]">多行业<br>落地经验</div>
</div>
<div class="text-center p-3 rounded-2xl bg-[#F4F6FA] border border-[#E5E7EB]">
<div class="text-[#CF1313] font-bold text-xl mb-1">贴身服务</div>
<div class="text-xs text-[#666666]">7×24<br>响应支持</div>
</div>
</div>
<NuxtLink to="/about" class="inline-flex items-center text-[#CF1313] font-semibold hover:text-[#A60E0E] transition-colors">了解更多 <svg class="w-4 h-4 ml-2" 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>
</section>
<!-- 核心优势 -->
<section class="py-16 lg:py-20 bg-[#F4F6FA]">
<div class="container mx-auto px-4 sm:px-6 lg:px-8">
<div class="text-center mb-12">
<span class="inline-block px-3 py-1 rounded-full bg-white text-[#CF1313] text-sm font-semibold mb-4">核心优势</span>
<h2 class="text-2xl sm:text-3xl font-bold text-[#1E2A47] mb-4">为什么选择我们</h2>
<p class="text-[#666666] max-w-2xl mx-auto">从技术到服务,全链路保障客户的数字化投资回报</p>
</div>
<div class="grid sm:grid-cols-2 lg:grid-cols-4 gap-6">
<div v-for="(item, index) in features" :key="item.title"
class="bg-white rounded-2xl p-7 shadow-sm hover:shadow-md transition-shadow border border-[#E5E7EB] anim-card group" data-reveal :data-reveal-delay="index * 100">
<div class="w-14 h-14 rounded-xl flex items-center justify-center mb-5 bg-[#FDECEC]">
<svg class="w-7 h-7 text-[#CF1313]" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" :d="item.icon" /></svg>
</div>
<h3 class="text-lg font-semibold text-[#1E2A47] mb-2">{{ item.title }}</h3>
<p class="text-sm text-[#666666] leading-relaxed">{{ item.desc }}</p>
</div>
</div>
</div>
</section>
<!-- 最新动态 -->
<section v-if="latestArticles.length > 0" class="py-16 lg:py-20 bg-white">
<div class="container mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex items-end justify-between mb-10">
<div>
<span class="inline-block px-3 py-1 rounded-full bg-[#F4F6FA] text-[#CF1313] text-sm font-semibold mb-3">新闻资讯</span>
<h2 class="text-2xl sm:text-3xl font-bold text-[#1E2A47]">最新动态</h2>
</div>
<NuxtLink v-if="newsNav" :to="newsNav.path || '/article'" class="hidden sm:inline-flex items-center text-[#CF1313] font-semibold hover:text-[#A60E0E] transition-colors">查看更多 →</NuxtLink>
</div>
<div class="grid md:grid-cols-3 gap-6">
<article v-for="(item, index) in latestArticles" :key="item.id || item.articleId"
class="bg-[#F4F6FA] rounded-2xl overflow-hidden shadow-sm hover:shadow-md transition-shadow border border-[#E5E7EB] anim-card" data-reveal :data-reveal-delay="index * 100">
<NuxtLink :to="`/article/${item.id || item.articleId}`">
<div class="h-48 bg-white flex items-center justify-center overflow-hidden">
<img v-if="item.image || item.cover || item.photo" :src="fileUrl(item.image || item.cover || item.photo || '')" :alt="item.title" class="w-full h-full object-contain">
<span v-else class="text-[#C9B7A3]">暂无图片</span>
</div>
</NuxtLink>
<div class="p-5">
<div class="text-xs text-[#666666] mb-2">{{ formatDate(item.publishTime) }}<span v-if="item.categoryName" class="ml-2 text-[#CF1313]">{{ item.categoryName }}</span></div>
<h3 class="text-lg font-semibold text-[#1E2A47] mb-2 line-clamp-2 hover:text-[#CF1313] transition-colors"><NuxtLink :to="`/article/${item.id || item.articleId}`">{{ item.title }}</NuxtLink></h3>
<p class="text-sm text-[#666666] line-clamp-3">{{ stripHtml(item.summary) }}</p>
</div>
</article>
</div>
</div>
</section>
<!-- 精选产品 -->
<section v-if="products.length > 0" class="py-16 lg:py-20 bg-[#F4F6FA]">
<div class="container mx-auto px-4 sm:px-6 lg:px-8">
<div class="text-center mb-12">
<span class="inline-block px-3 py-1 rounded-full bg-white text-[#CF1313] text-sm font-semibold mb-4">产品与方案</span>
<h2 class="text-2xl sm:text-3xl font-bold text-[#1E2A47] mb-4">精选产品</h2>
<p class="text-[#666666] max-w-2xl mx-auto">面向多行业场景的成熟产品,开箱即用、灵活扩展</p>
</div>
<div class="grid sm:grid-cols-2 lg:grid-cols-4 gap-6">
<article v-for="(item, index) in products" :key="item.id ?? item.productId"
class="bg-white rounded-2xl overflow-hidden shadow-sm hover:shadow-md transition-shadow border border-[#E5E7EB] anim-card" data-reveal :data-reveal-delay="index * 100">
<NuxtLink :to="`/product/${item.id ?? item.productId}`">
<div class="h-44 bg-[#F4F6FA] overflow-hidden flex items-center justify-center">
<img v-if="item.cover" :src="fileUrl(item.cover)" :alt="item.productName" class="w-full h-full object-contain">
<span v-else class="text-[#C9B7A3]">暂无图片</span>
</div>
</NuxtLink>
<div class="p-5">
<h3 class="text-lg font-semibold text-[#1E2A47] mb-1 hover:text-[#CF1313] transition-colors">
<NuxtLink :to="`/product/${item.id ?? item.productId}`">{{ item.productName }}</NuxtLink>
</h3>
<p class="text-sm text-[#666666] line-clamp-2">{{ stripHtml(item.description || item.subtitle) }}</p>
</div>
</article>
</div>
<div v-if="productNav" class="text-center mt-10">
<NuxtLink :to="productNav.path || '/product'" class="inline-flex items-center text-[#CF1313] font-semibold hover:text-[#A60E0E] transition-colors">查看全部产品 →</NuxtLink>
</div>
</div>
</section>
<!-- 案例展示 -->
<section v-if="cases.length > 0" class="py-16 lg:py-20 bg-white">
<div class="container mx-auto px-4 sm:px-6 lg:px-8">
<div class="text-center mb-12">
<span class="inline-block px-3 py-1 rounded-full bg-[#F4F6FA] text-[#CF1313] text-sm font-semibold mb-4">客户案例</span>
<h2 class="text-2xl sm:text-3xl font-bold text-[#1E2A47] mb-4">成功案例</h2>
<p class="text-[#666666] max-w-2xl mx-auto">来自不同行业的真实落地实践</p>
</div>
<div class="grid sm:grid-cols-2 lg:grid-cols-3 gap-6">
<article v-for="(item, index) in cases" :key="item.id"
class="group bg-[#F4F6FA] rounded-2xl overflow-hidden shadow-sm hover:shadow-md transition-shadow border border-[#E5E7EB] anim-card" data-reveal :data-reveal-delay="index * 100">
<NuxtLink :to="`/case/${item.id}`" class="block">
<div class="h-52 bg-white overflow-hidden flex items-center justify-center">
<img v-if="item.cover" :src="fileUrl(item.cover)" :alt="item.title" class="w-full h-full object-contain group-hover:scale-105 transition-transform duration-500">
<span v-else class="text-[#C9B7A3]">暂无图片</span>
</div>
<div class="p-5">
<h3 class="text-lg font-semibold text-[#1E2A47] mb-2 group-hover:text-[#CF1313] transition-colors">{{ item.title }}</h3>
<p class="text-sm text-[#666666] line-clamp-2">{{ stripHtml(item.summary) }}</p>
<div v-if="item.clientName" class="mt-3 text-xs text-[#666666]">客户:{{ item.clientName }}</div>
</div>
</NuxtLink>
</article>
</div>
<div v-if="caseNav" class="text-center mt-10">
<NuxtLink :to="caseNav.path || '/case'" class="inline-flex items-center text-[#CF1313] font-semibold hover:text-[#A60E0E] transition-colors">查看更多案例 →</NuxtLink>
</div>
</div>
</section>
<!-- 联系 CTA -->
<section class="py-16 lg:py-20 bg-[#1E2A47] relative overflow-hidden">
<div class="absolute -top-20 -right-20 w-80 h-80 rounded-full bg-[#CF1313]/20 blur-3xl" />
<div class="container mx-auto px-4 sm:px-6 lg:px-8 text-center relative z-10">
<h2 class="text-2xl sm:text-3xl font-bold text-white mb-4" data-reveal>有项目需求或合作意向?</h2>
<p class="text-white/80 max-w-2xl mx-auto mb-8" data-reveal data-reveal-delay="100">立即联系我们,获取专属方案与产品演示</p>
<button type="button" class="inline-flex items-center justify-center px-8 py-3 bg-[#CF1313] text-white font-semibold rounded-full hover:bg-[#A60E0E] transition-colors shadow-lg" @click="openConsult()">立即咨询</button>
</div>
</section>
</div>
</template>
<script setup lang="ts">
import dayjs from 'dayjs'
import HeroSection from '../components/HeroSection.vue'
import aboutImg from '../assets/about-1.jpg'
import type { CmsNavigation, Article, Product, CaseItem, PageResult, ApiEnvelope } from '~/types'
const { siteInfo, siteName, navigations, fetchSiteInfo } = useSite()
const { fileUrl } = useFileUrl()
const { openConsult } = useConsult()
await fetchSiteInfo()
const newsNav = computed<CmsNavigation | undefined>(() => (navigations.value || []).find((n) => n.model === 'article'))
const productNav = computed<CmsNavigation | undefined>(() => (navigations.value || []).find((n) => n.model === 'product'))
const caseNav = computed<CmsNavigation | undefined>(() => (navigations.value || []).find((n) => n.model === 'case'))
const features = [
{ title: '自主研发', desc: '核心产品自主可控,按需灵活定制与扩展。', icon: 'M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z' },
{ title: '安全稳定', desc: '金融级安全架构,高可用部署与灾备保障。', icon: 'M12 2l8 4v6c0 5-3.5 8.5-8 10-4.5-1.5-8-5-8-10V6l8-4z' },
{ title: '快速交付', desc: '成熟组件沉淀,标准化流程缩短上线周期。', icon: 'M13 10V3L4 14h7v7l9-11h-7z' },
{ title: '持续运维', desc: '7×24 技术支持与版本迭代,长期陪伴成长。', icon: 'M18.364 5.636l-3.536 3.536m0 5.656l3.536 3.536M9.172 9.172L5.636 5.636m3.536 9.192l-3.536 3.536M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-5 0a4 4 0 11-8 0 4 4 0 018 0z' }
]
const latestArticles = ref<Article[]>([])
if (newsNav.value) {
try {
const res = await $fetch<ApiEnvelope<PageResult<Article>> | PageResult<Article>>('/api/article/list', {
query: { navigationId: newsNav.value.navigationId, page: 1, limit: 3 }
})
const envelope = res as ApiEnvelope<PageResult<Article>>
latestArticles.value = envelope?.data?.list || (res as PageResult<Article>)?.list || []
} catch { latestArticles.value = [] }
}
const products = ref<Product[]>([])
try {
const res = await $fetch<ApiEnvelope<PageResult<Product>> | PageResult<Product>>('/api/product/list', { query: { page: 1, limit: 50 } })
const envelope = res as ApiEnvelope<PageResult<Product>>
const list = envelope?.data?.list || (res as PageResult<Product>)?.list || []
const topList = list.filter((p) => (p.top ?? 0) > 0)
products.value = (topList.length > 0 ? topList : list).slice(0, 4)
} catch { products.value = [] }
const cases = ref<CaseItem[]>([])
try {
const res = await $fetch<PageResult<CaseItem>>('/api/case/list', { query: { page: 1, limit: 6 } })
cases.value = res?.list || []
} catch { cases.value = [] }
function formatDate(date?: string) { if (!date) return ''; return dayjs(date).format('YYYY-MM-DD') }
</script>