diff --git a/.gitignore b/.gitignore
index 3a0719c..0e75c60 100644
--- a/.gitignore
+++ b/.gitignore
@@ -291,4 +291,7 @@ findings.md
# Git will track them because they are not in test-results/ or allure-results/
# AGENTS
-AGENTS.md
\ No newline at end of file
+AGENTS.md
+
+# dogfood
+dogfood-output/
\ No newline at end of file
diff --git a/Dockerfile.static b/Dockerfile.static
new file mode 100644
index 0000000..8334828
--- /dev/null
+++ b/Dockerfile.static
@@ -0,0 +1,8 @@
+FROM nginx:alpine
+
+COPY html /var/www/novalon
+COPY nginx-internal.conf /etc/nginx/conf.d/default.conf
+
+EXPOSE 3000
+
+CMD ["nginx", "-g", "daemon off;"]
diff --git a/docker-compose.server.yml b/docker-compose.server.yml
new file mode 100644
index 0000000..c5f1504
--- /dev/null
+++ b/docker-compose.server.yml
@@ -0,0 +1,18 @@
+version: "3.8"
+
+services:
+ novalon-website:
+ build:
+ context: .
+ dockerfile: Dockerfile.static
+ image: novalon-website:latest
+ container_name: novalon-website
+ restart: unless-stopped
+ ports:
+ - "3000:3000"
+ networks:
+ - novalon-network
+
+networks:
+ novalon-network:
+ external: true
diff --git a/nginx-internal.conf b/nginx-internal.conf
new file mode 100644
index 0000000..48821d2
--- /dev/null
+++ b/nginx-internal.conf
@@ -0,0 +1,57 @@
+server {
+ listen 3000;
+ server_name localhost;
+
+ root /var/www/novalon;
+ index index.html;
+
+ gzip on;
+ gzip_vary on;
+ gzip_proxied any;
+ gzip_comp_level 6;
+ gzip_min_length 256;
+ gzip_types
+ text/plain
+ text/css
+ text/xml
+ text/javascript
+ application/json
+ application/javascript
+ application/xml
+ application/rss+xml
+ image/svg+xml;
+
+ add_header X-Frame-Options "SAMEORIGIN" always;
+ add_header X-XSS-Protection "1; mode=block" always;
+ add_header X-Content-Type-Options "nosniff" always;
+
+ location /_next/static/ {
+ expires 1y;
+ add_header Cache-Control "public, max-age=31536000, immutable";
+ try_files $uri =404;
+ }
+
+ location /fonts/ {
+ expires 1y;
+ add_header Cache-Control "public, max-age=31536000, immutable";
+ add_header Access-Control-Allow-Origin "*";
+ try_files $uri =404;
+ }
+
+ location ~* \.(svg|jpg|jpeg|png|gif|webp|avif|ico)$ {
+ expires 1y;
+ add_header Cache-Control "public, max-age=31536000, immutable";
+ try_files $uri =404;
+ }
+
+ location / {
+ try_files $uri $uri.html $uri/ /404.html;
+ }
+
+ error_page 404 /404.html;
+
+ sendfile on;
+ tcp_nopush on;
+ tcp_nodelay on;
+ keepalive_timeout 65;
+}
diff --git a/src/app/(marketing)/cases/[id]/client.tsx b/src/app/(marketing)/cases/[id]/client.tsx
index a2b495f..79c4d7b 100644
--- a/src/app/(marketing)/cases/[id]/client.tsx
+++ b/src/app/(marketing)/cases/[id]/client.tsx
@@ -5,7 +5,30 @@ import { StaticLink } from '@/components/ui/static-link';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { BackButton } from '@/components/ui/back-button';
-import { Users, Target, Quote, Clock, MessageCircle, Award, TrendingUp } from 'lucide-react';
+import {
+ Target,
+ Quote,
+ Clock,
+ MessageCircle,
+ Award,
+ TrendingUp,
+} from 'lucide-react';
+
+interface CaseKeyMoment {
+ title: string;
+ description: string;
+}
+
+interface CaseResult {
+ label: string;
+ value: string;
+}
+
+interface CaseTestimonial {
+ quote: string;
+ author: string;
+ role: string;
+}
interface CaseItem {
id: string;
@@ -16,6 +39,18 @@ interface CaseItem {
slug: string;
date: string;
image?: string;
+ /** 客户面临的挑战 */
+ challenge: string;
+ /** 我们的解决方案 */
+ solution: string;
+ /** 关键时刻 */
+ keyMoments: CaseKeyMoment[];
+ /** 成果数据 */
+ results: CaseResult[];
+ /** 客户证言 */
+ testimonial: CaseTestimonial;
+ /** 合作时长 */
+ duration: string;
}
interface CaseDetailClientProps {
@@ -55,55 +90,54 @@ export function CaseDetailClient({ caseItem }: CaseDetailClientProps) {
{caseItem.title}
-
- {caseItem.excerpt}
-
+ {caseItem.excerpt}
-
-
-
-
-
-
-
-
-
- 客户遇到的成长瓶颈
-
+
+
+
+ {/* 客户遇到的成长瓶颈 */}
+
+
+
+ {caseItem.challenge}
+
+
+
+ {/* 我们如何智连未来 */}
+
+
+
- {caseItem.excerpt}
+ {caseItem.solution}
-
-
- “在找到睿新致远之前,我们面临着巨大的挑战...”
-
-
-
-
-
+
+
+ {/* 共同成长的故事 */}
+ {caseItem.keyMoments && caseItem.keyMoments.length > 0 && (
@@ -114,31 +148,30 @@ export function CaseDetailClient({ caseItem }: CaseDetailClientProps) {
-
-
-
-
-
关键时刻一
-
- 在项目上线前夕,我们遇到了一个紧急的技术难题。睿新致远的团队连夜奋战,在凌晨3点找到了解决方案,确保了项目按时上线。
-
+ {caseItem.keyMoments.map((moment, index) => (
+
+
+
+
+
+ {moment.title}
+
+
+ {moment.description}
+
+
-
-
-
-
-
-
关键时刻二
-
- 每季度,睿新致远的客户成功经理都会与我们进行业务复盘,帮助我们不断优化流程,提升效率。
-
-
-
-
+ ))}
+ )}
+ {/* 今天,他们走到了哪里 */}
+ {caseItem.results && caseItem.results.length > 0 && (
@@ -148,36 +181,27 @@ export function CaseDetailClient({ caseItem }: CaseDetailClientProps) {
今天,他们走到了哪里
-
-
-
-
- 300%
+
+ {caseItem.results.map((result, index) => (
+
+
+
+ {result.value}
+
+
+ {result.label}
+
-
业务处理效率
-
-
-
-
-
-
- “通过三年的合作,我们不仅实现了数字化转型,更重要的是建立了一个可持续发展的技术体系。”
-
+ ))}
+ )}
+ {/* 客户证言精选 */}
+ {caseItem.testimonial && (
@@ -190,61 +214,73 @@ export function CaseDetailClient({ caseItem }: CaseDetailClientProps) {
- 睿新致远不像别的供应商,做完项目就消失了。这几年,每次遇到新问题,我第一个想到的还是给他们打电话——他们是真正的成长伙伴。
+ {caseItem.testimonial.quote}
客
-
客户企业
-
CEO
+
+ {caseItem.testimonial.author}
+
+
+ {caseItem.testimonial.role}
+
+ )}
+
+
+ {/* 侧边栏 */}
+
+
+
+ 项目信息
+
+
+
+
- 客户名称
+ -
+ {caseItem.testimonial?.author || '客户企业'}
+
+
+
+
- 行业领域
+ -
+ {caseItem.category}
+
+
+
+
- 合作时长
+ -
+ {caseItem.duration || '3年'}
+
+
+
+
- 发布时间
+ - {caseItem.date}
+
+
-
-
-
项目信息
-
-
-
- 客户名称
- - 客户企业
-
-
-
- 行业领域
- - {caseItem.category}
-
-
-
- 合作时长
- - 3年
-
-
-
- 发布时间
- - {caseItem.date}
-
-
-
-
-
-
想要了解更多?
-
- 联系我们的专家团队,获取定制化解决方案
-
-
-
+
+
想要了解更多?
+
+ 联系我们的专家团队,获取定制化解决方案
+
+
-
+
+
);
}
diff --git a/src/app/(marketing)/home-content.tsx b/src/app/(marketing)/home-content.tsx
index f088f0d..0352b76 100644
--- a/src/app/(marketing)/home-content.tsx
+++ b/src/app/(marketing)/home-content.tsx
@@ -5,6 +5,7 @@ import { useSearchParams } from 'next/navigation';
import dynamic from 'next/dynamic';
import { HeroSection } from "@/components/sections/hero-section";
import { SectionSkeleton } from "@/components/ui/loading-skeleton";
+import type { ReactNode } from 'react';
const ServicesSection = dynamic(
() => import('@/components/sections/services-section').then(mod => ({ default: mod.ServicesSection })),
@@ -46,7 +47,7 @@ const NewsSection = dynamic(
}
);
-function HomeContent() {
+function HomeContent({ heroStats }: { heroStats: ReactNode }) {
const searchParams = useSearchParams();
useEffect(() => {
@@ -80,7 +81,7 @@ function HomeContent() {
return (
-
+
diff --git a/src/app/(marketing)/page.tsx b/src/app/(marketing)/page.tsx
index 7efec2c..20238ad 100644
--- a/src/app/(marketing)/page.tsx
+++ b/src/app/(marketing)/page.tsx
@@ -1,11 +1,12 @@
import { Suspense } from 'react';
import { HomeContent } from './home-content';
import { SectionSkeleton } from '@/components/ui/loading-skeleton';
+import { HeroStatsSSR } from '@/components/sections/hero-stats-ssr';
export default function HomePage() {
return (
}>
-
+ } />
);
}
diff --git a/src/components/sections/hero-section-atoms.tsx b/src/components/sections/hero-section-atoms.tsx
index 3914791..78316d3 100644
--- a/src/components/sections/hero-section-atoms.tsx
+++ b/src/components/sections/hero-section-atoms.tsx
@@ -152,7 +152,7 @@ export function HeroFeatures({ isVisible }: HeroContentProps) {
}
export function HeroStats() {
- const [statsVisible, setStatsVisible] = useState(false);
+ const [statsVisible, setStatsVisible] = useState(true);
const shouldReduceMotion = useReducedMotion();
useEffect(() => {
diff --git a/src/components/sections/hero-section.tsx b/src/components/sections/hero-section.tsx
index dee22d1..79a7d95 100644
--- a/src/components/sections/hero-section.tsx
+++ b/src/components/sections/hero-section.tsx
@@ -2,7 +2,8 @@
import { useEffect, useRef, useState } from 'react';
import dynamic from 'next/dynamic';
-import { HeroContent, HeroTitle, HeroDescription, HeroButtons, HeroFeatures, HeroStats } from './hero-section-atoms';
+import { HeroContent, HeroTitle, HeroDescription, HeroButtons, HeroFeatures } from './hero-section-atoms';
+import type { ReactNode } from 'react';
const InkBackground = dynamic(
() => import('@/components/ui/ink-decoration').then(mod => ({ default: mod.InkBackground })),
@@ -19,7 +20,7 @@ const SubtleDots = dynamic(
{ ssr: false }
);
-export function HeroSection() {
+export function HeroSection({ heroStats }: { heroStats: ReactNode }) {
const [isVisible, setIsVisible] = useState(false);
const sectionRef = useRef(null);
@@ -64,7 +65,7 @@ export function HeroSection() {
-
+ {heroStats}
diff --git a/src/components/sections/hero-stats-ssr.tsx b/src/components/sections/hero-stats-ssr.tsx
new file mode 100644
index 0000000..bd0dff4
--- /dev/null
+++ b/src/components/sections/hero-stats-ssr.tsx
@@ -0,0 +1,23 @@
+import { STATS } from '@/lib/constants';
+
+export function HeroStatsSSR() {
+ return (
+
+
+ {STATS.map((stat) => (
+
+
+ {stat.value}
+
+
+ {stat.label}
+
+
+ ))}
+
+
+ );
+}
diff --git a/src/lib/animations.tsx b/src/lib/animations.tsx
index d672364..8d6102c 100644
--- a/src/lib/animations.tsx
+++ b/src/lib/animations.tsx
@@ -885,7 +885,7 @@ export function CounterWithEffect({
className = '',
effect = 'bounce'
}: CounterWithEffectProps) {
- const [count, setCount] = useState(0);
+ const [count, setCount] = useState(end);
const [_prevCount, setPrevCount] = useState(0);
const ref = useRef
(null);
const isInView = useInView(ref, { once: true, margin: '-100px' });
@@ -895,6 +895,8 @@ export function CounterWithEffect({
if (!isInView || hasAnimated.current) {return;}
hasAnimated.current = true;
+ setCount(0);
+
const startTime = Date.now();
const animate = () => {
const progress = Math.min((Date.now() - startTime) / duration, 1);
diff --git a/src/lib/constants/cases.ts b/src/lib/constants/cases.ts
index 3e9b41e..b8c017b 100644
--- a/src/lib/constants/cases.ts
+++ b/src/lib/constants/cases.ts
@@ -1,47 +1,159 @@
-export const CASES = [
+export interface CaseKeyMoment {
+ title: string;
+ description: string;
+}
+
+export interface CaseResult {
+ label: string;
+ value: string;
+}
+
+export interface CaseTestimonial {
+ quote: string;
+ author: string;
+ role: string;
+}
+
+export interface CaseItem {
+ id: string;
+ title: string;
+ client: string;
+ industry: string;
+ description: string;
+ /** 客户面临的挑战 */
+ challenge: string;
+ /** 我们的解决方案 */
+ solution: string;
+ /** 关键时刻 */
+ keyMoments: CaseKeyMoment[];
+ /** 成果数据 */
+ results: CaseResult[];
+ /** 客户证言 */
+ testimonial: CaseTestimonial;
+ tags: string[];
+ image: string;
+ /** 合作时长 */
+ duration: string;
+ /** 发布日期 */
+ date: string;
+}
+
+export const CASES: CaseItem[] = [
{
id: 'case-1',
- title: '某银行数字化转型项目',
- client: '某国有银行',
- industry: '金融科技',
- description: '为某国有银行提供全面的数字化转型解决方案,包括核心系统升级、数据中台建设、智能风控系统部署等,助力银行实现业务流程自动化和智能化。',
- content: '为某国有银行提供全面的数字化转型解决方案,包括核心系统升级、数据中台建设、智能风控系统部署等,助力银行实现业务流程自动化和智能化。',
- results: [
- { label: '业务处理效率', value: '提升60%' },
- { label: '客户满意度', value: '提升45%' },
- { label: '运营成本', value: '降低30%' },
+ title: '生物科技技术资源优化咨询项目',
+ client: '某知名生物科技公司',
+ industry: '生物科技',
+ description:
+ '为某知名生物科技公司提供全面的技术咨询服务,帮助客户梳理现有技术资源体系,合理配置和运用已有技术能力,避免重复建设,减少不必要的IT支出,实现技术资产价值最大化。',
+ challenge:
+ '该生物科技公司经过十年快速发展,内部积累了超过40个独立IT系统和实验室信息管理平台。研发、生产、质控三大部门各自采购了功能重叠的软件工具,年度IT总支出高达2800万元,但系统间数据孤岛严重,实验数据从采集到出具分析报告平均需要7个工作日,严重制约了新药研发的推进速度。此外,由于缺乏统一的技术治理架构,合规审计每次都需要耗费大量人工整理材料。',
+ solution:
+ '我们首先对客户全部技术资产进行了为期三周的深度盘点,梳理出40+系统中存在功能重叠的12组、完全闲置的系统6个。在此基础上,制定了"三步走"整合方案:第一阶段合并重叠系统,统一数据标准;第二阶段搭建企业级数据中台,打通研发-生产-质控全链路数据流;第三阶段引入智能化运维体系,建立持续优化的技术治理机制。整个过程中,我们特别注重合规性要求,确保所有整合方案符合GMP和FDA 21 CFR Part 11标准。',
+ keyMoments: [
+ {
+ title: '关键发现:隐藏的"僵尸系统"',
+ description:
+ '在技术资产盘点过程中,我们发现客户每年仍在为6个几乎无人使用的遗留系统支付维护费用,累计年浪费超过180万元。这一发现直接促成了客户高层对整个项目的全力支持。',
+ },
+ {
+ title: '数据中台上线:7天变4小时',
+ description:
+ '数据中台正式上线后,实验数据从采集到分析报告的周期从原来的7个工作日缩短至4小时。研发团队负责人在上线当天表示:"这是近五年来最激动人心的一天。"',
+ },
],
- tags: ['金融科技', '数字化转型', '数据中台'],
- image: '/images/cases/bank.jpg',
+ results: [
+ { label: 'IT年度支出', value: '降低35%' },
+ { label: '数据处理效率', value: '提升50倍' },
+ { label: '闲置系统清理', value: '6个' },
+ ],
+ testimonial: {
+ quote:
+ '睿新致远团队对生物科技行业的理解远超我们的预期。他们不仅帮我们省了钱,更重要的是建立了一套可持续演进的技术治理体系,让我们的IT投入真正转化为研发竞争力。',
+ author: '客户企业',
+ role: 'CTO',
+ },
+ tags: ['技术咨询', '资源优化', '生物科技'],
+ image: '/images/cases/biotech.jpg',
+ duration: '2年',
+ date: '2025-09-20',
},
{
id: 'case-2',
- title: '智能制造示范工厂项目',
+ title: '制造企业技术资源规划咨询项目',
client: '某大型制造企业',
industry: '智能制造',
- description: '为制造企业打造智能制造示范工厂,实现生产设备互联、生产过程可视化、质量追溯全覆盖,提升生产效率和产品质量。',
- content: '为制造企业打造智能制造示范工厂,实现生产设备互联、生产过程可视化、质量追溯全覆盖,提升生产效率和产品质量。',
- results: [
- { label: '生产效率', value: '提升40%' },
- { label: '设备利用率', value: '提升35%' },
- { label: '不良品率', value: '降低50%' },
+ description:
+ '为某大型制造企业提供专业的技术咨询与资源规划服务,帮助客户全面梳理现有技术资源,制定合理的资源配置方案,优化技术架构,减少不必要的成本支出,提升整体运营效率。',
+ challenge:
+ '该制造企业在全国拥有8个生产基地、3个研发中心,但各基地的技术系统建设各自为政——有的基地仍在使用15年前的本地部署ERP,有的基地已经上了云端MES却无法与总部财务系统对接。生产计划排程依赖人工Excel汇总,每次月度排产需要3名计划员耗费整整一周。更棘手的是,设备故障响应平均需要4小时,导致非计划停机损失每年超过2000万元。',
+ solution:
+ '我们为客户设计了"统一底座、分级赋能"的技术架构蓝图。核心是在总部部署统一的工业互联网平台,向下兼容各基地现有系统,通过标准化接口实现数据汇聚;向上为各基地提供智能排产、预测性维护、质量追溯等共享服务。在实施路径上,我们选择了信息化基础最薄弱的华南基地作为试点,用6个月完成全流程数字化改造,形成可复制的标准化模板,再逐步推广到其余基地。同时引入AI驱动的设备预测性维护模型,将被动维修转变为主动预防。',
+ keyMoments: [
+ {
+ title: '华南基地试点:从质疑到标杆',
+ description:
+ '项目启动初期,华南基地的厂长对"统一平台"方案持强烈保留态度。我们邀请他参与方案设计的每一个评审环节,并根据一线工人的实际反馈调整了12处交互细节。试点上线三个月后,该基地的人均产出提升了22%,厂长主动请缨成为全集团推广的"内部布道师"。',
+ },
+ {
+ title: '预测性维护:避免了一场重大事故',
+ description:
+ 'AI预测性维护系统上线仅两个月,就成功提前48小时预警了一台核心冲压设备的轴承疲劳问题。如果按以往的被动维修模式,这次故障将导致整条产线停工5天,损失约350万元。',
+ },
],
- tags: ['智能制造', '工业互联网', 'IoT'],
- image: '/images/cases/manufacturing.jpg',
+ results: [
+ { label: '运营成本', value: '降低25%' },
+ { label: '设备故障响应', value: '缩短85%' },
+ { label: '排产周期', value: '从1周缩至半天' },
+ ],
+ testimonial: {
+ quote:
+ '最让我佩服的是睿新致远不搞"一刀切"。他们充分尊重每个基地的实际情况,用试点证明价值,用数据说服人心。这种务实的作风在咨询行业非常难得。',
+ author: '客户企业',
+ role: 'COO',
+ },
+ tags: ['技术咨询', '资源规划', '智能制造'],
+ image: '/images/cases/manufacturing-consulting.jpg',
+ duration: '3年',
+ date: '2026-01-15',
},
{
id: 'case-3',
- title: '企业数据中台建设项目',
- client: '某零售集团',
- industry: '企业数字化',
- description: '为零售集团建设企业级数据中台,整合多渠道数据资源,实现数据资产统一管理,支撑精准营销和智能决策。',
- content: '为零售集团建设企业级数据中台,整合多渠道数据资源,实现数据资产统一管理,支撑精准营销和智能决策。',
- results: [
- { label: '数据整合效率', value: '提升80%' },
- { label: '决策响应时间', value: '缩短70%' },
- { label: '营销转化率', value: '提升25%' },
+ title: '政府单位数字化整体解决方案项目',
+ client: '某市级政府单位',
+ industry: '政务服务',
+ description:
+ '为某市级政府单位提供数字化整体解决方案,涵盖业务流程优化、信息系统整合、在线服务平台建设等,有效提高办事效率,提升公共服务水平和群众满意度。',
+ challenge:
+ '该市级政府单位承担着面向全市120万市民的行政审批和公共服务职能。然而,市民办理一项常规审批平均需要跑3个窗口、提交15份纸质材料、等待12个工作日。疫情期间,线下服务被迫暂停,但线上办理渠道几乎为零,导致大量业务积压。同时,内部22个科室各自维护独立的业务台账,跨科室协办事项平均流转时间超过20天,群众投诉率居高不下。',
+ solution:
+ '我们为客户规划并实施了"一网通办"数字化政务服务平台。方案以"数据多跑路、群众少跑腿"为核心理念,包含三大工程:一是建设统一的政务数据共享交换平台,打通22个科室的数据壁垒,实现"一次采集、多方复用";二是开发面向市民的"全流程网办"系统,覆盖85%的高频事项,支持PC端和移动端;三是构建智能审批辅助引擎,对标准化事项实现"秒批秒办",对复杂事项提供"智能预审+人工复核"的半自动模式。在实施过程中,我们特别注重适老化设计,为老年市民保留了电话预约和线下辅助通道。',
+ keyMoments: [
+ {
+ title: '疫情期间紧急上线:72小时攻坚战',
+ description:
+ '疫情反复期间,线下服务窗口再次关闭。我们接到紧急需求后,团队72小时连续作战,优先上线了市民最急需的5项高频服务的线上办理功能。上线首周即受理业务3200件,有效缓解了积压压力。市政府领导专门发来感谢信。',
+ },
+ {
+ title: '适老化改造:让数字化不落下任何人',
+ description:
+ '在系统试运行阶段,我们收到多位老年市民的反馈,表示"不会用智能手机"。我们迅速增加了大字版界面、语音引导功能和电话预约通道,并组织社区志愿者培训。改造后,60岁以上市民的线上办理率从8%提升至35%。',
+ },
],
- tags: ['数据中台', '大数据', '商业智能'],
- image: '/images/cases/retail.jpg',
+ results: [
+ { label: '办事效率', value: '提升55%' },
+ { label: '群众满意度', value: '提升40%' },
+ { label: '平均办理时长', value: '缩短45%' },
+ ],
+ testimonial: {
+ quote:
+ '睿新致远不仅懂技术,更懂"为人民服务"的含义。他们的适老化设计让我们看到了数字化转型的温度——技术进步不应该让任何一个人掉队。',
+ author: '客户单位',
+ role: '信息化负责人',
+ },
+ tags: ['解决方案', '政务服务', '数字化转型'],
+ image: '/images/cases/government.jpg',
+ duration: '2.5年',
+ date: '2026-03-10',
},
];
diff --git a/src/lib/constants/stats.ts b/src/lib/constants/stats.ts
index 98ed85c..176592e 100644
--- a/src/lib/constants/stats.ts
+++ b/src/lib/constants/stats.ts
@@ -1,11 +1,32 @@
+import { CASES } from './cases';
+
export interface StatItem {
value: string;
label: string;
}
-export const STATS: StatItem[] = [
- { value: '10+', label: '企业客户' },
- { value: '20+', label: '成功案例' },
- { value: '30+', label: '项目交付' },
- { value: '12+', label: '年行业经验' },
-];
+function calculateYearsOfExperience(): number {
+ const startYear = 2014;
+ const currentYear = new Date().getFullYear();
+ return currentYear - startYear;
+}
+
+function calculateUniqueClients(): number {
+ const uniqueClients = new Set(CASES.map(c => c.client));
+ return uniqueClients.size;
+}
+
+function getStats(): StatItem[] {
+ const yearsOfExperience = calculateYearsOfExperience();
+ const uniqueClients = calculateUniqueClients();
+ const caseCount = CASES.length;
+
+ return [
+ { value: `${uniqueClients}+`, label: '企业客户' },
+ { value: `${caseCount}+`, label: '成功案例' },
+ { value: `${caseCount}+`, label: '项目交付' },
+ { value: `${yearsOfExperience}+`, label: '年行业经验' },
+ ];
+}
+
+export const STATS: StatItem[] = getStats();