1
0

nuxt初始化

This commit is contained in:
2026-04-20 09:45:20 +08:00
parent e90903a378
commit d3eb1d3424
508 changed files with 35562 additions and 0 deletions

View File

@@ -0,0 +1,89 @@
<script setup lang="ts">
import { footerLinks } from '~/data/navigation'
import { siteInfo } from '~/data/site'
</script>
<template>
<footer class="site-footer">
<div class="upper-footer">
<div class="container">
<div class="row">
<div class="col col-lg-3 col-md-3 col-sm-6">
<div class="widget about-widget">
<div class="logo widget-title">
<img :src="siteInfo.footerLogo" :alt="siteInfo.companyName">
</div>
<img :src="siteInfo.wechatOfficialImage" alt="微信公众号" style="width: 100px; margin-left: 10px;">
<p style="margin-left: 10px;">关注公司微信号</p>
</div>
</div>
<div class="col col-lg-2 col-md-3 col-sm-6">
<div class="widget link-widget">
<div class="widget-title">
<h3>相关链接</h3>
</div>
<ul>
<li v-for="link in footerLinks" :key="link.label">
<NuxtLink :to="link.to">{{ link.label }}</NuxtLink>
</li>
</ul>
</div>
</div>
<div class="col col-lg-3 col-md-3 col-sm-6">
<div class="widget contact-widget service-link-widget">
<div class="widget-title">
<h3>公司信息</h3>
</div>
<ul>
<li><span>地址</span>{{ siteInfo.address }}</li>
<li><span>电话</span>{{ siteInfo.phone }}</li>
<li><span>邮箱</span>{{ siteInfo.email }}</li>
</ul>
</div>
</div>
<div class="col col-lg-3 col-md-3 col-sm-6">
<div class="widget newsletter-widget">
<div class="widget-title">
<h3>业务咨询邮箱</h3>
</div>
<form>
<div class="input-1">
<p style="font-size: 18px; font-weight: 600;">{{ siteInfo.email }}</p>
<p><span>工作时间</span>{{ siteInfo.businessHours }}</p>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<div class="lower-footer">
<div class="container">
<div class="row">
<div class="separator" />
<div class="col col-xs-12">
<p class="copyright">
Copyright © {{ siteInfo.companyName }}
<a :href="siteInfo.icpHref" target="_blank" rel="noreferrer">{{ siteInfo.icpText }}</a>
</p>
<a
:href="siteInfo.publicSecurityHref"
target="_blank"
rel="noreferrer"
style="display: inline-block; text-decoration: none; height: 20px; line-height: 20px;"
>
<img :src="siteInfo.publicSecurityImage" :alt="siteInfo.publicSecurityText" style="float: left;">
<p style="float: left; height: 20px; font-size: 0.875rem; line-height: 20px; margin: 0 0 0 5px;">
{{ siteInfo.publicSecurityText }}
</p>
</a>
</div>
</div>
</div>
</div>
</footer>
</template>

View File

@@ -0,0 +1,264 @@
<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { topNavigation } from '~/data/navigation'
import { siteInfo } from '~/data/site'
type NavNode = (typeof topNavigation)[number]
type ChildNode = {
label: string
to?: string
children?: Array<{ label: string; to: string }>
}
const route = useRoute()
const isMenuOpen = ref(false)
const isMobile = ref(false)
const isSticky = ref(false)
const isStickyVisible = ref(false)
const expandedItems = ref<Record<string, boolean>>({})
let lastScrollTop = 0
const stickyThreshold = 1000
const getItemKey = (segments: string[]) => segments.join('::')
const hasChildren = (item: NavNode | ChildNode) => 'children' in item && Boolean(item.children?.length)
const updateStickyState = () => {
if (typeof window === 'undefined') {
return
}
const currentScrollTop = Math.max(window.scrollY, window.pageYOffset, document.documentElement.scrollTop, 0)
if (isMobile.value || currentScrollTop <= stickyThreshold) {
isSticky.value = false
isStickyVisible.value = false
lastScrollTop = currentScrollTop
return
}
isSticky.value = true
isStickyVisible.value = currentScrollTop < lastScrollTop
lastScrollTop = currentScrollTop
}
const updateViewport = () => {
if (typeof window === 'undefined') {
return
}
const mobile = window.innerWidth <= 991
isMobile.value = mobile
if (!mobile) {
isMenuOpen.value = false
expandedItems.value = {}
}
updateStickyState()
}
const setOverlay = (enabled: boolean) => {
if (typeof document === 'undefined') {
return
}
document.querySelector('.page-wrapper')?.classList.toggle('body-overlay', enabled)
}
const openMenu = () => {
isMenuOpen.value = true
}
const closeMenu = () => {
isMenuOpen.value = false
expandedItems.value = {}
}
const toggleExpanded = (key: string) => {
expandedItems.value[key] = !expandedItems.value[key]
}
const isExpanded = (key: string) => Boolean(expandedItems.value[key])
const matchesRoute = (to?: string) => {
if (!to) {
return false
}
if (to === '/') {
return route.path === '/'
}
return route.path === to || route.path.startsWith(`${to}/`)
}
const isItemActive = (item: NavNode | ChildNode) => {
if ('to' in item && matchesRoute(item.to)) {
return true
}
if (hasChildren(item)) {
return item.children.some((child) => isItemActive(child))
}
return false
}
const handleItemClick = (item: NavNode | ChildNode, key: string, event: MouseEvent) => {
if (!isMobile.value) {
if (!hasChildren(item)) {
closeMenu()
}
return
}
if (!hasChildren(item)) {
closeMenu()
return
}
const itemHasRoute = 'to' in item && Boolean(item.to)
const currentlyExpanded = isExpanded(key)
if (!currentlyExpanded) {
event.preventDefault()
toggleExpanded(key)
return
}
if (!itemHasRoute) {
event.preventDefault()
toggleExpanded(key)
}
}
watch(isMenuOpen, setOverlay)
watch(
() => route.fullPath,
() => {
closeMenu()
updateStickyState()
}
)
onMounted(() => {
updateViewport()
updateStickyState()
window.addEventListener('resize', updateViewport)
window.addEventListener('scroll', updateStickyState, { passive: true })
})
onBeforeUnmount(() => {
window.removeEventListener('resize', updateViewport)
window.removeEventListener('scroll', updateStickyState)
setOverlay(false)
})
</script>
<template>
<header
id="header"
:class="['site-header', 'header-style-1', { 'site-header--stuck': isSticky, 'site-header--visible': isStickyVisible }]"
>
<nav class="navigation navbar navbar-default">
<div class="container">
<div class="navbar-header">
<button type="button" class="open-btn" @click="openMenu">
<span class="sr-only">打开菜单</span>
<span class="icon-bar" />
<span class="icon-bar" />
<span class="icon-bar" />
</button>
<NuxtLink class="navbar-brand" to="/" @click="closeMenu">
<img :src="siteInfo.headerLogo" :alt="siteInfo.companyName">
</NuxtLink>
</div>
<div
id="navbar"
:class="['navbar-collapse', 'collapse', 'navbar-right', 'navigation-holder', { slideInn: isMenuOpen }]"
>
<button class="close-navbar" @click="closeMenu">
<i class="ti-close" />
</button>
<ul :class="['nav', 'navbar-nav', { 'small-nav': isMobile }]">
<li
v-for="item in topNavigation"
:key="item.label"
:class="[
{
current: isItemActive(item),
'menu-item-has-children': hasChildren(item),
'product-menu': item.label === '产品中心',
'solution-menu': item.label === '解决方案'
}
]"
>
<NuxtLink
v-if="'to' in item && item.to"
:to="item.to"
@click="handleItemClick(item, getItemKey([item.label]), $event)"
>
{{ item.label }}
</NuxtLink>
<a
v-else
href="#"
@click.prevent="handleItemClick(item, getItemKey([item.label]), $event)"
>
{{ item.label }}
</a>
<ul
v-if="hasChildren(item)"
v-show="!isMobile || isExpanded(getItemKey([item.label]))"
class="sub-menu"
>
<li
v-for="child in item.children"
:key="child.label"
:class="[{ current: isItemActive(child), 'menu-item-has-children': hasChildren(child) }]"
>
<NuxtLink
v-if="'to' in child && child.to"
:to="child.to"
@click="handleItemClick(child, getItemKey([item.label, child.label]), $event)"
>
{{ child.label }}
</NuxtLink>
<a
v-else
href="#"
@click.prevent="handleItemClick(child, getItemKey([item.label, child.label]), $event)"
>
{{ child.label }}
</a>
<ul
v-if="hasChildren(child)"
v-show="!isMobile || isExpanded(getItemKey([item.label, child.label]))"
class="sub-menu"
>
<li v-for="leaf in child.children" :key="leaf.label" :class="{ current: matchesRoute(leaf.to) }">
<NuxtLink :to="leaf.to" @click="closeMenu">{{ leaf.label }}</NuxtLink>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</div>
<div class="search-contact">
<div class="contact">
<a :href="`tel:${siteInfo.phone}`" class="theme-btn">{{ siteInfo.phone }}</a>
</div>
</div>
<div class="separator" />
</div>
</nav>
</header>
</template>

View File

@@ -0,0 +1,21 @@
<script setup lang="ts">
defineProps<{
items: Array<{ label: string; to?: string }>
}>()
</script>
<template>
<div class="container">
<div class="row">
<div class="col col-xs-12">
<nav aria-label="面包屑" class="page-path">
<template v-for="(item, index) in items" :key="`${item.label}-${item.to || 'static'}`">
<NuxtLink v-if="item.to" :to="item.to">{{ item.label }}</NuxtLink>
<span v-else>{{ item.label }}</span>
<span v-if="index < items.length - 1"> &gt; </span>
</template>
</nav>
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,76 @@
<script setup lang="ts">
import { siteInfo } from '~/data/site'
</script>
<template>
<aside class="kefu-widget" aria-label="客服联系方式">
<a class="widget-item" :href="`tel:${siteInfo.phone}`" aria-label="拨打电话">
<strong>电话咨询</strong>
<span>{{ siteInfo.phone }}</span>
</a>
<a
class="widget-item"
:href="`http://wpa.qq.com/msgrd?v=3&uin=${siteInfo.qq}&site=qq&menu=yes`"
target="_blank"
rel="noreferrer"
aria-label="打开QQ咨询"
>
<strong>QQ 咨询</strong>
<span>{{ siteInfo.qq }}</span>
</a>
<div class="widget-item qr-item">
<strong>微信公众号</strong>
<img :src="siteInfo.wechatOfficialImage" alt="微信公众号二维码">
</div>
</aside>
</template>
<style scoped>
.kefu-widget {
position: fixed;
right: 20px;
bottom: 20px;
z-index: 120;
display: grid;
gap: 12px;
}
.widget-item {
min-width: 148px;
padding: 12px 14px;
border-radius: 14px;
background: rgba(16, 37, 65, 0.96);
color: #fff;
text-decoration: none;
box-shadow: 0 12px 28px rgba(0, 0, 0, 0.18);
}
.widget-item strong,
.widget-item span {
display: block;
color: inherit;
}
.qr-item img {
width: 96px;
margin-top: 8px;
border-radius: 10px;
background: #fff;
}
@media (max-width: 767px) {
.kefu-widget {
right: 10px;
bottom: 10px;
}
.widget-item {
min-width: 112px;
padding: 10px;
}
.qr-item img {
width: 72px;
}
}
</style>

View File

@@ -0,0 +1,41 @@
<script setup lang="ts">
const props = defineProps<{
heading?: string
summary?: string
items: Array<{
title: string
description: string
icon: string
}>
}>()
</script>
<template>
<section class="product-advantages-section">
<div class="container">
<div class="row">
<div class="product-advantages-header">
<div>
<h1 class="product-advantages-title">{{ heading || '产品优势' }}</h1>
<p v-if="summary">{{ summary }}</p>
</div>
</div>
</div>
<div class="row">
<div class="col col-xs-12">
<div class="service-grids services-slider clearfix">
<div v-for="item in props.items" :key="item.title" class="feature-card">
<div class="feature-card-icon">
<img :src="item.icon" :alt="item.title">
</div>
<div class="feature-card-body">
<h4>{{ item.title }}</h4>
<p>{{ item.description }}</p>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
</template>

View File

@@ -0,0 +1,22 @@
<script setup lang="ts">
defineProps<{
title?: string
description?: string
image: string
imageAlt: string
}>()
</script>
<template>
<section class="product-architecture-section">
<div class="container product-architecture-container">
<div class="row">
<div class="col col-xs-12 text-center">
<h1 class="product-architecture-title">{{ title || '产品架构' }}</h1>
<p v-if="description">{{ description }}</p>
<img :src="image" :alt="imageAlt" class="img-responsive center-block">
</div>
</div>
</div>
</section>
</template>

View File

@@ -0,0 +1,22 @@
<script setup lang="ts">
defineProps<{
title: string
description: string
image: string
to: string
}>()
</script>
<template>
<NuxtLink :to="to" class="product-card">
<div class="card-img">
<img :src="image" :alt="title">
</div>
<div class="card-content">
<div class="card-header">
<h4>{{ title }}</h4>
</div>
<p>{{ description }}</p>
</div>
</NuxtLink>
</template>

View File

@@ -0,0 +1,42 @@
<script setup lang="ts">
const props = defineProps<{
heading?: string
summary?: string
items: Array<{
title: string
description: string
to: string
}>
}>()
const headerClasses = ['blue', 'cyan', 'teal', 'indigo', 'green']
</script>
<template>
<section class="cases-section" id="cases">
<div class="container">
<div class="row">
<div class="product-advantages-header">
<div>
<h1 class="product-advantages-title">{{ heading || '客户案例' }}</h1>
<p v-if="summary">{{ summary }}</p>
</div>
</div>
</div>
<div class="row">
<div class="col col-xs-12">
<NuxtLink v-for="(item, index) in props.items" :key="item.to" :to="item.to" class="case-card-link">
<div class="case-card">
<div :class="['case-card-header', headerClasses[index % headerClasses.length]]">
<h3>{{ item.title }}</h3>
</div>
<div class="case-card-body">
<p>{{ item.description }}</p>
</div>
</div>
</NuxtLink>
</div>
</div>
</div>
</section>
</template>

View File

@@ -0,0 +1,94 @@
<script setup lang="ts">
import { computed } from 'vue'
type LinkItem = { label: string; to: string }
const props = withDefaults(defineProps<{
title: string
description?: string
bannerImage?: string
breadcrumbItems?: Array<{ label: string; to?: string }>
relatedProducts?: LinkItem[]
relatedCases?: LinkItem[]
}>(), {
description: undefined,
bannerImage: undefined,
breadcrumbItems: () => [],
relatedProducts: () => [],
relatedCases: () => []
})
const bannerStyle = computed(() => {
if (!props.bannerImage) {
return undefined
}
return {
backgroundImage: `linear-gradient(rgba(16, 37, 65, 0.32), rgba(16, 37, 65, 0.32)), url(${props.bannerImage})`,
backgroundSize: 'cover',
backgroundPosition: 'center'
}
})
</script>
<template>
<div>
<section class="page-title" :style="bannerStyle">
<div class="container">
<div class="row">
<div class="col col-xs-12">
<h2>{{ title }}</h2>
<p v-if="description">{{ description }}</p>
</div>
</div>
</div>
</section>
<Breadcrumb v-if="breadcrumbItems.length" :items="breadcrumbItems" />
<slot />
<section v-if="relatedProducts.length" class="product-related-section section-padding">
<div class="container">
<div class="row">
<div class="col col-xs-12">
<div class="product-advantages-header">
<div>
<h1 class="product-advantages-title">相关产品</h1>
<p>继续查看与当前方案高度关联的产品能力</p>
</div>
</div>
<div class="product-related-links">
<NuxtLink v-for="item in relatedProducts" :key="item.to" :to="item.to" class="product-related-link">
{{ item.label }}
</NuxtLink>
</div>
</div>
</div>
</div>
</section>
</div>
</template>
<style scoped>
.product-related-section {
padding-top: 20px;
}
.product-related-links {
display: flex;
flex-wrap: wrap;
gap: 14px;
}
.product-related-link {
display: inline-flex;
align-items: center;
min-height: 48px;
padding: 0 20px;
border-radius: 999px;
background: #fff;
border: 1px solid rgba(20, 129, 255, 0.2);
color: #102541;
text-decoration: none;
box-shadow: 0 10px 24px rgba(16, 37, 65, 0.06);
}
</style>

View File

@@ -0,0 +1,78 @@
<script setup lang="ts">
import { ref, watch } from 'vue'
type FeatureSection = {
label: string
title: string
description: string
bullets: string[]
image: string
imageAlt: string
}
const props = defineProps<{
heading: string
summary: string
sections: FeatureSection[]
}>()
const activeIndex = ref(0)
watch(
() => props.sections.length,
() => {
activeIndex.value = 0
}
)
</script>
<template>
<section class="feature-highlight-section fun-fact-section">
<div class="container">
<div class="feature-highlight-heading">
<h1>{{ heading }}</h1>
<p>{{ summary }}</p>
</div>
<ul class="feature-highlight-tabs" role="tablist">
<li v-for="(section, index) in sections" :key="section.label" role="presentation">
<button
type="button"
:class="['feature-highlight-tab', { active: activeIndex === index }]"
role="tab"
:aria-selected="activeIndex === index"
:aria-controls="`feature-panel-${index + 1}`"
@click="activeIndex = index"
>
{{ section.label }}
</button>
</li>
</ul>
<div class="feature-highlight-panels">
<div
v-for="(section, index) in sections"
:id="`feature-panel-${index + 1}`"
:key="section.label"
:class="['feature-highlight-panel', { active: activeIndex === index }]"
role="tabpanel"
>
<div class="feature-highlight-detail">
<div class="feature-highlight-copy">
<h2>{{ section.title }}</h2>
<p>{{ section.description }}</p>
<ul class="feature-highlight-points">
<li v-for="bullet in section.bullets" :key="bullet">{{ bullet }}</li>
</ul>
</div>
<div class="feature-highlight-visual">
<div class="feature-highlight-visual-card">
<img :src="section.image" :alt="section.imageAlt">
</div>
</div>
</div>
</div>
</div>
</div>
</section>
</template>

View File

@@ -0,0 +1,34 @@
<script setup lang="ts">
import { computed } from 'vue'
const props = defineProps<{
title: string
description?: string
backgroundImage?: string
}>()
const bannerStyle = computed(() => {
if (!props.backgroundImage) {
return undefined
}
return {
backgroundImage: `linear-gradient(rgba(16, 37, 65, 0.32), rgba(16, 37, 65, 0.32)), url(${props.backgroundImage})`,
backgroundSize: 'cover',
backgroundPosition: 'center'
}
})
</script>
<template>
<section class="page-title" :style="bannerStyle">
<div class="container">
<div class="row">
<div class="col col-xs-12">
<h2>{{ title }}</h2>
<p v-if="description">{{ description }}</p>
</div>
</div>
</div>
</section>
</template>

View File

@@ -0,0 +1,155 @@
<script setup lang="ts">
type InfoItem = {
title: string
description: string
}
defineProps<{
title: string
description: string
overview: string[]
capabilityItems: InfoItem[]
featureItems: InfoItem[]
architectureImage: string
architectureImageAlt: string
caseImages?: string[]
}>()
</script>
<template>
<div>
<section class="page-title">
<div class="container">
<div class="row">
<div class="col col-xs-12">
<h2>{{ title }}</h2>
<p>{{ description }}</p>
</div>
</div>
</div>
</section>
<section class="features-section-s2 section-padding">
<div class="container">
<div class="main_pro col col-xs-12">
<div class="feature-grids clearfix">
<div class="part_1 col col-xs-12">
<div>
<h1>系统概述</h1>
<p v-for="paragraph in overview" :key="paragraph">{{ paragraph }}</p>
</div>
</div>
</div>
<div class="feature-grids2 clearfix">
<div class="part_5 col">
<h1>系统功能</h1>
<div class="gongneng">
<ul>
<li v-for="(item, index) in capabilityItems" :key="item.title" class="grid" style="background: #fff;">
<i class="service-pill">{{ String(index + 1).padStart(2, '0') }}</i>
<p>
<span>{{ item.title }}</span>
{{ item.description }}
</p>
</li>
</ul>
</div>
</div>
</div>
<div class="bg_hui feature-grids clearfix">
<div class="part_6">
<h1>系统特性</h1>
<div class="case1">
<dl v-for="item in featureItems" :key="item.title" class="grid service-feature-card">
<dd><span class="service-feature-mark"></span></dd>
<dt class="tit_2">{{ item.title }}</dt>
<p>{{ item.description }}</p>
</dl>
</div>
</div>
</div>
<div class="bg_white feature-grids clearfix">
<div class="part_3 col col-xs-12">
<h1>系统架构</h1>
<div class="jiagou_box">
<div class="jiagou_main" style="display: block;">
<img :src="architectureImage" :alt="architectureImageAlt" class="service-architecture-image">
</div>
</div>
</div>
</div>
<div v-if="caseImages?.length" class="bg_white feature-grids clearfix">
<div class="part_3 col col-xs-12">
<h1>成功案例</h1>
<div class="service-logo-grid">
<div v-for="item in caseImages" :key="item" class="service-logo-card">
<img :src="item" alt="成功案例标识" class="service-logo-image">
</div>
</div>
</div>
</div>
</div>
</div>
</section>
</div>
</template>
<style scoped>
.service-pill {
display: inline-flex;
align-items: center;
justify-content: center;
width: 46px;
height: 46px;
border-radius: 50%;
background: linear-gradient(135deg, #0f3e71, #1481ff);
color: #fff;
font-style: normal;
font-weight: 700;
}
.service-feature-card {
min-height: 260px;
}
.service-feature-mark {
display: inline-flex;
align-items: center;
justify-content: center;
width: 52px;
height: 52px;
border-radius: 50%;
background: linear-gradient(135deg, rgba(20, 129, 255, 0.1), rgba(20, 129, 255, 0.18));
color: #1481ff;
font-size: 28px;
line-height: 1;
}
.service-architecture-image {
width: 100%;
display: block;
}
.service-logo-grid {
display: grid;
gap: 18px;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
}
.service-logo-card {
background: #fff;
border-radius: 18px;
box-shadow: 0 12px 24px rgba(16, 37, 65, 0.08);
padding: 18px;
}
.service-logo-image {
width: 100%;
height: 96px;
object-fit: contain;
}
</style>

View File

@@ -0,0 +1,181 @@
<script setup lang="ts">
type ContentCard = {
title: string
description: string
}
type ProductLink = {
title: string
description: string
to?: string
}
defineProps<{
title: string
description: string
badge: string
heroStats?: Array<{ value: string; label: string }>
painPoints: ContentCard[]
architectureTitle: string
architectureSteps: ContentCard[]
architectureImage: string
architectureImageAlt: string
productLinks: ProductLink[]
valueItems: ContentCard[]
}>()
const statIconClasses = ['blue ti-stats-up', 'cyan ti-layers', 'indigo ti-shield', 'green ti-money']
const painPointIconClasses = ['red ti-server', 'orange ti-time', 'yellow ti-settings', 'purple ti-lock']
const productBarClasses = ['blue', 'cyan', 'indigo', 'slate', 'green']
</script>
<template>
<div>
<section class="solution-hero">
<div class="container">
<div class="row">
<div class="col-md-6 hero-content">
<div class="reveal active">
<div class="hero-badge">
<span class="pulse-dot" />
{{ badge }}
</div>
<h1 class="hero-title">{{ title }}</h1>
<p class="hero-desc">{{ description }}</p>
</div>
</div>
<div v-if="heroStats?.length" class="col-md-6 hero-stats hidden-sm hidden-xs">
<div class="reveal active">
<div class="stats-card-container">
<div class="stats-grid">
<div v-for="(item, index) in heroStats" :key="`${item.value}-${item.label}`" class="stat-item">
<i :class="['stat-icon', ...statIconClasses[index % statIconClasses.length].split(' ')]" />
<div class="stat-number">{{ item.value }}</div>
<div class="stat-label">{{ item.label }}</div>
</div>
<div class="stats-center-icon">
<i class="ti-server" />
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<section class="pain-points-section" id="solution">
<div class="container">
<div class="section-header reveal active">
<h2>直击行业核心痛点</h2>
<p>行业困境亟待破局数智升级势在必行</p>
</div>
<div class="row">
<div v-for="(item, index) in painPoints" :key="item.title" class="col-md-3 col-sm-6">
<div class="pain-point-card reveal active">
<div :class="['pain-point-icon', painPointIconClasses[index % painPointIconClasses.length].split(' ')[0]]">
<i :class="painPointIconClasses[index % painPointIconClasses.length].split(' ')[1]" />
</div>
<h3>{{ item.title }}</h3>
<p>{{ item.description }}</p>
</div>
</div>
</div>
</div>
</section>
<section class="architecture-section">
<div class="container">
<div class="row">
<div class="col-md-4">
<div class="reveal active">
<h2 style="font-size: 32px; font-weight: 700; margin-bottom: 40px; color: #fff;">{{ architectureTitle }}</h2>
<div v-for="(item, index) in architectureSteps" :key="item.title" class="arch-step">
<div :class="['arch-step-number', productBarClasses[index % 3]]">{{ `0${index + 1}` }}</div>
<div class="arch-step-content">
<h4 :class="productBarClasses[index % 3]">{{ item.title }}</h4>
<p>{{ item.description }}</p>
</div>
</div>
</div>
</div>
<div class="col-md-8">
<div class="reveal active">
<div class="arch-visual-container">
<img :src="architectureImage" :alt="architectureImageAlt">
</div>
</div>
</div>
</div>
</div>
</section>
<section class="products-section" id="products">
<div class="container">
<div class="section-header-flex reveal active">
<div class="section-header-left">
<div class="section-label">Product Matrix</div>
<h2>部分产品与服务</h2>
<p>从智能生产到供应链协同全方位覆盖港航集团业务场景</p>
</div>
<NuxtLink to="/products/logistics" class="view-all-link hidden-xs">
查看所有产品 <i class="ti-arrow-right" />
</NuxtLink>
</div>
<div class="row">
<div v-for="(item, index) in productLinks" :key="item.title" class="col-md-4">
<div class="product-card reveal active">
<div :class="['product-card-bar', productBarClasses[index % productBarClasses.length]]" />
<div class="product-card-body">
<h3>{{ item.title }}</h3>
<p>{{ item.description }}</p>
<component :is="item.to ? 'NuxtLink' : 'div'" :to="item.to" class="product-card-footer">
<span>了解详情</span>
<div class="product-card-arrow">
<i class="ti-arrow-right" />
</div>
</component>
</div>
</div>
</div>
</div>
</div>
</section>
<section class="advantages-section">
<div class="container">
<div class="row">
<div class="col-md-6">
<div class="section-header reveal active" style="text-align: left; margin-bottom: 40px;">
<h2>数字化转型价值</h2>
<p>把行业经验沉淀为可复制可运营可持续放大的数智能力</p>
</div>
<div v-for="item in valueItems" :key="item.title" class="advantage-item">
<div class="advantage-check">
<i class="ti-check-box" />
</div>
<div>
<h4>{{ item.title }}</h4>
<p>{{ item.description }}</p>
</div>
</div>
</div>
<div class="col-md-6">
<div class="stats-boxes">
<div v-for="item in heroStats" :key="`${item.value}-${item.label}-box`" class="stat-box">
<div class="stat-number">{{ item.value }}</div>
<div class="stat-label">{{ item.label }}</div>
</div>
</div>
</div>
</div>
</div>
</section>
</div>
</template>

View File

@@ -0,0 +1,74 @@
<script setup lang="ts">
defineProps<{
items: Array<{
date: string
title: string
summary: string
highlight?: boolean
}>
}>()
</script>
<template>
<section class="timeline">
<div v-for="item in items" :key="`${item.date}-${item.title}`" :class="['timeline-item', { highlight: item.highlight }]">
<div class="timeline-marker" />
<div class="timeline-content">
<p class="timeline-date">{{ item.date }}</p>
<h3>{{ item.title }}</h3>
<p>{{ item.summary }}</p>
</div>
</div>
</section>
</template>
<style scoped>
.timeline {
position: relative;
display: grid;
gap: 18px;
}
.timeline::before {
content: '';
position: absolute;
left: 14px;
top: 0;
bottom: 0;
width: 2px;
background: #d7e4f2;
}
.timeline-item {
position: relative;
display: grid;
grid-template-columns: 28px minmax(0, 1fr);
gap: 16px;
}
.timeline-marker {
width: 12px;
height: 12px;
margin-top: 10px;
border-radius: 50%;
background: #4f6f93;
z-index: 1;
}
.timeline-item.highlight .timeline-marker {
background: #1481ff;
}
.timeline-content {
background: #fff;
border-radius: 16px;
padding: 18px 20px;
box-shadow: 0 12px 24px rgba(16, 37, 65, 0.08);
}
.timeline-date {
margin: 0 0 8px;
color: #4f6f93;
font-size: 14px;
}
</style>

View File

@@ -0,0 +1,42 @@
<script setup lang="ts">
import { companySectionLinks } from '~/data/company'
const route = useRoute()
</script>
<template>
<nav class="company-nav" aria-label="公司栏目导航">
<NuxtLink
v-for="item in companySectionLinks"
:key="item.to"
:to="item.to"
:class="['company-nav__link', { active: route.path === item.to }]"
>
{{ item.label }}
</NuxtLink>
</nav>
</template>
<style scoped>
.company-nav {
display: flex;
flex-wrap: wrap;
gap: 10px;
max-width: 1200px;
margin: 0 auto;
padding: 0 20px 16px;
}
.company-nav__link {
padding: 8px 14px;
border-radius: 999px;
border: 1px solid #c6d6e7;
background: #fff;
color: #102541;
}
.company-nav__link.active {
background: #102541;
color: #fff;
}
</style>

View File

@@ -0,0 +1,30 @@
<script setup lang="ts">
defineProps<{
title: string
description?: string
}>()
</script>
<template>
<main class="route-stub">
<div class="route-stub__inner">
<h1>{{ title }}</h1>
<p>{{ description || 'This route has been reserved and will be migrated from the legacy site in a later batch.' }}</p>
</div>
</main>
</template>
<style scoped>
.route-stub {
padding: 64px 20px;
}
.route-stub__inner {
max-width: 960px;
margin: 0 auto;
background: #fff;
border-radius: 16px;
padding: 32px;
box-shadow: 0 12px 24px rgba(16, 37, 65, 0.08);
}
</style>