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
+252
View File
@@ -0,0 +1,252 @@
<template>
<div>
<!-- Hero 横幅 -->
<section class="relative overflow-hidden bg-gradient-to-r from-blue-700 to-blue-500">
<div class="absolute -top-20 -right-16 w-72 h-72 rounded-full bg-white/10 blur-3xl" />
<div class="absolute -bottom-24 left-10 w-64 h-64 rounded-full bg-black/10 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/70 mb-3" aria-label="面包屑">
<NuxtLink to="/" class="hover:text-white transition-colors">首页</NuxtLink>
<span class="mx-2">/</span>
<span class="text-white">关于我们</span>
</nav>
<h1 class="text-3xl sm:text-4xl font-bold text-white">关于我们</h1>
<p class="text-white/75 mt-3 max-w-2xl leading-relaxed">
{{ slogan || '汇全咨之力·吉政企发展·采专业服务' }}
</p>
</div>
</section>
<!-- 公司简介 -->
<section class="py-16 lg:py-20 bg-white">
<div class="container mx-auto px-4 sm:px-6 lg:px-8">
<div class="max-w-5xl mx-auto">
<div class="grid lg:grid-cols-2 gap-12 items-center">
<div class="order-1 lg:order-2">
<span class="inline-block px-3 py-1 rounded-full bg-blue-50 text-blue-600 text-sm font-semibold mb-4">公司简介</span>
<h2 class="text-2xl sm:text-3xl font-bold text-gray-900 mb-6">汇吉采政企信赖的全生命周期咨询伙伴</h2>
<div class="text-gray-600 leading-relaxed mb-8 space-y-4">
<p v-if="introText">{{ introText }}</p>
<template v-else>
<p>我们深耕企业软件与行业信息化领域聚焦客户需求提供从咨询规划产品设计到研发交付运维支持的一站式服务</p>
<p>以稳定可靠的架构与持续优化的服务帮助客户降低数字化门槛提升运营效率实现可持续的业务增长</p>
</template>
</div>
<div class="flex flex-wrap gap-3 mb-8">
<span v-for="tag in tags" :key="tag"
class="px-3 py-1.5 rounded-full bg-blue-50 border border-blue-100 text-sm text-gray-700">{{ tag }}</span>
</div>
<NuxtLink to="/contact" class="inline-flex items-center px-6 py-3 bg-blue-600 text-white font-semibold rounded-lg hover:bg-blue-700 transition-colors shadow-sm">
联系我们
<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>
<!-- 右侧装饰图区域 coverImage 时显示实际图片否则显示 CSS 渐变装饰块 -->
<div class="order-2 lg:order-1 rounded-2xl overflow-hidden shadow-lg"
:class="coverImage ? '' : 'bg-gradient-to-br from-blue-100 to-blue-50 flex items-center justify-center min-h-[280px] lg:min-h-[384px]'">
<img v-if="coverImage" :src="coverImage" alt="关于我们" class="w-full h-72 lg:h-96 object-cover">
<div v-else class="p-8 text-center">
<svg class="w-20 h-20 mx-auto text-blue-300 mb-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
</svg>
<p class="text-blue-400 font-medium">{{ pageTitle }}</p>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- 核心优势 -->
<section class="py-16 lg:py-20 bg-gray-50">
<div class="container mx-auto px-4 sm:px-6 lg:px-8">
<div class="max-w-5xl mx-auto">
<div class="text-center mb-12">
<span class="inline-block px-3 py-1 rounded-full bg-white text-blue-600 text-sm font-semibold mb-4">核心优势</span>
<h2 class="text-2xl sm:text-3xl font-bold text-gray-900 mb-4">为什么选择我们</h2>
<p class="text-gray-500 max-w-2xl mx-auto">从技术到服务全链路保障客户的数字化投资回报</p>
</div>
<div class="grid sm:grid-cols-2 lg:grid-cols-4 gap-6">
<div v-for="(item, index) in advantages" :key="item.title"
class="bg-white rounded-2xl p-7 shadow-sm hover:shadow-md transition-shadow border border-gray-100 group">
<div class="w-14 h-14 rounded-xl flex items-center justify-center mb-5 bg-blue-50">
<svg class="w-7 h-7 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" :d="item.icon" /></svg>
</div>
<h3 class="text-lg font-semibold text-gray-900 mb-2">{{ item.title }}</h3>
<p class="text-sm text-gray-500 leading-relaxed">{{ item.desc }}</p>
</div>
</div>
</div>
</div>
</section>
<!-- 资质与荣誉 -->
<section class="py-16 lg:py-20 bg-white">
<div class="container mx-auto px-4 sm:px-6 lg:px-8">
<div class="max-w-5xl mx-auto">
<div class="text-center mb-12">
<span class="inline-block px-3 py-1 rounded-full bg-blue-50 text-blue-600 text-sm font-semibold mb-4">资质与荣誉</span>
<h2 class="text-2xl sm:text-3xl font-bold text-gray-900 mb-4">权威认证实力背书</h2>
<p class="text-gray-500 max-w-2xl mx-auto">全面通过国际标准管理体系认证与行业信用评价以规范运营筑牢发展基石</p>
</div>
<div class="grid sm:grid-cols-2 lg:grid-cols-3 gap-6">
<div v-for="(h, index) in honors" :key="h.title"
class="bg-gray-50 rounded-2xl p-7 border border-gray-100 hover:shadow-md transition-shadow">
<div class="flex items-center gap-3 mb-3">
<span class="w-10 h-10 rounded-lg bg-blue-50 flex items-center justify-center text-blue-600 font-bold">{{ index + 1 }}</span>
<h3 class="text-lg font-semibold text-gray-900">{{ h.title }}</h3>
</div>
<p class="text-sm text-gray-500 leading-relaxed">{{ h.desc }}</p>
</div>
</div>
</div>
</div>
</section>
<!-- 数据实力 -->
<section class="py-16 lg:py-20 bg-gray-50">
<div class="container mx-auto px-4 sm:px-6 lg:px-8">
<div class="max-w-5xl mx-auto">
<div class="text-center mb-12">
<span class="inline-block px-3 py-1 rounded-full bg-white text-blue-600 text-sm font-semibold mb-4">数据实力</span>
<h2 class="text-2xl sm:text-3xl font-bold text-gray-900 mb-4">用结果说话</h2>
<p class="text-gray-500 max-w-2xl mx-auto">沉淀多年的交付能力与行业口碑是我们最扎实的底气</p>
</div>
<div class="grid grid-cols-2 lg:grid-cols-4 gap-6">
<div v-for="(s, index) in stats" :key="s.label"
class="bg-white rounded-2xl p-8 text-center shadow-sm border border-gray-100">
<div class="text-gray-900 font-bold text-3xl sm:text-4xl">
{{ s.value }}<span class="text-blue-600">{{ s.suffix }}</span>
</div>
<div class="text-sm text-gray-500 mt-2">{{ s.label }}</div>
</div>
</div>
</div>
</div>
</section>
<!-- CMS 富文本扩展区仅当后台有录入内容时显示 -->
<section v-if="hasContent" class="py-16 lg:py-20 bg-white">
<div class="container mx-auto px-4 sm:px-6 lg:px-8">
<div class="max-w-5xl mx-auto">
<div class="bg-white rounded-2xl shadow-sm border border-gray-100 overflow-hidden">
<div class="p-6 sm:p-10">
<div class="flex items-center gap-3 mb-6 pb-6 border-b border-gray-100">
<span class="w-1.5 h-6 rounded-full bg-blue-600" />
<h2 class="text-xl sm:text-2xl font-bold text-gray-900">{{ pageData?.title || '详细介绍' }}</h2>
<span v-if="updateTimeText" class="sm:ml-auto text-sm text-gray-400">
更新于 {{ updateTimeText }}
</span>
</div>
<RichText :content="pageData?.content" />
<PageAttachments
v-if="pageData?.attachments?.length"
:attachments="pageData.attachments"
/>
</div>
</div>
</div>
</div>
</section>
<!-- 联系 CTA -->
<section class="py-16 lg:py-20 bg-gradient-to-r from-blue-700 to-blue-500 relative overflow-hidden">
<div class="absolute -top-20 -right-20 w-80 h-80 rounded-full bg-white/10 blur-3xl" />
<div class="container mx-auto px-4 sm:px-6 lg:px-8 text-center relative z-10">
<h2 class="text-2xl sm:text-3xl font-bold text-white mb-4">有项目需求或合作意向</h2>
<p class="text-white/80 max-w-2xl mx-auto mb-8">立即联系我们获取专属方案与产品演示</p>
<div class="flex flex-wrap items-center justify-center gap-4">
<NuxtLink
to="/contact"
class="inline-flex items-center px-8 py-3 bg-white text-blue-700 font-semibold rounded-lg hover:bg-gray-100 transition-colors shadow-lg"
>
立即咨询
</NuxtLink>
<a
v-if="siteInfo?.phone"
:href="`tel:${siteInfo.phone}`"
class="inline-flex items-center px-6 py-3 border-2 border-white/40 text-white font-semibold rounded-lg hover:bg-white/10 transition-colors"
>
{{ siteInfo.phone }}
</a>
</div>
</div>
</section>
</div>
</template>
<script setup lang="ts">
/**
* 模板 7 - 关于我们(科技蓝)
*
* 重构为「模板内置内容 + 可选 CMS 增强」模式:
* - 默认展示公司简介 / 核心优势 / 资质与荣誉 / 数据实力 / CTA 五大板块
* - 汇吉采(租户 10626)专属内容:简介/优势/资质/数据均写死模板内置,底部可选 CMS 富文本增强
* - 若后台「单页管理」录入了 about 正文(cms_page),底部追加富文本扩展区
* - 彻底解决 API 410/空数据导致页面白屏的问题
*/
import dayjs from 'dayjs'
import type { PageDetail } from '~/types'
const { siteInfo, slogan, fetchSiteInfo } = useSite()
const { fileUrl } = useFileUrl()
await fetchSiteInfo()
// ========== CMS 单页数据(可选增强) ==========
const { data: pageData } = await useFetch<PageDetail>('/api/page/detail', {
key: 'page-about',
query: { path: 'about' }
})
const pageTitle = computed(() => pageData.value?.title?.trim() || '关于我们')
const coverImage = computed(() => fileUrl(pageData.value?.photo || ''))
const hasContent = computed(() => Boolean(pageData.value?.hasContent && pageData.value?.content))
const updateTimeText = computed(() => {
const t = pageData.value?.updateTime
return t ? dayjs(t).format('YYYY-MM-DD') : ''
})
// ========== 公司简介(模板内置,汇吉采专属) ==========
const introText = '广西汇吉采咨询有限公司成立于2017年,是具备独立法人资质的综合性专业咨询服务机构,总部设于南宁,布局柳州、崇左、北海、梧州、百色、贺州、玉林多地分支机构,建成覆盖广西全域的服务网络。公司聚焦项目全生命周期采购咨询服务,搭建政府采购全过程咨询、工程全过程咨询、投融资全过程咨询、政府采购AI定制化开发服务四大核心业务板块,配套30余项咨询服务,为各级党政机关、事业单位、国有企业及产业园区提供一站式全生命周期一体化服务。'
const tags = ['政府采购全过程咨询', '工程全过程咨询', '投融资全过程咨询', '政府采购AI定制开发']
// ========== 核心优势(汇吉采专属) ==========
const advantages = [
{ title: '标准化服务体系', desc: '恪守合同履约率 100%、客户满意度 98% 以上,全流程规范化管控,让项目合规高效落地。', icon: 'M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z' },
{ title: '数智化管理平台', desc: '招标业务系统、线上审批、OA 协同一体,实现全区远程移动办公,提升服务规范性。', icon: 'M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z' },
{ title: '自有专属场地', desc: '近 2000㎡ 专属办公场地,独立开标室与评标室,满足各类评审、研讨需求。', icon: 'M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4' },
{ title: '专业人才支撑', desc: '高级专业技术人员占比 77%,中高级工程师及各类注册持证人才超 90%,务实求精。', icon: 'M12 14l9-5-9-5-9 5 9 5z M12 14l6.16-3.422a12.083 12.083 0 01.665 6.479A11.952 11.952 0 0012 20.055a11.952 11.952 0 00-6.824-2.998 12.078 12.078 0 01.665-6.479L12 14z' }
]
// ========== 资质与荣誉(汇吉采专属) ==========
const honors = [
{ title: 'ISO9001 质量管理体系', desc: '确保产品与服务质量的稳定可靠,建立科学的质量管理机制。' },
{ title: 'ISO14001 环境管理体系', desc: '坚持绿色发展,履行环境责任,推动可持续运营。' },
{ title: 'ISO45001 职业健康安全', desc: '以人为本,保障员工健康与安全,规范作业环境管理。' },
{ title: 'AAA 信用评价体系', desc: '通过中盛华科(北京)信用评价有限公司全面认证,诚信服务筑牢基石。' },
{ title: '政府采购双重备案', desc: '完成中国政府采购网与广西政府采购网双重登记备案,具备全国承接资格。' },
{ title: '全国投资项目平台', desc: '接入全国投资项目在线审批监管平台,投融资咨询服务规范可溯。' }
]
// ========== 数据实力(汇吉采专属,写死真实指标) ==========
const stats = [
{ value: '2017', suffix: '年', label: '公司成立' },
{ value: '7', suffix: '地', label: '分支服务网络' },
{ value: '2000', suffix: '㎡', label: '自有专属场地' },
{ value: '90', suffix: '%+', label: '注册持证人才' }
]
// ========== SEO ==========
usePageSeo(
{
title: pageTitle.value,
path: '/about',
keywords: pageData.value?.keywords || undefined,
description: pageData.value?.description || undefined,
image: coverImage.value || undefined
},
siteInfo.value
)
</script>
@@ -0,0 +1,62 @@
<template>
<article class="min-h-screen py-16 bg-white">
<!-- Hero -->
<header class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 mb-12 text-center">
<span class="inline-block px-3 py-1 rounded-full bg-blue-50 text-blue-600 text-sm font-semibold mb-4">关于汇吉采</span>
<h1 class="text-3xl sm:text-4xl font-bold text-gray-900 mb-3">分支机构</h1>
<p class="text-gray-600 max-w-2xl mx-auto">立足南宁辐射广西覆盖全区 7 个地市与近 2000自有办公场地的本地化咨询服务网络</p>
</header>
<!-- 正文 -->
<section class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 mb-16">
<RichText :content="content" />
</section>
<!-- CTA -->
<section class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="bg-blue-600 rounded-2xl p-8 sm:p-10 text-center text-white">
<h2 class="text-2xl font-bold mb-3">需要本地化专业服务支持</h2>
<p class="mb-6 opacity-90">汇吉采七地分支随时为您提供贴身响应</p>
<NuxtLink
to="/contact"
class="inline-block px-6 py-3 bg-white text-blue-600 rounded-full font-semibold hover:bg-blue-50 transition-colors"
>联系我们</NuxtLink>
</div>
</section>
</article>
</template>
<script setup lang="ts">
/**
* 模板 7 - 分支机构(写死,10626 租户专属)
* 内容来自 PPT 第 4 页「汇吉采简介」中关于 7 地分支+近 2000㎡ 自有场地的描述。
*/
useHead({
title: '分支机构 - 广西汇吉采咨询有限公司',
meta: [
{ name: 'description', content: '汇吉采总部位于南宁,下设柳州、崇左、北海、梧州、百色、贺州、玉林等7地分支机构,自有近2000㎡专属办公场地,覆盖广西全区,提供本地化专业咨询服务。' },
{ name: 'keywords', content: '汇吉采,分支机构,广西,南宁,7地' }
]
})
const content = `<h2>广西全域服务网络</h2>
<p>广西汇吉采咨询有限公司总部设于南宁,并在广西多地市设立分支机构,建成覆盖广西全域、辐射各级党政机关与企事业单位的本地化专业咨询服务网络,可快速响应各地项目全流程采购咨询服务需求。</p>
<h3>总部:南宁</h3>
<p>南宁总部承担整体业务统筹、技术研发、品控体系与数智化平台运营职能,自有近 <strong>2000㎡专属办公场地</strong>,可独立承载大型项目研讨、专家评审与远程协同会议,满足各类招标评审与项目研讨需求。</p>
<p>地址:南宁市良庆区盘歌路 4 号碧园中心 A 座 10、11 层</p>
<h3>七地分支机构</h3>
<ul>
<li><strong>柳州市</strong></li>
<li><strong>崇左市</strong></li>
<li><strong>北海市</strong></li>
<li><strong>梧州市</strong></li>
<li><strong>百色市</strong></li>
<li><strong>贺州市</strong></li>
<li><strong>玉林市</strong></li>
</ul>
<p>七地分支机构与南宁总部紧密联动,可在采购需求调查、招标文件编制、评审组织、合同履约等关键环节提供本地化贴身响应,让专业服务不因地域差异而打折。</p>
<h3>服务范围</h3>
<p>业务辐射 <strong>各级党政机关、事业单位、国有企业、产业园区</strong>,全域覆盖政府采购、公共资源交易、市政房建、水利水务、综合交通、专项债投融资、产业投资合作等领域,整合政府采购咨询、前期策划、招标代理、工程监理、造价管控、数智化系统定制等综合能力。</p>`
</script>
@@ -0,0 +1,11 @@
<script setup lang="ts">
</script>
<template>
<div>sdfsdfsdsdfbuy---</div>
</template>
<style scoped>
</style>
@@ -0,0 +1,56 @@
<template>
<div class="min-h-screen py-16 bg-white">
<div class="container mx-auto px-4 sm:px-6 lg:px-8">
<div v-if="caseItem" class="max-w-4xl mx-auto">
<div class="mb-8">
<NuxtLink :to="`/case${caseItem?.navigationId ? '?navId=' + caseItem.navigationId : ''}`" class="text-sm text-gray-500 hover:text-blue-600"> 返回案例列表</NuxtLink>
</div>
<h1 class="text-3xl sm:text-4xl font-bold text-gray-900 mb-4">{{ caseItem.title }}</h1>
<div class="flex flex-wrap items-center gap-4 text-sm text-gray-500 mb-8 pb-8 border-b border-gray-100">
<span v-if="caseItem.clientName" class="px-3 py-1 bg-blue-50 text-blue-600 rounded-full">
{{ caseItem.clientName }}
</span>
<span v-if="caseItem.projectTime">项目时间{{ caseItem.projectTime }}</span>
<span v-if="caseItem.categoryName">行业{{ caseItem.categoryName }}</span>
</div>
<div class="aspect-video bg-gray-100 rounded-xl overflow-hidden mb-10">
<img
v-if="caseItem.cover"
:src="fileUrl(caseItem.cover)"
:alt="caseItem.title"
class="w-full h-full object-cover"
>
</div>
<p v-if="caseItem.summary" class="text-lg text-gray-600 mb-8 leading-relaxed">
{{ caseItem.summary }}
</p>
<RichText :content="caseItem.content" />
</div>
<div v-else class="text-center py-20">
<h1 class="text-2xl font-bold text-gray-900 mb-4">案例不存在或已下架</h1>
<NuxtLink :to="`/case${caseItem?.navigationId ? '?navId=' + caseItem.navigationId : ''}`" class="text-blue-600 hover:underline">返回案例列表</NuxtLink>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import type { CaseItem } from '~/types'
/**
* 模板 1 - 案例详情页
*/
const route = useRoute()
const id = route.params.id as string
const { fileUrl } = useFileUrl()
const { data: caseItem } = await useFetch<CaseItem | null>(`/api/case/detail?id=${id}`, {
key: `case-${id}`
})
</script>
@@ -0,0 +1,130 @@
<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">{{ pageTitle }}</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 cases"
:key="item.id"
class="group bg-white rounded-xl overflow-hidden shadow-sm hover:shadow-md transition-shadow"
>
<NuxtLink :to="`/case/${item.id}`" class="block">
<div class="aspect-[4/3] bg-gray-100 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-gray-400">暂无图片</span>
</div>
<div class="p-5">
<h2 class="text-lg font-semibold text-gray-900 mb-2 group-hover:text-blue-600 transition-colors">
{{ item.title }}
</h2>
<p class="text-sm text-gray-600 line-clamp-2">{{ stripHtml(item.summary) }}</p>
<div v-if="item.clientName" class="mt-3 text-xs text-gray-500">
客户{{ item.clientName }}
</div>
</div>
</NuxtLink>
</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 { CaseItem, PageResult } from '~/types'
import { collectDescendantNavIds } from '~/utils/nav-tree'
/**
* 模板 1 - 案例列表页
*/
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>
@@ -0,0 +1,91 @@
<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="max-w-6xl mx-auto">
<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 class="grid lg:grid-cols-2 gap-10">
<!-- 联系信息 -->
<div class="bg-white rounded-xl p-8 shadow-sm">
<h2 class="text-xl font-bold text-gray-900 mb-6">联系方式</h2>
<div class="space-y-6">
<div class="flex items-start gap-4">
<div class="w-10 h-10 rounded-lg bg-blue-50 flex items-center justify-center flex-shrink-0">
<svg class="w-5 h-5 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
</svg>
</div>
<div>
<h3 class="font-medium text-gray-900">电话咨询</h3>
<p class="text-gray-600 mt-1">{{ siteInfo?.phone || '400-000-0000' }}</p>
</div>
</div>
<div class="flex items-start gap-4">
<div class="w-10 h-10 rounded-lg bg-blue-50 flex items-center justify-center flex-shrink-0">
<svg class="w-5 h-5 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
</div>
<div>
<h3 class="font-medium text-gray-900">电子邮箱</h3>
<p class="text-gray-600 mt-1">{{ siteInfo?.email || 'contact@example.com' }}</p>
</div>
</div>
<div class="flex items-start gap-4">
<div class="w-10 h-10 rounded-lg bg-blue-50 flex items-center justify-center flex-shrink-0">
<svg class="w-5 h-5 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
</div>
<div>
<h3 class="font-medium text-gray-900">公司地址</h3>
<p class="text-gray-600 mt-1">{{ siteInfo?.address || '请填写公司地址' }}</p>
</div>
</div>
</div>
</div>
<!-- 在线表单 -->
<div class="bg-white rounded-xl p-8 shadow-sm">
<h2 class="text-xl font-bold text-gray-900 mb-6">在线留言</h2>
<ContactForm
input-class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-blue-500"
submit-class="w-full px-6 py-3 bg-blue-600 text-white font-semibold rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
label-class="block text-sm font-medium text-gray-700 mb-1"
:accent="config.themeConfig.primaryColor"
/>
</div>
</div>
</div>
</div>
<PageAttachments
v-if="contactAttachments.length"
:attachments="contactAttachments"
class="mt-10"
/>
</div>
</template>
<script setup lang="ts">
import type { PageDetail, PageAttachment } from '~/types'
import config from '../config'
/**
* 模板 1 - 联系我们页
*/
const { siteInfo, fetchSiteInfo } = useSite()
await fetchSiteInfo()
// 联系页附件(CMS「单页管理」按 path=contact 维护)
const { data: contactPage } = await useFetch<PageDetail>('/api/page/detail', { query: { path: 'contact' } })
const contactAttachments = computed<PageAttachment[]>(() => contactPage.value?.attachments || [])
</script>
@@ -0,0 +1,64 @@
<template>
<article class="min-h-screen py-16 bg-white">
<!-- Hero -->
<header class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 mb-12 text-center">
<span class="inline-block px-3 py-1 rounded-full bg-blue-50 text-blue-600 text-sm font-semibold mb-4">关于汇吉采</span>
<h1 class="text-3xl sm:text-4xl font-bold text-gray-900 mb-3">企业文化</h1>
<p class="text-gray-600 max-w-2xl mx-auto">让专业采购咨询引领行业发展我们的使命价值观与服务承诺</p>
</header>
<!-- 正文 -->
<section class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 mb-16">
<RichText :content="content" />
</section>
<!-- CTA -->
<section class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="bg-blue-600 rounded-2xl p-8 sm:p-10 text-center text-white">
<h2 class="text-2xl font-bold mb-3">与汇吉采同行</h2>
<p class="mb-6 opacity-90">诚信 · 务实 · 创新 · 高效期待与您共建长期价值</p>
<NuxtLink
to="/contact"
class="inline-block px-6 py-3 bg-white text-blue-600 rounded-full font-semibold hover:bg-blue-50 transition-colors"
>联系我们</NuxtLink>
</div>
</section>
</article>
</template>
<script setup lang="ts">
/**
* 模板 7 - 企业文化(写死,10626 租户专属)
* 来源 PPT 第 5 页使命 + 核心价值观 + 质量承诺
*/
useHead({
title: '企业文化 - 广西汇吉采咨询有限公司',
meta: [
{ name: 'description', content: '让专业采购咨询引领行业发展——汇吉采以诚信、务实、创新、高效为核心价值观,履约率100%、客户满意度98%以上。' },
{ name: 'keywords', content: '汇吉采,企业文化,使命,价值观,诚信,务实,创新,高效' }
]
})
const content = `<h2>使命</h2>
<blockquote style="border-left: 4px solid #2563eb; padding: 12px 16px; background: #f8fafc; font-size: 18px; font-weight: 600; color: #1e3a8a;">让专业采购咨询引领行业发展</blockquote>
<p>这是汇吉采的立业原点,也是面向所有客户、合作伙伴、员工的共同承诺。</p>
<h3>核心价值观</h3>
<ul>
<li><strong>诚信</strong>:透明、合规、负责任,对每一项委托负责到底。</li>
<li><strong>务实</strong>:以解决客户实际痛点为出发点,不做表面文章。</li>
<li><strong>创新</strong>:以数智化与 AI 配套服务持续提升咨询价值。</li>
<li><strong>高效</strong>:以制度化流程与平台工具保障响应速度与交付质量。</li>
</ul>
<h3>服务理念</h3>
<p>以<strong>"全生命周期一体化"</strong>思路,整合政府采购咨询、前期策划、招标代理、工程监理、造价管控、数智化系统定制等综合能力,为各级党政机关、事业单位、国有企业及产业园区提供<strong>一站式服务</strong>。</p>
<h3>质量承诺</h3>
<ul>
<li>合同履约率 <strong>100%</strong></li>
<li>客户满意度 <strong>98% 以上</strong></li>
</ul>
<h3>面向未来</h3>
<p>公司将以广西区域采购全产业链高质量发展为主线,持续深耕政府采购全过程咨询、工程全过程咨询与投融资咨询领域,深化数智化 AI 配套服务,全力打造 <strong>诚信、务实、创新、高效</strong> 的行业标杆品牌,为各级政企客户持续赋能。</p>`
</script>
@@ -0,0 +1,61 @@
<template>
<article class="min-h-screen py-16 bg-white">
<!-- Hero -->
<header class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 mb-12 text-center">
<span class="inline-block px-3 py-1 rounded-full bg-blue-50 text-blue-600 text-sm font-semibold mb-4">关于汇吉采</span>
<h1 class="text-3xl sm:text-4xl font-bold text-gray-900 mb-3">发展历程</h1>
<p class="text-gray-600 max-w-2xl mx-auto"> 2017 年起步到体系化平台化持续发力汇吉采的关键发展脉络</p>
</header>
<!-- 正文 -->
<section class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 mb-16">
<RichText :content="content" />
</section>
<!-- CTA -->
<section class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="bg-blue-600 rounded-2xl p-8 sm:p-10 text-center text-white">
<h2 class="text-2xl font-bold mb-3">了解汇吉采最新进展</h2>
<p class="mb-6 opacity-90">吉采动态 / 行业洞察 / 政策合规讲堂</p>
<NuxtLink
to="/article/4709"
class="inline-block px-6 py-3 bg-white text-blue-600 rounded-full font-semibold hover:bg-blue-50 transition-colors"
>查看吉采动态</NuxtLink>
</div>
</section>
</article>
</template>
<script setup lang="ts">
/**
* 模板 7 - 发展历程(写死,10626 租户专属)
* PPT 暂无现成时间线,本页采用 PPT 提供的「资质 / 平台 / 体系」关键节点作为发展脉络。
*/
useHead({
title: '发展历程 - 广西汇吉采咨询有限公司',
meta: [
{ name: 'description', content: '汇吉采自2017年成立以来,已通过ISO9001/14001/45001三大国际认证、AAA信用评价、国家级与省级政府采购双重备案、全国投资项目在线审批监管平台登记。' },
{ name: 'keywords', content: '汇吉采,发展历程,ISO9001,AAA信用,政府采购' }
]
})
const content = `<h2>汇吉采发展脉络</h2>
<p>广西汇吉采咨询有限公司自 <strong>2017 年</strong>成立以来,以"让专业采购咨询引领行业发展"为使命,依托广西区域政府采购数智化发展契机,逐步建成立足南宁、辐射广西、覆盖多板块的综合性专业咨询服务机构。</p>
<h3>2017 · 起步</h3>
<p>在南宁设立总部,聚焦政府采购咨询主业,组建核心顾问与执行团队,覆盖政府采购、货物服务、内控咨询等核心赛道。</p>
<h3>体系化建设 · 三大 ISO 国际认证</h3>
<p>全面通过 ISO9001 质量管理体系、ISO14001 环境管理体系、ISO45001 职业健康安全管理体系三大国际标准认证,建立严谨、科学的内部管理机制,确保运营的规范化与标准化。</p>
<h3>权威信用背书 · AAA 信用评价</h3>
<p>通过中盛华科(北京)信用评价有限公司全面认证,建立 <strong>AAA 信用评价体系</strong>,以规范化运营、诚信服务筑牢发展基石。</p>
<h3>国家级 + 省级双重平台备案</h3>
<p>完成 <strong>中国政府采购网(国家级权威平台)</strong> 与 <strong>广西政府采购网(省级平台)</strong> 的双重登记备案,具备承接各级政府及企事业单位采购代理业务的合法资质与专业能力;同步完成 <strong>全国投资项目在线审批监管平台</strong> 登记。</p>
<h3>数智化平台搭建</h3>
<p>搭建数智化管理平台,配备招标业务管理系统、线上流程审批系统、OA 办公系统,实现全区远程移动办公,以数智化手段提升业务办理效率与服务规范性。</p>
<h3>持续发力</h3>
<p>履行合同履约率 <strong>100%</strong>、客户满意度 <strong>98%</strong> 以上的质量管理目标;持续深耕政府采购全过程咨询、工程全过程咨询、投融资咨询领域,深化数智化 AI 配套服务,全力打造诚信、务实、创新、高效的行业标杆品牌。</p>`
</script>
+123
View File
@@ -0,0 +1,123 @@
<template>
<div>
<!-- 首屏受后台 hero / banner 开关控制 -->
<HeroSection :show-banner="homeBlocks.banner" :show-hero-features="homeBlocks.hero" />
<!-- 我们的优势FeatureSection 内部已根据 advantages.enabled 自显隐 -->
<FeatureSection />
<!-- 关于我们 -->
<AboutSection
title-color="text-gray-900"
accent-color="#3b82f6"
summary-color="text-gray-600"
link-color="text-blue-600"
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="bg-gray-100"
image-object-fit="object-cover"
custom-class="py-16 lg:py-24"
/>
<!-- 产品展示受后台产品展示区块开关控制全站跨栏目精选置顶产品 top>0 -->
<RecommendProductsSection
v-if="showProducts"
title="产品展示"
subtitle="了解我们的核心产品与解决方案"
accent-color="#3b82f6"
title-color="#111827"
subtitle-color="#4b5563"
:limit="3"
:more-link="productNav?.path || '/products'"
more-text="查看更多"
/>
<!-- 案例展示受后台案例展示区块开关控制推荐/置顶优先无推荐回退最新案例 -->
<RecommendCasesSection
v-if="showCases"
title="标杆案例"
subtitle="真实案例呈现,见证客户成功"
accent-color="#3b82f6"
title-color="#111827"
subtitle-color="#4b5563"
:limit="3"
:more-link="caseNav?.path || '/case'"
more-text="查看更多案例"
/>
<!-- 最新动态受后台最新动态区块开关控制推荐文章优先无推荐回退最新 -->
<RecommendArticlesSection
v-if="showNews"
title="最新动态"
subtitle="了解企业最新资讯与行业动态"
accent-color="#3b82f6"
title-color="#111827"
subtitle-color="#4b5563"
:limit="3"
:fallback-to-latest="true"
:more-link="newsNav?.path || '/news'"
more-text="查看更多"
/>
<!-- 联系我们受后台 cta 开关控制 -->
<section v-if="showCta" class="py-16 lg:py-20 bg-blue-50">
<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-gray-900 mb-4" data-reveal>
准备好开始了吗
</h2>
<p class="text-gray-600 max-w-2xl mx-auto mb-8" data-reveal data-reveal-delay="100">
立即联系我们获取专属企业官网解决方案
</p>
<button
type="button"
class="inline-flex items-center justify-center px-8 py-3 bg-blue-600 text-white font-semibold rounded-lg hover:bg-blue-700 transition-colors anim-shimmer"
@click="openConsult()"
>
立即咨询
</button>
</div>
</section>
</div>
</template>
<script setup lang="ts">
/**
* 模板 7 - 首页
* 对齐后台「首页区块总览」开关(siteInfo.setting.features,经 useFeatures 读取):
* hero / banner 控制 HeroSectionadvantages 由 FeatureSection 内部处理;
* products / cases / news / cta 由各区块 v-if 控制。
* 产品 / 案例 / 资讯三个区块复用共享组件
* RecommendProductsSection / RecommendCasesSection / RecommendArticlesSection
* (读取后台推荐 / 置顶精选内容,全站跨栏目)。
*/
import HeroSection from '../components/HeroSection.vue'
import FeatureSection from '../components/FeatureSection.vue'
import { useFeatures } from '~/composables/useFeatures'
import type { CmsNavigation } from '~/types'
const { navigations, fetchSiteInfo } = useSite()
const { openConsult } = useConsult()
const { homeBlocks } = useFeatures([])
// 获取站点信息(含导航)
await fetchSiteInfo()
/** 各区块显隐开关(后台首页区块总览) */
const showProducts = computed(() => homeBlocks.value.products)
const showCases = computed(() => homeBlocks.value.cases)
const showNews = computed(() => homeBlocks.value.news)
const showCta = computed(() => homeBlocks.value.cta)
/** 从导航中定位各模块,用于「查看更多」跳转 */
const newsNav = computed<CmsNavigation | undefined>(() => {
const navs = navigations.value || []
return navs.find((n) => n.model === 'article')
})
const productNav = computed<CmsNavigation | undefined>(() => {
const navs = navigations.value || []
return navs.find((n) => n.model === 'product')
})
const caseNav = computed<CmsNavigation | undefined>(() => {
const navs = navigations.value || []
return navs.find((n) => n.model === 'case')
})
</script>
@@ -0,0 +1,63 @@
<template>
<div class="min-h-screen py-16 bg-white">
<div class="container mx-auto px-4 sm:px-6 lg:px-8">
<div v-if="article" class="max-w-4xl mx-auto">
<div class="mb-8">
<NuxtLink :to="`/article${article?.navigationId ? '?navId=' + article.navigationId : ''}`" class="text-sm text-gray-500 hover:text-blue-600"> 返回新闻列表</NuxtLink>
</div>
<h1 class="text-3xl sm:text-4xl font-bold text-gray-900 mb-4">
{{ article.title }}
</h1>
<div class="flex flex-wrap items-center gap-4 text-sm text-gray-500 mb-8 pb-8 border-b border-gray-100">
<span v-if="article.categoryName" class="px-3 py-1 bg-blue-50 text-blue-600 rounded-full">
{{ article.categoryName }}
</span>
<span>发布时间{{ formatDate(article.publishTime || article.createTime) }}</span>
<span v-if="article.author">作者{{ article.author }}</span>
</div>
<div v-if="article.image || article.cover" class="aspect-video bg-gray-100 rounded-xl overflow-hidden mb-10">
<img
:src="fileUrl(article.image || article.cover)"
:alt="article.title"
class="w-full h-full object-cover"
>
</div>
<RichText :content="article.content" />
<PageAttachments
v-if="article?.files?.length"
:attachments="article.files"
/>
</div>
<div v-else class="text-center py-20">
<h1 class="text-2xl font-bold text-gray-900 mb-4">文章不存在或已下架</h1>
<NuxtLink :to="`/article${article?.navigationId ? '?navId=' + article.navigationId : ''}`" class="text-blue-600 hover:underline">返回新闻列表</NuxtLink>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import dayjs from 'dayjs'
import type { Article } from '~/types'
/**
* 模板 1 - 新闻详情页
*/
const route = useRoute()
const id = route.params.id as string
const { fileUrl } = useFileUrl()
const { data: article } = await useFetch<Article | null>(`/api/article/detail?id=${id}`, {
key: `article-${id}`
})
function formatDate(date?: string) {
if (!date) return ''
return dayjs(date).format('YYYY-MM-DD')
}
</script>
@@ -0,0 +1,191 @@
<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">{{ pageTitle }}</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>
<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-xl overflow-hidden shadow-sm hover:shadow-md transition-shadow"
>
<NuxtLink :to="`/article/${item.id || item.articleId}`">
<div class="aspect-video bg-gray-100 flex items-center justify-center">
<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-cover"
>
<span v-else class="text-gray-400">暂无图片</span>
</div>
</NuxtLink>
<div class="p-5">
<div class="text-xs text-gray-500 mb-2">
{{ formatDate(item.publishTime || item.createTime) }}
<span v-if="item.categoryName" class="ml-2 text-blue-600">{{ item.categoryName }}</span>
</div>
<h2 class="text-lg font-semibold text-gray-900 mb-2 line-clamp-2 hover:text-blue-600 transition-colors">
<NuxtLink :to="`/article/${item.id || item.articleId}`">{{ item.title }}</NuxtLink>
</h2>
<p class="text-sm text-gray-600 line-clamp-2">{{ stripHtml(item.summary) }}</p>
</div>
</article>
</div>
<!-- 空状态 -->
<div v-else class="text-center py-20">
<svg class="w-16 h-16 mx-auto text-gray-300 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-gray-500">暂无新闻内容</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-blue-600 text-white'
: 'bg-white text-gray-700 hover:bg-blue-50'"
@click="currentPage = p"
>
{{ p }}
</button>
</nav>
</div>
</template>
</div>
</div>
</template>
<script setup lang="ts">
/**
* 模板 1 - 新闻列表页
* 从路由参数获取 navigationId,调用 /api/article/list 获取文章列表
* 支持两种入口:
* 1. /article/:navigationId → CMS 导航路径
* 2. /news → 传统路径(自动从 topNavs 中查找 model=article 的栏目)
*/
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
*
* 优先级(2026-07-21 修正):
* 1. route.query.navId(查询参数 ?navId=xxx,优先——导航子分类下拉切换时通过此方式传入,
* 必须高于 route.params.id,否则 /article/4277?navId=4278 会因 params.id=4277 先命中而忽略 navId
* 2. route.params.id(路由参数 /article/:navigationId
* 3. navigations 中 model=article 的第一个栏目(兜底)
*/
const navigationId = computed<number | undefined>(() => {
// 1. 查询参数优先(导航子分类下拉切换时传入)
const queryNavId = route.query.navId as string
if (queryNavId) {
const num = Number(queryNavId)
if (!Number.isNaN(num)) return num
}
// 2. 路由参数
const routeId = route.params.id as string
if (routeId) {
const num = Number(routeId)
if (!Number.isNaN(num)) return num
}
// 3. 兜底:从 navigations 中查找 model=article 的导航
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 found = findNav(item.children)
if (found) return found
}
}
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
// 响应式 query + watch 选项触发翻页/换栏目的重新请求;
// key 带页码,保证每一页独立缓存、翻页必然重新拉取(避免共用 key 被缓存返回第 1 页)。
// keywords 变化时同样触发重新请求,关键词搜索忽略栏目、全局检索。
const { data, pending, error } = await useFetch<
ApiEnvelope<PageResult<Article>> | PageResult<Article>
>('/api/article/list', {
key: `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(() => {
return Math.ceil(totalCount.value / limit)
})
// 栏目切换 / 关键词变化时回到第 1 页(watch 选项已负责重新请求)
watch([navigationId, keywords], () => {
currentPage.value = 1
})
function formatDate(date?: string) {
if (!date) return ''
return dayjs(date).format('YYYY-MM-DD')
}
</script>
+190
View File
@@ -0,0 +1,190 @@
<template>
<!--
10626 租户专属路由 path 路由到写死模板覆盖走 cms-page 的通用逻辑
其他 cms-page 仍走下方默认渲染保证非专属 cms-page 行为不变
-->
<BranchOffice v-if="isBranchOffice" />
<History v-else-if="isHistory" />
<Culture v-else-if="isCulture" />
<div v-else class="min-h-[60vh] flex items-center py-20 bg-white">
<div class="container mx-auto px-4 sm:px-6 lg:px-8">
<!-- 顶部栏目标题 + 自身内容 -->
<div v-if="pageTitle" class="max-w-4xl mx-auto">
<h1 class="text-3xl sm:text-4xl font-bold text-gray-900 mb-6 text-center">
{{ pageTitle }}
</h1>
<div v-if="pageData && pageData.hasContent">
<div v-if="pageData.updateTime" class="text-gray-500 text-sm text-center mb-10">
更新时间{{ formatDate(pageData.updateTime) }}
</div>
<RichText :content="pageData.content" />
</div>
<PageAttachments
v-if="pageData?.attachments?.length"
:attachments="pageData.attachments"
/>
</div>
<!-- 子栏目聚合列表 -->
<div v-if="childCards.length" class="max-w-5xl mx-auto mt-16">
<h2 class="text-2xl font-bold text-gray-900 mb-8 text-center">子栏目</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<NuxtLink
v-for="p in childCards"
:key="p.navigationId"
:to="`/page/${p.navigationId}`"
class="block p-6 rounded-xl border border-gray-200 hover:shadow-lg hover:border-blue-400 transition"
>
<h3 class="text-lg font-semibold text-gray-900 mb-2">{{ p.title }}</h3>
<p class="text-sm text-gray-500 leading-relaxed line-clamp-3">{{ excerpt(p.content) }}</p>
</NuxtLink>
</div>
</div>
<!-- 空状态 -->
<div v-if="!pageData?.hasContent && !childCards.length" class="text-center py-20">
<h1 class="text-2xl font-bold text-gray-900 mb-4">{{ pageTitle }}</h1>
<p class="text-gray-500 mb-2">{{ emptyState.title }}</p>
<p class="text-gray-400 text-sm mb-6 leading-relaxed">
{{ emptyState.desc }}<br />
当前导航{{ currentNav?.title || pageTitle }}ID{{ navigationId }}
</p>
<NuxtLink to="/" class="text-blue-600 hover:underline">返回首页</NuxtLink>
</div>
</div>
</div>
</template>
<script setup lang="ts">
/**
* 模板 7 - 通用 CMS 页面
* 支持两种入口:
* 1. /page/:navigationId → CMS 导航路径(优先)
* 2. /:slug → 传统路径
*
* 父栏目聚合:当当前 page 栏目拥有子栏目时,递归收集其下所有子栏目的单页,
* 以卡片列表形式聚合展示(每个子页卡片链接到 /page/{childNavId} 详情页)。
*/
import dayjs from 'dayjs'
import type { CmsNavigation, PageDetail } from '~/types'
import { collectDescendantNavIds, isParentNavigation } from '~/utils/nav-tree'
// 10626 租户专属:path=jigou/fzlc/wenhua 时直接走写死模板,不依赖 cms-page
import BranchOffice from './BranchOffice.vue'
import History from './History.vue'
import Culture from './Culture.vue'
interface ChildPage {
pageId?: number
title?: string
path?: string
content?: string
navigationId?: number
image?: string | null
}
const route = useRoute()
const { allNavigations, fetchSiteInfo } = useSite()
await fetchSiteInfo()
/** 从路由获取 navigationId */
const navigationId = computed(() => {
const id = route.params.id as string
if (id) {
const num = Number(id)
if (!Number.isNaN(num)) return num
}
return undefined
})
/** 当前栏目是否为父栏目(拥有子栏目) */
const hasChildren = computed(() => isParentNavigation(navigationId.value, allNavigations.value || []))
/** 递归收集当前栏目自身 + 所有后代 navigationId */
const descendantNavIds = computed(() =>
collectDescendantNavIds(navigationId.value, allNavigations.value || [])
)
/** 从导航中查找当前页面信息(使用 allNavigations,已做标题映射) */
const currentNav = computed<CmsNavigation | undefined>(() => {
if (!navigationId.value) return undefined
const findNav = (items: CmsNavigation[]): CmsNavigation | undefined => {
for (const item of items) {
if (item.navigationId === navigationId.value) return item
if (item.children?.length) {
const found = findNav(item.children)
if (found) return found
}
}
return undefined
}
return findNav(allNavigations.value)
})
/** 获取页面内容 */
const navId = navigationId.value
const pagePath = (() => {
const num = Number(route.params.id)
return Number.isNaN(num) ? (route.params.id as string) : undefined
})()
const { data: pageData } = await useFetch<PageDetail>('/api/page/detail', {
key: `page-${navId ?? pagePath ?? 'default'}`,
query: { navigationId: navId, path: pagePath }
})
/** 父栏目聚合:取其下所有子栏目的单页(不含父栏目自身,避免与顶部内容重复) */
const childPages = ref<ChildPage[]>([])
if (hasChildren.value && descendantNavIds.value.length) {
const { data: childrenData } = await useFetch<{ list: ChildPage[]; count: number }>('/api/page/children', {
key: `page-children-${navId}`,
query: { navigationIds: descendantNavIds.value.join(',') }
})
childPages.value = childrenData.value?.list || []
}
const childCards = computed(() => childPages.value.filter(p => p.navigationId !== navId))
const pageTitle = computed(() => {
return pageData.value?.title || currentNav.value?.title || '页面'
})
/** 空内容兜底文案,按状态区分「未录入」与「接口异常」 */
const emptyState = computed(() => {
const status = pageData.value?.status
if (status === 'error') {
return { title: '内容暂时无法加载', desc: '内容接口暂时不可用,请稍后再试。' }
}
return { title: '内容尚未录入', desc: '该页面正文尚未在 CMS 管理后台配置,请前往 CMS 后台为该导航补充内容。' }
})
/** 富文本正文截取纯文本摘要 */
function excerpt(html?: string, len = 80) {
if (!html) return ''
const text = (html || '')
.replace(/<[^>]+>/g, '')
.replace(/&nbsp;/g, ' ')
.replace(/\s+/g, ' ')
.trim()
return text.length > len ? text.slice(0, len) + '…' : text
}
function formatDate(date?: string) {
if (!date) return ''
return dayjs(date).format('YYYY-MM-DD')
}
/**
* 10626 租户专属:slug 路径命中时路由到写死模板,跳过 cms-page 渲染。
* - jigou → 分支机构(BranchOffice
* - fzlc → 发展历程(History
* - wenhua → 企业文化(Culture
* 数字 navigationId 走下方原逻辑(cms_design / cms_design.content),保持不变。
*/
const slug = computed(() => {
const id = route.params.id as string
if (!id || !Number.isNaN(Number(id))) return ''
return id
})
const isBranchOffice = computed(() => slug.value === 'jigou')
const isHistory = computed(() => slug.value === 'fzlc')
const isCulture = computed(() => slug.value === 'wenhua')
</script>
@@ -0,0 +1,64 @@
<template>
<div class="min-h-screen py-16 bg-white">
<div class="container mx-auto px-4 sm:px-6 lg:px-8">
<div v-if="product" class="max-w-5xl mx-auto">
<div class="mb-8">
<NuxtLink :to="`/product${product?.navigationId ? '?navId=' + product.navigationId : ''}`" class="text-sm text-gray-500 hover:text-blue-600"> 返回产品列表</NuxtLink>
</div>
<div class="grid lg:grid-cols-2 gap-10 mb-12">
<div class="aspect-video bg-gray-100 rounded-xl overflow-hidden">
<img
v-if="product.cover"
:src="fileUrl(product.cover)"
:alt="product.productName"
class="w-full h-full object-cover"
>
</div>
<div>
<h1 class="text-3xl font-bold text-gray-900 mb-4">{{ product.productName }}</h1>
<p v-if="product.subtitle" class="text-lg text-gray-600 mb-6">{{ product.subtitle }}</p>
<p class="text-gray-600 leading-relaxed mb-6">{{ stripHtml(product.description) }}</p>
<div v-if="product.price" class="text-2xl font-bold text-blue-600 mb-6">
{{ product.price }}
</div>
<button
type="button"
class="inline-flex items-center justify-center px-8 py-3 bg-blue-600 text-white font-semibold rounded-lg hover:bg-blue-700 transition-colors"
@click="openConsult({ need: `咨询产品:${product.productName}`, source: 'product-detail' })"
>
立即咨询
</button>
</div>
</div>
<div class="border-t border-gray-100 pt-10">
<h2 class="text-2xl font-bold text-gray-900 mb-6">产品详情</h2>
<RichText :content="product.content" />
</div>
</div>
<div v-else class="text-center py-20">
<h1 class="text-2xl font-bold text-gray-900 mb-4">产品不存在或已下架</h1>
<NuxtLink :to="`/product${product?.navigationId ? '?navId=' + product.navigationId : ''}`" class="text-blue-600 hover:underline">返回产品列表</NuxtLink>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import type { Product } from '~/types'
/**
* 模板 1 - 产品详情页
*/
const route = useRoute()
const id = route.params.id as string
const { fileUrl } = useFileUrl()
const { openConsult } = useConsult()
const { data: product } = await useFetch<Product | null>(`/api/product/detail?id=${id}`, {
key: `product-${id}`
})
</script>
@@ -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>
@@ -0,0 +1,80 @@
<template>
<div class="min-h-screen flex items-center justify-center bg-gradient-to-br from-[#1a6dff] to-[#0d4fb8] text-white">
<div class="container mx-auto px-4 sm:px-6 lg:px-8">
<div class="max-w-2xl mx-auto text-center">
<div class="w-20 h-20 mx-auto mb-8 rounded-full bg-white/20 flex items-center justify-center">
<svg class="w-10 h-10 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<h1 class="text-3xl sm:text-4xl font-bold mb-4">
网站服务已到期
</h1>
<p class="text-lg text-blue-100 mb-8">
您访问的企业官网服务已过期请联系管理员续费以恢复正常访问
</p>
<div class="bg-white/10 backdrop-blur-sm rounded-2xl p-6 sm:p-8 mb-8 text-left">
<h2 class="text-xl font-semibold mb-4">续费后可继续使用</h2>
<ul class="space-y-3 text-blue-100">
<li class="flex items-center gap-2">
<svg class="w-5 h-5 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
企业官网正常展示
</li>
<li class="flex items-center gap-2">
<svg class="w-5 h-5 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
产品/案例/新闻内容展示
</li>
<li class="flex items-center gap-2">
<svg class="w-5 h-5 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
在线留言与客户咨询
</li>
<li class="flex items-center gap-2">
<svg class="w-5 h-5 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
独立域名访问
</li>
</ul>
</div>
<button
class="inline-flex items-center justify-center px-8 py-3 bg-white text-blue-600 font-semibold rounded-lg hover:bg-blue-50 transition-colors"
@click="goToRenew"
>
立即续费
</button>
<p v-if="siteInfo?.phone" class="mt-6 text-sm text-blue-100">
如需帮助请拨打{{ siteInfo.phone }}
</p>
</div>
</div>
</div>
</template>
<script setup lang="ts">
/**
* 模板 1 - 续费引导页
*/
const { siteInfo, fetchSiteInfo } = useSite()
await fetchSiteInfo()
definePageMeta({
layout: 'blank'
})
function goToRenew() {
// 实际项目中应跳转 SaaS 管理后台续费页面
const appId = useRuntimeConfig().public.appId
window.open(`/api/subscription/renew?appId=${appId}`, '_blank')
}
</script>
@@ -0,0 +1,84 @@
<template>
<article class="min-h-screen py-16 bg-white">
<!-- Hero -->
<header class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 mb-12 text-center">
<span class="inline-block px-3 py-1 rounded-full bg-blue-50 text-blue-600 text-sm font-semibold mb-4">关于汇吉采</span>
<h1 class="text-3xl sm:text-4xl font-bold text-gray-900 mb-3">专业人才团队</h1>
<p class="text-gray-600 max-w-2xl mx-auto">高级专业技术人员占比 77%注册持证人才占比 90%+多赛道实战型咨询专家团队</p>
</header>
<!-- 数据 -->
<section class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 mb-16">
<div class="grid grid-cols-1 sm:grid-cols-3 gap-6">
<div class="text-center p-6 rounded-2xl bg-blue-50">
<div class="text-3xl sm:text-4xl font-bold text-blue-600 mb-2">77%</div>
<div class="text-gray-700">高级专业技术人员占比</div>
</div>
<div class="text-center p-6 rounded-2xl bg-blue-50">
<div class="text-3xl sm:text-4xl font-bold text-blue-600 mb-2">90%+</div>
<div class="text-gray-700">中高级工程师 / 注册持证人才占比</div>
</div>
<div class="text-center p-6 rounded-2xl bg-blue-50">
<div class="text-3xl sm:text-4xl font-bold text-blue-600 mb-2">多赛道</div>
<div class="text-gray-700">覆盖工程造价 / 咨询 / 监理 / 招标等</div>
</div>
</div>
</section>
<!-- 正文 -->
<section class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 mb-16">
<RichText :content="content" />
</section>
<!-- CTA -->
<section class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="bg-blue-600 rounded-2xl p-8 sm:p-10 text-center text-white">
<h2 class="text-2xl font-bold mb-3">需要专家团队服务</h2>
<p class="mb-6 opacity-90">持证专家领衔多赛道协同为各类项目提供专业人才支撑</p>
<NuxtLink
to="/contact"
class="inline-block px-6 py-3 bg-white text-blue-600 rounded-full font-semibold hover:bg-blue-50 transition-colors"
>联系我们</NuxtLink>
</div>
</section>
</article>
</template>
<script setup lang="ts">
/**
* 模板 7 - 专业人才团队(写死,10626 租户专属)
* 来源 PPT 第 4 页「汇吉采简介 · 人才板块」
*/
useHead({
title: '专业人才团队 - 广西汇吉采咨询有限公司',
meta: [
{ name: 'description', content: '汇吉采高级专业技术人员占比77%,中高级工程师、各类注册持证人才超90%;覆盖政府采购、货物服务、内控咨询、土建施工、全过程造价、工程评估、招标监理、质量检测等核心赛道。' },
{ name: 'keywords', content: '汇吉采,专业人才,注册造价工程师,招标师,注册监理工程师,政府采购' }
]
})
const content = `<h2>专业人才团队</h2>
<p>人才是汇吉采最核心的竞争力。公司坚持"以专业人才支撑专业服务"的理念,构建了一支覆盖政府采购、工程建设、造价管控、项目管理多领域的高素质咨询与执行队伍。</p>
<h3>人才结构</h3>
<ul>
<li>高级专业技术人员占比 <strong>77%</strong></li>
<li>中高级工程师、各类<strong>注册持证人才</strong>占比 <strong>90% 以上</strong></li>
</ul>
<h3>持证专家</h3>
<p>团队由多名注册造价工程师、水利造价工程师、注册咨询师、招标师、注册监理工程师等持证专家领衔,专业覆盖:</p>
<ul>
<li>政府采购</li>
<li>货物服务</li>
<li>内控咨询</li>
<li>土建施工</li>
<li>全过程造价</li>
<li>工程评估</li>
<li>招标监理</li>
<li>质量检测</li>
</ul>
<h3>实战能力</h3>
<p>团队实操经验充足,职业素养过硬,以务实求精的服务标准,为各类项目提供从前期策划、过程咨询到结算交付的<strong>专业人才支撑</strong>。</p>
<p>一体化咨询服务的标准化流程与品控机制,确保每项目"有制度、有专员、有复核、有总结",让客户在每一个关键节点都能获得稳定的专业输出。</p>`
</script>
+59
View File
@@ -0,0 +1,59 @@
<template>
<Component :is="component" v-if="component" />
<SiteLoading v-else />
</template>
<script setup lang="ts">
/**
* 模板 1 - 通用模板页面选择器
* 根据路由动态选择对应页面组件
*/
const route = useRoute()
const component = shallowRef<Component | null>(null)
const routeMap: Record<string, () => Promise<{ default: Component }>> = {
news: () => import('./NewsList.vue'),
'news-id': () => import('./NewsDetail.vue'),
products: () => import('./ProductList.vue'),
'products-id': () => import('./ProductDetail.vue'),
cases: () => import('./CaseList.vue'),
'cases-id': () => import('./CaseDetail.vue'),
contact: () => import('./Contact.vue'),
renewal: () => import('./Renewal.vue'),
buy: () => import('./BuyDocument.vue')
}
async function resolveComponent() {
const name = route.name as string | undefined
if (name === 'index') {
const mod = await import('./Home.vue')
component.value = mod.default
return
}
if (name === 'slug') {
const mod = await import('./Page.vue')
component.value = mod.default
return
}
if (name) {
const loader = routeMap[name]
if (loader) {
const mod = await loader()
component.value = mod.default
return
}
}
component.value = null
}
if (import.meta.server) {
await resolveComponent()
}
onMounted(resolveComponent)
</script>