feat(home): 重构首页界面并移除文章相关页面
- 添加公司信息配置文件,包含项目名称、地址、经营范围等 - 实现404页面路由,显示页面建设中提示和导航按钮 - 在首页集成公司信息展示,包括经营范围和资质信息 - 移除文章列表页、文章详情页、栏目页和单页内容相关功能 - 更新Ant Design主题配色为绿色主色调 - 简化首页布局,突出业务板块和服务导向设计 - 删除部署方案和开通流程等临时页面内容
This commit is contained in:
@@ -1,242 +0,0 @@
|
||||
<template>
|
||||
<main class="min-h-screen bg-gray-50 px-4 py-8">
|
||||
<div class="mx-auto max-w-screen-xl space-y-6">
|
||||
<a-skeleton v-if="navPending" active :paragraph="{ rows: 8 }" />
|
||||
|
||||
<a-result
|
||||
v-else-if="navError"
|
||||
status="error"
|
||||
title="页面加载失败"
|
||||
:sub-title="String(navError)"
|
||||
/>
|
||||
|
||||
<template v-else>
|
||||
<a-card class="shadow-sm">
|
||||
<template #title>
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="text-lg font-semibold text-gray-900">
|
||||
{{ parentTitle }}
|
||||
</div>
|
||||
<div v-if="parentDescription" class="text-sm text-gray-500">
|
||||
{{ parentDescription }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<a-image
|
||||
v-if="parentBanner"
|
||||
:src="parentBanner"
|
||||
:preview="false"
|
||||
class="mb-5 w-full"
|
||||
/>
|
||||
|
||||
<a-layout class="bg-transparent">
|
||||
<a-layout-sider
|
||||
v-if="children.length"
|
||||
theme="light"
|
||||
:width="240"
|
||||
class="rounded-xl border border-black/5 bg-white overflow-hidden"
|
||||
breakpoint="lg"
|
||||
collapsed-width="0"
|
||||
>
|
||||
<a-menu
|
||||
mode="inline"
|
||||
:selected-keys="selectedKeys"
|
||||
@click="onMenuClick"
|
||||
>
|
||||
<a-menu-item v-for="n in children" :key="String(n.navigationId)">
|
||||
{{ n.title || n.label || `栏目 ${n.navigationId}` }}
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</a-layout-sider>
|
||||
|
||||
<a-layout-content :class="children.length ? 'pl-0 lg:pl-6 pt-6 lg:pt-0' : ''">
|
||||
<a-skeleton v-if="contentPending" active :paragraph="{ rows: 10 }" />
|
||||
|
||||
<a-result
|
||||
v-else-if="contentError"
|
||||
status="error"
|
||||
title="内容加载失败"
|
||||
:sub-title="String(contentError)"
|
||||
/>
|
||||
|
||||
<template v-else>
|
||||
<div class="space-y-2">
|
||||
<div class="text-2xl font-semibold text-gray-900">
|
||||
{{ contentTitle }}
|
||||
</div>
|
||||
<div class="text-sm text-gray-500">
|
||||
<span v-if="articleMeta?.createTime">发布时间:{{ articleMeta.createTime }}</span>
|
||||
<span v-if="articleMeta?.author" class="ml-4">作者:{{ articleMeta.author }}</span>
|
||||
<span v-if="articleMeta?.source" class="ml-4">来源:{{ articleMeta.source }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a-alert
|
||||
v-if="articleMeta?.overview"
|
||||
type="info"
|
||||
show-icon
|
||||
class="my-5"
|
||||
:message="String(articleMeta.overview)"
|
||||
/>
|
||||
|
||||
<a-image
|
||||
v-if="articleMeta?.image"
|
||||
:src="String(articleMeta.image)"
|
||||
:preview="false"
|
||||
class="mb-5 w-full"
|
||||
/>
|
||||
|
||||
<RichText :content="contentBody" />
|
||||
</template>
|
||||
</a-layout-content>
|
||||
</a-layout>
|
||||
</a-card>
|
||||
</template>
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { getCmsArticle } from '@/api/cms/cmsArticle'
|
||||
import { listCmsArticleContent } from '@/api/cms/cmsArticleContent'
|
||||
import RichText from '@/components/RichText.vue'
|
||||
import { usePageSeo } from '@/composables/usePageSeo'
|
||||
import type { ApiEnvelope } from '~/types/api'
|
||||
import type { CmsNavigation } from '@/api/cms/cmsNavigation/model'
|
||||
import type { CmsArticle } from '@/api/cms/cmsArticle/model'
|
||||
|
||||
const route = useRoute()
|
||||
const parentId = computed(() => {
|
||||
const raw = route.params.id
|
||||
const val = Array.isArray(raw) ? raw[0] : raw
|
||||
const n = Number(val)
|
||||
return Number.isFinite(n) ? n : NaN
|
||||
})
|
||||
|
||||
const selectedNavId = computed(() => {
|
||||
const raw = route.query.nav
|
||||
const val = Array.isArray(raw) ? raw[0] : raw
|
||||
const n = Number(val)
|
||||
return Number.isFinite(n) ? n : NaN
|
||||
})
|
||||
|
||||
function pickNavigationList(payload: unknown): CmsNavigation[] {
|
||||
if (Array.isArray(payload)) return payload as CmsNavigation[]
|
||||
const env = payload as ApiEnvelope<CmsNavigation[]>
|
||||
if (env && typeof env === 'object' && Array.isArray(env.data)) return env.data
|
||||
return []
|
||||
}
|
||||
|
||||
const { data: navData, pending: navPending, error: navError } = useAsyncData(
|
||||
() => `cms-navigation-single-${parentId.value}`,
|
||||
async () => {
|
||||
if (!Number.isFinite(parentId.value)) throw new Error('无效的页面ID')
|
||||
// Single-page type: fetch navigation list by parentId.
|
||||
const res = await $fetch<unknown>('/api/cms-navigation', {
|
||||
query: { parentId: String(parentId.value) }
|
||||
})
|
||||
const children = pickNavigationList(res)
|
||||
.filter((n) => (n.hide ?? 0) !== 1)
|
||||
.slice()
|
||||
.sort((a, b) => (a.sortNumber ?? 0) - (b.sortNumber ?? 0))
|
||||
return { children }
|
||||
},
|
||||
{ watch: [parentId] }
|
||||
)
|
||||
|
||||
const children = computed<CmsNavigation[]>(() => navData.value?.children ?? [])
|
||||
|
||||
const activeNav = computed<CmsNavigation | null>(() => {
|
||||
const list = children.value
|
||||
if (!list.length) return null
|
||||
if (Number.isFinite(selectedNavId.value)) {
|
||||
const hit = list.find((n) => n.navigationId === selectedNavId.value)
|
||||
if (hit) return hit
|
||||
}
|
||||
return list[0]
|
||||
})
|
||||
|
||||
// Keep URL stable so refresh/back works (default to first child).
|
||||
if (import.meta.client) {
|
||||
watchEffect(() => {
|
||||
if (!children.value.length) return
|
||||
if (Number.isFinite(selectedNavId.value)) return
|
||||
const first = children.value[0]?.navigationId
|
||||
if (!first) return
|
||||
navigateTo({ path: route.path, query: { ...route.query, nav: String(first) } }, { replace: true })
|
||||
})
|
||||
}
|
||||
|
||||
const selectedKeys = computed(() => {
|
||||
const key = activeNav.value?.navigationId
|
||||
return key ? [String(key)] : []
|
||||
})
|
||||
|
||||
function onMenuClick(info: { key: string }) {
|
||||
navigateTo({ path: route.path, query: { ...route.query, nav: String(info.key) } })
|
||||
}
|
||||
|
||||
async function loadSinglePageContent(nav: CmsNavigation): Promise<{
|
||||
title: string
|
||||
body: string
|
||||
article?: CmsArticle
|
||||
}> {
|
||||
// Common pattern: navigation.itemId binds to an articleId for "single page" content.
|
||||
const articleId =
|
||||
(typeof nav.itemId === 'number' && Number.isFinite(nav.itemId) ? nav.itemId : 0) ||
|
||||
(typeof nav.pageId === 'number' && Number.isFinite(nav.pageId) ? nav.pageId : 0)
|
||||
|
||||
if (articleId > 0) {
|
||||
const article = await getCmsArticle(articleId)
|
||||
let body = String(article.content || article.detail || '').trim()
|
||||
if (!body) {
|
||||
try {
|
||||
const list = await listCmsArticleContent({ articleId, limit: 1 })
|
||||
const hit = Array.isArray(list) ? list[0] : null
|
||||
if (hit?.content) body = String(hit.content).trim()
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
return { title: article.title?.trim() || String(nav.title || '单页'), body, article }
|
||||
}
|
||||
|
||||
// Fallback: show navigation meta as content when no binding exists.
|
||||
return {
|
||||
title: String(nav.title || '单页'),
|
||||
body: String(nav.comments || nav.meta || '').trim()
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
data: contentData,
|
||||
pending: contentPending,
|
||||
error: contentError
|
||||
} = useAsyncData(
|
||||
() => `cms-single-page-content-${parentId.value}-${activeNav.value?.navigationId ?? 'none'}`,
|
||||
async () => {
|
||||
if (!activeNav.value) return { title: '', body: '' }
|
||||
return loadSinglePageContent(activeNav.value)
|
||||
},
|
||||
{ watch: [activeNav] }
|
||||
)
|
||||
|
||||
const parentTitle = computed(() => children.value[0]?.parentName?.trim() || `页面 ${parentId.value}`)
|
||||
const parentDescription = computed(() => '')
|
||||
const parentBanner = computed(() => '')
|
||||
|
||||
const contentTitle = computed(() => contentData.value?.title?.trim() || '')
|
||||
const contentBody = computed(() => contentData.value?.body || '')
|
||||
const articleMeta = computed(() => contentData.value?.article)
|
||||
|
||||
watchEffect(() => {
|
||||
if (!Number.isFinite(parentId.value)) return
|
||||
usePageSeo({
|
||||
title: contentTitle.value || parentTitle.value,
|
||||
description:
|
||||
(articleMeta.value?.overview?.trim() || parentDescription.value || contentTitle.value || parentTitle.value),
|
||||
path: route.fullPath
|
||||
})
|
||||
})
|
||||
</script>
|
||||
Reference in New Issue
Block a user