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

272 lines
10 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 class="bg-[#0f0f0f] flex-1">
<!-- Hero 首屏 -->
<HeroSection v-if="homeBlocks.hero" />
<!-- 核心业务 -->
<FeatureSection v-if="homeBlocks.advantages" />
<!-- 关于我们 -->
<AboutSection
v-if="homeBlocks.banner"
title-color="#ffffff"
accent-color="#d4af37"
summary-color="#a0a0a0"
link-color="#d4af37"
bg-color="#0f0f0f"
container-class="container mx-auto px-4 sm:px-6 lg:px-8 grid lg:grid-cols-2 gap-12 items-center"
image-bg-color="#1a1a1a"
image-object-fit="object-cover"
/>
<!-- 最新动态 -->
<section v-if="homeBlocks.news && latestArticles.length > 0" class="py-16 lg:py-20 bg-[#0f0f0f]">
<div class="container mx-auto px-4 sm:px-6 lg:px-8">
<div class="text-center mb-12">
<h2 class="text-2xl sm:text-3xl font-bold text-white mb-4" data-reveal>最新动态</h2>
<div class="w-16 h-1 bg-[#d4af37] mx-auto mb-4" data-reveal data-reveal-delay="100" />
<p class="text-[#a0a0a0] max-w-2xl mx-auto" data-reveal data-reveal-delay="200">了解企业最新资讯与行业动态</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-[#1a1a1a] border border-[#333] rounded-xl overflow-hidden hover:border-[#d4af37]/50 transition-all anim-card group"
data-reveal
:data-reveal-delay="index * 100"
>
<NuxtLink :to="`/article/${item.id || item.articleId}`">
<div class="aspect-video bg-[#222] flex items-center justify-center anim-card-media overflow-hidden">
<img
:src="articleImage(item)"
:alt="item.title"
class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
loading="lazy"
@error="onArticleImgError($event)"
>
</div>
</NuxtLink>
<div class="p-5">
<div class="text-xs text-[#a0a0a0] mb-2">
{{ formatDate(item.publishTime) }}
<span v-if="item.categoryName" class="ml-2 text-[#d4af37]">{{ item.categoryName }}</span>
</div>
<h3 class="text-lg font-semibold text-white mb-2 line-clamp-2 group-hover:text-[#d4af37] transition-colors">
<NuxtLink :to="`/article/${item.id || item.articleId}`">{{ item.title }}</NuxtLink>
</h3>
<p class="text-sm text-[#a0a0a0] line-clamp-2">{{ stripHtml(item.summary) }}</p>
</div>
</article>
</div>
<div v-if="newsNav" class="text-center mt-10">
<NuxtLink
:to="newsNav.path || '/article'"
class="inline-flex items-center text-[#d4af37] font-semibold hover:text-[#e6c35a] 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="homeBlocks.cases && cases.length > 0" class="py-16 lg:py-20 bg-[#0f0f0f]">
<div class="container mx-auto px-4 sm:px-6 lg:px-8">
<div class="text-center mb-12">
<h2 class="text-2xl sm:text-3xl font-bold text-white mb-4" data-reveal>成功案例</h2>
<div class="w-16 h-1 bg-[#d4af37] mx-auto mb-4" data-reveal data-reveal-delay="100" />
<p class="text-[#a0a0a0] max-w-2xl mx-auto" data-reveal data-reveal-delay="200">来自不同行业的真实落地实践</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-[#1a1a1a] border border-[#333] rounded-xl overflow-hidden hover:border-[#d4af37]/50 transition-all anim-card"
data-reveal
:data-reveal-delay="index * 100"
>
<NuxtLink :to="`/case/${item.id}`" class="block">
<div class="aspect-[4/3] bg-[#222] overflow-hidden">
<img
v-if="item.cover"
:src="fileUrl(item.cover)"
:alt="item.title"
class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
loading="lazy"
>
<span v-else class="flex items-center justify-center h-full text-[#a0a0a0]">暂无图片</span>
</div>
<div class="p-5">
<h3 class="text-lg font-semibold text-white mb-2 group-hover:text-[#d4af37] transition-colors">
{{ item.title }}
</h3>
<p class="text-sm text-[#a0a0a0] line-clamp-2">{{ stripHtml(item.summary) }}</p>
<div v-if="item.clientName" class="mt-3 text-xs text-[#a0a0a0]">
客户:{{ 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-[#d4af37] font-semibold hover:text-[#e6c35a] 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="homeBlocks.cta" class="py-16 lg:py-20 bg-[#161616]">
<div class="container mx-auto px-4 sm:px-6 lg:px-8 text-center">
<h2 class="text-2xl sm:text-3xl font-bold text-white mb-4" data-reveal>
准备好开始了吗?
</h2>
<div class="w-16 h-1 bg-[#d4af37] mx-auto mb-6" data-reveal data-reveal-delay="100" />
<p class="text-[#a0a0a0] max-w-2xl mx-auto mb-8" data-reveal data-reveal-delay="200">
立即联系我们,获取专属人力资源解决方案
</p>
<button
type="button"
class="inline-flex items-center justify-center px-8 py-3 bg-[#d4af37] text-[#1a1a1a] font-semibold rounded hover:bg-[#e6c35a] transition-colors anim-shimmer"
@click="openConsult()"
>
立即咨询
</button>
</div>
</section>
</div>
</template>
<script setup lang="ts">
/**
* 模板 4 - 首页(黑金高端风)
* 参考 template-07 结构,使用黑金配色并适配人力资源行业文案。
*/
import dayjs from 'dayjs'
import HeroSection from '../components/HeroSection.vue'
import FeatureSection from '../components/FeatureSection.vue'
import type { CmsNavigation, Article, CaseItem, PageResult, ApiEnvelope } from '~/types'
const { siteName, navigations, bottomNavigations, fetchSiteInfo } = useSite()
const { fileUrl } = useFileUrl()
const { openConsult } = useConsult()
const { homeBlocks } = useFeatures()
await fetchSiteInfo()
/** 从导航中找到"新闻/动态"相关的导航项 */
const newsNav = computed<CmsNavigation | undefined>(() => {
const all = [
...(navigations.value || []),
...(bottomNavigations.value || [])
]
const articleNavs = all.filter((n) => n.model === 'article')
const byTitle = articleNavs.find((n) => /新闻|动态|资讯/.test(n.title || ''))
if (byTitle) return byTitle
return articleNavs[0]
})
/** 从导航中找到"案例"相关的导航项 */
const caseNav = computed<CmsNavigation | undefined>(() => {
const navs = navigations.value || []
return navs.find((n) => n.model === 'case')
})
/** 所有 article 类型栏目(含子栏目) */
const articleNavs = computed<CmsNavigation[]>(() => {
const list: CmsNavigation[] = []
const walk = (arr?: CmsNavigation[]) => {
if (!arr) return
for (const n of arr) {
if (n.model === 'article' && !n.hide && !n.deleted) {
list.push(n)
}
if (n.children?.length) walk(n.children)
}
}
walk(navigations.value)
walk(bottomNavigations.value)
return list
})
/** 获取最新文章 */
const latestArticles = ref<Article[]>([])
for (const nav of articleNavs.value) {
try {
const res = await $fetch<ApiEnvelope<PageResult<Article>> | PageResult<Article>>(
'/api/article/list',
{
query: {
navigationId: nav.navigationId,
page: 1,
limit: 3
}
}
)
const envelope = res as ApiEnvelope<PageResult<Article>>
const list = envelope?.data?.list || (res as PageResult<Article>)?.list || []
if (list.length > 0) {
latestArticles.value = list
break
}
} catch {
// 跳过失败栏目
}
}
/** 兜底:不限栏目拉最新文章 */
if (latestArticles.value.length === 0) {
try {
const res = await $fetch<ApiEnvelope<PageResult<Article>> | PageResult<Article>>(
'/api/article/list',
{ query: { 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 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')
}
/** 文章封面图兜底 */
const FALLBACK_NEWS_IMAGE = '/images/template-07/news-default.jpg'
function articleImage(item: Article) {
const raw = item.image || item.cover || item.photo || ''
if (!raw) return FALLBACK_NEWS_IMAGE
return fileUrl(raw)
}
function onArticleImgError(e: Event) {
const el = e.target as HTMLImageElement
if (el && el.src !== FALLBACK_NEWS_IMAGE) {
el.src = FALLBACK_NEWS_IMAGE
}
}
</script>