feat:接口联调

This commit is contained in:
2026-07-21 11:09:17 +08:00
parent eea6319c41
commit 538194d8a5
39 changed files with 3553 additions and 3386 deletions
+1
View File
@@ -8,6 +8,7 @@ dist/
# Local Codex agent skills and instructions
.agents/
.codex/
skills-lock.json
# Local environment files
File diff suppressed because it is too large Load Diff
+46 -2
View File
@@ -6,6 +6,37 @@
font-family: 'Source Han Sans', 'PingFang SC', 'Microsoft YaHei', Arial, sans-serif;
color: #404040;
background: #fff;
--color-brand-primary: #1478fb;
--color-brand-bright: #0875f9;
--color-brand-blue: #287cf9;
--color-brand-cyan: #00adf6;
--color-brand-violet: #453cff;
--gradient-action: linear-gradient(100deg, #453cff, #00adf6);
--color-text-strong: #404040;
--color-text-default: #525966;
--color-text-body: #777;
--color-text-muted: #a5a5a5;
--color-surface-blue: #edf8ff;
--color-surface-case: #eef9ff;
--color-surface-soft: #f9fbff;
--color-border-soft: #d7e9ff;
--radius-control: 5px;
--radius-small: 8px;
--radius-card: 12px;
--radius-panel: 16px;
--radius-feature: 20px;
--shadow-button: 0 8px 15px rgba(25, 106, 255, 0.16);
--shadow-button-hover: 0 11px 20px rgba(25, 106, 255, 0.24);
--shadow-card: 0 10px 24px rgba(112, 163, 255, 0.09);
--shadow-card-hover: 0 17px 32px rgba(73, 139, 234, 0.14);
--container-max: 1296px;
--page-gutter-desktop: 24px;
--page-gutter-mobile: 16px;
--section-space-desktop: 80px;
--section-space-mobile: 64px;
--duration-fast: 200ms;
--duration-normal: 320ms;
--ease-standard: cubic-bezier(0.2, 0.75, 0.2, 1);
}
* {
box-sizing: border-box;
@@ -1188,7 +1219,7 @@ a:focus-visible {
.article-page
)
:is(.page-eyebrow, .section-kicker) {
color: #286dae !important;
color: #286dae;
font-size: 14px !important;
font-weight: 700 !important;
letter-spacing: 0.08em !important;
@@ -1267,6 +1298,19 @@ a:focus-visible {
color: var(--inner-copy) !important;
font-size: 14px !important;
}
.about-page .method-flow .method-marker {
color: #fff !important;
}
.about-page .center-heading .method-kicker {
width: fit-content !important;
display: block !important;
margin-right: auto !important;
margin-left: auto !important;
text-align: center !important;
}
.about-page .center-heading .method-kicker::before {
display: none !important;
}
/* Advantages */
.advantages-page :is(.hero-advantage-links button, .story-action, .proof-layout header > a) {
@@ -1334,7 +1378,7 @@ a:focus-visible {
.industry-list > article > span,
.proof-data span
) {
color: var(--inner-muted) !important;
color: var(--color-surface-soft) !important;
font-size: 12px !important;
}
+2 -2
View File
@@ -15,7 +15,7 @@ function validate() {
if (!form.value.name.trim()) nextErrors.name = '请填写您的姓名'
if (!form.value.company.trim()) nextErrors.company = '请填写公司名称'
if (!form.value.contact.trim()) {
nextErrors.contact = '请填写手机号或邮箱'
nextErrors.contact = '请填写手机号'
} else if (
!/^(1\d{10}|[\w.+-]+@[\w.-]+\.[A-Za-z]{2,}|[\d\s()+-]{6,})$/.test(form.value.contact.trim())
) {
@@ -143,7 +143,7 @@ onBeforeUnmount(() => {
v-model="form.contact"
type="text"
autocomplete="tel"
placeholder="手机号或工作邮箱"
placeholder="手机号"
/>
<small v-if="errors.contact">{{ errors.contact }}</small>
</label>
+128
View File
@@ -0,0 +1,128 @@
<script setup>
defineProps({
content: {
type: String,
default: ''
}
})
</script>
<template>
<div class="ueditor-content" v-html="content"></div>
</template>
<style scoped>
.ueditor-content {
color: #334155;
font-size: 16px;
line-height: 2;
word-break: break-word;
}
.ueditor-content :deep(*) {
box-sizing: border-box;
}
.ueditor-content :deep(h1),
.ueditor-content :deep(h2),
.ueditor-content :deep(h3),
.ueditor-content :deep(h4),
.ueditor-content :deep(h5),
.ueditor-content :deep(h6) {
margin: 46px 0 18px;
color: #0f1e33;
font-weight: 700;
letter-spacing: -0.02em;
line-height: 1.42;
}
.ueditor-content :deep(h2) {
font-size: clamp(22px, 2.1vw, 28px);
}
.ueditor-content :deep(h3) {
font-size: 22px;
}
.ueditor-content :deep(h4) {
font-size: 19px;
}
.ueditor-content :deep(p) {
margin: 0 0 22px;
}
.ueditor-content :deep(a) {
color: #2f74ff;
text-decoration: none;
}
.ueditor-content :deep(a:hover) {
text-decoration: underline;
}
.ueditor-content :deep(strong),
.ueditor-content :deep(b) {
color: #132238;
font-weight: 700;
}
.ueditor-content :deep(ul),
.ueditor-content :deep(ol) {
display: grid;
gap: 12px;
margin: 18px 0 28px;
padding-left: 1.35em;
}
.ueditor-content :deep(li) {
padding-left: 4px;
line-height: 1.9;
}
.ueditor-content :deep(blockquote) {
margin: 34px 0;
padding: 24px 28px;
border-left: 4px solid #3d7cff;
border-radius: 0 14px 14px 0;
color: #24364f;
background: #f4f7ff;
}
.ueditor-content :deep(img) {
max-width: 100%;
height: auto;
display: block;
margin: 34px auto;
border-radius: 16px;
}
.ueditor-content :deep(table) {
width: 100%;
margin: 32px 0;
overflow: hidden;
border-collapse: collapse;
border-radius: 12px;
background: #fff;
box-shadow: 0 0 0 1px #e7edf8;
}
.ueditor-content :deep(th),
.ueditor-content :deep(td) {
padding: 15px 18px;
border: 1px solid #e7edf8;
text-align: left;
vertical-align: top;
}
.ueditor-content :deep(th) {
color: #14233a;
background: #f3f6fc;
font-weight: 700;
}
.ueditor-content :deep(hr) {
height: 1px;
margin: 42px 0;
border: 0;
background: #e6edf8;
}
@media (max-width: 640px) {
.ueditor-content {
font-size: 15px;
line-height: 1.9;
}
.ueditor-content :deep(h2) {
font-size: 21px;
}
.ueditor-content :deep(h3) {
font-size: 19px;
}
.ueditor-content :deep(blockquote) {
padding: 20px;
}
}
</style>
+1 -1
View File
@@ -50,7 +50,7 @@ const sourceLabel = computed(() => {
<div>
<strong>联系我们</strong>
<span class="contact-line"
><AppIcon name="location" />成都市武侯区芯通科技大厦 B 9 </span
><AppIcon name="location" />成都市武侯区芯通科技大厦B座9</span
>
<span class="contact-line"><AppIcon name="phone" />联系电话待补充</span>
<span class="contact-line"><AppIcon name="message" />商务邮箱待补充</span>
+131
View File
@@ -0,0 +1,131 @@
const ALL_LABEL = '全部'
const LATEST_LABEL = '最新文章'
const FALLBACK_IMAGE = '/images/lanhu/slices/home-group95-image21@2x.png'
function pickUrl(asset) {
return asset?.url || asset?.fileUrl || asset?.filePath || ''
}
function buildMediaMap(media = []) {
return new Map(media.map((asset) => [asset.id, asset]))
}
function toTags(value) {
if (Array.isArray(value)) return value.filter(Boolean)
return String(value || '')
.split(/[,,、\s]+/)
.map((item) => item.trim())
.filter(Boolean)
}
function bySortAndTime(left, right) {
const leftSort = Number.isFinite(Number(left.sortOrder)) ? Number(left.sortOrder) : 9999
const rightSort = Number.isFinite(Number(right.sortOrder)) ? Number(right.sortOrder) : 9999
if (leftSort !== rightSort) return leftSort - rightSort
return String(right.updatedAt || right.publishedAt || '').localeCompare(
String(left.updatedAt || left.publishedAt || '')
)
}
export function useOfficialContent() {
const config = useRuntimeConfig()
const serverApiBase = `${String(config.officialApiProxyTarget || 'http://localhost:8080').replace(/\/$/, '')}/api`
const apiBase = import.meta.server ? serverApiBase : (config.public.officialApiBase || '/api')
function fetchPublicContent(type) {
console.log(apiBase)
return $fetch('/public/content', {
baseURL: apiBase,
query: { type }
})
}
function fetchHomepageSections() {
return $fetch('/public/homepage/sections', { baseURL: apiBase })
}
function fetchPublicSiteConfig() {
return $fetch('/public/site-config', { baseURL: apiBase })
}
function assetUrl(assetId, mediaMap, fallback = FALLBACK_IMAGE) {
if (!assetId) return fallback
return pickUrl(mediaMap.get(assetId)) || fallback
}
function assetAlt(assetId, mediaMap, fallback = '') {
if (!assetId) return fallback
return mediaMap.get(assetId)?.alt || fallback
}
function normalizeCase(item, mediaMap) {
const metric = item.metricValue || ''
const metricLabel = item.metricLabel || ''
const clientName = item.isAnonymous && item.clientName ? `匿名客户:${item.clientName}` : item.clientName
return {
...item,
industry: item.industry || item.category || '未分类',
client: clientName || '匿名客户',
cover: item.coverUrl || assetUrl(item.coverAssetId, mediaMap),
coverAlt: item.coverAlt || assetAlt(item.coverAssetId, mediaMap, item.title),
summary: item.summary || item.description || '',
description: item.description || item.summary || '',
metric,
metricLabel,
metrics: [[metric, metricLabel], [item.secondary, '辅助成果']].filter((row) => row[0]),
secondary: item.secondary || '',
contentHtml: item.contentHtml || ''
}
}
function normalizeArticle(item, mediaMap) {
return {
...item,
category: item.category || '未分类',
excerpt: item.summary || item.description || '',
date: item.publishDate || item.publishedAt?.slice?.(0, 10) || '',
readingTime: item.readingTime || '约 5 分钟',
views: item.views || '0',
cover: item.coverUrl || assetUrl(item.coverAssetId, mediaMap),
coverAlt: item.coverAlt || assetAlt(item.coverAssetId, mediaMap, item.title),
featured: Boolean(item.isFeatured),
tags: toTags(item.tags),
contentHtml: item.contentHtml || ''
}
}
function normalizeWhitepaper(item, mediaMap) {
const documentAsset = item.documentAssetId ? mediaMap.get(item.documentAssetId) : null
return {
...item,
category: item.category || '行业白皮书',
excerpt: item.summary || item.description || '',
cta: item.cta || '获取白皮书',
cover: item.coverUrl || assetUrl(item.coverAssetId, mediaMap),
documentUrl: item.documentUrl || pickUrl(documentAsset),
documentName: item.documentName || documentAsset?.name || item.title,
contentHtml: item.contentHtml || ''
}
}
function categoryList(items, field = 'category', extras = []) {
const values = items.map((item) => item[field]).filter(Boolean)
return [ALL_LABEL, ...extras, ...Array.from(new Set(values))]
}
return {
ALL_LABEL,
LATEST_LABEL,
bySortAndTime,
buildMediaMap,
fetchPublicContent,
fetchHomepageSections,
fetchPublicSiteConfig,
normalizeCase,
normalizeArticle,
normalizeWhitepaper,
categoryList
}
}
+16
View File
@@ -1,4 +1,12 @@
const officialApiProxyTarget = (process.env.NUXT_OFFICIAL_API_PROXY_TARGET || 'http://localhost:8080').replace(/\/$/, '')
export default defineNuxtConfig({
runtimeConfig: {
officialApiProxyTarget,
public: {
officialApiBase: process.env.NUXT_PUBLIC_OFFICIAL_API_BASE || '/api'
}
},
future: {
compatibilityVersion: 3
},
@@ -13,6 +21,14 @@ export default defineNuxtConfig({
}
}
},
nitro: {
devProxy: {
'/api': {
target: `${officialApiProxyTarget}/api`,
changeOrigin: true
}
}
},
app: {
head: {
title: '大马棒 · 云途全域 GEO',
+65 -15
View File
@@ -5,18 +5,44 @@ import SiteFooter from '~/components/site/SiteFooter.vue'
const pageRoot = ref(null)
const { openConsultation } = useConsultation()
const { fetchPublicSiteConfig } = useOfficialContent()
let animationContext
useHead({
const { data: siteConfigRows } = await useAsyncData('public-site-config-about', () =>
fetchPublicSiteConfig()
)
function parseConfigValue(value, fallback) {
if (value == null || value === '') return fallback
if (typeof value !== 'string') return value
try {
return JSON.parse(value)
} catch {
return fallback
}
}
const siteConfig = computed(() => siteConfigRows.value || {})
const brandConfig = computed(() => parseConfigValue(siteConfig.value.BRAND_PROFILE, {}))
const contactConfig = computed(() =>
parseConfigValue(siteConfig.value.CONTACT, {
phone: '待补充',
email: '待补充',
address: '待补充'
})
)
useHead(() => ({
title: '关于我们|大马棒·云途全域 GEO',
meta: [
{
name: 'description',
content:
brandConfig.value.seoDescription ||
'大马棒·云途全域 GEO 是聚焦 AI 搜索增长的全链路服务商,提供可见性诊断、策略规划、内容建设、信源分发与效果复盘。'
}
]
})
}))
const stats = [
['1000+', '累计服务客户'],
@@ -33,7 +59,7 @@ const serviceFlow = [
['05', '效果复盘', '持续监测覆盖率、推荐率、流量与咨询线索质量。']
]
const timeline = [
const fallbackTimeline = [
{
year: '2023',
title: '核心团队组建',
@@ -58,6 +84,22 @@ const timeline = [
events: ['打通品牌曝光与营销获客闭环', '服务客户突破 1000 家,覆盖 12+ 主流行业']
}
]
const timeline = computed(() => {
const rows = parseConfigValue(siteConfig.value.ABOUT_HISTORY, [])
if (!Array.isArray(rows) || rows.length === 0) return fallbackTimeline
return rows
.filter((item) => item.status !== 'OFFLINE')
.sort((left, right) => (Number(left.sortOrder) || 0) - (Number(right.sortOrder) || 0))
.map((item) => ({
year: item.year || '',
title: item.title || '',
events:
Array.isArray(item.events) && item.events.length
? item.events
: [item.description].filter(Boolean)
}))
})
const teamCapabilities = [
['搜索理解', '经历 PC 搜索、移动搜索到 AI 搜索的持续变化。'],
@@ -211,7 +253,7 @@ onBeforeUnmount(() => animationContext?.revert())
<section class="method-section">
<div class="page-width">
<header class="center-heading">
<p class="section-kicker">全链路服务方法</p>
<p class="section-kicker method-kicker">全链路服务方法</p>
<h2> AI 可见性诊断到效果复盘</h2>
<p>每一个环节都围绕真实业务目标推进避免内容媒体和转化承接各做一段</p>
</header>
@@ -289,20 +331,16 @@ onBeforeUnmount(() => animationContext?.revert())
<div class="contact-details">
<dl>
<div>
<dt>公司名称</dt>
<dd>成都大马棒传媒有限公司</dd>
<dt>咨询电话</dt>
<dd>{{ contactConfig.phone || '待补充' }}</dd>
</div>
<div>
<dt>公司地址</dt>
<dd>成都市武侯区芯通科技大厦 B 9 </dd>
<dt>联系邮箱</dt>
<dd>{{ contactConfig.email || '待补充' }}</dd>
</div>
<div>
<dt>联系电话</dt>
<dd>待补充</dd>
</div>
<div>
<dt>商务邮箱</dt>
<dd>待补充</dd>
<dt>办公地址</dt>
<dd>{{ contactConfig.address || '待补充' }}</dd>
</div>
</dl>
<div class="contact-qr">
@@ -819,6 +857,15 @@ onBeforeUnmount(() => animationContext?.revert())
margin: 0 auto;
text-align: center;
}
.center-heading .section-kicker {
width: 100%;
display: block;
justify-content: center;
text-align: center;
}
.center-heading .section-kicker::before {
display: none;
}
.center-heading > p:last-child {
max-width: 640px;
margin: 18px auto 0;
@@ -856,7 +903,7 @@ onBeforeUnmount(() => animationContext?.revert())
place-items: center;
border: 7px solid rgba(255, 255, 255, 0.92);
border-radius: 50%;
color: #fff;
color: #fff !important;
background: #358af0;
box-shadow: 0 11px 24px rgba(51, 135, 235, 0.22);
font-size: 10px;
@@ -1104,10 +1151,13 @@ onBeforeUnmount(() => animationContext?.revert())
url('/images/lanhu/slices/home-group105-image31@2x.png') center / cover no-repeat;
}
.about-cta-content {
display: grid;
justify-items: center;
padding: 75px 0;
text-align: center;
}
.about-cta-content > p {
width: 100%;
margin: 0;
color: #237df0;
font-size: 12px;
+146 -151
View File
@@ -106,6 +106,20 @@ const advantages = [
}
]
const introSequenceIcons = {
'human-content': '/images/advantages/intro-human-content.png',
'media-network': '/images/advantages/intro-media-network.png',
'closed-loop': '/images/advantages/intro-closed-loop.png',
'custom-service': '/images/advantages/intro-custom-service.png'
}
const capabilityImages = {
'human-content': '/images/advantages/capability-human-content.png',
'media-network': '/images/advantages/capability-media-network.png',
'closed-loop': '/images/advantages/capability-closed-loop.png',
'custom-service': '/images/advantages/capability-custom-service.png'
}
const comparisonRows = [
['核心目标', '抢排名、买点击', '蹭热度、抓眼球', '让品牌在 AI 推荐中被选中'],
['内容逻辑', '迎合搜索规则', '什么火就写什么', '真实可信,适配 AI 搜索与用户决策'],
@@ -120,14 +134,14 @@ const valueAddedServices = [
title: '创始人 / 企业 IP 打造',
text: '结合品牌调性与创始人特质,搭建差异化人设定位、内容脚本体系与长期更新规划,形成高辨识度的专属 IP。',
items: ['人设定位', '选题与脚本', '内容生产', '长期运营'],
image: '/images/lanhu/slices/home-group96-image22@2x.png'
image: '/images/advantages/founder-ip-visual.png'
},
{
number: '02',
title: '短视频与直播获客',
text: '从短视频批量产出到直播间常态化运营,配置主播、运营、投流与中控团队,覆盖拍摄、剪辑、投放和转化。',
items: ['短视频生产', '直播运营', '投流优化', '成交承接'],
image: '/images/lanhu/slices/home-group97-image23@2x.png'
image: '/images/advantages/short-video-live-visual.png'
}
]
@@ -239,10 +253,19 @@ onBeforeUnmount(() => animationContext?.revert())
</p>
</header>
<div class="intro-sequence">
<article v-for="advantage in advantages" :key="`intro-${advantage.id}`">
<article
v-for="advantage in advantages"
:key="`intro-${advantage.id}`"
:class="`intro-card--${advantage.id}`"
>
<span>{{ advantage.number }}</span
><strong>{{ advantage.short }}</strong>
<p>{{ advantage.metric }}</p>
<img
class="intro-sequence-icon"
:src="introSequenceIcons[advantage.id]"
:alt="`${advantage.short}图标`"
/>
</article>
</div>
</div>
@@ -293,20 +316,10 @@ onBeforeUnmount(() => animationContext?.revert())
<span>{{ advantage.short }}能力模型</span><small>YUNTU CAPABILITY MAP</small>
</header>
<div class="capability-map">
<div class="capability-center">
<small>{{ advantage.number }}</small
><strong>{{ advantage.short }}</strong>
</div>
<div
v-for="(item, itemIndex) in advantage.visual"
:key="item"
class="capability-node"
:class="`capability-node-${itemIndex + 1}`"
>
<span>0{{ itemIndex + 1 }}</span
><strong>{{ item }}</strong>
</div>
<i v-for="line in 4" :key="line" :class="`capability-line line-${line}`"></i>
<img
:src="capabilityImages[advantage.id]"
:alt="`${advantage.short}核心能力图`"
/>
</div>
<div class="capability-metric">
<strong>{{ advantage.metric }}</strong
@@ -777,8 +790,10 @@ onBeforeUnmount(() => animationContext?.revert())
gap: 14px;
}
.intro-sequence article {
position: relative;
min-height: 165px;
padding: 26px;
overflow: hidden;
padding: 26px 118px 26px 26px;
border-radius: 14px 5px 14px 5px;
background: #f2f7fd;
transition: 0.24s;
@@ -799,12 +814,58 @@ onBeforeUnmount(() => animationContext?.revert())
font-size: 17px;
}
.intro-sequence p {
position: relative;
z-index: 2;
margin: 7px 0 0;
color: #277fec;
font-size: 20px;
font-weight: 700;
font-variant-numeric: tabular-nums;
}
.intro-sequence span,
.intro-sequence strong {
position: relative;
z-index: 2;
}
.intro-sequence-icon {
position: absolute;
right: 16px;
bottom: 12px;
z-index: 1;
width: 108px;
max-height: 128px;
object-fit: contain;
pointer-events: none;
filter: drop-shadow(0 18px 26px rgba(40, 126, 235, 0.18));
transition: transform 0.24s;
}
.intro-sequence article:hover .intro-sequence-icon {
transform: translate3d(0, -4px, 0) scale(1.03);
}
.intro-card--human-content .intro-sequence-icon {
right: 22px;
bottom: 24px;
width: 66px;
max-height: 86px;
}
.intro-card--media-network .intro-sequence-icon {
right: 16px;
bottom: 18px;
width: 106px;
max-height: 112px;
}
.intro-card--closed-loop .intro-sequence-icon {
right: 12px;
bottom: 41x;
width: 100px;
max-height: 128px;
}
.intro-card--custom-service .intro-sequence-icon {
right: 2px;
bottom: -4px;
width: 130px;
max-height: 132px;
}
.advantage-story {
scroll-margin-top: 80px;
padding: 116px 0 132px;
@@ -919,20 +980,25 @@ onBeforeUnmount(() => animationContext?.revert())
letter-spacing: 0.1em;
}
.story-action {
display: inline-flex;
min-width: 164px;
min-height: 46px;
margin-top: 24px;
padding: 0 0 5px;
border-bottom: 1px solid #8eb9e9;
color: #247be8;
background: transparent;
padding: 0 26px;
align-items: center;
justify-content: center;
gap: 9px;
white-space: nowrap;
font-size: 12px;
font-weight: 600;
}
.story-action b {
margin-left: 8px;
transition: margin 0.2s;
.story-action :deep(.app-icon) {
width: 1em;
height: 1em;
transition: transform 0.2s;
}
.story-action:hover b {
margin-left: 13px;
.story-action:hover :deep(.app-icon) {
transform: translateX(4px);
}
.advantage-story-visual {
position: relative;
@@ -978,110 +1044,14 @@ onBeforeUnmount(() => animationContext?.revert())
position: absolute;
top: 52%;
left: 50%;
width: 410px;
height: 360px;
width: min(88%, 460px);
height: 400px;
transform: translate(-50%, -50%);
}
.capability-map::before {
position: absolute;
top: 50%;
left: 50%;
width: 280px;
height: 280px;
content: '';
border: 1px dashed rgba(55, 137, 232, 0.34);
border-radius: 50%;
transform: translate(-50%, -50%);
}
.capability-center {
position: absolute;
z-index: 3;
top: 50%;
left: 50%;
width: 142px;
height: 142px;
display: grid;
place-content: center;
justify-items: center;
border: 11px solid rgba(255, 255, 255, 0.6);
border-radius: 50%;
color: #fff;
background: linear-gradient(145deg, #3185ee, #50bce7);
box-shadow: 0 22px 42px rgba(36, 123, 225, 0.28);
transform: translate(-50%, -50%);
}
.capability-center small {
font-size: 9px;
opacity: 0.7;
}
.capability-center strong {
margin-top: 5px;
font-size: 17px;
}
.capability-node {
position: absolute;
z-index: 3;
min-width: 110px;
padding: 12px 14px;
border: 1px solid rgba(255, 255, 255, 0.92);
border-radius: 7px;
color: #4e7198;
background: rgba(255, 255, 255, 0.84);
box-shadow: 0 12px 25px rgba(54, 113, 185, 0.11);
text-align: center;
backdrop-filter: blur(8px);
}
.capability-node span {
display: block;
color: #91a9c3;
font-size: 8px;
}
.capability-node strong {
display: block;
margin-top: 3px;
font-size: 10px;
}
.capability-node-1 {
top: 0;
left: 50%;
transform: translateX(-50%);
}
.capability-node-2 {
top: 50%;
right: 0;
transform: translateY(-50%);
}
.capability-node-3 {
bottom: 0;
left: 50%;
transform: translateX(-50%);
}
.capability-node-4 {
top: 50%;
left: 0;
transform: translateY(-50%);
}
.capability-line {
position: absolute;
z-index: 1;
top: 50%;
left: 50%;
width: 2px;
height: 135px;
background: linear-gradient(#5aa1ed, transparent);
transform-origin: top;
}
.line-1 {
transform: rotate(180deg);
}
.line-2 {
transform: rotate(90deg);
}
.line-3 {
transform: rotate(0);
}
.line-4 {
transform: rotate(-90deg);
.capability-map img {
width: 100%;
height: 100%;
object-fit: contain;
}
.capability-metric {
position: absolute;
@@ -1140,7 +1110,7 @@ onBeforeUnmount(() => animationContext?.revert())
font-size: 11px;
}
.comparison-table-wrap thead th:last-child {
color: #fff;
color: #fff !important;
background: #3187ee;
}
.comparison-table-wrap thead span {
@@ -1193,17 +1163,26 @@ onBeforeUnmount(() => animationContext?.revert())
}
.value-added-image {
position: relative;
display: grid;
place-items: center;
overflow: hidden;
background: #e5f1ff;
background:
radial-gradient(circle at 28% 20%, rgba(255, 255, 255, 0.86) 0, rgba(255, 255, 255, 0) 31%),
radial-gradient(circle at 72% 32%, rgba(96, 207, 255, 0.36) 0, rgba(96, 207, 255, 0) 34%),
linear-gradient(135deg, #edf7ff 0%, #d8efff 48%, #f2edff 100%);
}
.value-added-image img {
width: 100%;
height: 100%;
position: relative;
z-index: 1;
width: min(108%, 560px);
height: min(92%, 370px);
display: block;
object-fit: cover;
object-fit: contain;
filter: drop-shadow(0 24px 34px rgba(34, 122, 229, 0.18));
}
.value-added-image > span {
position: absolute;
z-index: 2;
top: 24px;
left: 24px;
display: grid;
@@ -1327,10 +1306,13 @@ onBeforeUnmount(() => animationContext?.revert())
url('/images/lanhu/slices/home-group105-image31@2x.png') center / cover no-repeat;
}
.advantages-cta-content {
display: grid;
justify-items: center;
padding: 75px 0;
text-align: center;
}
.advantages-cta-content > p {
width: 100%;
margin: 0;
color: #237df0;
font-size: 12px;
@@ -1442,6 +1424,31 @@ onBeforeUnmount(() => animationContext?.revert())
}
.intro-sequence article {
min-height: 140px;
padding-right: 118px;
}
.intro-sequence-icon {
right: 14px;
bottom: 10px;
width: 96px;
max-height: 106px;
}
.intro-card--human-content .intro-sequence-icon {
right: 20px;
bottom: 20px;
width: 68px;
}
.intro-card--media-network .intro-sequence-icon {
right: 14px;
bottom: 16px;
width: 94px;
max-height: 100px;
}
.intro-card--closed-loop .intro-sequence-icon,
.intro-card--custom-service .intro-sequence-icon {
right: 10px;
bottom: 0;
width: 108px;
max-height: 112px;
}
.advantage-story-copy h2 {
font-size: 31px;
@@ -1450,25 +1457,9 @@ onBeforeUnmount(() => animationContext?.revert())
min-height: 430px;
}
.capability-map {
width: 310px;
width: min(92%, 320px);
height: 280px;
}
.capability-map::before {
width: 205px;
height: 205px;
}
.capability-center {
width: 105px;
height: 105px;
border-width: 8px;
}
.capability-node {
min-width: 83px;
padding: 9px;
}
.capability-line {
height: 100px;
}
.capability-metric {
right: 18px;
bottom: 17px;
@@ -1486,6 +1477,10 @@ onBeforeUnmount(() => animationContext?.revert())
.value-added-image {
min-height: 260px;
}
.value-added-image img {
width: min(112%, 430px);
height: 230px;
}
.value-added-copy {
padding: 32px 23px;
}
+499 -1139
View File
File diff suppressed because it is too large Load Diff
+31 -8
View File
@@ -1,11 +1,30 @@
<script setup>
import SiteHeader from '~/components/site/SiteHeader.vue'
import SiteFooter from '~/components/site/SiteFooter.vue'
import { caseCategories, featuredCase, resultSnapshots } from '~/data/cases'
const { ALL_LABEL, bySortAndTime, categoryList, fetchPublicContent, normalizeCase } =
useOfficialContent()
const activeCategory = ref('全部')
const activeCategory = ref(ALL_LABEL)
const { openConsultation } = useConsultation()
const { data: caseRows } = await useAsyncData('public-cases', async () => {
const rows = await fetchPublicContent('CASE')
return rows.sort(bySortAndTime).map((item) => normalizeCase(item, new Map()))
})
const cases = computed(() => caseRows.value || [])
const caseCategories = computed(() => categoryList(cases.value, 'industry'))
const featuredCase = computed(
() =>
cases.value.find((item) => item.isFeatured) ||
cases.value.find((item) => item.isFullCase) ||
cases.value[0] ||
null
)
const resultSnapshots = computed(() =>
cases.value.filter((item) => item.id !== featuredCase.value?.id)
)
useHead({
title: '客户案例|大马棒·云途全域 GEO',
meta: [
@@ -17,12 +36,13 @@ useHead({
]
})
const showFeatured = computed(
() => activeCategory.value === '全部' || activeCategory.value === featuredCase.industry
)
const showFeatured = computed(() => {
if (!featuredCase.value) return false
return activeCategory.value === ALL_LABEL || activeCategory.value === featuredCase.value.industry
})
const visibleSnapshots = computed(() => {
if (activeCategory.value === '全部') return resultSnapshots
return resultSnapshots.filter((item) => item.industry === activeCategory.value)
if (activeCategory.value === ALL_LABEL) return resultSnapshots.value
return resultSnapshots.value.filter((item) => item.industry === activeCategory.value)
})
const hasResults = computed(() => showFeatured.value || visibleSnapshots.value.length > 0)
@@ -141,7 +161,7 @@ function requestCustomPlan(source) {
<article
v-for="(snapshot, index) in visibleSnapshots"
:key="`${activeCategory}-${snapshot.title}`"
:class="{ wide: index === 0 && activeCategory === '全部' }"
:class="{ wide: index === 0 && activeCategory === ALL_LABEL }"
>
<div class="snapshot-image">
<img :src="snapshot.cover" :alt="`${snapshot.industry}结果快照`" /><span>{{
@@ -853,10 +873,13 @@ function requestCustomPlan(source) {
url('/images/lanhu/slices/home-group105-image31@2x.png') center / cover no-repeat;
}
.cases-cta-content {
display: grid;
justify-items: center;
padding: 75px 0;
text-align: center;
}
.cases-cta-content > p {
width: 100%;
margin: 0;
color: #237df0;
font-size: 12px;
+185 -43
View File
@@ -6,10 +6,28 @@ import SiteFooter from '~/components/site/SiteFooter.vue'
const pageRoot = ref(null)
const { openConsultation: openGlobalConsultation } = useConsultation()
const { fetchHomepageSections } = useOfficialContent()
let animationContext
let animationMedia
const logos = [
const { data: homepageSectionRows } = await useAsyncData('public-homepage-sections', () =>
fetchHomepageSections()
)
const homepageSections = computed(() => homepageSectionRows.value || [])
const homepageSectionMap = computed(() =>
Object.fromEntries(homepageSections.value.map((section) => [section.key, section]))
)
function homepageSection(key) {
return homepageSectionMap.value[key] || {}
}
function backgroundStyle(url) {
return url ? { backgroundImage: `url("${url}")` } : null
}
const fallbackLogos = [
'Bond',
'WaSun',
'西西能源',
@@ -23,17 +41,18 @@ const logos = [
'SUNAC',
'B+ WORKS'
]
const stats = [
const fallbackStats = [
['300+', '累计服务客户', '/images/lanhu/slices/home-group100-image26@2x.png'],
['12+', '覆盖行业', '/images/lanhu/slices/home-group101-image27@2x.png'],
['126%', '客户AI搜索曝光平均提升', '/images/lanhu/slices/home-group102-image28@2x.png'],
['65%', '客户转化链路效率平均提升', '/images/lanhu/slices/home-group103-image29@2x.png']
]
const corePoints = [
const heroBackgroundImage = '/images/lanhu/slices/hero-geo-visual@2x.png'
const fallbackCorePoints = [
['曝光', '打造AI搜索语境下的品牌信任声量,一次布局,长期复用'],
['获客', '让曝光直接沉淀为可跟进的精准线索,告别无效流量']
]
const ringModels = [
const fallbackRingModels = [
{
title: '声量资产环',
image: '/images/lanhu/slices/home-group80-group83-image11@2x.png',
@@ -47,18 +66,18 @@ const ringModels = [
labels: ['AI搜索曝光', '精准流量承接', '全链路转化', '数据复盘优化']
}
]
const advantageCards = [
const fallbackAdvantageCards = [
['全域媒体资源优势', '5000+权威官媒+10万+优质自媒体矩阵'],
['全链路闭环优势', '行业唯一打通「纯曝光+内容资产沉淀+转化闭环」'],
['全客群定制优势', '覆盖全类型经营主体,一对一专属定制专属方案']
]
const caseImages = [
const fallbackCaseImages = [
'/images/lanhu/slices/home-group90-image15@2x.png',
'/images/lanhu/slices/home-group91-image16@2x.png',
'/images/lanhu/slices/home-group92-image17@2x.png',
'/images/lanhu/slices/home-group93-image18@2x.png'
]
const industryCards = [
const fallbackIndustryCards = [
[
'本地生活',
'AI搜索全域曝光提升320%月均精准线索新增80+,持续获得品牌声量与业务线索',
@@ -75,7 +94,7 @@ const industryCards = [
'/images/lanhu/slices/home-group36-group71-image5@2x.png'
]
]
const deliverySteps = [
const fallbackDeliverySteps = [
[
'需求诊断',
'一对一沟通,分析品牌现状与增长痛点',
@@ -103,6 +122,112 @@ const deliverySteps = [
]
]
const heroSection = computed(() => homepageSection('HERO'))
const customerHallSection = computed(() => homepageSection('CUSTOMER_HALL'))
const capabilitiesSection = computed(() => homepageSection('CAPABILITIES'))
const advantagesSection = computed(() => homepageSection('ADVANTAGES'))
const caseBlockSection = computed(() => homepageSection('CASE_BLOCK'))
const industryBlockSection = computed(() => homepageSection('INDUSTRY_BLOCK'))
const processSection = computed(() => homepageSection('PROCESS'))
const testimonialSection = computed(() => homepageSection('TESTIMONIAL'))
const bottomCtaSection = computed(() => homepageSection('BOTTOM_CTA'))
const heroTitle = computed(
() => heroSection.value.title || '大马棒 · 云途全域GEO\n让品牌在AI时代拥有持续增长力'
)
const heroTags = computed(() =>
heroSection.value.tags?.length
? heroSection.value.tags
: ['5万真人内容生产网络', '5000+自媒体资源矩阵', '全链路转化闭环']
)
const logos = computed(() => fallbackLogos)
const stats = computed(() => {
const cards = customerHallSection.value.cards
if (!cards?.length) return fallbackStats
return cards.map((card, index) => [
card.value,
card.label,
fallbackStats[index % fallbackStats.length][2]
])
})
const corePoints = computed(() => {
const cards = capabilitiesSection.value.cards
if (!cards?.length) return fallbackCorePoints
return cards.slice(0, 2).map((card, index) => [
card.title || fallbackCorePoints[index]?.[0] || '',
card.details?.[0]?.description || card.details?.[0]?.heading || fallbackCorePoints[index]?.[1] || ''
])
})
const ringModels = computed(() => fallbackRingModels)
const leadAdvantage = computed(() => {
const groups = advantagesSection.value.groups || []
const lead = groups[0]
return {
title: lead?.heading || '真人内容生产优势',
bodyOne: lead?.bodyOne || '行业独家5万真人分布式内容生产网络',
bodyTwo: lead?.bodyTwo || '用C端爆款内容思维做B端GEO,打造真实可信的AI信任型声量',
image: advantagesSection.value.assetUrl || '/images/lanhu/slices/home-group90-image15@2x.png'
}
})
const advantageCards = computed(() => {
const groups = advantagesSection.value.groups?.slice(1)
if (!groups?.length) return fallbackAdvantageCards
return groups.map((group) => [
group.heading,
[group.bodyOne, group.bodyTwo].filter(Boolean).join('')
])
})
const caseCards = computed(() => {
const contents = caseBlockSection.value.contents
if (!contents?.length) {
return fallbackCaseImages.map((image) => ({
image,
description: '某企业服务客户 AI搜索全域曝光提升\n320%月均精准线索新增80+'
}))
}
return contents.map((item, index) => ({
image:
item.imageUrl ||
item.assetUrl ||
item.url ||
item.image ||
fallbackCaseImages[index % fallbackCaseImages.length],
description: item.description || ''
}))
})
const industryCards = computed(() => {
const groups = industryBlockSection.value.groups
if (!groups?.length) return fallbackIndustryCards
return groups.map((group, index) => [
group.title,
group.description,
group.imageUrl || fallbackIndustryCards[index % fallbackIndustryCards.length][2],
group.tags || []
])
})
const deliverySteps = computed(() => {
const groups = processSection.value.groups
if (!groups?.length) return fallbackDeliverySteps
return groups.map((group, index) => [
group.heading,
group.body,
fallbackDeliverySteps[index % fallbackDeliverySteps.length][2]
])
})
const testimonials = computed(() => {
const groups = testimonialSection.value.groups
if (!groups?.length) {
return Array.from({ length: 5 }, (_, index) => ({
name: '数据优化:',
date: `2026-12-${42 + index}`,
description:
'创作者每日必读的前沿设计趋势、设计方法论和大厂落地经验之谈,14年的行业专注,脑突入口的优质内容沉淀,为每一个从入行到进阶的设计师、创作者赋能。'
}))
}
return groups
})
function requestConsultation(source, title = '预约免费 GEO 增长诊断') {
openGlobalConsultation({ title, source })
}
@@ -372,14 +497,21 @@ onBeforeUnmount(() => {
<SiteHeader />
<main>
<section class="hero-section">
<div class="hero-art" aria-hidden="true"></div>
<section class="hero-section">
<div
class="hero-art"
:style="backgroundStyle(heroBackgroundImage)"
aria-hidden="true"
></div>
<div class="page-width hero-content">
<div class="hero-copy">
<h1>大马棒 · 云途全域GEO<br />让品牌在AI时代拥有持续增长力</h1>
<h1>
<template v-for="(line, index) in heroTitle.split('\n')" :key="`${line}-${index}`">
<br v-if="index" />{{ line }}
</template>
</h1>
<p class="hero-points">
<span> 5万真人内容生产网络</span><span> 5000+自媒体资源矩阵</span
><span> 全链路转化闭环</span>
<span v-for="tag in heroTags" :key="tag"> {{ tag }}</span>
</p>
<div class="button-row">
<button
@@ -407,8 +539,8 @@ onBeforeUnmount(() => {
<section class="intro-section page-width">
<SectionTitle
>已服务<span class="blue">300+</span>全类型经营主体覆盖<span class="blue">12+</span
>主流行业<template #subtitle
>{{ customerHallSection.title || '已服务300+全类型经营主体,覆盖12+主流行业'
}}<template #subtitle
>We Have Provided Services To Over 300 Full-spectrum Business Entities<br />Spanning
More Than A Dozen Mainstream Industry Sectors</template
></SectionTitle
@@ -424,8 +556,8 @@ onBeforeUnmount(() => {
<section id="model" class="model-section">
<div class="page-width">
<SectionTitle
>独创<span class="blue">GEO双环增长模型</span>实现品效协同长效增长<template
#subtitle
>{{ capabilitiesSection.title || '独创「GEO双环增长模型」,实现品效协同长效增长'
}}<template #subtitle
>Original "geo Dual-ring Growth Model" Delivers Long-term Growth Through<br />Integrated
Brand Building And Conversion Performance</template
></SectionTitle
@@ -476,16 +608,15 @@ onBeforeUnmount(() => {
<section id="advantages" class="advantages-section">
<div class="page-width">
<SectionTitle
><span class="blue">四大核心</span>优势<template #subtitle
>Four Core Advantages</template
></SectionTitle
>{{ advantagesSection.title || '四大核心优势'
}}<template #subtitle>Four Core Advantages</template></SectionTitle
>
<div class="lead-advantage">
<img src="/images/lanhu/slices/home-group90-image15@2x.png" alt="真人内容生产" />
<img :src="leadAdvantage.image" :alt="leadAdvantage.title" />
<div>
<h3>真人内容生产优势</h3>
<p><b>核心点</b>行业独家5万真人分布式内容生产网络</p>
<p><b>价值</b>用C端爆款内容思维做B端GEO打造真实可信的AI信任型声量</p>
<h3>{{ leadAdvantage.title }}</h3>
<p><b>核心点</b>{{ leadAdvantage.bodyOne }}</p>
<p><b>价值</b>{{ leadAdvantage.bodyTwo }}</p>
</div>
</div>
<div class="advantage-grid">
@@ -508,15 +639,15 @@ onBeforeUnmount(() => {
<div class="case-background" aria-hidden="true"></div>
<div class="page-width case-content">
<SectionTitle
><span class="blue">真实落地成果</span>见证<span class="blue">增长实效</span
><template #subtitle
>{{ caseBlockSection.title || '真实落地成果,见证增长实效'
}}<template #subtitle
>Practical Delivered Results That Demonstrate Tangible Growth Outcomes</template
></SectionTitle
>
<div class="case-panel">
<article v-for="(image, index) in caseImages" :key="image">
<img :src="image" :alt="`客户案例 ${index + 1}`" />
<p>某企业服务客户: AI搜索全域曝光提升<br />320%月均精准线索新增80+</p>
<article v-for="(item, index) in caseCards" :key="`${item.image}-${index}`">
<img :src="item.image" :alt="`客户案例 ${index + 1}`" />
<p>{{ item.description }}</p>
</article>
</div>
<div class="center-action">
@@ -527,7 +658,8 @@ onBeforeUnmount(() => {
<section id="industries" class="industries-section page-width">
<SectionTitle
>深耕<span class="blue">12+</span>行业定制专属GEO增长方案<template #subtitle
>{{ industryBlockSection.title || '深耕12+行业,定制专属GEO增长方案'
}}<template #subtitle
>With In-depth Experience Across 12+ Industries, We Craft Exclusive Geo Growth
Solutions<br />Tailored To Clients Needs</template
></SectionTitle
@@ -538,7 +670,11 @@ onBeforeUnmount(() => {
<img :src="card[2]" alt="" />
<div class="quote"></div>
<p>某企业服务客户<br />{{ card[1] }}</p>
<div class="tags"><span>#生活服务</span><span>#全域曝光</span></div>
<div class="tags">
<span v-for="tag in card[3] || ['生活服务', '全域曝光']" :key="`${card[0]}-${tag}`"
>#{{ tag }}</span
>
</div>
</article>
</div>
</section>
@@ -546,12 +682,13 @@ onBeforeUnmount(() => {
<section id="delivery" class="delivery-section">
<div class="page-width">
<SectionTitle
><span class="blue">5步标准化</span>交付流程透明可控效果可追溯<template #subtitle
>{{ processSection.title || '5步标准化交付流程,透明可控效果可追溯'
}}<template #subtitle
>With In-depth Experience Across 12+ Industries, We Craft Exclusive Geo Growth
Solutions<br />Tailored To Clients Needs</template
></SectionTitle
>
<div class="delivery-banner"></div>
<div class="delivery-banner" :style="backgroundStyle(processSection.assetUrl)"></div>
<div class="delivery-grid">
<article v-for="step in deliverySteps" :key="step[0]">
<img :src="step[2]" alt="" />
@@ -567,29 +704,34 @@ onBeforeUnmount(() => {
<section class="testimonial-section">
<div class="page-width">
<SectionTitle
>客户真实评价见证<span class="blue">服务品质</span
><template #subtitle
>{{ testimonialSection.title || '客户真实评价,见证服务品质'
}}<template #subtitle
>Genuine Client Testimonials Attest To Our Service Quality</template
></SectionTitle
>
<div class="testimonial-track">
<article v-for="index in 5" :key="index">
<header><span class="avatar"></span><b>数据优化</b><time>2026-12-42</time></header>
<p>
创作者每日必读的前沿设计趋势设计方法论和大厂落地经验之谈14年的行业专注脑突入口的优质内容沉淀为每一个从入行到进阶的设计师创作者赋能
</p>
<article v-for="(item, index) in testimonials" :key="`${item.name}-${index}`">
<header>
<span class="avatar" :style="backgroundStyle(item.avatarUrl)"></span
><b>{{ item.name }}</b
><time>{{ item.date }}</time>
</header>
<p>{{ item.description }}</p>
</article>
</div>
</div>
</section>
<section class="cta-section">
<div class="cta-art"></div>
<div class="cta-art" :style="backgroundStyle(bottomCtaSection.assetUrl)"></div>
<div class="cta-content">
<h2>
AI搜索增长风口已至大马棒 · 云途全域GEO助<br />力企业打造品效双闭环的长效增长体系
{{
bottomCtaSection.title ||
'AI搜索增长风口已至,大马棒 · 云途全域GEO助力企业打造品效双闭环的长效增长体系'
}}
</h2>
<p>免费获取1对1GEO增长诊断方案</p>
<p>{{ bottomCtaSection.subtitle || '免费获取1对1GEO增长诊断方案' }}</p>
<button class="primary-button" type="button" @click="requestConsultation('首页底部 CTA')">
立即免费咨询 <b></b>
</button>
+426 -600
View File
File diff suppressed because it is too large Load Diff
+203 -65
View File
@@ -1,12 +1,43 @@
<script setup>
import SiteHeader from '~/components/site/SiteHeader.vue'
import SiteFooter from '~/components/site/SiteFooter.vue'
import { featuredInsight, insightCategories, insights } from '~/data/insights'
const activeCategory = ref('全部')
const {
ALL_LABEL,
LATEST_LABEL,
bySortAndTime,
categoryList,
fetchPublicContent,
normalizeArticle,
normalizeWhitepaper
} = useOfficialContent()
const activeCategory = ref(ALL_LABEL)
const visibleCount = ref(4)
const { openConsultation } = useConsultation()
const { data: pageData } = await useAsyncData('public-insights', async () => {
const [articleRows, whitepaperRows] = await Promise.all([
fetchPublicContent('ARTICLE'),
fetchPublicContent('WHITEPAPER')
])
return {
articles: articleRows.sort(bySortAndTime).map((item) => normalizeArticle(item, new Map())),
whitepapers: whitepaperRows.sort(bySortAndTime).map((item) => normalizeWhitepaper(item, new Map()))
}
})
const insights = computed(() => pageData.value?.articles || [])
const whitepapers = computed(() => pageData.value?.whitepapers || [])
const featuredInsight = computed(
() => insights.value.find((item) => item.featured || item.isFeatured) || insights.value[0] || null
)
const featuredWhitepaper = computed(
() => whitepapers.value.find((item) => item.isFeatured) || whitepapers.value[0] || null
)
const insightCategories = computed(() => categoryList(insights.value, 'category', [LATEST_LABEL]))
useHead({
title: '增长洞察|大马棒·云途全域 GEO',
meta: [
@@ -19,9 +50,9 @@ useHead({
})
const filteredInsights = computed(() => {
const list = insights.filter((item) => !item.featured)
if (activeCategory.value === '全部') return list
if (activeCategory.value === '最新文章') return list.slice(0, 3)
const list = insights.value.filter((item) => item.slug !== featuredInsight.value?.slug)
if (activeCategory.value === ALL_LABEL) return list
if (activeCategory.value === LATEST_LABEL) return list.slice(0, 3)
return list.filter((item) => item.category === activeCategory.value)
})
const visibleInsights = computed(() => filteredInsights.value.slice(0, visibleCount.value))
@@ -36,12 +67,25 @@ function requestDiagnosis(source) {
}
function requestWhitepaper(source) {
if (featuredWhitepaper.value?.documentUrl) {
window.open(featuredWhitepaper.value.documentUrl, '_blank', 'noopener,noreferrer')
return
}
openConsultation({
title: '获取 2026 AI 搜索与品牌增长观察',
need: '行业白皮书',
title: featuredWhitepaper.value?.title || '获取行业白皮书',
need: featuredWhitepaper.value?.title || '行业白皮书',
source
})
}
function articleTags(article) {
if (article.tags?.length) return article.tags
const baseTags = ['GEO', '生成式引擎优化', 'AI搜索营销', 'SEO', '内容营销']
if (article.slug?.includes('report')) return [...baseTags, '行业报告']
if (article.slug?.includes('matrix')) return [...baseTags, '信源建设']
if (article.slug?.includes('metrics')) return [...baseTags, '增长指标']
return baseTags
}
</script>
<template>
@@ -88,7 +132,7 @@ function requestWhitepaper(source) {
<p>AI 不只是新的流量入口它正在改变用户理解品牌比较方案和做出选择的方式</p>
</header>
<article class="featured-insight">
<article v-if="featuredInsight" class="featured-insight">
<NuxtLink :to="`/insights/${featuredInsight.slug}`" class="featured-image">
<img :src="featuredInsight.cover" :alt="featuredInsight.title" />
<span>EDITOR'S PICK</span>
@@ -142,10 +186,16 @@ function requestWhitepaper(source) {
<time>{{ article.date }}</time
><span>{{ article.views }} 阅读</span>
</div>
<span class="article-category">{{ article.category }}</span>
<h3>
<NuxtLink :to="`/insights/${article.slug}`">{{ article.title }}</NuxtLink>
</h3>
<p>{{ article.excerpt }}</p>
<div class="article-tags" aria-label="文章标签">
<span v-for="tag in articleTags(article)" :key="`${article.slug}-${tag}`">{{
tag
}}</span>
</div>
<NuxtLink :to="`/insights/${article.slug}`" class="read-link"
>查看详情 <AppIcon name="arrow-right"
/></NuxtLink>
@@ -177,12 +227,12 @@ function requestWhitepaper(source) {
<div class="page-width whitepaper-grid">
<div class="whitepaper-number"><span>REPORT</span><strong>2026</strong></div>
<div class="whitepaper-copy">
<p>行业白皮书</p>
<h2>AI 搜索与品牌增长观察</h2>
<span>用户行为 · 内容信源 · 衡量框架 · 行业实践</span>
<p>{{ featuredWhitepaper?.category || '行业白皮书' }}</p>
<h2>{{ featuredWhitepaper?.title || 'AI 搜索与品牌增长观察' }}</h2>
<span>{{ featuredWhitepaper?.excerpt || '用户行为 · 内容信源 · 衡量框架 · 行业实践' }}</span>
</div>
<button type="button" @click="requestWhitepaper('增长洞察页白皮书')">
获取白皮书 <AppIcon name="arrow-right" />
{{ featuredWhitepaper?.cta || '获取白皮书' }} <AppIcon name="arrow-right" />
</button>
</div>
</section>
@@ -493,43 +543,78 @@ function requestWhitepaper(source) {
}
.insight-filters {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 75px;
padding-bottom: 18px;
border-bottom: 1px solid #e0e9f3;
flex-wrap: nowrap;
justify-content: center;
gap: clamp(20px, 3.2vw, 44px);
width: min(100%, 980px);
margin: 72px auto 0;
padding: 0 6px;
overflow-x: auto;
border-bottom: 0;
scrollbar-width: none;
}
.insight-filters::-webkit-scrollbar {
display: none;
}
.insight-filters button {
height: 38px;
padding: 0 17px;
border-radius: 4px;
color: #61758c;
background: #f1f6fc;
font-size: 12px;
position: relative;
flex: 0 0 auto;
height: 54px;
padding: 0 8px;
border-radius: 0;
color: #101827;
background: transparent;
font-size: 22px !important;
font-weight: 500;
transition: 0.2s;
}
.insight-filters button:hover,
.insight-filters button.active {
color: #fff;
background: #287ff0;
box-shadow: 0 8px 18px rgba(39, 126, 238, 0.2);
transform: translateY(-1px);
color: #0978ff;
background: transparent;
box-shadow: none;
transform: none;
}
.insight-filters button::after {
position: absolute;
right: 0;
bottom: 0;
left: 0;
height: 2px;
content: '';
background: currentColor;
opacity: 0;
transform: scaleX(0.75);
transition:
opacity 0.2s,
transform 0.2s;
}
.insight-filters button.active::after {
opacity: 1;
transform: scaleX(1);
}
.article-results {
margin-top: 40px;
margin-top: 28px;
}
.article-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 28px;
grid-template-columns: 1fr;
gap: 20px;
width: min(100%, 1180px);
margin: 0 auto;
}
.article-grid article {
display: grid;
grid-template-columns: 0.85fr 1.15fr;
min-height: 285px;
grid-template-columns: minmax(280px, 332px) 1fr;
gap: 48px;
align-items: center;
min-height: 268px;
overflow: hidden;
border-radius: 14px 4px 14px 4px;
background: #f3f7fc;
padding: 18px 48px 18px 28px;
border: 1px solid #edf1f6;
border-radius: 18px;
background: linear-gradient(100deg, #fff 0%, #fff 72%, #f0f7ff 100%);
box-shadow: 0 10px 26px rgba(43, 83, 130, 0.05);
transition:
transform 0.25s,
box-shadow 0.25s,
@@ -537,18 +622,20 @@ function requestWhitepaper(source) {
}
.article-grid article:hover {
background: #fff;
box-shadow: 0 20px 42px rgba(61, 119, 189, 0.13);
transform: translateY(-5px);
box-shadow: 0 18px 42px rgba(61, 119, 189, 0.12);
transform: translateY(-3px);
}
.article-grid article.lead {
grid-column: 1 / -1;
grid-template-columns: 1.05fr 0.95fr;
min-height: 350px;
grid-column: auto;
grid-template-columns: minmax(280px, 332px) 1fr;
min-height: 268px;
}
.article-cover {
position: relative;
min-height: 100%;
height: 232px;
min-height: 0;
overflow: hidden;
border-radius: 14px;
}
.article-cover img {
width: 100%;
@@ -561,40 +648,64 @@ function requestWhitepaper(source) {
transform: scale(1.04);
}
.article-cover > span {
position: absolute;
top: 18px;
left: 18px;
padding: 7px 9px;
border-radius: 3px;
color: #fff;
background: rgba(32, 105, 200, 0.82);
font-size: 9px;
display: none;
}
.article-copy {
display: flex;
flex-direction: column;
justify-content: center;
padding: 28px;
min-width: 0;
padding: 4px 0;
}
.article-grid .article-meta,
.article-grid .read-link {
display: none;
}
.article-category {
color: #50667f;
font-size: 13px;
line-height: 1.4;
}
.article-copy h3 {
margin: 15px 0 0;
font-size: 20px;
line-height: 1.45;
margin: 12px 0 0;
color: #142033;
font-size: 24px;
font-weight: 500;
line-height: 1.38;
}
.article-grid article.lead h3 {
font-size: 28px;
font-size: 24px;
}
.article-copy h3 a {
color: inherit;
text-decoration: none;
}
.article-copy > p {
margin: 12px 0 0;
color: #75859a;
font-size: 11px;
line-height: 1.75;
display: -webkit-box;
overflow: hidden;
margin: 16px 0 0;
color: #435776;
font-size: 16px;
line-height: 1.68;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.read-link {
align-self: flex-start;
margin-top: 22px;
padding-bottom: 4px;
border-bottom: 1px solid #9bc1e9;
.article-tags {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 24px;
}
.article-tags span {
min-height: 34px;
display: inline-flex;
align-items: center;
padding: 0 13px;
border: 1px solid #d9e5f5;
border-radius: 5px;
color: #096fff;
background: #f7fbff;
font-size: 12px;
}
.load-more {
min-width: 150px;
@@ -736,10 +847,13 @@ function requestWhitepaper(source) {
url('/images/lanhu/slices/home-group105-image31@2x.png') center / cover no-repeat;
}
.insights-cta-content {
display: grid;
justify-items: center;
padding: 74px 0;
text-align: center;
}
.insights-cta-content p {
width: 100%;
margin: 0;
color: #237df0;
font-size: 12px;
@@ -850,24 +964,48 @@ function requestWhitepaper(source) {
flex-direction: column;
}
.insight-filters {
justify-content: flex-start;
gap: 30px;
width: 100%;
margin-top: 52px;
padding-bottom: 4px;
flex-wrap: nowrap;
overflow-x: auto;
}
.insight-filters button {
flex: 0 0 auto;
height: 48px;
font-size: 18px !important;
}
.article-grid article,
.article-grid article.lead {
grid-template-columns: 1fr;
gap: 18px;
padding: 14px;
border-radius: 16px;
}
.article-cover {
min-height: 210px;
height: auto;
min-height: 0;
aspect-ratio: 1.43;
}
.article-copy {
padding: 25px 22px;
padding: 0 4px 4px;
}
.article-copy h3,
.article-grid article.lead h3 {
font-size: 22px;
font-size: 21px;
}
.article-copy > p {
font-size: 14px;
}
.article-tags {
gap: 8px;
margin-top: 18px;
}
.article-tags span {
min-height: 30px;
padding: 0 10px;
}
.whitepaper-grid {
grid-template-columns: 1fr;
+101 -32
View File
@@ -37,6 +37,12 @@ const ringModels = [
}
]
const serviceVisualImages = [
'/images/services/brand-exposure-visual.png',
'/images/services/lead-conversion-visual.png',
'/images/services/omnichannel-visual.png'
]
const services = [
{
id: 'brand-exposure',
@@ -190,11 +196,11 @@ onBeforeUnmount(() => animationContext?.revert())
<div class="hero-core">
<small>GEO</small><strong>品效双环</strong><span>持续增长</span>
</div>
<div class="hero-data hero-data-top"><small>品牌提及</small><strong>+217%</strong></div>
<div class="hero-data hero-data-bottom">
<small>转化效率</small><strong>+64%</strong>
</div>
<div class="hero-visual-caption"><span></span> AI 搜索增长系统正在运行</div>
<!-- <div class="hero-data hero-data-top"><small>品牌提及</small><strong>+217%</strong></div>-->
<!-- <div class="hero-data hero-data-bottom">-->
<!-- <small>转化效率</small><strong>+64%</strong>-->
<!-- </div>-->
<!-- <div class="hero-visual-caption"><span></span> AI 搜索增长系统正在运行</div>-->
</div>
</div>
</section>
@@ -224,7 +230,7 @@ onBeforeUnmount(() => animationContext?.revert())
<section class="model-deep-section">
<div class="page-width">
<header class="model-heading services-reveal">
<p class="section-kicker">双环增长模型</p>
<p class="section-kicker section-center">双环增长模型</p>
<h2>一边沉淀品牌声量一边推动精准获客</h2>
<p>两个环路共享同一套业务事实内容资产与数据目标让品牌建设和转化结果互相增强</p>
</header>
@@ -279,7 +285,7 @@ onBeforeUnmount(() => animationContext?.revert())
<div class="page-width">
<header class="service-system-heading services-reveal">
<div>
<p class="section-kicker">服务体系</p>
<p class="section-kicker section-center">服务体系</p>
<h2>三类能力服务同一个增长目标</h2>
</div>
<p>
@@ -337,7 +343,13 @@ onBeforeUnmount(() => animationContext?.revert())
<span>{{ service.visualTitle }}</span
><small>YUNTU GEO SYSTEM</small>
</div>
<div class="visual-network">
<img
v-if="serviceVisualImages[serviceIndex]"
class="visual-network-image"
:src="serviceVisualImages[serviceIndex]"
:alt="`${service.title}示意图`"
/>
<div v-else class="visual-network">
<div class="network-core">
<small>{{ service.index }}</small
><strong>{{ service.title.split('')[0] }}</strong>
@@ -350,7 +362,10 @@ onBeforeUnmount(() => animationContext?.revert())
>
<i v-for="line in 4" :key="line" :class="`network-line line-${line}`"></i>
</div>
<div class="visual-metric">
<div
v-if="serviceIndex === 0 || !serviceVisualImages[serviceIndex]"
class="visual-metric"
>
<strong>{{ service.metric }}</strong
><span>{{ service.metricLabel }}</span>
</div>
@@ -364,7 +379,7 @@ onBeforeUnmount(() => animationContext?.revert())
<div class="process-background" aria-hidden="true"></div>
<div class="page-width">
<header class="process-heading services-reveal">
<p class="section-kicker">标准化交付</p>
<p class="section-kicker section-center">标准化交付</p>
<h2>五步推进每一步都有依据</h2>
<p>从第一次沟通到内容资产交付节点透明数据可查方向可调整</p>
</header>
@@ -424,7 +439,7 @@ onBeforeUnmount(() => animationContext?.revert())
<article class="featured-proof">
<div class="proof-tag">教育培训行业 · GEO 全链路案例</div>
<h2>420 个真实问题5 个月进入 AI 推荐列表</h2>
<p>
<p class="add-white">
围绕家长找方法选机构比课程看口碑做决策的完整路径搭建问题库可信内容与持续监测体系
</p>
<div class="proof-data">
@@ -460,7 +475,7 @@ onBeforeUnmount(() => animationContext?.revert())
<section class="services-cta">
<div class="cta-wave" aria-hidden="true"></div>
<div class="page-width services-cta-content services-reveal">
<p>从一次真实诊断开始</p>
<p class="section-center">从一次真实诊断开始</p>
<h2>你的品牌应该先解决曝光问题<br />还是转化问题</h2>
<span>聊聊行业现状和目标我们会给出第一步建议</span>
<button
@@ -520,6 +535,10 @@ onBeforeUnmount(() => animationContext?.revert())
font-weight: 700;
letter-spacing: 0.14em;
}
.section-center {
justify-content: center;
margin: 0 auto !important;
}
.services-hero {
position: relative;
min-height: 650px;
@@ -666,10 +685,10 @@ onBeforeUnmount(() => animationContext?.revert())
flex-direction: column;
align-items: center;
justify-content: center;
border: 1px solid rgba(255, 255, 255, 0.9);
border: 0;
border-radius: 50%;
color: #fff;
background: linear-gradient(145deg, rgba(35, 126, 241, 0.94), rgba(43, 180, 231, 0.84));
background: linear-gradient(145deg, #176fd8, #1ca9de);
box-shadow:
0 22px 45px rgba(33, 116, 224, 0.28),
inset 0 1px 5px rgba(255, 255, 255, 0.7);
@@ -695,7 +714,7 @@ onBeforeUnmount(() => animationContext?.revert())
z-index: 3;
top: 50%;
left: 50%;
border: 1px solid rgba(50, 133, 234, 0.3);
border: 0;
border-radius: 50%;
transform: translate(-50%, -50%);
}
@@ -706,14 +725,13 @@ onBeforeUnmount(() => animationContext?.revert())
.hero-orbit-two {
width: 490px;
height: 490px;
border-style: dashed;
opacity: 0.72;
}
.hero-orbit i {
position: absolute;
width: 13px;
height: 13px;
border: 3px solid #fff;
border: 0;
border-radius: 50%;
background: #2383f4;
box-shadow: 0 5px 15px rgba(32, 125, 237, 0.35);
@@ -896,11 +914,20 @@ onBeforeUnmount(() => animationContext?.revert())
position: absolute;
width: 345px;
aspect-ratio: 1;
overflow: hidden;
border: 0;
outline: 0;
box-shadow: none;
}
.model-ring img {
display: block;
width: 100%;
width: calc(100% + 4px);
max-width: none;
margin: -2px;
border: 0;
outline: 0;
filter: drop-shadow(0 25px 32px rgba(58, 123, 205, 0.12));
box-shadow: none;
}
.ring-1 {
top: 10px;
@@ -916,16 +943,23 @@ onBeforeUnmount(() => animationContext?.revert())
left: 50%;
display: grid;
justify-items: center;
min-width: 106px;
padding: 10px 14px 11px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.9);
box-shadow: 0 10px 24px rgba(42, 100, 180, 0.14);
transform: translate(-50%, -50%);
backdrop-filter: blur(10px);
}
.ring-copy small {
color: #87a4c6;
color: #6e8eb4;
font-size: 11px;
}
.ring-copy strong {
margin-top: 5px;
color: #2d76d8;
color: #135fbd;
font-size: 17px;
font-weight: 800;
}
.ring-connector {
position: absolute;
@@ -1040,6 +1074,7 @@ onBeforeUnmount(() => animationContext?.revert())
letter-spacing: 0.12em;
}
.service-index span {
font-size: 18px;
margin-left: 12px;
color: #2c80ed;
}
@@ -1095,9 +1130,9 @@ onBeforeUnmount(() => animationContext?.revert())
.service-details li {
position: relative;
padding-left: 14px;
color: var(--service-copy);
font-size: 14px;
line-height: 1.65;
color: var(--service-copy) !important;
font-size: 14px !important;
line-height: 1.65 !important;
}
.service-details li::before {
position: absolute;
@@ -1110,20 +1145,30 @@ onBeforeUnmount(() => animationContext?.revert())
background: #4b97ef;
}
.service-action {
min-height: 48px;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 9px;
margin-top: 27px;
padding: 0 0 5px;
border-bottom: 1px solid #8eb8e9;
color: #257ce8;
background: transparent;
padding: 0 24px !important;
border: 0;
border-radius: 999px;
color: #fff;
background: linear-gradient(100deg, #3158ff, #12aef5);
box-shadow: 0 12px 24px rgba(25, 106, 255, 0.2);
font-size: 14px;
font-weight: 600;
line-height: 1;
white-space: nowrap;
}
.service-action b {
margin-left: 8px;
transition: margin 0.2s;
.service-action :deep(.app-icon) {
flex: 0 0 auto;
font-size: 1.05em;
transition: transform 0.2s;
}
.service-action:hover b {
margin-left: 13px;
.service-action:hover :deep(.app-icon) {
transform: translateX(3px);
}
.service-story-visual {
position: relative;
@@ -1149,6 +1194,8 @@ onBeforeUnmount(() => animationContext?.revert())
linear-gradient(145deg, #f4f8ff, #dcecff);
}
.visual-topline {
position: relative;
z-index: 2;
display: flex;
justify-content: space-between;
padding: 24px 27px;
@@ -1168,6 +1215,19 @@ onBeforeUnmount(() => animationContext?.revert())
height: 340px;
transform: translate(-50%, -50%);
}
.visual-network-image {
position: absolute;
z-index: 1;
top: 54%;
left: 50%;
width: min(620px, 108%);
height: min(500px, 90%);
max-width: none;
object-fit: contain;
pointer-events: none;
filter: drop-shadow(0 22px 34px rgba(46, 121, 220, 0.16));
transform: translate(-50%, -48%);
}
.network-core {
position: absolute;
z-index: 3;
@@ -1252,6 +1312,7 @@ onBeforeUnmount(() => animationContext?.revert())
}
.visual-metric {
position: absolute;
z-index: 2;
right: 24px;
bottom: 22px;
display: grid;
@@ -1444,6 +1505,9 @@ onBeforeUnmount(() => animationContext?.revert())
letter-spacing: 0.12em;
opacity: 0.72;
}
.add-white {
color: white !important;
}
.featured-proof h2 {
max-width: 720px;
margin: 26px 0 0;
@@ -1815,6 +1879,11 @@ onBeforeUnmount(() => animationContext?.revert())
width: 310px;
height: 280px;
}
.visual-network-image {
top: 52%;
width: min(500px, 118%);
height: 360px;
}
.network-core {
width: 108px;
height: 108px;
+688 -1141
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 645 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 563 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 688 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 734 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 602 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 810 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 731 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 458 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 601 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 566 KiB

+10
View File
@@ -0,0 +1,10 @@
export default defineEventHandler((event) => {
const config = useRuntimeConfig()
const targetBase = String(config.officialApiProxyTarget || 'http://localhost:8080').replace(/\/$/, '')
const rawPath = event.context.params?.path || ''
const path = Array.isArray(rawPath) ? rawPath.join('/') : rawPath
const requestUrl = getRequestURL(event)
const targetUrl = `${targetBase}/api/${path}${requestUrl.search}`
return proxyRequest(event, targetUrl)
})