This commit is contained in:
2026-07-10 18:23:41 +08:00
commit bc9e794a57
80 changed files with 19016 additions and 0 deletions
+69
View File
@@ -0,0 +1,69 @@
<script setup>
import { computed, onBeforeUnmount, onMounted, reactive, ref } from "vue";
const props = defineProps({
label: {
type: String,
required: true,
},
value: {
type: Number,
required: true,
},
suffix: {
type: String,
default: "",
},
});
const elRef = ref(null);
const state = reactive({ value: 0 });
let animation;
let trigger;
const displayValue = computed(() => Math.round(state.value));
onMounted(() => {
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
state.value = props.value;
return;
}
const { $gsap, $ScrollTrigger } = useNuxtApp();
if (!elRef.value || !$gsap || !$ScrollTrigger) {
state.value = props.value;
return;
}
trigger = $ScrollTrigger.create({
trigger: elRef.value,
start: "top 85%",
once: true,
onEnter: () => {
animation = $gsap.to(state, {
value: props.value,
duration: 1.4,
ease: "power2.out",
});
},
});
});
onBeforeUnmount(() => {
animation?.kill?.();
trigger?.kill?.();
});
</script>
<template>
<div
ref="elRef"
class="rounded-lg border border-gm-border bg-white p-6 text-center shadow-sm"
>
<div class="text-4xl font-semibold text-gm-green">
{{ displayValue }}<span>{{ suffix }}</span>
</div>
<p class="mt-2 text-sm text-[#777]">{{ label }}</p>
</div>
</template>
+32
View File
@@ -0,0 +1,32 @@
<script setup>
import { announcement } from "~/data/navigation";
defineEmits(["close"]);
</script>
<template>
<div class="fixed inset-x-0 top-0 z-[1000] h-[59px] bg-[url('/images/top-pop-bg.jpg')] bg-cover bg-center text-white max-sm:h-11">
<div class="mx-auto flex h-full max-w-[1440px] items-center justify-center gap-4 px-[5%] text-sm max-sm:gap-2 max-sm:text-xs">
<span class="hidden text-center sm:block">
{{ announcement.text }}
</span>
<NuxtLink
:to="announcement.ctaHref"
class="inline-flex h-9 items-center gap-2 rounded-full bg-white px-5 text-sm font-medium text-gm-green transition-all duration-300 hover:scale-95 hover:bg-gm-green hover:text-white max-sm:h-7 max-sm:px-4 max-sm:text-xs"
>
{{ announcement.ctaLabel }}
<span aria-hidden="true">→</span>
</NuxtLink>
</div>
<button
type="button"
class="absolute right-4 top-1/2 flex -translate-y-1/2 items-center gap-1 text-sm text-white/80 transition hover:text-white"
aria-label="关闭公告"
@click="$emit('close')"
>
<span class="hidden sm:inline">关闭</span>
<span aria-hidden="true">×</span>
</button>
</div>
</template>
+39
View File
@@ -0,0 +1,39 @@
<script setup>
import { ctaSection as defaultCta } from "~/data/navigation";
defineProps({
cta: {
type: Object,
default: () => defaultCta,
},
sectionId: {
type: String,
default: "consult",
},
});
</script>
<template>
<section :id="sectionId" class="relative overflow-hidden bg-gm-green py-20 text-white">
<div class="absolute inset-0 bg-[radial-gradient(circle_at_top_right,rgba(255,255,255,0.18),transparent_40%)]" />
<div data-cta-orbit class="absolute left-[8%] right-[8%] top-10 h-px bg-white/30" />
<div data-cta-orbit class="absolute bottom-10 left-[14%] right-[14%] h-px bg-white/20" />
<div class="container-gm relative flex flex-col items-center justify-center text-center" data-reveal>
<p class="text-balance text-3xl font-semibold leading-tight md:text-[42px]">
{{ cta.title }}
</p>
<p v-if="cta.subtitle" class="mt-4 max-w-2xl text-base leading-8 text-white/80">
{{ cta.subtitle }}
</p>
<NuxtLink
:to="cta.href"
class="gm-btn gm-btn-secondary mt-8 px-8 text-base font-semibold"
>
{{ cta.button }}
</NuxtLink>
<div class="mt-6">
<SectionRouteLinks :routes="cta.routes" light />
</div>
</div>
</section>
</template>
+31
View File
@@ -0,0 +1,31 @@
<script setup>
defineProps({
routes: {
type: Array,
default: () => [],
},
light: {
type: Boolean,
default: false,
},
});
</script>
<template>
<div v-if="routes.length" class="flex flex-wrap gap-2">
<NuxtLink
v-for="route in routes"
:key="`${route.label}-${route.href}`"
:to="route.href"
class="inline-flex items-center gap-2 rounded-full border px-3 py-2 text-xs transition-all duration-300"
:class="
light
? 'border-white/20 bg-white/10 text-white/88 hover:border-white/35 hover:bg-white/15'
: 'border-gm-border bg-white text-[#555] hover:border-gm-green hover:text-gm-green'
"
>
<span class="opacity-65">{{ route.label }}</span>
<span class="font-mono text-[11px]">{{ route.path || route.href }}</span>
</NuxtLink>
</div>
</template>
+81
View File
@@ -0,0 +1,81 @@
<script setup>
import { brand, footerData } from "~/data/navigation";
</script>
<template>
<footer id="contact" class="bg-gm-dark text-white">
<div class="container-gm py-14">
<div class="grid gap-12 xl:grid-cols-[1.1fr_1.2fr_1fr]">
<div data-reveal>
<p class="text-[22px] font-semibold text-white">{{ brand.name }}</p>
<p class="mt-5 text-sm text-white/75">{{ footerData.tagline }}</p>
<p class="mt-4 max-w-[360px] text-sm leading-7 text-white/60">
{{ footerData.description }}
</p>
</div>
<div class="grid gap-8 sm:grid-cols-2 lg:grid-cols-4" data-stagger>
<div
v-for="(links, title) in footerData.links"
:key="title"
data-stagger-item
>
<h3 class="text-base font-semibold text-white">{{ title }}</h3>
<ul class="mt-4 space-y-3">
<li v-for="link in links" :key="link.label">
<NuxtLink :to="link.href" class="text-sm text-white/65 transition hover:text-gm-green">
{{ link.label }}
</NuxtLink>
</li>
</ul>
</div>
</div>
<div class="rounded-lg bg-white/6 p-6" data-reveal>
<p class="text-[22px] font-semibold text-white">
{{ footerData.contact.title }}
</p>
<p class="mt-2 text-sm text-white/65">
{{ footerData.contact.subtitle }}
</p>
<NuxtLink
:to="footerData.contact.href"
class="gm-btn gm-btn-primary mt-6 px-7"
>
{{ footerData.contact.button }}
</NuxtLink>
<div class="mt-8 flex items-start gap-5">
<img
src="/images/footer-qr.png"
alt="新媒体矩阵二维码"
class="w-[110px] rounded-2xl bg-white p-2"
/>
<div class="space-y-3 text-sm text-white/75">
<p>{{ footerData.contact.qrLabel }}</p>
<p>业务咨询:{{ footerData.contact.phone }}</p>
<p>{{ footerData.contact.email }}</p>
<p>{{ footerData.contact.address }}</p>
</div>
</div>
</div>
</div>
</div>
<div class="border-t border-white/10">
<div class="container-gm flex flex-col gap-4 py-5 text-xs text-white/45 md:flex-row md:items-center md:justify-between">
<p>{{ footerData.copyright }}</p>
<div class="flex flex-wrap gap-4">
<NuxtLink
v-for="item in footerData.legal"
:key="item.label"
:to="item.href"
class="transition hover:text-gm-green"
>
{{ item.label }}
</NuxtLink>
</div>
</div>
</div>
</footer>
</template>
+247
View File
@@ -0,0 +1,247 @@
<script setup>
import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue";
import { brand, navItems } from "~/data/navigation";
const props = defineProps({
announcementVisible: {
type: Boolean,
default: true,
},
});
const route = useRoute();
const mobileOpen = ref(false);
const expandedItem = ref("");
const headerRef = ref(null);
const barRef = ref(null);
let ctx;
let scrollTrigger;
const topOffset = computed(() => (props.announcementVisible ? "59px" : "0px"));
const contactHref = computed(() => `${route.path === "/" ? "" : route.path}#contact`);
const isActive = (item) => {
if (item.href === "/") {
return route.path === "/";
}
return route.path === item.href || route.path.startsWith(`${item.href}/`);
};
const closeMobile = () => {
mobileOpen.value = false;
expandedItem.value = "";
};
const toggleMobileItem = (label) => {
expandedItem.value = expandedItem.value === label ? "" : label;
};
watch(
() => props.announcementVisible,
() => {
closeMobile();
},
);
watch(
() => route.fullPath,
() => {
closeMobile();
},
);
onMounted(() => {
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
return;
}
const { $gsap, $ScrollTrigger } = useNuxtApp();
if (!headerRef.value || !barRef.value || !$gsap || !$ScrollTrigger) {
return;
}
ctx = $gsap.context(() => {
scrollTrigger = $ScrollTrigger.create({
start: 40,
end: 99999,
onToggle: (self) => {
$gsap.to(barRef.value, {
height: self.isActive ? 64 : 75,
duration: 0.28,
ease: "power2.out",
});
$gsap.to(headerRef.value, {
boxShadow: self.isActive
? "0 18px 50px rgba(0, 0, 0, 0.08)"
: "0 0 0 rgba(0, 0, 0, 0)",
duration: 0.28,
ease: "power2.out",
});
},
});
}, headerRef.value);
});
onBeforeUnmount(() => {
scrollTrigger?.kill?.();
ctx?.revert();
});
</script>
<template>
<header
ref="headerRef"
class="fixed inset-x-0 z-[999] border-b border-gm-border bg-white/95 backdrop-blur supports-[backdrop-filter]:bg-white/85"
:style="{ top: topOffset }"
>
<div ref="barRef" class="container-gm flex h-[75px] items-center justify-between">
<NuxtLink to="/" class="shrink-0" :aria-label="brand.name">
<span class="block text-[18px] font-semibold leading-none text-[#1f2430] md:text-[20px]">
{{ brand.shortName }}
</span>
<span class="mt-1 block text-[11px] leading-none text-[#666]">
{{ brand.productName }}
</span>
</NuxtLink>
<nav class="hidden items-center gap-5 lg:flex xl:gap-7">
<div
v-for="item in navItems"
:key="item.label"
class="group relative py-6"
>
<NuxtLink
:to="item.href"
class="inline-flex items-center gap-2 text-[14px] transition hover:text-gm-green"
:class="isActive(item) ? 'text-gm-green' : 'text-[#1f2430]'"
>
{{ item.label }}
<span v-if="item.children?.length" class="text-[10px]" aria-hidden="true">▼</span>
</NuxtLink>
<div
v-if="item.children?.length"
class="pointer-events-none absolute left-0 top-full w-[260px] translate-y-2 rounded-b-[6px] border border-gm-border bg-white p-2 opacity-0 shadow-lg transition-all duration-200 group-hover:pointer-events-auto group-hover:translate-y-0 group-hover:opacity-100"
>
<NuxtLink
v-for="child in item.children"
:key="child.label"
:to="child.href"
class="block rounded px-4 py-3 text-sm text-[#555] transition hover:bg-gm-light hover:text-gm-green"
>
{{ child.label }}
</NuxtLink>
</div>
</div>
</nav>
<div class="flex items-center gap-3">
<NuxtLink :to="contactHref" class="gm-btn gm-btn-primary hidden md:inline-flex">
获取方案
</NuxtLink>
<button
type="button"
class="inline-flex size-11 items-center justify-center rounded-full border border-gm-border text-gm-text transition hover:border-gm-green hover:text-gm-green lg:hidden"
aria-label="打开菜单"
@click="mobileOpen = true"
>
<span class="space-y-1.5">
<span class="block h-0.5 w-5 rounded-full bg-current" />
<span class="block h-0.5 w-5 rounded-full bg-current" />
<span class="block h-0.5 w-5 rounded-full bg-current" />
</span>
</button>
</div>
</div>
<transition
enter-active-class="transition duration-300 ease-out"
enter-from-class="opacity-0"
enter-to-class="opacity-100"
leave-active-class="transition duration-200 ease-in"
leave-from-class="opacity-100"
leave-to-class="opacity-0"
>
<div
v-if="mobileOpen"
class="fixed inset-0 z-[1001] bg-white lg:hidden"
:style="{ top: topOffset }"
>
<div class="container-gm flex h-[75px] items-center justify-between border-b border-gm-border">
<div>
<span class="block text-lg font-semibold text-[#1f2430]">{{ brand.shortName }}</span>
<span class="text-xs text-[#666]">{{ brand.productName }}</span>
</div>
<button
type="button"
class="inline-flex size-11 items-center justify-center rounded-full border border-gm-border text-gm-text transition hover:border-gm-green hover:text-gm-green"
aria-label="关闭菜单"
@click="closeMobile"
>
<span aria-hidden="true" class="text-2xl leading-none">×</span>
</button>
</div>
<nav class="container-gm h-[calc(100vh-75px)] overflow-y-auto py-8">
<div
v-for="item in navItems"
:key="item.label"
class="border-b border-gm-border py-2"
>
<div class="flex items-center justify-between gap-4">
<NuxtLink
:to="item.href"
class="block flex-1 py-4 text-xl text-[#1f2430]"
@click="closeMobile"
>
{{ item.label }}
</NuxtLink>
<button
v-if="item.children?.length"
type="button"
class="flex size-10 items-center justify-center rounded-full border border-gm-border text-gm-text"
:aria-label="`展开${item.label}菜单`"
@click="toggleMobileItem(item.label)"
>
<span
class="transition-transform duration-200"
:class="expandedItem === item.label ? 'rotate-180' : ''"
aria-hidden="true"
>
↓
</span>
</button>
</div>
<div
v-if="item.children?.length && expandedItem === item.label"
class="space-y-2 pb-4"
>
<NuxtLink
v-for="child in item.children"
:key="child.label"
:to="child.href"
class="block rounded bg-gm-light px-4 py-3 text-sm text-[#555]"
@click="closeMobile"
>
{{ child.label }}
</NuxtLink>
</div>
</div>
<NuxtLink
:to="contactHref"
class="mt-8 inline-flex h-12 items-center justify-center rounded-full bg-gm-green px-6 text-base font-medium text-white"
@click="closeMobile"
>
获取方案 →
</NuxtLink>
</nav>
</div>
</transition>
</header>
</template>
+28
View File
@@ -0,0 +1,28 @@
<script setup>
import { computed, ref } from "vue";
import { useGsapReveal } from "~/composables/useGsapReveal";
import { usePageShellMotion } from "~/composables/usePageShellMotion";
const announcementVisible = ref(true);
const root = ref(null);
const pageOffset = computed(() => (announcementVisible.value ? "134px" : "75px"));
useGsapReveal(root);
usePageShellMotion(root);
</script>
<template>
<div ref="root" class="bg-white">
<AnnouncementBar
v-if="announcementVisible"
@close="announcementVisible = false"
/>
<SiteHeader :announcement-visible="announcementVisible" />
<main :style="{ '--page-offset': pageOffset, paddingTop: pageOffset }">
<slot />
</main>
<SiteFooter />
</div>
</template>
@@ -0,0 +1,65 @@
<script setup>
import { advantages as defaultAdvantages } from "~/data/advantages";
defineProps({
advantages: {
type: Array,
default: () => defaultAdvantages,
},
title: {
type: String,
default: "四大核心优势",
},
description: {
type: String,
default: "从真人内容生产、媒体矩阵到增长闭环与行业定制能力,帮助用户快速理解大马棒的差异化竞争力。",
},
routes: {
type: Array,
default: () => [],
},
preview: {
type: Boolean,
default: false,
},
sectionId: {
type: String,
default: "advantages",
},
});
</script>
<template>
<section :id="sectionId" class="bg-white py-16 md:py-20">
<div class="container-gm">
<div class="flex flex-col gap-5 md:flex-row md:items-end md:justify-between" data-reveal>
<div class="max-w-3xl">
<p class="text-sm uppercase tracking-[0.3em] text-[#999]">Advantages</p>
<h2 class="section-title mt-4">{{ title }}</h2>
<p class="mt-4 text-base leading-8 text-[#666]">{{ description }}</p>
</div>
<div class="flex flex-col items-start gap-3 md:items-end">
<SectionRouteLinks :routes="routes" />
<NuxtLink v-if="preview" to="/advantages" class="text-sm font-medium text-gm-green">
查看核心优势 →
</NuxtLink>
</div>
</div>
<div class="mt-10 grid gap-5 md:grid-cols-2 xl:grid-cols-4" data-stagger>
<article
v-for="item in advantages"
:key="item.slug"
class="rounded-lg border border-gm-border bg-white p-6 shadow-sm"
data-stagger-item
data-hover-card
>
<p class="text-[28px] font-semibold leading-none text-gm-green">{{ item.metric }}</p>
<h3 class="mt-5 text-[20px] font-semibold text-[#262626]">{{ item.title }}</h3>
<p class="mt-3 text-sm font-medium leading-7 text-[#333]">{{ item.point }}</p>
<p class="mt-3 text-sm leading-7 text-[#666]">{{ item.value }}</p>
</article>
</div>
</div>
</section>
</template>
+68
View File
@@ -0,0 +1,68 @@
<script setup>
import { computed, ref } from "vue";
import { insights, insightCategories } from "~/data/insights";
const categories = insightCategories.filter((category) => category !== "全部");
const activeCategory = ref(categories[0]);
const filteredArticles = computed(() =>
insights.filter((article) => article.category === activeCategory.value),
);
</script>
<template>
<section class="bg-[#fafafa] py-20" id="insights">
<div class="container-gm">
<div class="text-center" data-reveal>
<p class="text-sm text-[#a1a1a1]">聚焦于</p>
<h2 class="section-title mt-4">原创观点</h2>
</div>
<div class="mt-8 flex flex-wrap justify-center gap-3" data-reveal>
<button
v-for="category in categories"
:key="category"
type="button"
class="rounded-full border px-6 py-2 text-sm transition-all duration-300"
:class="
activeCategory === category
? 'border-gm-green bg-gm-green text-white'
: 'border-gm-border bg-white text-[#666] hover:border-[#d5d5d5]'
"
@click="activeCategory = category"
>
{{ category }}
</button>
</div>
<div class="mt-10 grid gap-6 md:grid-cols-2 xl:grid-cols-3" data-stagger>
<article
v-for="article in filteredArticles"
:key="article.title"
class="overflow-hidden rounded-[28px] border border-gm-border bg-white shadow-sm transition hover:-translate-y-1 hover:shadow-lg"
data-stagger-item
>
<div class="h-56 overflow-hidden bg-gm-light">
<img
:src="article.image"
:alt="article.title"
class="h-full w-full object-cover"
loading="lazy"
/>
</div>
<div class="p-6">
<p class="text-xs font-medium text-gm-green">{{ article.category }}</p>
<h3 class="mt-3 text-lg font-semibold leading-7 text-[#262626]">
{{ article.title }}
</h3>
<p class="mt-3 text-xs text-[#9b9b9b]">{{ article.date }}</p>
<NuxtLink to="/insights" class="mt-4 inline-flex items-center gap-2 text-sm font-medium text-gm-green">
查看详情
<span aria-hidden="true">→</span>
</NuxtLink>
</div>
</article>
</div>
</div>
</section>
</template>
@@ -0,0 +1,38 @@
<script setup>
defineProps({
capabilities: {
type: Array,
required: true,
},
});
</script>
<template>
<section class="bg-[#f7f8f7] py-20">
<div class="container-gm">
<div class="text-center" data-reveal>
<p class="text-sm uppercase tracking-[0.3em] text-[#a1a1a1]">Capability</p>
<h2 class="section-title mt-4">从项目交付到增长资产</h2>
</div>
<div class="mt-12 overflow-x-auto rounded-[28px] border border-gm-border bg-white" data-reveal>
<div class="min-w-[720px]">
<div class="grid grid-cols-[0.8fr_1fr_1fr] border-b border-gm-border bg-gm-dark text-sm font-medium text-white">
<div class="p-5">能力维度</div>
<div class="p-5">基础交付</div>
<div class="p-5">增长超人升级</div>
</div>
<div
v-for="item in capabilities"
:key="item.label"
class="grid grid-cols-[0.8fr_1fr_1fr] border-b border-gm-border last:border-b-0"
>
<div class="p-5 font-medium text-[#262626]">{{ item.label }}</div>
<div class="p-5 text-sm leading-7 text-[#777]">{{ item.basic }}</div>
<div class="p-5 text-sm font-medium leading-7 text-gm-green">{{ item.advanced }}</div>
</div>
</div>
</div>
</div>
</section>
</template>
+150
View File
@@ -0,0 +1,150 @@
<script setup>
import { computed, ref, watch } from "vue";
import { caseCategories, cases as defaultCases } from "~/data/cases";
const props = defineProps({
cases: {
type: Array,
default: () => defaultCases,
},
title: {
type: String,
default: "",
},
previewLimit: {
type: Number,
default: 3,
},
preview: {
type: Boolean,
default: false,
},
routes: {
type: Array,
default: () => [],
},
sectionId: {
type: String,
default: "cases",
},
});
const route = useRoute();
const activeCategory = ref("全部");
const visibleCategories = computed(() =>
props.preview ? ["全部"] : caseCategories,
);
const filteredCases = computed(() => {
const items =
activeCategory.value === "全部"
? props.cases
: props.cases.filter((item) => item.category === activeCategory.value);
return props.preview
? items.filter((item) => item.featured).slice(0, props.previewLimit)
: items;
});
watch(
() => route.query.category,
(category) => {
activeCategory.value =
typeof category === "string" && caseCategories.includes(category)
? category
: "全部";
},
{ immediate: true },
);
</script>
<template>
<section :id="sectionId" class="bg-[#f7f8f7] py-20">
<div class="container-gm">
<div class="flex flex-col gap-6 md:flex-row md:items-end md:justify-between" data-reveal>
<div class="max-w-3xl">
<p class="text-sm uppercase tracking-[0.3em] text-[#9a9a9a]">Cases</p>
<h2 class="section-title mt-4">
{{ title || (preview ? "真实落地成果,见证增长实效" : "案例库") }}
</h2>
</div>
<div class="flex flex-col items-start gap-3 md:items-end">
<SectionRouteLinks :routes="routes" />
<NuxtLink
v-if="preview"
to="/cases"
class="inline-flex items-center gap-2 text-sm font-medium text-gm-green"
>
查看全部案例 <span aria-hidden="true">→</span>
</NuxtLink>
</div>
</div>
<div v-if="!preview" class="mt-8 flex flex-wrap gap-3" data-reveal>
<button
v-for="category in visibleCategories"
:key="category"
type="button"
class="rounded-full border px-6 py-2 text-sm transition-all duration-300"
:class="
activeCategory === category
? 'border-gm-green bg-gm-green text-white'
: 'border-gm-border bg-white text-[#666] hover:border-[#d5d5d5]'
"
@click="activeCategory = category"
>
{{ category }}
</button>
</div>
<div class="mt-10 grid gap-6 md:grid-cols-2 xl:grid-cols-3" data-stagger>
<NuxtLink
v-for="item in filteredCases"
:key="item.title"
:to="`/cases/${item.slug}`"
class="overflow-hidden rounded-lg border border-gm-border bg-white shadow-sm transition hover:shadow-hover"
data-stagger-item
data-hover-card
>
<div class="h-56 overflow-hidden bg-gm-light">
<img
:src="item.image"
:alt="item.title"
class="h-full w-full object-cover transition duration-500 hover:scale-105"
loading="lazy"
/>
</div>
<div class="p-6">
<p class="text-xs font-medium text-gm-green">{{ item.category }}</p>
<h3 class="mt-3 text-xl font-semibold leading-7 text-[#262626]">
{{ item.title }}
</h3>
<p class="mt-3 text-sm leading-7 text-[#777]">{{ item.summary }}</p>
<div class="mt-5 flex flex-wrap gap-2">
<span
v-for="tag in item.tags"
:key="tag"
class="rounded-full bg-gm-light px-3 py-1 text-xs text-[#666]"
>
{{ tag }}
</span>
</div>
<div class="mt-6 grid gap-2">
<p
v-for="metric in item.metrics"
:key="metric"
class="text-sm font-medium text-[#262626]"
>
{{ metric }}
</p>
</div>
<p class="mt-5 font-mono text-[11px] text-[#999]">
案例路径 {{ item.path }}
</p>
</div>
</NuxtLink>
</div>
</div>
</section>
</template>
@@ -0,0 +1,48 @@
<script setup>
import { computed } from "vue";
import { clientLogos as defaultLogos } from "~/data/home";
const props = defineProps({
logos: {
type: Array,
default: () => defaultLogos,
},
});
const marqueeTrackA = computed(() => [...props.logos, ...props.logos]);
const marqueeTrackB = computed(() => [...props.logos, ...props.logos]);
</script>
<template>
<section class="overflow-hidden bg-[#fafafa] py-16" id="cases">
<div class="container-gm" data-reveal>
<p class="text-center text-sm uppercase tracking-[0.3em] text-[#8a8a8a]">
我们的客户
</p>
</div>
<div class="relative mt-8 overflow-hidden">
<div class="flex w-max animate-marquee hover:[animation-play-state:paused]">
<div
v-for="(name, index) in marqueeTrackA"
:key="`a-${name}-${index}`"
class="mx-4 flex h-14 w-32 shrink-0 items-center justify-center rounded-2xl border border-gm-border bg-white text-sm font-medium text-[#6b6b6b] shadow-sm"
>
{{ name }}
</div>
</div>
</div>
<div class="relative mt-6 overflow-hidden">
<div class="flex w-max animate-marquee-reverse hover:[animation-play-state:paused]">
<div
v-for="(name, index) in marqueeTrackB"
:key="`b-${name}-${index}`"
class="mx-4 flex h-14 w-32 shrink-0 items-center justify-center rounded-2xl border border-gm-border bg-white text-sm font-medium text-[#6b6b6b] shadow-sm"
>
{{ name }}
</div>
</div>
</div>
</section>
</template>
@@ -0,0 +1,86 @@
<script setup>
import { homeHero as defaultHero } from "~/data/home";
defineProps({
hero: {
type: Object,
default: () => defaultHero,
},
sectionId: {
type: String,
default: "hero",
},
});
</script>
<template>
<section
:id="sectionId"
class="relative flex min-h-[calc(100vh-var(--page-offset,134px)-40px)] items-center overflow-hidden bg-black bg-cover bg-center pb-10 text-white"
:style="{ backgroundImage: `url(${hero.backgroundImage})` }"
>
<div class="absolute inset-0 bg-black/40" />
<div class="absolute inset-0 bg-[radial-gradient(circle_at_80%_20%,rgba(27,214,41,0.18),transparent_32%),radial-gradient(circle_at_82%_78%,rgba(0,92,230,0.18),transparent_26%)]" />
<div class="absolute inset-x-0 bottom-0 h-24 bg-gradient-to-t from-white to-transparent" />
<div class="container-gm relative z-10 grid gap-12 py-16 md:py-20 lg:grid-cols-[1.05fr_0.95fr] lg:items-center">
<div>
<p class="text-sm uppercase tracking-[0.34em] text-white/70" data-hero-reveal>
{{ hero.eyebrow }}
</p>
<h1 class="mt-5 max-w-4xl text-balance text-[36px] font-semibold leading-tight md:text-[42px]" data-hero-reveal>
{{ hero.title }}
</h1>
<p class="mt-5 max-w-2xl text-base leading-8 text-white/82 md:text-lg" data-hero-reveal>
{{ hero.description }}
</p>
<div class="mt-8 flex flex-wrap gap-3" data-hero-reveal>
<span
v-for="tag in hero.tags"
:key="tag"
class="rounded border border-white/20 bg-white/10 px-4 py-2 text-xs text-white/82 backdrop-blur"
>
{{ tag }}
</span>
</div>
<div class="mt-10 flex flex-wrap gap-4" data-hero-reveal>
<NuxtLink :to="hero.primaryCta.href" class="gm-btn gm-btn-primary px-8">
{{ hero.primaryCta.label }}
</NuxtLink>
<NuxtLink
:to="hero.secondaryCta.href"
class="gm-btn border border-white/35 bg-white/10 px-8 text-white hover:border-white hover:bg-white/20"
>
{{ hero.secondaryCta.label }}
</NuxtLink>
</div>
<div class="mt-6" data-hero-reveal>
<SectionRouteLinks :routes="hero.paths" light />
</div>
</div>
<div class="hidden gap-4 lg:grid" data-hero-reveal>
<div class="rounded-[28px] border border-white/10 bg-white/10 p-6 backdrop-blur">
<p class="text-xs uppercase tracking-[0.26em] text-white/55">Growth Signal</p>
<div class="mt-6 grid gap-4 sm:grid-cols-3 lg:grid-cols-1">
<div class="rounded-2xl border border-white/10 bg-black/10 p-4">
<p class="text-sm text-white/65">真人内容网络</p>
<p class="mt-2 text-[28px] font-semibold text-white">50,000+</p>
</div>
<div class="rounded-2xl border border-white/10 bg-black/10 p-4">
<p class="text-sm text-white/65">官媒资源矩阵</p>
<p class="mt-2 text-[28px] font-semibold text-white">5,000+</p>
</div>
<div class="rounded-2xl border border-white/10 bg-black/10 p-4">
<p class="text-sm text-white/65">标准交付流程</p>
<p class="mt-2 text-[28px] font-semibold text-white">5 Steps</p>
</div>
</div>
</div>
</div>
</div>
</section>
</template>
@@ -0,0 +1,69 @@
<script setup>
defineProps({
item: {
type: Object,
required: true,
},
type: {
type: String,
default: "service",
},
});
</script>
<template>
<section class="bg-white py-16 md:py-20">
<div class="container-gm grid gap-10 lg:grid-cols-[0.75fr_1.25fr]">
<aside class="lg:sticky lg:top-28 lg:self-start" data-reveal>
<p class="text-sm uppercase tracking-[0.3em] text-[#999]">{{ type }}</p>
<h1 class="mt-4 text-balance text-[34px] font-semibold leading-tight text-[#262626] md:text-[42px]">
{{ item.title }}
</h1>
<p class="mt-5 text-base leading-8 text-[#666]">{{ item.summary || item.excerpt }}</p>
<p v-if="item.path" class="mt-5 font-mono text-xs text-[#999]">
页面路径 {{ item.path }}
</p>
<NuxtLink to="#contact" class="gm-btn gm-btn-primary mt-8">立即免费咨询</NuxtLink>
</aside>
<div class="space-y-6">
<div v-if="item.image" class="overflow-hidden rounded-lg border border-gm-border" data-reveal>
<img :src="item.image" :alt="item.title" class="h-[320px] w-full object-cover" data-parallax="24" />
</div>
<div v-if="item.metrics?.length" class="grid gap-4 md:grid-cols-2" data-stagger>
<div v-for="metric in item.metrics" :key="metric" class="rounded-lg border border-gm-border bg-gm-light p-5" data-stagger-item>
<p class="text-sm font-medium text-[#262626]">{{ metric }}</p>
</div>
</div>
<div v-if="item.outcomes?.length" class="grid gap-4 md:grid-cols-2" data-stagger>
<div v-for="outcome in item.outcomes" :key="outcome" class="rounded-lg border border-gm-border bg-gm-light p-5" data-stagger-item>
<p class="text-sm font-medium text-[#262626]">{{ outcome }}</p>
</div>
</div>
<div v-if="item.content?.length" class="rounded-lg border border-gm-border p-6" data-reveal>
<p v-for="paragraph in item.content" :key="paragraph" class="mb-4 text-base leading-8 text-[#666] last:mb-0">
{{ paragraph }}
</p>
</div>
<div v-if="item.challenge || item.solution || item.result" class="grid gap-4" data-stagger>
<article v-if="item.challenge" class="rounded-lg border border-gm-border p-6" data-stagger-item>
<h2 class="text-[20px] font-semibold text-[#262626]">项目挑战</h2>
<p class="mt-3 text-sm leading-7 text-[#666]">{{ item.challenge }}</p>
</article>
<article v-if="item.solution" class="rounded-lg border border-gm-border p-6" data-stagger-item>
<h2 class="text-[20px] font-semibold text-[#262626]">解决方案</h2>
<p class="mt-3 text-sm leading-7 text-[#666]">{{ item.solution }}</p>
</article>
<article v-if="item.result" class="rounded-lg border border-gm-border p-6" data-stagger-item>
<h2 class="text-[20px] font-semibold text-[#262626]">落地结果</h2>
<p class="mt-3 text-sm leading-7 text-[#666]">{{ item.result }}</p>
</article>
</div>
</div>
</div>
</section>
</template>
@@ -0,0 +1,63 @@
<script setup>
import { geoLoopModel } from "~/data/home";
defineProps({
model: {
type: Object,
default: () => geoLoopModel,
},
routes: {
type: Array,
default: () => [],
},
sectionId: {
type: String,
default: "model",
},
});
</script>
<template>
<section :id="sectionId" class="bg-[#f4f4f4] py-16 md:py-20">
<div class="container-gm">
<div class="grid gap-10 lg:grid-cols-[0.82fr_1.18fr]">
<div data-reveal>
<p class="text-sm uppercase tracking-[0.3em] text-[#999]">GEO Model</p>
<h2 class="section-title mt-4">{{ model.title }}</h2>
<p class="mt-5 text-base leading-8 text-[#666]">{{ model.description }}</p>
<div class="mt-6">
<SectionRouteLinks :routes="routes" />
</div>
<NuxtLink :to="model.cta.href" class="gm-btn gm-btn-primary mt-8">
{{ model.cta.label }}
</NuxtLink>
</div>
<div class="grid gap-5 md:grid-cols-2" data-stagger data-geo-loop>
<article
v-for="loop in model.loops"
:key="loop.title"
class="rounded-lg border border-gm-border bg-white p-6 shadow-soft"
data-stagger-item
data-hover-card
>
<p class="text-sm font-medium text-gm-green">{{ loop.title }}</p>
<p class="mt-3 text-[32px] font-semibold leading-none text-[#262626]">{{ loop.value }}</p>
<p class="mt-4 text-sm leading-7 text-[#666]">{{ loop.description }}</p>
<div class="mt-6 space-y-3">
<div
v-for="node in loop.nodes"
:key="node"
class="flex items-center gap-3 rounded border border-gm-border bg-white px-4 py-3 text-sm text-[#555]"
data-geo-node
>
<span class="size-2 shrink-0 rounded-full bg-gm-green" />
<span>{{ node }}</span>
</div>
</div>
</article>
</div>
</div>
</div>
</section>
</template>
+49
View File
@@ -0,0 +1,49 @@
<script setup>
import { homeHero as defaultHero } from "~/data/home";
defineProps({
hero: {
type: Object,
default: () => defaultHero,
},
});
</script>
<template>
<section
class="relative flex min-h-[calc(100vh-var(--page-offset,134px)-40px)] items-center overflow-hidden bg-black pb-14"
:style="{ backgroundImage: `url(${hero.backgroundImage})` }"
>
<div class="absolute inset-0 bg-black/45" />
<div class="absolute inset-x-0 bottom-0 z-10 h-28 bg-gradient-to-t from-white to-transparent" />
<div class="container-gm relative z-10 py-20 text-center text-white">
<p class="text-sm uppercase tracking-[0.35em] text-white/70" data-hero-reveal>
{{ hero.eyebrow }}
</p>
<h1 class="mx-auto mt-6 max-w-4xl text-balance text-4xl font-semibold leading-tight md:text-6xl">
<span data-hero-reveal>{{ hero.title }}</span>
</h1>
<p class="mx-auto mt-6 max-w-2xl text-balance text-base text-white/80 md:text-lg" data-hero-reveal>
{{ hero.description }}
</p>
<div class="mt-10 flex flex-wrap items-center justify-center gap-4" data-hero-reveal>
<NuxtLink :to="hero.primaryCta.href" class="gm-btn bg-gm-green px-8 text-white hover:scale-95 hover:bg-gm-green-dark">
{{ hero.primaryCta.label }}
</NuxtLink>
<NuxtLink :to="hero.secondaryCta.href" class="gm-btn border border-white/35 bg-white/10 px-8 text-white hover:border-white hover:bg-white/20">
{{ hero.secondaryCta.label }}
</NuxtLink>
</div>
<div class="mx-auto mt-14 grid max-w-3xl gap-3 rounded-3xl border border-white/15 bg-white/10 p-3 backdrop-blur md:grid-cols-3" data-hero-reveal>
<div
v-for="metric in hero.metrics"
:key="metric.label"
class="rounded-2xl bg-black/20 px-4 py-4"
>
<p class="text-2xl font-semibold text-white">{{ metric.value }}</p>
<p class="mt-1 text-xs text-white/65">{{ metric.label }}</p>
</div>
</div>
</div>
</section>
</template>
+128
View File
@@ -0,0 +1,128 @@
<script setup>
import { computed, ref, watch } from "vue";
import { insightCategories, insights as defaultInsights } from "~/data/insights";
const props = defineProps({
insights: {
type: Array,
default: () => defaultInsights,
},
routes: {
type: Array,
default: () => [],
},
sectionId: {
type: String,
default: "insights",
},
});
const route = useRoute();
const activeCategory = ref("全部");
const filteredInsights = computed(() =>
activeCategory.value === "全部"
? props.insights
: props.insights.filter((item) => item.category === activeCategory.value),
);
const featured = computed(() => props.insights.find((item) => item.featured));
watch(
() => route.query.category,
(category) => {
activeCategory.value =
typeof category === "string" && insightCategories.includes(category)
? category
: "全部";
},
{ immediate: true },
);
</script>
<template>
<section :id="sectionId" class="bg-white py-20">
<div class="container-gm">
<div class="mb-8 flex flex-col gap-4 md:flex-row md:items-end md:justify-between" data-reveal>
<div class="max-w-3xl">
<p class="text-sm uppercase tracking-[0.3em] text-[#a1a1a1]">Insights</p>
<h2 class="section-title mt-4">文章列表、热门推荐与分类筛选</h2>
</div>
<SectionRouteLinks :routes="routes" />
</div>
<article
v-if="featured"
class="grid overflow-hidden rounded-lg border border-gm-border bg-[#f7f8f7] lg:grid-cols-[1.05fr_0.95fr]"
data-reveal
>
<div class="p-8 md:p-12">
<p class="text-sm font-medium text-gm-green">精选观点</p>
<h2 class="mt-4 text-balance text-3xl font-semibold leading-tight text-[#262626] md:text-[42px]">
{{ featured.title }}
</h2>
<p class="mt-5 max-w-2xl text-base leading-8 text-[#777]">
{{ featured.excerpt }}
</p>
<p class="mt-8 text-sm text-[#999]">{{ featured.date }} · {{ featured.category }}</p>
<p class="mt-3 font-mono text-[11px] text-[#999]">
内容路径 {{ featured.path }}
</p>
</div>
<img
:src="featured.image"
:alt="featured.title"
class="h-full min-h-[320px] w-full object-cover"
/>
</article>
<div class="mt-12 flex flex-wrap gap-3" data-reveal>
<button
v-for="category in insightCategories"
:key="category"
type="button"
class="rounded-full border px-6 py-2 text-sm transition-all duration-300"
:class="
activeCategory === category
? 'border-gm-green bg-gm-green text-white'
: 'border-gm-border bg-white text-[#666] hover:border-[#d5d5d5]'
"
@click="activeCategory = category"
>
{{ category }}
</button>
</div>
<div class="mt-10 grid gap-6 md:grid-cols-2 xl:grid-cols-3" data-stagger>
<NuxtLink
v-for="item in filteredInsights"
:key="item.title"
:to="`/insights/${item.slug}`"
class="overflow-hidden rounded-lg border border-gm-border bg-white shadow-sm transition hover:shadow-hover"
data-stagger-item
data-hover-card
>
<div class="h-52 overflow-hidden bg-gm-light">
<img
:src="item.image"
:alt="item.title"
class="h-full w-full object-cover"
loading="lazy"
/>
</div>
<div class="p-6">
<p class="text-xs font-medium text-gm-green">{{ item.category }}</p>
<h3 class="mt-3 text-lg font-semibold leading-7 text-[#262626]">
{{ item.title }}
</h3>
<p class="mt-3 text-sm leading-7 text-[#777]">{{ item.excerpt }}</p>
<p class="mt-5 text-xs text-[#9b9b9b]">{{ item.date }}</p>
<p class="mt-3 font-mono text-[11px] text-[#999]">
内容路径 {{ item.path }}
</p>
</div>
</NuxtLink>
</div>
</div>
</section>
</template>
+74
View File
@@ -0,0 +1,74 @@
<script setup>
defineProps({
eyebrow: {
type: String,
default: "Proof",
},
title: {
type: String,
required: true,
},
description: {
type: String,
default: "",
},
metrics: {
type: Array,
required: true,
},
logos: {
type: Array,
default: () => [],
},
routes: {
type: Array,
default: () => [],
},
sectionId: {
type: String,
default: "clients",
},
});
</script>
<template>
<section :id="sectionId" class="bg-white py-16 md:py-20">
<div class="container-gm">
<div class="flex flex-col gap-5 md:flex-row md:items-end md:justify-between" data-reveal>
<div class="max-w-3xl">
<p class="text-sm uppercase tracking-[0.3em] text-[#999]">{{ eyebrow }}</p>
<h2 class="section-title mt-4">{{ title }}</h2>
<p v-if="description" class="mt-4 text-base leading-8 text-[#666]">
{{ description }}
</p>
</div>
<SectionRouteLinks :routes="routes" />
</div>
<div class="mt-10 grid gap-4 sm:grid-cols-2 lg:grid-cols-4" data-stagger>
<AnimatedStat
v-for="metric in metrics"
:key="metric.label"
:label="metric.label"
:value="metric.value"
:suffix="metric.suffix"
data-stagger-item
/>
</div>
<div v-if="logos.length" class="mt-10">
<p class="mb-4 text-xs uppercase tracking-[0.28em] text-[#999]">覆盖行业</p>
<div class="grid grid-cols-2 gap-3 md:grid-cols-4 xl:grid-cols-6" data-stagger>
<div
v-for="logo in logos"
:key="logo"
class="flex h-12 items-center justify-center rounded border border-gm-border bg-gm-light text-sm text-[#666]"
data-stagger-item
>
{{ logo }}
</div>
</div>
</div>
</div>
</section>
</template>
+56
View File
@@ -0,0 +1,56 @@
<script setup>
defineProps({
eyebrow: {
type: String,
default: "",
},
title: {
type: String,
required: true,
},
description: {
type: String,
default: "",
},
image: {
type: String,
default: "/images/banner-bg.jpg",
},
routes: {
type: Array,
default: () => [],
},
});
</script>
<template>
<section
class="relative overflow-hidden bg-black bg-cover bg-center text-white"
:style="{ backgroundImage: `url(${image})` }"
>
<div class="absolute inset-0 bg-black/55" />
<div class="absolute inset-x-0 bottom-0 h-24 bg-gradient-to-t from-white to-transparent" />
<div class="container-gm relative z-10 py-24 md:py-32">
<p
v-if="eyebrow"
class="text-sm uppercase tracking-[0.34em] text-white/65"
data-hero-reveal
>
{{ eyebrow }}
</p>
<h1 class="mt-5 max-w-4xl text-balance text-[34px] font-semibold leading-tight md:text-[42px]" data-hero-reveal>
{{ title }}
</h1>
<p
v-if="description"
class="mt-6 max-w-2xl text-base leading-8 text-white/78 md:text-lg"
data-hero-reveal
>
{{ description }}
</p>
<div v-if="routes.length" class="mt-8" data-hero-reveal>
<SectionRouteLinks :routes="routes" light />
</div>
</div>
</section>
</template>
@@ -0,0 +1,56 @@
<script setup>
import { processSteps } from "~/data/home";
defineProps({
steps: {
type: Array,
default: () => processSteps,
},
title: {
type: String,
default: "5 步标准化交付流程,透明可控效果可追溯",
},
description: {
type: String,
default: "标准化流程加透明化交付,让服务可控、效果清晰可见。",
},
routes: {
type: Array,
default: () => [],
},
sectionId: {
type: String,
default: "process",
},
});
</script>
<template>
<section :id="sectionId" class="bg-white py-16 md:py-20">
<div class="container-gm">
<div class="flex flex-col gap-5 md:flex-row md:items-end md:justify-between" data-reveal>
<div class="max-w-3xl">
<p class="text-sm uppercase tracking-[0.3em] text-[#999]">Process</p>
<h2 class="section-title mt-4">{{ title }}</h2>
<p class="mt-4 text-base leading-8 text-[#666]">{{ description }}</p>
</div>
<SectionRouteLinks :routes="routes" />
</div>
<div class="relative mt-10 grid gap-4 md:grid-cols-5" data-stagger data-process-line>
<div class="absolute left-0 top-8 hidden h-px w-full bg-gm-border md:block" />
<div class="absolute left-0 top-8 hidden h-px w-0 bg-gm-green md:block" data-flow-line />
<article
v-for="step in steps"
:key="step.step"
class="relative rounded-lg border border-gm-border bg-white p-5 shadow-sm"
data-stagger-item
>
<p class="text-[24px] font-semibold text-gm-green">{{ step.step }}</p>
<h3 class="mt-4 text-[18px] font-semibold text-[#262626]">{{ step.title }}</h3>
<p class="mt-3 text-sm leading-7 text-[#666]">{{ step.text }}</p>
</article>
</div>
</div>
</section>
</template>
+35
View File
@@ -0,0 +1,35 @@
<script setup>
import { roiStats as defaultStats } from "~/data/home";
defineProps({
stats: {
type: Array,
default: () => defaultStats,
},
title: {
type: String,
default: "我们交付投资回报率",
},
});
</script>
<template>
<section class="bg-white py-20">
<div class="container-gm">
<div class="max-w-2xl" data-reveal>
<p class="text-sm uppercase tracking-[0.3em] text-[#a1a1a1]">ROI</p>
<h2 class="section-title mt-4">{{ title }}</h2>
</div>
<div class="mt-12 grid gap-6 sm:grid-cols-2 xl:grid-cols-3">
<AnimatedStat
v-for="stat in stats"
:key="stat.label + stat.value"
:label="stat.label"
:value="stat.value"
:suffix="stat.suffix"
/>
</div>
</div>
</section>
</template>
@@ -0,0 +1,94 @@
<script setup>
defineProps({
service: {
type: Object,
required: true,
},
});
</script>
<template>
<section class="bg-white py-20">
<div class="container-gm grid gap-12 lg:grid-cols-[0.85fr_1.15fr]">
<div class="lg:sticky lg:top-28 lg:self-start" data-reveal>
<p class="text-sm uppercase tracking-[0.3em] text-[#a1a1a1]">{{ service.tier }}</p>
<h1 class="mt-4 text-balance text-4xl font-semibold leading-tight text-[#262626] md:text-[56px]">
{{ service.title }}
</h1>
<p class="mt-5 text-base leading-8 text-[#777]">{{ service.summary }}</p>
<NuxtLink :to="service.cta.href" class="gm-btn gm-btn-primary mt-8 px-8">
{{ service.cta.label }}
</NuxtLink>
</div>
<div class="space-y-8">
<div class="overflow-hidden rounded-[28px] border border-gm-border" data-reveal>
<img
:src="service.image"
:alt="service.title"
class="h-[340px] w-full object-cover"
data-parallax="30"
/>
</div>
<div class="grid gap-6 md:grid-cols-3" data-stagger>
<article
v-for="metric in service.metrics"
:key="metric.label"
class="rounded-[24px] border border-gm-border bg-[#f7f8f7] p-6"
data-stagger-item
>
<p class="text-3xl font-semibold text-gm-green">{{ metric.value }}</p>
<p class="mt-2 text-sm text-[#777]">{{ metric.label }}</p>
</article>
</div>
<div class="grid gap-6 md:grid-cols-2" data-stagger>
<article class="rounded-[24px] border border-gm-border p-6" data-stagger-item>
<h2 class="text-xl font-semibold text-[#262626]">适用场景</h2>
<ul class="mt-5 space-y-3">
<li
v-for="pain in service.pains"
:key="pain"
class="flex gap-3 text-sm leading-7 text-[#666]"
>
<span class="mt-2 size-1.5 shrink-0 rounded-full bg-gm-green" />
<span>{{ pain }}</span>
</li>
</ul>
</article>
<article class="rounded-[24px] border border-gm-border p-6" data-stagger-item>
<h2 class="text-xl font-semibold text-[#262626]">核心交付物</h2>
<ul class="mt-5 space-y-3">
<li
v-for="item in service.deliverables"
:key="item"
class="flex gap-3 text-sm leading-7 text-[#666]"
>
<span class="mt-2 size-1.5 shrink-0 rounded-full bg-gm-green" />
<span>{{ item }}</span>
</li>
</ul>
</article>
</div>
<div class="rounded-[24px] border border-gm-border bg-white p-6" data-reveal>
<h2 class="text-xl font-semibold text-[#262626]">常见问题</h2>
<div class="mt-5 divide-y divide-gm-border">
<details
v-for="faq in service.faqs"
:key="faq.question"
class="group py-4"
>
<summary class="cursor-pointer list-none text-base font-medium text-[#262626]">
{{ faq.question }}
</summary>
<p class="mt-3 text-sm leading-7 text-[#777]">{{ faq.answer }}</p>
</details>
</div>
</div>
</div>
</div>
</section>
</template>
@@ -0,0 +1,92 @@
<script setup>
defineProps({
eyebrow: {
type: String,
default: "Three Directions",
},
title: {
type: String,
required: true,
},
description: {
type: String,
default: "",
},
services: {
type: Array,
required: true,
},
routes: {
type: Array,
default: () => [],
},
sectionId: {
type: String,
default: "service-directions",
},
});
</script>
<template>
<section :id="sectionId" class="bg-[#f4f4f4] py-16 md:py-20">
<div class="container-gm">
<div class="flex flex-col gap-5 md:flex-row md:items-end md:justify-between" data-reveal>
<div class="max-w-3xl">
<p class="text-sm uppercase tracking-[0.3em] text-[#999]">{{ eyebrow }}</p>
<h2 class="section-title mt-4">{{ title }}</h2>
<p v-if="description" class="mt-4 text-base leading-8 text-[#666]">{{ description }}</p>
</div>
<SectionRouteLinks :routes="routes" />
</div>
<div class="mt-10 grid gap-5 lg:grid-cols-3" data-stagger>
<NuxtLink
v-for="service in services"
:key="service.slug"
:to="service.href"
class="group rounded-lg border border-gm-border bg-white p-6 shadow-sm transition hover:shadow-hover"
data-stagger-item
data-hover-card
>
<div class="flex items-start justify-between gap-4">
<div>
<p class="text-xs uppercase tracking-[0.28em] text-gm-green">{{ service.tags.join(" / ") }}</p>
<h3 class="mt-4 text-[22px] font-semibold text-[#262626]">{{ service.title }}</h3>
</div>
<span class="rounded-full bg-gm-light px-3 py-1 text-xs text-[#666]">{{ service.metrics[0].value }}</span>
</div>
<p class="mt-4 text-sm leading-7 text-[#666]">{{ service.summary }}</p>
<div class="mt-5 flex flex-wrap gap-2">
<span
v-for="client in service.fitClients"
:key="client"
class="rounded-full border border-gm-border px-3 py-1 text-xs text-[#666]"
>
{{ client }}
</span>
</div>
<div class="mt-6 grid gap-3">
<div
v-for="item in service.deliverables.slice(0, 3)"
:key="item"
class="flex gap-3 text-sm text-[#555]"
>
<span class="mt-2 size-1.5 shrink-0 rounded-full bg-gm-green" />
<span>{{ item }}</span>
</div>
</div>
<p class="mt-6 font-mono text-[11px] text-[#999]">
服务路径 {{ service.path }}
</p>
<span class="mt-4 inline-flex items-center gap-2 text-sm font-medium text-gm-green">
查看服务详情 <span aria-hidden="true">→</span>
</span>
</NuxtLink>
</div>
</div>
</section>
</template>
@@ -0,0 +1,61 @@
<script setup>
import { serviceOverview } from "~/data/services";
defineProps({
hero: {
type: Object,
default: () => serviceOverview.hero,
},
services: {
type: Array,
default: () => [],
},
});
</script>
<template>
<section class="bg-white py-20">
<div class="container-gm grid items-start gap-12 lg:grid-cols-[1fr_508px]">
<div class="max-w-[720px]">
<p class="text-[18px] uppercase leading-none text-[#b8b8b8]" data-reveal>
{{ hero.eyebrow }}
</p>
<h1 class="mt-4 text-balance text-[42px] font-medium leading-[1.18] text-[#262626] md:text-[52px]" data-reveal>
{{ hero.title }}
</h1>
<p class="mt-5 max-w-2xl text-base leading-8 text-[#777]" data-reveal>
{{ hero.description }}
</p>
<div class="mt-12 space-y-8" data-stagger>
<NuxtLink
v-for="service in services"
:key="service.href"
:to="service.href"
class="block"
data-stagger-item
data-hover-card
>
<p class="mb-4 text-[18px] font-medium text-[#12d12c]">
{{ service.tier }}
</p>
<h2 class="text-[28px] font-medium leading-tight text-[#262626] md:text-[34px]">
{{ service.title }}
</h2>
<p class="mt-4 max-w-[680px] text-base leading-8 text-[#999]">
{{ service.summary }}
</p>
</NuxtLink>
</div>
</div>
<div class="relative hidden justify-center lg:flex" data-reveal>
<img
:src="hero.image"
alt="增长超人服务能力展示"
class="h-auto w-[508px] select-none"
/>
</div>
</div>
</section>
</template>
@@ -0,0 +1,63 @@
<script setup>
defineProps({
model: {
type: Object,
required: true,
},
routes: {
type: Array,
default: () => [],
},
sectionId: {
type: String,
default: "service-model",
},
});
</script>
<template>
<section :id="sectionId" class="bg-white py-16 md:py-20">
<div class="container-gm">
<div class="flex flex-col gap-5 md:flex-row md:items-end md:justify-between" data-reveal>
<div class="max-w-3xl">
<p class="text-sm uppercase tracking-[0.3em] text-[#999]">{{ model.eyebrow }}</p>
<h2 class="section-title mt-4">{{ model.title }}</h2>
<p class="mt-4 text-base leading-8 text-[#666]">{{ model.description }}</p>
</div>
<SectionRouteLinks :routes="routes" />
</div>
<div class="mt-10 grid gap-5 lg:grid-cols-3" data-stagger>
<article
v-for="stage in model.stages"
:key="stage.phase"
class="rounded-lg border border-gm-border bg-[#f7f8f7] p-6"
data-stagger-item
>
<p class="text-[28px] font-semibold text-gm-green">{{ stage.phase }}</p>
<h3 class="mt-4 text-[22px] font-semibold text-[#262626]">{{ stage.title }}</h3>
<p class="mt-3 text-sm leading-7 text-[#666]">{{ stage.summary }}</p>
<div class="mt-5 space-y-3">
<div
v-for="point in stage.points"
:key="point"
class="flex gap-3 text-sm text-[#555]"
>
<span class="mt-2 size-1.5 shrink-0 rounded-full bg-gm-green" />
<span>{{ point }}</span>
</div>
</div>
<NuxtLink :to="stage.href" class="mt-6 inline-flex items-center gap-2 text-sm font-medium text-gm-green">
{{ stage.service }}
<span aria-hidden="true">→</span>
</NuxtLink>
<p class="mt-4 font-mono text-[11px] text-[#999]">
模型对应路径 {{ stage.path }}
</p>
</article>
</div>
</div>
</section>
</template>
@@ -0,0 +1,62 @@
<script setup>
defineProps({
overview: {
type: Object,
required: true,
},
routes: {
type: Array,
default: () => [],
},
sectionId: {
type: String,
default: "service-overview",
},
});
</script>
<template>
<section :id="sectionId" class="bg-white py-16 md:py-20">
<div class="container-gm">
<div class="flex flex-col gap-5 md:flex-row md:items-end md:justify-between" data-reveal>
<div class="max-w-3xl">
<p class="text-sm uppercase tracking-[0.3em] text-[#999]">{{ overview.eyebrow }}</p>
<h2 class="section-title mt-4">{{ overview.title }}</h2>
<p class="mt-4 text-base leading-8 text-[#666]">{{ overview.description }}</p>
</div>
<SectionRouteLinks :routes="routes" />
</div>
<div class="mt-10 grid gap-6 lg:grid-cols-[1.15fr_0.85fr]">
<div class="rounded-lg border border-gm-border bg-[#f7f8f7] p-6 md:p-8" data-stagger>
<p class="text-xs uppercase tracking-[0.28em] text-[#999]">Service Value</p>
<div class="mt-6 space-y-4">
<div
v-for="item in overview.highlights"
:key="item"
class="flex gap-3 rounded-lg border border-gm-border bg-white px-4 py-4 text-sm leading-7 text-[#555]"
data-stagger-item
>
<span class="mt-2 size-2 shrink-0 rounded-full bg-gm-green" />
<span>{{ item }}</span>
</div>
</div>
</div>
<div class="rounded-lg border border-gm-border bg-white p-6 md:p-8" data-reveal>
<p class="text-xs uppercase tracking-[0.28em] text-[#999]">Fit Clients</p>
<h3 class="mt-4 text-[22px] font-semibold text-[#262626]">适用客户场景</h3>
<div class="mt-6 flex flex-wrap gap-3">
<span
v-for="item in overview.audiences"
:key="item"
class="rounded-full border border-gm-border bg-gm-light px-4 py-2 text-sm text-[#555]"
>
{{ item }}
</span>
</div>
</div>
</div>
</div>
</section>
</template>
@@ -0,0 +1,87 @@
<script setup>
import { serviceOverview } from "~/data/services";
defineProps({
problem: {
type: Object,
default: () => serviceOverview.problem,
},
});
</script>
<template>
<section
class="bg-top bg-no-repeat py-6 md:py-16"
:style="{ backgroundImage: `url(${problem.backgroundImage})` }"
>
<div class="container-gm">
<div class="mx-auto max-w-[760px] text-center" data-reveal>
<h2 class="section-title text-balance">
企业的营销数字化面临的
<span class="text-gm-green">三个问题</span>
,增长超人将如何帮您解决?
</h2>
<div class="mx-auto mt-7 flex size-12 items-center justify-center rounded-full border border-[#b7f7c2] text-gm-green shadow-[0_0_0_8px_rgba(27,214,41,0.08)]">
↓
</div>
</div>
<article class="mt-14 grid gap-10 rounded-[20px] bg-white px-6 py-8 shadow-soft lg:mt-[92px] lg:grid-cols-[1.05fr_0.95fr] lg:px-12 lg:py-10" data-stagger>
<div data-stagger-item>
<span class="rounded-full bg-gm-green px-4 py-1 text-xs font-medium text-white">
{{ problem.problemLabel }}
</span>
<h3 class="mt-8 text-[24px] font-medium leading-[1.5] text-[#7f7f7f] md:text-[30px]">
{{ problem.problem }}
</h3>
<p class="mt-9 text-sm text-[#999]">{{ problem.solutionTitle }}</p>
<p class="mt-4 text-[24px] font-medium leading-[1.5] text-[#8f8f8f] md:text-[30px]">
{{ problem.solution }}
</p>
<div class="mt-16 space-y-5">
<p class="text-sm text-[#999]">网站开发服务</p>
<NuxtLink :to="problem.solutionLink.href" class="inline-flex items-center gap-2 text-sm font-medium text-gm-green">
{{ problem.solutionLink.label }}
<span aria-hidden="true">→</span>
</NuxtLink>
</div>
</div>
<div data-stagger-item>
<p class="mb-5 text-sm text-[#999]">相关案例</p>
<div class="grid grid-cols-2 border-l border-t border-[#edf0ee] md:grid-cols-3">
<div
v-for="item in problem.cases"
:key="item"
class="flex min-h-[96px] items-center justify-center border-b border-r border-[#edf0ee] px-3 text-center text-xs leading-relaxed text-[#828282]"
>
{{ item }}
</div>
</div>
<p class="mb-5 mt-10 text-sm text-[#999]">相关洞察</p>
<ul class="space-y-3">
<li
v-for="(item, index) in problem.insights"
:key="item"
class="flex gap-2 text-sm leading-relaxed text-[#555]"
>
<span class="mt-2 size-1 shrink-0 rounded-full bg-gm-green" />
<span>
{{ item }}
<span
v-if="index === 0"
class="ml-2 rounded-full bg-gm-green px-2 py-0.5 text-[10px] text-white"
>
HOT
</span>
</span>
</li>
</ul>
</div>
</article>
</div>
</section>
</template>
@@ -0,0 +1,48 @@
<script setup>
defineProps({
steps: {
type: Array,
required: true,
},
title: {
type: String,
default: "交付流程",
},
description: {
type: String,
default: "从诊断到上线,再到持续复盘,把增长动作落到可执行的节奏里。",
},
});
</script>
<template>
<section class="bg-white py-20">
<div class="container-gm">
<div class="max-w-2xl" data-reveal>
<p class="text-sm uppercase tracking-[0.3em] text-[#a1a1a1]">Process</p>
<h2 class="section-title mt-4">{{ title }}</h2>
<p class="mt-4 text-base leading-8 text-[#777]">{{ description }}</p>
</div>
<div class="mt-12 grid gap-5 md:grid-cols-2 xl:grid-cols-4" data-stagger>
<article
v-for="step in steps"
:key="step.step || step"
class="rounded-[24px] border border-gm-border bg-white p-6 shadow-sm"
data-stagger-item
data-hover-card
>
<p class="text-[42px] font-semibold leading-none text-gm-green">
{{ step.step || String(steps.indexOf(step) + 1).padStart(2, "0") }}
</p>
<h3 class="mt-5 text-xl font-semibold text-[#262626]">
{{ step.title || step }}
</h3>
<p v-if="step.text" class="mt-3 text-sm leading-7 text-[#777]">
{{ step.text }}
</p>
</article>
</div>
</div>
</section>
</template>
@@ -0,0 +1,67 @@
<script setup>
defineProps({
results: {
type: Object,
required: true,
},
routes: {
type: Array,
default: () => [],
},
sectionId: {
type: String,
default: "service-results",
},
});
</script>
<template>
<section :id="sectionId" class="bg-[#f4f4f4] py-16 md:py-20">
<div class="container-gm">
<div class="flex flex-col gap-5 md:flex-row md:items-end md:justify-between" data-reveal>
<div class="max-w-3xl">
<p class="text-sm uppercase tracking-[0.3em] text-[#999]">{{ results.eyebrow }}</p>
<h2 class="section-title mt-4">{{ results.title }}</h2>
<p class="mt-4 text-base leading-8 text-[#666]">{{ results.description }}</p>
</div>
<SectionRouteLinks :routes="routes" />
</div>
<div class="mt-10 grid gap-5 lg:grid-cols-[0.9fr_1.1fr]">
<div class="grid gap-4" data-stagger>
<article
v-for="item in results.proofs"
:key="item.label"
class="rounded-lg border border-gm-border bg-white p-6"
data-stagger-item
>
<p class="text-[34px] font-semibold leading-none text-gm-green">{{ item.value }}</p>
<h3 class="mt-4 text-[18px] font-semibold text-[#262626]">{{ item.label }}</h3>
<p class="mt-3 text-sm leading-7 text-[#666]">{{ item.note }}</p>
</article>
</div>
<div class="grid gap-5 md:grid-cols-2" data-stagger>
<NuxtLink
v-for="item in results.cases"
:key="item.href"
:to="item.href"
class="rounded-lg border border-gm-border bg-white p-6 shadow-sm transition hover:shadow-hover"
data-stagger-item
data-hover-card
>
<p class="text-xs uppercase tracking-[0.28em] text-gm-green">Case Proof</p>
<h3 class="mt-4 text-[20px] font-semibold leading-7 text-[#262626]">{{ item.title }}</h3>
<p class="mt-4 text-sm leading-7 text-[#666]">{{ item.summary }}</p>
<p class="mt-6 font-mono text-[11px] text-[#999]">
案例路径 {{ item.path }}
</p>
<span class="mt-4 inline-flex items-center gap-2 text-sm font-medium text-gm-green">
查看案例详情 <span aria-hidden="true">→</span>
</span>
</NuxtLink>
</div>
</div>
</div>
</section>
</template>
+61
View File
@@ -0,0 +1,61 @@
<script setup>
import { serviceItems } from "~/data/services";
defineProps({
services: {
type: Array,
default: () => serviceItems,
},
eyebrow: {
type: String,
default: "Method",
},
title: {
type: String,
default: "基于高楼营销法则",
},
description: {
type: String,
default: "提供全链路增长服务",
},
});
</script>
<template>
<section class="bg-white py-20" id="services">
<div class="container-gm grid gap-10 lg:grid-cols-[0.95fr_1.05fr]">
<div class="lg:sticky lg:top-28 lg:self-start" data-reveal>
<p class="text-sm uppercase tracking-[0.3em] text-[#a1a1a1]">{{ eyebrow }}</p>
<h2 class="mt-4 text-[34px] font-semibold leading-tight text-[#262626] md:text-[42px]">
{{ title }}
</h2>
<p class="mt-4 text-lg text-[#666]">{{ description }}</p>
</div>
<div class="space-y-8" data-stagger>
<NuxtLink
v-for="(service, index) in services"
:key="service.slug"
:to="service.href"
class="block border-b border-gm-border pb-8 last:border-b-0"
data-stagger-item
data-hover-card
>
<p class="text-[44px] font-semibold leading-none text-gm-green">
{{ String(index + 1).padStart(2, "0") }}
</p>
<h3 class="mt-4 text-[28px] font-semibold text-[#262626]">
{{ service.title }}
</h3>
<p class="mt-2 text-base text-[#666]">{{ service.tier }}</p>
<p class="mt-4 max-w-2xl text-sm leading-7 text-[#777]">
{{ service.summary }}
</p>
<span class="mt-5 inline-flex items-center gap-2 text-sm font-medium text-gm-green">
查看服务详情 <span aria-hidden="true">→</span>
</span>
</NuxtLink>
</div>
</div>
</section>
</template>
@@ -0,0 +1,77 @@
<script setup>
import { solutions as defaultSolutions } from "~/data/solutions";
defineProps({
solutions: {
type: Array,
default: () => defaultSolutions,
},
title: {
type: String,
default: "深耕 12+ 行业,定制专属 GEO 增长方案",
},
description: {
type: String,
default: "让不同行业客户快速找到与自身相关的增长方案,提升业务匹配度感知。",
},
routes: {
type: Array,
default: () => [],
},
preview: {
type: Boolean,
default: false,
},
sectionId: {
type: String,
default: "solutions",
},
});
</script>
<template>
<section :id="sectionId" class="bg-[#f4f4f4] py-16 md:py-20">
<div class="container-gm">
<div class="flex flex-col gap-5 md:flex-row md:items-end md:justify-between" data-reveal>
<div class="max-w-3xl">
<p class="text-sm uppercase tracking-[0.3em] text-[#999]">Solutions</p>
<h2 class="section-title mt-4">{{ title }}</h2>
<p class="mt-4 text-base leading-8 text-[#666]">{{ description }}</p>
</div>
<div class="flex flex-col items-start gap-3 md:items-end">
<SectionRouteLinks :routes="routes" />
<NuxtLink v-if="preview" to="/solutions" class="text-sm font-medium text-gm-green">
获取我的行业专属方案 →
</NuxtLink>
</div>
</div>
<div class="mt-10 grid gap-5 md:grid-cols-2 xl:grid-cols-3" data-stagger>
<NuxtLink
v-for="item in solutions"
:key="item.slug"
:to="item.href"
class="group overflow-hidden rounded-lg border border-gm-border bg-white shadow-sm"
data-stagger-item
data-hover-card
>
<div class="h-44 overflow-hidden bg-gm-light">
<img :src="item.image" :alt="item.title" class="h-full w-full object-cover transition duration-500 group-hover:scale-105" loading="lazy" />
</div>
<div class="p-6">
<h3 class="text-[20px] font-semibold text-[#262626]">{{ item.title }}</h3>
<p class="mt-3 text-sm leading-7 text-[#666]">{{ item.summary }}</p>
<div class="mt-5 flex flex-wrap gap-2">
<span v-for="tag in item.tags" :key="tag" class="rounded bg-gm-light px-3 py-1 text-xs text-[#666]">
{{ tag }}
</span>
</div>
<p class="mt-5 font-mono text-[11px] text-[#999]">
方案路径 {{ item.path || item.href }}
</p>
</div>
</NuxtLink>
</div>
</div>
</section>
</template>
@@ -0,0 +1,89 @@
<script setup>
defineProps({
mapping: {
type: Object,
required: true,
},
routes: {
type: Array,
default: () => [],
},
sectionId: {
type: String,
default: "solution-mapping",
},
});
</script>
<template>
<section :id="sectionId" class="bg-white py-16 md:py-20">
<div class="container-gm">
<div class="flex flex-col gap-5 md:flex-row md:items-end md:justify-between" data-reveal>
<div class="max-w-3xl">
<p class="text-sm uppercase tracking-[0.3em] text-[#999]">{{ mapping.eyebrow }}</p>
<h2 class="section-title mt-4">{{ mapping.title }}</h2>
<p class="mt-4 text-base leading-8 text-[#666]">{{ mapping.description }}</p>
</div>
<SectionRouteLinks :routes="routes" />
</div>
<div class="mt-10 grid gap-5 md:grid-cols-2" data-stagger>
<article
v-for="item in mapping.items"
:key="item.industry"
class="rounded-lg border border-gm-border bg-[#f7f8f7] p-6"
data-stagger-item
>
<div class="flex items-start justify-between gap-4">
<div>
<p class="text-xs uppercase tracking-[0.28em] text-gm-green">Industry</p>
<h3 class="mt-3 text-[22px] font-semibold text-[#262626]">{{ item.industry }}</h3>
</div>
<div class="flex flex-wrap justify-end gap-2">
<span
v-for="tag in item.tags"
:key="tag"
class="rounded-full border border-gm-border bg-white px-3 py-1 text-xs text-[#666]"
>
{{ tag }}
</span>
</div>
</div>
<div class="mt-6 space-y-4 text-sm leading-7">
<div>
<p class="font-medium text-[#262626]">行业痛点</p>
<p class="mt-1 text-[#666]">{{ item.pain }}</p>
</div>
<div>
<p class="font-medium text-[#262626]">对应方案</p>
<p class="mt-1 text-[#666]">{{ item.plan }}</p>
</div>
</div>
<div class="mt-6 flex flex-wrap gap-3">
<NuxtLink
:to="item.solutionHref"
class="inline-flex items-center gap-2 rounded-full border border-gm-green px-4 py-2 text-sm font-medium text-gm-green transition hover:bg-gm-green hover:text-white"
>
查看行业方案
<span aria-hidden="true">→</span>
</NuxtLink>
<NuxtLink
:to="item.caseHref"
class="inline-flex items-center gap-2 rounded-full border border-gm-border bg-white px-4 py-2 text-sm text-[#555] transition hover:border-gm-green hover:text-gm-green"
>
查看对应案例
<span aria-hidden="true">→</span>
</NuxtLink>
</div>
<div class="mt-5 space-y-2 font-mono text-[11px] text-[#999]">
<p>方案路径 {{ item.solutionPath }}</p>
<p>案例路径 {{ item.casePath }}</p>
</div>
</article>
</div>
</div>
</section>
</template>
+27
View File
@@ -0,0 +1,27 @@
<script setup>
import { aboutPage } from "~/data/about";
</script>
<template>
<section class="bg-white py-20" id="about">
<div class="container-gm grid items-center gap-12 lg:grid-cols-2">
<div data-reveal>
<p class="text-sm uppercase tracking-[0.3em] text-[#a1a1a1]">About</p>
<h2 class="mt-4 text-balance text-3xl font-semibold leading-snug text-[#262626] md:text-[40px]">
{{ aboutPage.hero.title }}
</h2>
<p class="mt-5 max-w-xl text-base leading-8 text-[#777]">
{{ aboutPage.hero.description }}
</p>
</div>
<div class="overflow-hidden rounded-[32px] border border-gm-border shadow-xl" data-reveal>
<img
:src="aboutPage.hero.image"
alt="增长超人团队"
class="h-full min-h-[320px] w-full object-cover"
/>
</div>
</div>
</section>
</template>
+132
View File
@@ -0,0 +1,132 @@
<script setup>
import { computed, onBeforeUnmount, onMounted, ref } from "vue";
import { testimonials as defaultTestimonials } from "~/data/home";
const props = defineProps({
testimonials: {
type: Array,
default: () => defaultTestimonials,
},
routes: {
type: Array,
default: () => [],
},
sectionId: {
type: String,
default: "testimonials",
},
});
const currentIndex = ref(0);
const isPaused = ref(false);
let timer;
const current = computed(() => props.testimonials[currentIndex.value]);
const goToSlide = (index) => {
currentIndex.value = index;
};
const goToPrev = () => {
currentIndex.value =
currentIndex.value === 0 ? props.testimonials.length - 1 : currentIndex.value - 1;
};
const goToNext = () => {
currentIndex.value =
currentIndex.value === props.testimonials.length - 1 ? 0 : currentIndex.value + 1;
};
const startAutoplay = () => {
timer = window.setInterval(() => {
if (!isPaused.value) {
goToNext();
}
}, 5000);
};
onMounted(() => {
startAutoplay();
});
onBeforeUnmount(() => {
window.clearInterval(timer);
});
</script>
<template>
<section
:id="sectionId"
class="bg-white py-20"
@mouseenter="isPaused = true"
@mouseleave="isPaused = false"
>
<div class="container-gm">
<div class="flex flex-col gap-5 md:flex-row md:items-end md:justify-between" data-reveal>
<div class="max-w-3xl">
<p class="text-sm uppercase tracking-[0.3em] text-[#a1a1a1]">Voice</p>
<h2 class="section-title mt-4">客户真实评价,见证服务品质</h2>
<p class="mt-3 text-[#777]">
从真实客户视角补充信任背书,进一步强化团队专业度和落地能力。
</p>
</div>
<SectionRouteLinks :routes="routes" />
</div>
<div class="mx-auto mt-12 max-w-3xl text-center" data-reveal>
<div class="mx-auto flex h-16 w-16 items-center justify-center rounded-lg bg-gm-light text-xl font-semibold text-gm-green">
{{ current?.name.slice(0, 1) }}
</div>
<transition
mode="out-in"
enter-active-class="transition duration-500 ease-out"
enter-from-class="translate-y-3 opacity-0"
enter-to-class="translate-y-0 opacity-100"
leave-active-class="transition duration-300 ease-in"
leave-from-class="translate-y-0 opacity-100"
leave-to-class="-translate-y-2 opacity-0"
>
<div :key="current.id" class="mt-6">
<blockquote class="text-balance text-lg italic leading-8 text-[#555] md:text-xl">
“{{ current.quote }}”
</blockquote>
<p class="mt-6 text-base font-semibold text-[#262626]">{{ current.name }}</p>
<p class="text-sm text-[#9b9b9b]">{{ current.title }}</p>
</div>
</transition>
</div>
<div class="mt-8 flex items-center justify-center gap-2">
<button
v-for="(item, index) in props.testimonials"
:key="item.id"
type="button"
:aria-label="`切换到第 ${index + 1} 条评价`"
class="size-3 rounded-full transition-all duration-300"
:class="index === currentIndex ? 'bg-gm-green' : 'bg-gray-300'"
@click="goToSlide(index)"
/>
</div>
<div class="mt-6 flex items-center justify-center gap-4">
<button
type="button"
class="flex size-10 items-center justify-center rounded-full border border-gm-border text-[#666] transition hover:border-gm-green hover:text-gm-green"
aria-label="上一条评价"
@click="goToPrev"
>
←
</button>
<button
type="button"
class="flex size-10 items-center justify-center rounded-full border border-gm-border text-[#666] transition hover:border-gm-green hover:text-gm-green"
aria-label="下一条评价"
@click="goToNext"
>
→
</button>
</div>
</div>
</section>
</template>