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

140 lines
5.2 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 py-16 bg-[#faf6ef]">
<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-[#f0e6d8] text-[#8b6f47] text-sm font-semibold mb-4">{{ pageTitle }}</span>
<h1 class="text-3xl font-bold text-[#4a3f35] mb-4">{{ pageTitle }}</h1>
<p class="text-[#8a7d6b] 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 cases"
:key="item.id"
class="group bg-white rounded-3xl overflow-hidden shadow-sm hover:shadow-md transition-shadow border border-[#e8e0d4]"
>
<NuxtLink :to="`/case/${item.id}`" class="block">
<div class="aspect-[4/3] bg-[#f3ebde] 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"
>
<span v-else class="flex items-center justify-center h-full text-[#b9a892]">暂无图片</span>
</div>
<div class="p-5">
<h2 class="text-lg font-semibold text-[#4a3f35] mb-2 group-hover:text-[#8b6f47] transition-colors">
{{ item.title }}
</h2>
<p class="text-sm text-[#8a7d6b] line-clamp-2">{{ stripHtml(item.summary) }}</p>
<div v-if="item.clientName" class="mt-3 text-xs text-[#8a7d6b]">
客户{{ item.clientName }}
</div>
</div>
</NuxtLink>
</article>
</div>
<!-- 空状态 -->
<div v-if="!pending && !error && cases.length === 0" class="text-center py-20">
<svg class="w-16 h-16 mx-auto text-[#e2d6c4] mb-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
</svg>
<p class="text-[#8a7d6b]">暂无案例内容</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="px-4 py-2 text-sm rounded-lg transition-colors"
:class="p === currentPage ? 'bg-[#8b6f47] text-white' : 'bg-white text-[#4a3f35] hover:bg-[#f0e6d8]'"
@click="currentPage = p"
>
{{ p }}
</button>
</nav>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import type { CaseItem, PageResult } from '~/types'
import { collectDescendantNavIds } from '~/utils/nav-tree'
/**
* 模板 3 - 案例列表页(暖米轻商风)
*/
const { fileUrl } = useFileUrl()
const route = useRoute()
const { allNavigations, fetchSiteInfo } = useSite()
// 确保导航数据已加载(用于栏目标题与分类判定)
await fetchSiteInfo()
// 列表(栏目)入口 /case/{navigationId} 时按分类过滤;/case 根目录展示全部案例
const navigationId = computed<number | undefined>(() => {
const id = route.params.id
return id ? Number(id) : 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<PageResult<CaseItem>>('/api/case/list', {
key: `case-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 cases = computed(() => data.value?.list || [])
const totalCount = computed(() => data.value?.count ?? 0)
const totalPages = computed(() => Math.max(1, Math.ceil(totalCount.value / limit)))
// 切换栏目时回到第 1 页
watch([categoryIds, navigationId], () => {
currentPage.value = 1
})
// 栏目标题:命中导航取导航标题,否则用模块默认名
const pageTitle = computed(() => {
const id = navigationId.value
if (id == null) return '案例展示'
const navs = (allNavigations.value || []) as any[]
const find = (items: any[]): any => {
for (const it of items) {
if (it.navigationId === id) return it
if (it.children?.length) {
const f = find(it.children)
if (f) return f
}
}
return undefined
}
return find(navs)?.title || '案例展示'
})
</script>