Files
shop-pc/app/pages/index.vue
赵忠林 91e9a8c20f feat(pages): 添加文章和商品详情页及API代理配置
- 添加了.dockerignore、.env.example和.gitignore配置文件
- 实现了文件服务器、模块API和服务器API的代理功能
- 创建了动态路由页面用于展示文章列表和详情
- 实现了商品详情页面包括图片展示和价格信息
- 添加了静态页面展示功能支持富文本内容渲染
- 配置了SEO元数据和面包屑导航组件
2026-02-12 13:52:55 +08:00

511 lines
12 KiB
Vue
Raw 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>
<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>