- 将硬编码的新闻栏目数据替换为从CMS动态获取 - 添加了新闻栏目数据类型定义和解析函数 - 实现了新闻文章链接生成和标题解析功能 - 更新了页面参数传递,将navigationId改为categoryId - 添加了加载状态和空数据状态的UI显示 - 集成了文章详情页的动态路由跳转功能
511 lines
12 KiB
Vue
511 lines
12 KiB
Vue
<template>
|
||
<main class="home">
|
||
<Carousel arrows class="mx-auto" :items="flashSlides" :height="flashHeight">
|
||
<template #prevArrow>
|
||
<div class="custom-slick-arrow" style="left: 10px; z-index: 1">
|
||
<left-circle-outlined />
|
||
</div>
|
||
</template>
|
||
<template #nextArrow>
|
||
<div class="custom-slick-arrow" style="right: 10px">
|
||
<right-circle-outlined />
|
||
</div>
|
||
</template>
|
||
</Carousel>
|
||
|
||
<!-- <section class="banner">-->
|
||
<!-- <div class="mx-auto max-w-screen-xl px-4 py-6">-->
|
||
<!-- <div class="banner-title">以合规经营与品质服务为核心</div>-->
|
||
<!-- </div>-->
|
||
<!-- </section>-->
|
||
|
||
<section class="mx-auto max-w-screen-xl px-4 py-6">
|
||
<div class="section-title">
|
||
<div class="section-title-main">资讯与公告</div>
|
||
<div class="section-title-sub">NEWS & UPDATES</div>
|
||
</div>
|
||
|
||
<div class="mt-6 grid grid-cols-12 gap-6">
|
||
<div v-for="c in columns" :key="c.key" class="col-span-12 lg:col-span-4">
|
||
<div class="panel">
|
||
<div class="column-head">
|
||
<div class="column-title">{{ c.title }}</div>
|
||
<NuxtLink v-if="c.moreTo" :to="c.moreTo" class="column-more">更多 +</NuxtLink>
|
||
<span v-else class="column-more opacity-60">更多 +</span>
|
||
</div>
|
||
<div class="column-list">
|
||
<template v-if="newsPending">
|
||
<div class="column-empty">加载中...</div>
|
||
</template>
|
||
<template v-else-if="!c.items.length">
|
||
<div class="column-empty">暂无文章</div>
|
||
</template>
|
||
<template v-else>
|
||
<NuxtLink
|
||
v-for="(it, idx) in c.items"
|
||
:key="String(it.articleId ?? it.code ?? `${c.key}-${idx}`)"
|
||
class="column-item"
|
||
:to="resolveArticleLink(it, c.navId)"
|
||
>
|
||
{{ resolveArticleTitle(it) }}
|
||
</NuxtLink>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mt-10">
|
||
<div class="section-title">
|
||
<div class="section-title-main">资质与合作</div>
|
||
<div class="section-title-sub">COMPLIANCE</div>
|
||
</div>
|
||
|
||
<div class="mt-6 grid grid-cols-12 gap-6">
|
||
<a-card
|
||
v-for="c in compliance"
|
||
:key="c.title"
|
||
hoverable
|
||
class="case-card col-span-12 sm:col-span-6 lg:col-span-3"
|
||
@click="navigateTo(c.to)"
|
||
>
|
||
<a-typography-title :level="5" class="!mb-2 case-title">{{ c.title }}</a-typography-title>
|
||
<div class="case-desc">{{ c.desc }}</div>
|
||
</a-card>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</main>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import {
|
||
AppstoreOutlined,
|
||
FileTextOutlined,
|
||
LeftCircleOutlined,
|
||
RightCircleOutlined,
|
||
SafetyCertificateOutlined,
|
||
ShopOutlined
|
||
} from '@ant-design/icons-vue'
|
||
import { usePageSeo } from '@/composables/usePageSeo'
|
||
import { getAdByCode } from '@/api/cms/cmsAd'
|
||
import type { CmsAd } from '@/api/cms/cmsAd/model'
|
||
import { COMPANY } from '@/config/company'
|
||
import { listCmsNavigation } from '@/api/cms/cmsNavigation'
|
||
import type { CmsNavigation } from '@/api/cms/cmsNavigation/model'
|
||
import { pageCmsArticle } from '@/api/cms/cmsArticle'
|
||
import type { CmsArticle } from '@/api/cms/cmsArticle/model'
|
||
|
||
usePageSeo({
|
||
title: '首页',
|
||
description:
|
||
'桂乐淘:生物基材料技术研发、技术服务与食品/农产品流通服务。提供经营范围与资质信息查询及合作咨询入口。',
|
||
path: '/'
|
||
})
|
||
|
||
function parsePx(value?: string) {
|
||
if (!value) return undefined
|
||
const m = String(value).match(/(\d+)/)
|
||
if (!m) return undefined
|
||
const n = Number(m[1])
|
||
return Number.isFinite(n) ? n : undefined
|
||
}
|
||
|
||
type FlashImage = {
|
||
url?: string
|
||
path?: string
|
||
title?: string
|
||
}
|
||
|
||
const { data: flashAd } = await useAsyncData<CmsAd | null>('cms-ad-flash', () =>
|
||
getAdByCode('flash').catch(() => null)
|
||
)
|
||
|
||
const flashSlides = computed(() => {
|
||
const list = (flashAd.value?.imageList || []) as FlashImage[]
|
||
const slides = list
|
||
.map((it) => {
|
||
if (!it?.url) return null
|
||
return {
|
||
src: it.url,
|
||
href: it.path || undefined,
|
||
alt: it.title || undefined
|
||
}
|
||
})
|
||
.filter(Boolean) as Array<{ src: string; href?: string; alt?: string }>
|
||
|
||
return slides.length
|
||
? slides
|
||
: [{ src: 'https://file-cloud.yst.com.cn/photo/website/2024/11/28/5a5cc07336224e54a84561c80899bcac.jpg' }]
|
||
})
|
||
|
||
const flashHeight = computed(() => parsePx(flashAd.value?.height) ?? 360)
|
||
|
||
function splitScope(text: string) {
|
||
return text
|
||
.split(/[;;]+/g)
|
||
.map((s) => s.trim())
|
||
.filter(Boolean)
|
||
.map((s) => s.replace(/[。.]$/, '').trim())
|
||
}
|
||
|
||
const generalScopeItems = computed(() => splitScope(COMPANY.scope.general))
|
||
const licensedScopeItems = computed(() => splitScope(COMPANY.scope.licensed))
|
||
|
||
const services = [
|
||
{
|
||
title: '生物基材料技术研发',
|
||
desc: '面向应用场景开展研发与技术支持',
|
||
icon: SafetyCertificateOutlined
|
||
},
|
||
{
|
||
title: '技术服务与技术推广',
|
||
desc: '技术开发/咨询/交流/转让/推广',
|
||
icon: AppstoreOutlined
|
||
},
|
||
{
|
||
title: '食品销售(预包装)',
|
||
desc: '含保健食品(预包装)销售等',
|
||
icon: ShopOutlined
|
||
},
|
||
{
|
||
title: '农产品与生鲜流通',
|
||
desc: '鲜肉/水产/蔬果/蛋类等零售与批发',
|
||
icon: FileTextOutlined
|
||
}
|
||
]
|
||
|
||
const NEWS_PARENT_ID = 4548
|
||
|
||
type HomeNewsColumn = {
|
||
key: string
|
||
navId?: number
|
||
title: string
|
||
moreTo: string | null
|
||
items: CmsArticle[]
|
||
}
|
||
|
||
function resolveNavTitle(nav: CmsNavigation) {
|
||
return String(nav.title || nav.label || nav.code || '').trim()
|
||
}
|
||
|
||
function resolveArticleTitle(a: CmsArticle) {
|
||
return String(a.title || a.code || '未命名文章').trim()
|
||
}
|
||
|
||
function resolveArticleLink(a: CmsArticle, navId?: number) {
|
||
const articleId = typeof a.articleId === 'number' && Number.isFinite(a.articleId) ? a.articleId : NaN
|
||
const code = String(a.code || '').trim()
|
||
return {
|
||
path: '/article-item',
|
||
query: {
|
||
id: Number.isFinite(articleId) ? String(articleId) : undefined,
|
||
code: !Number.isFinite(articleId) && code ? code : undefined,
|
||
navId: typeof navId === 'number' && Number.isFinite(navId) ? String(navId) : undefined
|
||
}
|
||
}
|
||
}
|
||
|
||
const { data: newsRaw, pending: newsPending } = await useAsyncData<HomeNewsColumn[]>(
|
||
`home-news-${NEWS_PARENT_ID}`,
|
||
async () => {
|
||
const navs = await listCmsNavigation({ parentId: NEWS_PARENT_ID }).catch(() => [])
|
||
const top3 = [...navs]
|
||
.filter((it) => typeof it?.navigationId === 'number')
|
||
.sort((a, b) => (a.sortNumber ?? 0) - (b.sortNumber ?? 0))
|
||
.slice(0, 3)
|
||
|
||
const bundles = await Promise.all(
|
||
top3.map(async (nav) => {
|
||
const navId = nav.navigationId
|
||
const title = resolveNavTitle(nav) || (typeof navId === 'number' ? `栏目 ${navId}` : '栏目')
|
||
const page = typeof navId === 'number'
|
||
? await pageCmsArticle({ categoryId: navId, page: 1, limit: 10 }).catch(() => null)
|
||
: null
|
||
|
||
return {
|
||
key: String(navId ?? title),
|
||
navId,
|
||
title,
|
||
moreTo: typeof navId === 'number' ? `/article/${navId}` : null,
|
||
items: page?.list ?? []
|
||
} satisfies HomeNewsColumn
|
||
})
|
||
)
|
||
|
||
return bundles
|
||
}
|
||
)
|
||
|
||
const columns = computed<HomeNewsColumn[]>(() => {
|
||
if (newsRaw.value?.length) return newsRaw.value
|
||
if (!newsPending.value) return []
|
||
// Keep layout stable while SSR/client is loading.
|
||
return [
|
||
{ key: 'news-loading-1', navId: undefined, title: '加载中', moreTo: null, items: [] },
|
||
{ key: 'news-loading-2', navId: undefined, title: '加载中', moreTo: null, items: [] },
|
||
{ key: 'news-loading-3', navId: undefined, title: '加载中', moreTo: null, items: [] }
|
||
]
|
||
})
|
||
|
||
const compliance = [
|
||
{
|
||
title: '产品展示',
|
||
desc: '一般项目/许可项目明细与说明',
|
||
to: '/goods/4476'
|
||
},
|
||
{
|
||
title: '注册地址',
|
||
desc: '南宁市江南区国凯大道东13号神冠胶原智库项目加工厂房',
|
||
to: '/contact'
|
||
},
|
||
{
|
||
title: '合作咨询',
|
||
desc: '提交需求与合作意向,我们尽快联系',
|
||
to: '/contact'
|
||
},
|
||
{
|
||
title: '更多内容',
|
||
desc: '后续可接入资讯、产品与资质展示',
|
||
to: '/contact'
|
||
}
|
||
]
|
||
|
||
function scrollToCompany() {
|
||
if (!import.meta.client) return
|
||
const el = document.querySelector('#company')
|
||
if (!el) return
|
||
el.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.home {
|
||
background: #f4f6f8;
|
||
}
|
||
|
||
.panel {
|
||
background: #fff;
|
||
height: 360px;
|
||
border: 1px solid rgba(0, 0, 0, 0.06);
|
||
border-radius: 10px;
|
||
overflow: hidden;
|
||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.04);
|
||
}
|
||
|
||
.panel-head {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 12px 14px;
|
||
background: linear-gradient(90deg, #16a34a, #22c55e);
|
||
color: #fff;
|
||
}
|
||
|
||
.panel-title {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
font-weight: 800;
|
||
}
|
||
|
||
.panel-more {
|
||
color: rgba(255, 255, 255, 0.9);
|
||
font-size: 12px;
|
||
text-decoration: none;
|
||
}
|
||
|
||
.scope-item {
|
||
padding: 6px 0;
|
||
color: rgba(0, 0, 0, 0.75);
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.section-pill {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 10px 14px;
|
||
border-radius: 9999px;
|
||
background: linear-gradient(90deg, #16a34a, #22c55e);
|
||
color: #fff;
|
||
font-weight: 800;
|
||
}
|
||
|
||
.pill-left {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.pill-right {
|
||
font-weight: 700;
|
||
font-size: 12px;
|
||
opacity: 0.9;
|
||
}
|
||
|
||
.guide-card {
|
||
border-radius: 10px;
|
||
}
|
||
|
||
.guide-icon {
|
||
background: rgba(22, 163, 74, 0.12);
|
||
color: #16a34a;
|
||
}
|
||
|
||
.service-icon {
|
||
background: rgba(22, 163, 74, 0.12);
|
||
color: #16a34a;
|
||
}
|
||
|
||
.guide-title {
|
||
font-size: 14px;
|
||
font-weight: 800;
|
||
color: rgba(0, 0, 0, 0.88);
|
||
}
|
||
|
||
.guide-desc {
|
||
margin-top: 3px;
|
||
font-size: 12px;
|
||
color: rgba(0, 0, 0, 0.55);
|
||
}
|
||
|
||
.login-hero {
|
||
padding: 18px 16px;
|
||
background:
|
||
radial-gradient(circle at 20% 20%, rgba(255, 255, 255, 0.22), transparent 55%),
|
||
linear-gradient(90deg, #16a34a, #22c55e);
|
||
color: #fff;
|
||
}
|
||
|
||
.login-hero-title {
|
||
font-size: 18px;
|
||
font-weight: 900;
|
||
}
|
||
|
||
.login-hero-sub {
|
||
margin-top: 6px;
|
||
font-size: 12px;
|
||
letter-spacing: 0.12em;
|
||
opacity: 0.9;
|
||
}
|
||
|
||
.banner {
|
||
background:
|
||
radial-gradient(circle at 20% 30%, rgba(22, 163, 74, 0.18), transparent 55%),
|
||
linear-gradient(180deg, #ffffff, #f8fafc);
|
||
border-top: 1px solid rgba(0, 0, 0, 0.06);
|
||
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
||
}
|
||
|
||
.banner-title {
|
||
text-align: center;
|
||
font-size: 28px;
|
||
font-weight: 900;
|
||
color: #15803d;
|
||
}
|
||
|
||
.section-title {
|
||
text-align: center;
|
||
}
|
||
|
||
.section-title-main {
|
||
font-size: 18px;
|
||
font-weight: 900;
|
||
color: rgba(0, 0, 0, 0.88);
|
||
}
|
||
|
||
.section-title-sub {
|
||
margin-top: 4px;
|
||
font-size: 12px;
|
||
letter-spacing: 0.12em;
|
||
color: rgba(0, 0, 0, 0.45);
|
||
}
|
||
|
||
.column-head {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 14px 14px 10px;
|
||
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
||
}
|
||
|
||
.column-title {
|
||
font-weight: 900;
|
||
color: rgba(0, 0, 0, 0.88);
|
||
}
|
||
|
||
.column-more {
|
||
font-size: 12px;
|
||
color: rgba(21, 128, 61, 0.95);
|
||
text-decoration: none;
|
||
}
|
||
|
||
.column-list {
|
||
padding: 10px 14px 14px;
|
||
}
|
||
|
||
.column-empty {
|
||
padding: 12px 0;
|
||
font-size: 13px;
|
||
color: rgba(0, 0, 0, 0.45);
|
||
}
|
||
|
||
.column-item {
|
||
display: block;
|
||
padding: 8px 0;
|
||
font-size: 13px;
|
||
color: rgba(0, 0, 0, 0.78);
|
||
text-decoration: none;
|
||
border-bottom: 1px dashed rgba(0, 0, 0, 0.08);
|
||
}
|
||
|
||
.column-item:last-child {
|
||
border-bottom: 0;
|
||
}
|
||
|
||
.case-card {
|
||
border-radius: 10px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.case-title {
|
||
color: rgba(21, 128, 61, 0.95);
|
||
}
|
||
|
||
.case-desc {
|
||
font-size: 12px;
|
||
color: rgba(0, 0, 0, 0.55);
|
||
line-height: 1.6;
|
||
}
|
||
:deep(.slick-slide) {
|
||
text-align: center;
|
||
height: 160px;
|
||
line-height: 160px;
|
||
background: #364d79;
|
||
overflow: hidden;
|
||
}
|
||
|
||
:deep(.slick-arrow.custom-slick-arrow) {
|
||
width: 25px;
|
||
height: 25px;
|
||
font-size: 25px;
|
||
color: #fff;
|
||
background-color: rgba(31, 45, 61, 0.11);
|
||
transition: ease all 0.3s;
|
||
opacity: 0.3;
|
||
z-index: 1;
|
||
}
|
||
:deep(.slick-arrow.custom-slick-arrow:before) {
|
||
display: none;
|
||
}
|
||
:deep(.slick-arrow.custom-slick-arrow:hover) {
|
||
color: #fff;
|
||
opacity: 0.5;
|
||
}
|
||
|
||
:deep(.slick-slide h3) {
|
||
color: #fff;
|
||
}
|
||
</style>
|