feat(app): 添加多模板关于我们页面及相关路由和404页面

- 新增404页面,优化未找到页面体验,避免被搜索引擎索引
- 增加文件代理接口,隐藏真实文件服务器地址,支持文件请求代理
- 实现/article、/case、/product及/page动态路由兼容列表与详情展示
- 添加动态CMS页面兼容入口处理旧式路径,统一路由与SEO设置
- 新增模板1、模板7、模板2、模板3关于我们页面,实现多模板支持
- 模板增强支持CMS单页内容加载及SEO信息动态设置
- 配置环境变量及Git忽略文件规则辅助开发和构建环境管理
This commit is contained in:
2026-09-08 12:13:44 +08:00
commit 2b69686795
381 changed files with 59891 additions and 0 deletions
@@ -0,0 +1,139 @@
<template>
<div class="min-h-screen py-16 bg-gray-50">
<div class="container mx-auto px-4 sm:px-6 lg:px-8">
<div class="text-center mb-12">
<h1 class="text-3xl font-bold text-gray-900 mb-4">产品中心</h1>
<p class="text-gray-600 max-w-2xl mx-auto">为企业提供全方位的数字化解决方案</p>
</div>
<div v-if="pending" class="flex justify-center py-12">
<SiteLoading />
</div>
<SiteError v-else-if="error" message="获取产品列表失败" />
<div v-else class="grid sm:grid-cols-2 lg:grid-cols-3 gap-6">
<article
v-for="item in products"
:key="item.id ?? item.productId"
class="bg-white rounded-xl overflow-hidden shadow-sm hover:shadow-md transition-shadow"
>
<div class="aspect-video bg-gray-100 flex items-center justify-center">
<img
v-if="item.cover"
:src="fileUrl(item.cover)"
:alt="item.productName"
class="w-full h-full object-cover"
>
<span v-else class="text-gray-400">暂无图片</span>
</div>
<div class="p-5">
<h2 class="text-lg font-semibold text-gray-900 mb-2 hover:text-blue-600 transition-colors">
<NuxtLink :to="`/product/${item.id ?? item.productId}`">{{ item.productName }}</NuxtLink>
</h2>
<p class="text-sm text-gray-600 line-clamp-2 mb-4">{{ stripHtml(item.description || item.subtitle) }}</p>
<div class="flex items-center justify-between">
<span v-if="item.price" class="text-lg font-bold text-blue-600">{{ item.price }}</span>
<NuxtLink
:to="`/product/${item.id ?? item.productId}`"
class="text-sm text-blue-600 hover:text-blue-700 font-medium"
>
了解详情
</NuxtLink>
</div>
</div>
</article>
</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="px-4 py-2 text-sm rounded-lg transition-colors"
:class="p === currentPage ? 'bg-blue-600 text-white' : 'bg-white text-gray-700 hover:bg-blue-50'"
@click="currentPage = p"
>
{{ p }}
</button>
</nav>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import type { Product, PageResult, ApiEnvelope } from '~/types'
import { collectDescendantNavIds } from '~/utils/nav-tree'
/**
* 产品列表页
*/
const { fileUrl } = useFileUrl()
const { allNavigations, fetchSiteInfo } = useSite()
const route = useRoute()
await fetchSiteInfo()
/**
* 栏目 navigationId:从查询参数或路由参数读取。
* 优先级(2026-07-21 修正):route.query.navId > route.params.id
* (子分类下拉切换通过 ?navId= 传入,必须优先于路径参数,否则切换不生效)
*/
const navigationId = computed<number | undefined>(() => {
// 1. 查询参数优先(导航子分类下拉切换时传入)
const qid = route.query.navId as string
if (qid) {
const n = Number(qid)
if (!Number.isNaN(n)) return n
}
// 2. 路由参数(/product/:navigationId
const pid = route.params.id as string
if (pid) {
const n = Number(pid)
if (!Number.isNaN(n)) return n
}
return undefined
})
// 聚合:父栏目访问时递归收集自身 + 所有后代栏目 navigationId,交给后端 IN 查询
const categoryIds = computed<string | undefined>(() => {
const ids = collectDescendantNavIds(navigationId.value, allNavigations.value || [])
return ids.length ? ids.join(',') : undefined
})
const currentPage = ref(1)
const limit = 12
const { data, pending, error } = await useFetch<
ApiEnvelope<PageResult<Product>> | PageResult<Product>
>('/api/product/list', {
key: `product-list-${categoryIds.value ?? navigationId.value ?? 'all'}-p${currentPage.value}`,
query: {
page: currentPage,
limit,
...(categoryIds.value
? { categoryIds: categoryIds.value }
: (navigationId.value ? { navigationId: navigationId.value } : {}))
},
watch: [currentPage, categoryIds, navigationId]
})
const products = computed<Product[]>(() => {
const envelope = data.value as ApiEnvelope<PageResult<Product>>
const direct = data.value as PageResult<Product>
return envelope?.data?.list || direct?.list || []
})
const totalCount = computed(() => {
const envelope = data.value as ApiEnvelope<PageResult<Product>>
const direct = data.value as PageResult<Product>
return envelope?.data?.count || envelope?.data?.total || direct?.count || direct?.total || 0
})
const totalPages = computed(() => Math.max(1, Math.ceil(totalCount.value / limit)))
// 切换栏目时回到第 1 页
watch([categoryIds, navigationId], () => {
currentPage.value = 1
})
</script>