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

114 lines
6.4 KiB
Vue
Raw Permalink 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 || item.createTime) }}</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>