Files
hjc-web/app/templates/template-05/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

275 lines
12 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 />
<FeatureSection />
<!-- 关于品牌 -->
<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="bg-gradient-to-br from-[#FFF1E6] to-[#FEF3DA] rounded-[2rem] h-64 lg:h-80 overflow-hidden" data-reveal data-reveal-delay="120">
<img
:src="brandStoryImg"
alt="品牌故事"
class="w-full h-full object-cover"
>
</div>
<div>
<span class="inline-block px-3 py-1 rounded-full bg-[#FFF1E6] text-[#E5472E] text-sm font-semibold mb-4">关于我们</span>
<h2 class="text-2xl sm:text-3xl font-bold text-[#33291F] mb-6" data-reveal>
专注好产品,传递自然好滋味
</h2>
<div class="text-[#8A7B6B] leading-relaxed mb-8 space-y-4">
<p v-if="siteInfo?.comments || siteInfo?.content" class="text-base">
{{ siteInfo?.comments || '坚守古法慢酿,还原十万大山原生山黄皮本味' }}
</p>
<template v-else>
<p class="text-base">
我们深耕山黄皮(鸡皮果)领域多年,从广西十万大山深处甄选每一颗果实,坚持传统古法腌制与现代工艺相结合,只为锁住那份来自山野的纯粹滋味。
</p>
<p class="text-base">
从原料种植到成品出库,每一道工序都严格把控——无添加、零防腐、纯天然发酵,让健康与美味在时光中自然沉淀。我们相信,真正的好味道,不需要太多修饰。
</p>
</template>
</div>
<!-- 品牌亮点 -->
<div class="grid grid-cols-3 gap-4 mb-8">
<div class="text-center p-3 rounded-2xl bg-[#FFF8F0] border border-[#F0E2D5]">
<div class="text-[#E5472E] font-bold text-xl mb-1">源头直采</div>
<div class="text-xs text-[#8A7B6B]">十万大山<br>核心产区</div>
</div>
<div class="text-center p-3 rounded-2xl bg-[#FFF8F0] border border-[#F0E2D5]">
<div class="text-[#E5472E] font-bold text-xl mb-1">传统工艺</div>
<div class="text-xs text-[#8A7B6B]">古法腌制<br>自然发酵</div>
</div>
<div class="text-center p-3 rounded-2xl bg-[#FFF8F0] border border-[#F0E2D5]">
<div class="text-[#E5472E] font-bold text-xl mb-1">零添加</div>
<div class="text-xs text-[#8A7B6B]">无防腐剂<br>纯天然</div>
</div>
</div>
<NuxtLink
v-if="aboutNav"
:to="aboutNav.path || '/about'"
class="inline-flex items-center text-[#E5472E] font-semibold hover:text-[#C53A22] 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 v-if="products.length > 0" class="py-16 lg:py-20 bg-[#FFF8F0]">
<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-[#EAF5E1] text-[#5BA53A] text-sm font-semibold mb-4">产品系列</span>
<h2 class="text-2xl sm:text-3xl font-bold text-[#33291F] mb-4" data-reveal>明星产品</h2>
<p class="text-[#8A7B6B] max-w-2xl mx-auto" data-reveal data-reveal-delay="100">严选人气好物,把自然美味装进每一份日常</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-3xl overflow-hidden shadow-sm hover:shadow-md transition-shadow border border-[#F0E2D5] anim-card"
data-reveal
:data-reveal-delay="index * 100"
>
<NuxtLink :to="`/product/${item.id ?? item.productId}`">
<div class="aspect-[4/3] bg-[#FFF1E6] overflow-hidden rounded-t-3xl anim-card-media">
<a-image
v-if="item.cover"
:src="fileUrl(item.cover)"
:alt="item.productName"
:preview="false"
class="w-full h-full"
:style="{ objectFit: 'cover' }"
/>
<div v-else class="w-full h-full flex items-center justify-center text-[#C9B7A3]">暂无图片</div>
</div>
</NuxtLink>
<div class="p-5">
<h3 class="text-lg font-semibold text-[#33291F] mb-1 hover:text-[#E5472E] transition-colors">
<NuxtLink :to="`/product/${item.id ?? item.productId}`">{{ item.productName }}</NuxtLink>
</h3>
<p class="text-sm text-[#8A7B6B] 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 || '/products'"
class="inline-flex items-center text-[#E5472E] font-semibold hover:text-[#C53A22] 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>
</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="text-center mb-12">
<span class="inline-block px-3 py-1 rounded-full bg-[#FEF3DA] text-[#F2A900] text-sm font-semibold mb-4">品牌动态</span>
<h2 class="text-2xl sm:text-3xl font-bold text-[#33291F] mb-4" data-reveal>最新资讯</h2>
<p class="text-[#8A7B6B] max-w-2xl mx-auto" data-reveal data-reveal-delay="100">了解品牌最新资讯与行业动态</p>
</div>
<div class="grid md:grid-cols-3 gap-6">
<article
v-for="(item, index) in latestArticles"
:key="item.id || item.articleId"
class="bg-[#FFF8F0] rounded-3xl overflow-hidden shadow-sm hover:shadow-md transition-shadow border border-[#F0E2D5] anim-card"
data-reveal
:data-reveal-delay="index * 100"
>
<NuxtLink :to="`/article/${item.id || item.articleId}`">
<div class="aspect-video bg-[#FFF1E6] flex items-center justify-center overflow-hidden anim-card-media">
<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-[#8A7B6B] mb-2">
{{ formatDate(item.publishTime) }}
<span v-if="item.categoryName" class="ml-2 text-[#E5472E]">{{ item.categoryName }}</span>
</div>
<h3 class="text-lg font-semibold text-[#33291F] mb-2 line-clamp-2 hover:text-[#E5472E] transition-colors">
<NuxtLink :to="`/article/${item.id || item.articleId}`">{{ item.title }}</NuxtLink>
</h3>
<p class="text-sm text-[#8A7B6B] line-clamp-2">{{ stripHtml(item.summary) }}</p>
</div>
</article>
</div>
<div v-if="newsNav" class="text-center mt-10">
<NuxtLink
:to="newsNav.path || '/news'"
class="inline-flex items-center text-[#E5472E] font-semibold hover:text-[#C53A22] 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>
</section>
<!-- 联系我们 CTA -->
<section class="py-16 lg:py-20 bg-gradient-to-br from-[#E5472E] to-[#F2A900] text-white">
<div class="container mx-auto px-4 sm:px-6 lg:px-8 text-center">
<h2 class="text-2xl sm:text-3xl font-bold mb-4" data-reveal>
想了解更多产品?
</h2>
<p class="text-white/90 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-white text-[#E5472E] font-semibold rounded-full hover:bg-[#FFF1E6] transition-colors shadow-lg anim-shimmer"
@click="openConsult()"
>
立即咨询
</button>
</div>
</section>
</div>
</template>
<script setup lang="ts">
/**
* 模板 5 - 首页(温暖自然食品风)
* 复用 getSiteInfo 站点信息 + cms-article / cms-product 数据
*/
import dayjs from 'dayjs'
import HeroSection from '../components/HeroSection.vue'
import FeatureSection from '../components/FeatureSection.vue'
import brandStoryImg from '../assets/brand-story.jpg'
import type { CmsNavigation, Article, Product, PageResult, ApiEnvelope } from '~/types'
const { siteInfo, siteName, navigations, fetchSiteInfo } = useSite()
const { fileUrl } = useFileUrl()
const { openConsult } = useConsult()
// 获取站点信息(含导航)
await fetchSiteInfo()
/** 从导航中找到"关于我们"相关的导航项 */
const aboutNav = computed<CmsNavigation | undefined>(() => {
const navs = navigations.value || []
return navs.find((n) => n.model === 'page' && n.title?.includes('公司'))
})
/** 从导航中找到"新闻/动态"相关的导航项 */
const newsNav = computed<CmsNavigation | undefined>(() => {
const navs = navigations.value || []
return navs.find((n) => n.model === 'article')
})
/** 从导航中找到"产品"相关的导航项 */
const productNav = computed<CmsNavigation | undefined>(() => {
const navs = navigations.value || []
return navs.find((n) => n.model === 'product')
})
/** 获取最新文章(取新闻导航下的文章) */
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 = []
}
}
/** 获取明星产品(只取置顶产品 top>0) */
const products = ref<Product[]>([])
try {
const res = await $fetch<ApiEnvelope<PageResult<Product>> | PageResult<Product>>(
'/api/product/list',
{
// 拉取足够数量以确保覆盖全部置顶产品,再在前端按 top 字段过滤
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)
// 优先只展示置顶产品(top > 0);后台尚未设置任何置顶产品时降级展示默认前 4 个,避免首页区块空洞
products.value = (topList.length > 0 ? topList : list).slice(0, 4)
} catch {
products.value = []
}
function formatDate(date?: string) {
if (!date) return ''
return dayjs(date).format('YYYY-MM-DD')
}
</script>