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} +
-
业务处理效率
-
-
- -
- 95% -
-
客户满意度
-
-
- -
- -40% -
-
运营成本
-
-
-
-

- “通过三年的合作,我们不仅实现了数字化转型,更重要的是建立了一个可持续发展的技术体系。” -

+ ))}
+ )} + {/* 客户证言精选 */} + {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();