Files
hjc-web/app/templates/template-06/pages/NewsList.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

114 lines
6.4 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="min-h-screen bg-[#F4F6FA]">
<!-- 栏目头图 -->
<div class="bg-[#1E2A47] relative overflow-hidden">
<div class="absolute -top-16 -right-16 w-72 h-72 rounded-full bg-[#CF1313]/20 blur-3xl" />
<div class="container mx-auto px-4 sm:px-6 lg:px-8 py-12 lg:py-16 relative z-10">
<nav class="text-xs text-white/60 mb-3" aria-label="面包屑">
<NuxtLink to="/" class="hover:text-white">首页</NuxtLink>
<span class="mx-2">/</span>
<span class="text-white">{{ pageTitle }}</span>
</nav>
<h1 class="text-3xl font-bold text-white mb-2">{{ pageTitle }}</h1>
<p class="text-white/70 max-w-2xl">了解企业最新动态、行业洞察与产品资讯</p>
</div>
</div>
<div class="container mx-auto px-4 sm:px-6 lg:px-8 py-12">
<div v-if="pending" class="flex justify-center py-12"><SiteLoading /></div>
<div v-else-if="error" class="text-center py-12"><SiteError message="获取新闻列表失败" /></div>
<template v-else>
<div v-if="articles.length > 0" class="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
<article v-for="item in articles" :key="item.id || item.articleId" class="bg-white rounded-2xl overflow-hidden shadow-sm hover:shadow-md transition-shadow border border-[#E5E7EB] group">
<NuxtLink :to="`/article/${item.id || item.articleId}`">
<div class="h-52 bg-[#F4F6FA] 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 group-hover:scale-105 transition-transform duration-500">
<span v-else class="text-[#C9B7A3]">暂无图片</span>
</div>
</NuxtLink>
<div class="p-5">
<div class="flex items-center gap-3 text-xs text-[#666666] mb-2">
<span>{{ formatDate(item.publishTime) }}</span>
<span v-if="item.categoryName" class="px-2 py-0.5 rounded-full bg-[#FDECEC] text-[#CF1313]">{{ item.categoryName }}</span>
</div>
<h2 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></h2>
<p class="text-sm text-[#666666] line-clamp-3">{{ stripHtml(item.summary) }}</p>
</div>
</article>
</div>
<div v-else class="text-center py-20">
<svg class="w-16 h-16 mx-auto text-[#C9B7A3] mb-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9a2 2 0 00-2-2h-2m-4-3H9M7 16h6M7 8h6v4H7V8z" /></svg>
<p class="text-[#666666]">暂无新闻内容</p>
</div>
<div v-if="totalPages > 1" class="flex justify-center mt-12">
<nav class="flex items-center gap-2">
<button v-for="p in totalPages" :key="p" class="w-10 h-10 text-sm rounded-full transition-colors" :class="p === currentPage ? 'bg-[#CF1313] text-white' : 'bg-white text-[#1E2A47] border border-[#E5E7EB] hover:bg-[#F4F6FA]'" @click="currentPage = p">{{ p }}</button>
</nav>
</div>
</template>
</div>
</div>
</template>
<script setup lang="ts">
import dayjs from 'dayjs'
import type { Article, PageResult, ApiEnvelope, CmsNavigation } from '~/types'
import { collectDescendantNavIds } from '~/utils/nav-tree'
const { allNavigations, fetchSiteInfo } = useSite()
const { fileUrl } = useFileUrl()
const route = useRoute()
await fetchSiteInfo()
/** navigationId 优先级:route.query.navId > route.params.id > model=article 第一个栏目 */
const navigationId = computed<number | undefined>(() => {
const queryNavId = route.query.navId as string
if (queryNavId) { const num = Number(queryNavId); if (!Number.isNaN(num)) return num }
const routeId = route.params.id as string
if (routeId) { const num = Number(routeId); if (!Number.isNaN(num)) return num }
const navs = allNavigations.value || []
const nav = navs.find((n) => n.model === 'article' || (n.path || '').split('/').filter(Boolean)[0] === 'article')
return nav?.navigationId
})
/** 聚合:父栏目访问时递归收集自身 + 所有后代栏目 navigationId,交给后端 IN 查询 */
const categoryIds = computed<string | undefined>(() => {
const ids = collectDescendantNavIds(navigationId.value, allNavigations.value || [])
return ids.length ? ids.join(',') : undefined
})
const pageTitle = computed(() => {
const navs = allNavigations.value || []
const findNav = (items: CmsNavigation[]): CmsNavigation | undefined => {
for (const item of items) {
if (item.navigationId === navigationId.value) return item
if (item.children?.length) { const f = findNav(item.children); if (f) return f }
}
return undefined
}
if (keywords.value) return `搜索:"${keywords.value}"`
return findNav(navs)?.title || '新闻资讯'
})
/** 搜索关键词(来自 Header 搜索框提交的 ?keywords=) */
const keywords = computed(() => ((route.query.keywords as string) || '').trim())
const currentPage = ref(1)
const limit = 12
const { data, pending, error } = await useFetch<ApiEnvelope<PageResult<Article>> | PageResult<Article>>('/api/article/list', {
key: `t6-news-list-${keywords.value || 'all'}-${categoryIds.value ?? navigationId.value ?? 'default'}-p${currentPage.value}`,
query: { categoryIds: keywords.value ? undefined : categoryIds.value, keywords: keywords.value || undefined, page: currentPage, limit },
watch: [currentPage, navigationId, categoryIds, keywords]
})
const articles = computed<Article[]>(() => {
const envelope = data.value as ApiEnvelope<PageResult<Article>>
const direct = data.value as PageResult<Article>
return envelope?.data?.list || direct?.list || []
})
const totalCount = computed(() => {
const envelope = data.value as ApiEnvelope<PageResult<Article>>
const direct = data.value as PageResult<Article>
return envelope?.data?.count || envelope?.data?.total || direct?.count || direct?.total || 0
})
const totalPages = computed(() => Math.ceil(totalCount.value / limit))
watch([navigationId, keywords], () => { currentPage.value = 1 })
function formatDate(date?: string) { if (!date) return ''; return dayjs(date).format('YYYY-MM-DD') }
</script>