feat(deploy): 添加 Docker 部署配置与 SSR 优化

- 添加 Dockerfile.static、docker-compose.server.yml 和 nginx-internal.conf
- 优化 Hero 统计数据为 SSR 渲染,提升首屏性能
- 更新案例数据为政府单位数字化解决方案
- 统计数据改为动态计算,基于案例数据和当前年份
- 修复计数器动画初始状态问题
This commit is contained in:
张翔
2026-04-21 15:51:03 +08:00
parent 933a831ab3
commit de94e931af
13 changed files with 459 additions and 176 deletions
+4 -1
View File
@@ -291,4 +291,7 @@ findings.md
# Git will track them because they are not in test-results/ or allure-results/ # Git will track them because they are not in test-results/ or allure-results/
# AGENTS # AGENTS
AGENTS.md AGENTS.md
# dogfood
dogfood-output/
+8
View File
@@ -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;"]
+18
View File
@@ -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
+57
View File
@@ -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;
}
+165 -129
View File
@@ -5,7 +5,30 @@ import { StaticLink } from '@/components/ui/static-link';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { BackButton } from '@/components/ui/back-button'; 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 { interface CaseItem {
id: string; id: string;
@@ -16,6 +39,18 @@ interface CaseItem {
slug: string; slug: string;
date: string; date: string;
image?: string; image?: string;
/** 客户面临的挑战 */
challenge: string;
/** 我们的解决方案 */
solution: string;
/** 关键时刻 */
keyMoments: CaseKeyMoment[];
/** 成果数据 */
results: CaseResult[];
/** 客户证言 */
testimonial: CaseTestimonial;
/** 合作时长 */
duration: string;
} }
interface CaseDetailClientProps { interface CaseDetailClientProps {
@@ -55,55 +90,54 @@ export function CaseDetailClient({ caseItem }: CaseDetailClientProps) {
<h1 className="text-3xl sm:text-4xl lg:text-5xl font-semibold text-[#1C1C1C] mb-2"> <h1 className="text-3xl sm:text-4xl lg:text-5xl font-semibold text-[#1C1C1C] mb-2">
{caseItem.title} {caseItem.title}
</h1> </h1>
<p className="text-lg text-[#5C5C5C]"> <p className="text-lg text-[#5C5C5C]">{caseItem.excerpt}</p>
{caseItem.excerpt}
</p>
</div> </div>
</div> </div>
</div> </div>
<div ref={contentRef} className="container-wide py-12 md:py-16"> <div ref={contentRef} className="container-wide py-12 md:py-16">
<div <div
className={` className={`
grid lg:grid-cols-3 gap-8 lg:gap-12 grid lg:grid-cols-3 gap-8 lg:gap-12
opacity-0 translate-y-4 opacity-0 translate-y-4
${isVisible ? 'animate-fade-in-up' : ''} ${isVisible ? 'animate-fade-in-up' : ''}
`} `}
> >
<div className="lg:col-span-2 space-y-12"> <div className="lg:col-span-2 space-y-12">
<section className="bg-gradient-to-br from-[#FFFBF5] to-white rounded-2xl p-8 border border-[#C41E3A]/20"> {/* 客户遇到的成长瓶颈 */}
<div className="flex items-center gap-3 mb-6"> <section className="bg-gradient-to-br from-[#FFFBF5] to-white rounded-2xl p-8 border border-[#C41E3A]/20">
<div className="w-12 h-12 bg-[#C41E3A] rounded-xl flex items-center justify-center"> <div className="flex items-center gap-3 mb-6">
<MessageCircle className="w-6 h-6 text-white" /> <div className="w-12 h-12 bg-[#C41E3A] rounded-xl flex items-center justify-center">
</div> <MessageCircle className="w-6 h-6 text-white" />
<h2 className="text-2xl font-semibold text-[#1C1C1C]">
</h2>
</div> </div>
<h2 className="text-2xl font-semibold text-[#1C1C1C]">
</h2>
</div>
<p className="text-[#5C5C5C] leading-relaxed text-lg">
{caseItem.challenge}
</p>
</section>
{/* 我们如何智连未来 */}
<section className="bg-gradient-to-br from-[#FFFBF5] to-white rounded-2xl p-8 border border-[#C41E3A]/20">
<div className="flex items-center gap-3 mb-6">
<div className="w-12 h-12 bg-[#C41E3A] rounded-xl flex items-center justify-center">
<Target className="w-6 h-6 text-white" />
</div>
<h2 className="text-2xl font-semibold text-[#1C1C1C]">
</h2>
</div>
<div className="prose prose-base max-w-none [&_h3]:text-xl [&_h3]:font-semibold [&_h3]:text-[#1C1C1C] [&_h3]:mt-8 [&_h3]:mb-4 [&_p]:text-[#5C5C5C] [&_p]:leading-[1.8] [&_p]:mb-4 [&_p]:text-base">
<p className="text-[#5C5C5C] leading-relaxed text-lg"> <p className="text-[#5C5C5C] leading-relaxed text-lg">
{caseItem.excerpt} {caseItem.solution}
</p> </p>
<div className="mt-6 p-4 bg-white rounded-lg border border-[#E5E5E5]"> </div>
<p className="text-sm text-[#737373] italic"> </section>
&ldquo;...&rdquo;
</p>
</div>
</section>
<section className="bg-gradient-to-br from-[#FFFBF5] to-white rounded-2xl p-8 border border-[#C41E3A]/20">
<div className="flex items-center gap-3 mb-6">
<div className="w-12 h-12 bg-[#C41E3A] rounded-xl flex items-center justify-center">
<Target className="w-6 h-6 text-white" />
</div>
<h2 className="text-2xl font-semibold text-[#1C1C1C]">
</h2>
</div>
<div className="prose prose-base max-w-none [&_h3]:text-xl [&_h3]:font-semibold [&_h3]:text-[#1C1C1C] [&_h3]:mt-8 [&_h3]:mb-4 [&_p]:text-[#5C5C5C] [&_p]:leading-[1.8] [&_p]:mb-4 [&_p]:text-base">
<div dangerouslySetInnerHTML={{ __html: caseItem.content }} />
</div>
</section>
{/* 共同成长的故事 */}
{caseItem.keyMoments && caseItem.keyMoments.length > 0 && (
<section className="bg-gradient-to-br from-[#FFFBF5] to-white rounded-2xl p-8 border border-[#C41E3A]/20"> <section className="bg-gradient-to-br from-[#FFFBF5] to-white rounded-2xl p-8 border border-[#C41E3A]/20">
<div className="flex items-center gap-3 mb-6"> <div className="flex items-center gap-3 mb-6">
<div className="w-12 h-12 bg-[#C41E3A] rounded-xl flex items-center justify-center"> <div className="w-12 h-12 bg-[#C41E3A] rounded-xl flex items-center justify-center">
@@ -114,31 +148,30 @@ export function CaseDetailClient({ caseItem }: CaseDetailClientProps) {
</h2> </h2>
</div> </div>
<div className="space-y-4"> <div className="space-y-4">
<div className="p-4 bg-white rounded-lg border border-[#E5E5E5]"> {caseItem.keyMoments.map((moment, index) => (
<div className="flex items-start gap-3"> <div
<Quote className="w-5 h-5 text-[#C41E3A] flex-shrink-0 mt-0.5" /> key={index}
<div> className="p-4 bg-white rounded-lg border border-[#E5E5E5]"
<h4 className="font-semibold text-[#1C1C1C] mb-2"></h4> >
<p className="text-sm text-[#737373]"> <div className="flex items-start gap-3">
线3线 <Quote className="w-5 h-5 text-[#C41E3A] flex-shrink-0 mt-0.5" />
</p> <div>
<h4 className="font-semibold text-[#1C1C1C] mb-2">
{moment.title}
</h4>
<p className="text-sm text-[#737373]">
{moment.description}
</p>
</div>
</div> </div>
</div> </div>
</div> ))}
<div className="p-4 bg-white rounded-lg border border-[#E5E5E5]">
<div className="flex items-start gap-3">
<Quote className="w-5 h-5 text-[#C41E3A] flex-shrink-0 mt-0.5" />
<div>
<h4 className="font-semibold text-[#1C1C1C] mb-2"></h4>
<p className="text-sm text-[#737373]">
</p>
</div>
</div>
</div>
</div> </div>
</section> </section>
)}
{/* 今天,他们走到了哪里 */}
{caseItem.results && caseItem.results.length > 0 && (
<section className="bg-gradient-to-br from-[#FFFBF5] to-white rounded-2xl p-8 border border-[#C41E3A]/20"> <section className="bg-gradient-to-br from-[#FFFBF5] to-white rounded-2xl p-8 border border-[#C41E3A]/20">
<div className="flex items-center gap-3 mb-6"> <div className="flex items-center gap-3 mb-6">
<div className="w-12 h-12 bg-[#C41E3A] rounded-xl flex items-center justify-center"> <div className="w-12 h-12 bg-[#C41E3A] rounded-xl flex items-center justify-center">
@@ -148,36 +181,27 @@ export function CaseDetailClient({ caseItem }: CaseDetailClientProps) {
</h2> </h2>
</div> </div>
<div className="grid sm:grid-cols-3 gap-4 mb-6"> <div className="grid sm:grid-cols-3 gap-4">
<div className="p-6 bg-white rounded-lg border border-[#E5E5E5] hover:border-[#C41E3A] transition-colors"> {caseItem.results.map((result, index) => (
<TrendingUp className="w-8 h-8 text-[#C41E3A] mb-3" /> <div
<div className="text-2xl font-semibold text-[#C41E3A] mb-1"> key={index}
300% className="p-6 bg-white rounded-lg border border-[#E5E5E5] hover:border-[#C41E3A] transition-colors"
>
<TrendingUp className="w-8 h-8 text-[#C41E3A] mb-3" />
<div className="text-2xl font-semibold text-[#C41E3A] mb-1">
{result.value}
</div>
<div className="text-sm text-[#737373]">
{result.label}
</div>
</div> </div>
<div className="text-sm text-[#737373]"></div> ))}
</div>
<div className="p-6 bg-white rounded-lg border border-[#E5E5E5] hover:border-[#C41E3A] transition-colors">
<Users className="w-8 h-8 text-[#C41E3A] mb-3" />
<div className="text-2xl font-semibold text-[#C41E3A] mb-1">
95%
</div>
<div className="text-sm text-[#737373]"></div>
</div>
<div className="p-6 bg-white rounded-lg border border-[#E5E5E5] hover:border-[#C41E3A] transition-colors">
<Target className="w-8 h-8 text-[#C41E3A] mb-3" />
<div className="text-2xl font-semibold text-[#C41E3A] mb-1">
-40%
</div>
<div className="text-sm text-[#737373]"></div>
</div>
</div>
<div className="p-4 bg-white rounded-lg border border-[#E5E5E5]">
<p className="text-sm text-[#737373] italic">
&ldquo;&rdquo;
</p>
</div> </div>
</section> </section>
)}
{/* 客户证言精选 */}
{caseItem.testimonial && (
<section className="bg-gradient-to-br from-[#FFFBF5] to-white rounded-2xl p-8 border border-[#C41E3A]/20"> <section className="bg-gradient-to-br from-[#FFFBF5] to-white rounded-2xl p-8 border border-[#C41E3A]/20">
<div className="flex items-center gap-3 mb-6"> <div className="flex items-center gap-3 mb-6">
<div className="w-12 h-12 bg-[#C41E3A] rounded-xl flex items-center justify-center"> <div className="w-12 h-12 bg-[#C41E3A] rounded-xl flex items-center justify-center">
@@ -190,61 +214,73 @@ export function CaseDetailClient({ caseItem }: CaseDetailClientProps) {
<div className="p-6 bg-white rounded-lg border border-[#E5E5E5]"> <div className="p-6 bg-white rounded-lg border border-[#E5E5E5]">
<Quote className="w-8 h-8 text-[#C41E3A] mb-4" /> <Quote className="w-8 h-8 text-[#C41E3A] mb-4" />
<p className="text-lg text-[#1C1C1C] leading-relaxed mb-4"> <p className="text-lg text-[#1C1C1C] leading-relaxed mb-4">
{caseItem.testimonial.quote}
</p> </p>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-12 h-12 bg-[#C41E3A] rounded-full flex items-center justify-center"> <div className="w-12 h-12 bg-[#C41E3A] rounded-full flex items-center justify-center">
<span className="text-white font-semibold"></span> <span className="text-white font-semibold"></span>
</div> </div>
<div> <div>
<p className="font-semibold text-[#1C1C1C]"></p> <p className="font-semibold text-[#1C1C1C]">
<p className="text-sm text-[#737373]">CEO</p> {caseItem.testimonial.author}
</p>
<p className="text-sm text-[#737373]">
{caseItem.testimonial.role}
</p>
</div> </div>
</div> </div>
</div> </div>
</section> </section>
)}
</div>
{/* 侧边栏 */}
<div className="space-y-6">
<div className="p-6 bg-[#FAFAFA] rounded-lg border border-[#E5E5E5]">
<h3 className="text-lg font-semibold text-[#1C1C1C] mb-4">
</h3>
<dl className="space-y-3">
<div>
<dt className="text-sm text-[#737373]"></dt>
<dd className="text-[#1C1C1C] font-medium">
{caseItem.testimonial?.author || '客户企业'}
</dd>
</div>
<div>
<dt className="text-sm text-[#737373]"></dt>
<dd className="text-[#1C1C1C] font-medium">
{caseItem.category}
</dd>
</div>
<div>
<dt className="text-sm text-[#737373]"></dt>
<dd className="text-[#1C1C1C] font-medium">
{caseItem.duration || '3年'}
</dd>
</div>
<div>
<dt className="text-sm text-[#737373]"></dt>
<dd className="text-[#1C1C1C] font-medium">{caseItem.date}</dd>
</div>
</dl>
</div> </div>
<div className="space-y-6"> <div className="p-6 bg-gradient-to-br from-[#C41E3A] to-[#8B1429] rounded-lg text-white">
<div className="p-6 bg-[#FAFAFA] rounded-lg border border-[#E5E5E5]"> <h3 className="text-lg font-semibold mb-2"></h3>
<h3 className="text-lg font-semibold text-[#1C1C1C] mb-4"></h3> <p className="text-sm text-white/80 mb-4">
<dl className="space-y-3">
<div> </p>
<dt className="text-sm text-[#737373]"></dt> <Button
<dd className="text-[#1C1C1C] font-medium"></dd> className="w-full bg-white text-[#C41E3A] hover:bg-white/90"
</div> asChild
<div> >
<dt className="text-sm text-[#737373]"></dt> <StaticLink href="/contact"></StaticLink>
<dd className="text-[#1C1C1C] font-medium">{caseItem.category}</dd> </Button>
</div>
<div>
<dt className="text-sm text-[#737373]"></dt>
<dd className="text-[#1C1C1C] font-medium">3</dd>
</div>
<div>
<dt className="text-sm text-[#737373]"></dt>
<dd className="text-[#1C1C1C] font-medium">{caseItem.date}</dd>
</div>
</dl>
</div>
<div className="p-6 bg-gradient-to-br from-[#C41E3A] to-[#8B1429] rounded-lg text-white">
<h3 className="text-lg font-semibold mb-2"></h3>
<p className="text-sm text-white/80 mb-4">
</p>
<Button
className="w-full bg-white text-[#C41E3A] hover:bg-white/90"
asChild
>
<StaticLink href="/contact">
</StaticLink>
</Button>
</div>
</div> </div>
</div> </div>
</div> </div>
</main> </div>
</main>
); );
} }
+3 -2
View File
@@ -5,6 +5,7 @@ import { useSearchParams } from 'next/navigation';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import { HeroSection } from "@/components/sections/hero-section"; import { HeroSection } from "@/components/sections/hero-section";
import { SectionSkeleton } from "@/components/ui/loading-skeleton"; import { SectionSkeleton } from "@/components/ui/loading-skeleton";
import type { ReactNode } from 'react';
const ServicesSection = dynamic( const ServicesSection = dynamic(
() => import('@/components/sections/services-section').then(mod => ({ default: mod.ServicesSection })), () => 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(); const searchParams = useSearchParams();
useEffect(() => { useEffect(() => {
@@ -80,7 +81,7 @@ function HomeContent() {
return ( return (
<main className="min-h-screen bg-white dark:bg-(--color-bg-primary)"> <main className="min-h-screen bg-white dark:bg-(--color-bg-primary)">
<HeroSection /> <HeroSection heroStats={heroStats} />
<ServicesSection /> <ServicesSection />
<ProductsSection /> <ProductsSection />
<CasesSection /> <CasesSection />
+2 -1
View File
@@ -1,11 +1,12 @@
import { Suspense } from 'react'; import { Suspense } from 'react';
import { HomeContent } from './home-content'; import { HomeContent } from './home-content';
import { SectionSkeleton } from '@/components/ui/loading-skeleton'; import { SectionSkeleton } from '@/components/ui/loading-skeleton';
import { HeroStatsSSR } from '@/components/sections/hero-stats-ssr';
export default function HomePage() { export default function HomePage() {
return ( return (
<Suspense fallback={<SectionSkeleton />}> <Suspense fallback={<SectionSkeleton />}>
<HomeContent /> <HomeContent heroStats={<HeroStatsSSR />} />
</Suspense> </Suspense>
); );
} }
@@ -152,7 +152,7 @@ export function HeroFeatures({ isVisible }: HeroContentProps) {
} }
export function HeroStats() { export function HeroStats() {
const [statsVisible, setStatsVisible] = useState(false); const [statsVisible, setStatsVisible] = useState(true);
const shouldReduceMotion = useReducedMotion(); const shouldReduceMotion = useReducedMotion();
useEffect(() => { useEffect(() => {
+4 -3
View File
@@ -2,7 +2,8 @@
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import dynamic from 'next/dynamic'; 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( const InkBackground = dynamic(
() => import('@/components/ui/ink-decoration').then(mod => ({ default: mod.InkBackground })), () => import('@/components/ui/ink-decoration').then(mod => ({ default: mod.InkBackground })),
@@ -19,7 +20,7 @@ const SubtleDots = dynamic(
{ ssr: false } { ssr: false }
); );
export function HeroSection() { export function HeroSection({ heroStats }: { heroStats: ReactNode }) {
const [isVisible, setIsVisible] = useState(false); const [isVisible, setIsVisible] = useState(false);
const sectionRef = useRef<HTMLElement>(null); const sectionRef = useRef<HTMLElement>(null);
@@ -64,7 +65,7 @@ export function HeroSection() {
<HeroDescription isVisible={isVisible} /> <HeroDescription isVisible={isVisible} />
<HeroButtons isVisible={isVisible} /> <HeroButtons isVisible={isVisible} />
<HeroFeatures isVisible={isVisible} /> <HeroFeatures isVisible={isVisible} />
<HeroStats /> {heroStats}
</div> </div>
</div> </div>
</section> </section>
@@ -0,0 +1,23 @@
import { STATS } from '@/lib/constants';
export function HeroStatsSSR() {
return (
<div className="pt-16 border-t border-[#E2E8F0]">
<div className="grid grid-cols-2 md:grid-cols-4 gap-8 md:gap-12">
{STATS.map((stat) => (
<div
key={stat.label}
className="group cursor-default text-center"
>
<div className="text-4xl sm:text-5xl font-bold text-[#C41E3A] mb-3">
{stat.value}
</div>
<div className="text-sm text-[#718096] group-hover:text-[#4A5568] transition-colors">
{stat.label}
</div>
</div>
))}
</div>
</div>
);
}
+3 -1
View File
@@ -885,7 +885,7 @@ export function CounterWithEffect({
className = '', className = '',
effect = 'bounce' effect = 'bounce'
}: CounterWithEffectProps) { }: CounterWithEffectProps) {
const [count, setCount] = useState(0); const [count, setCount] = useState(end);
const [_prevCount, setPrevCount] = useState(0); const [_prevCount, setPrevCount] = useState(0);
const ref = useRef<HTMLSpanElement>(null); const ref = useRef<HTMLSpanElement>(null);
const isInView = useInView(ref, { once: true, margin: '-100px' }); const isInView = useInView(ref, { once: true, margin: '-100px' });
@@ -895,6 +895,8 @@ export function CounterWithEffect({
if (!isInView || hasAnimated.current) {return;} if (!isInView || hasAnimated.current) {return;}
hasAnimated.current = true; hasAnimated.current = true;
setCount(0);
const startTime = Date.now(); const startTime = Date.now();
const animate = () => { const animate = () => {
const progress = Math.min((Date.now() - startTime) / duration, 1); const progress = Math.min((Date.now() - startTime) / duration, 1);
+144 -32
View File
@@ -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', id: 'case-1',
title: '某银行数字化转型项目', title: '生物科技技术资源优化咨询项目',
client: '某国有银行', client: '某知名生物科技公司',
industry: '金融科技', industry: '生物科技',
description: '为某国有银行提供全面的数字化转型解决方案,包括核心系统升级、数据中台建设、智能风控系统部署等,助力银行实现业务流程自动化和智能化。', description:
content: '为某国有银行提供全面的数字化转型解决方案,包括核心系统升级、数据中台建设、智能风控系统部署等,助力银行实现业务流程自动化和智能化。', '为某知名生物科技公司提供全面的技术咨询服务,帮助客户梳理现有技术资源体系,合理配置和运用已有技术能力,避免重复建设,减少不必要的IT支出,实现技术资产价值最大化。',
results: [ challenge:
{ label: '业务处理效率', value: '提升60%' }, '该生物科技公司经过十年快速发展,内部积累了超过40个独立IT系统和实验室信息管理平台。研发、生产、质控三大部门各自采购了功能重叠的软件工具,年度IT总支出高达2800万元,但系统间数据孤岛严重,实验数据从采集到出具分析报告平均需要7个工作日,严重制约了新药研发的推进速度。此外,由于缺乏统一的技术治理架构,合规审计每次都需要耗费大量人工整理材料。',
{ label: '客户满意度', value: '提升45%' }, solution:
{ label: '运营成本', value: '降低30%' }, '我们首先对客户全部技术资产进行了为期三周的深度盘点,梳理出40+系统中存在功能重叠的12组、完全闲置的系统6个。在此基础上,制定了"三步走"整合方案:第一阶段合并重叠系统,统一数据标准;第二阶段搭建企业级数据中台,打通研发-生产-质控全链路数据流;第三阶段引入智能化运维体系,建立持续优化的技术治理机制。整个过程中,我们特别注重合规性要求,确保所有整合方案符合GMP和FDA 21 CFR Part 11标准。',
keyMoments: [
{
title: '关键发现:隐藏的"僵尸系统"',
description:
'在技术资产盘点过程中,我们发现客户每年仍在为6个几乎无人使用的遗留系统支付维护费用,累计年浪费超过180万元。这一发现直接促成了客户高层对整个项目的全力支持。',
},
{
title: '数据中台上线:7天变4小时',
description:
'数据中台正式上线后,实验数据从采集到分析报告的周期从原来的7个工作日缩短至4小时。研发团队负责人在上线当天表示:"这是近五年来最激动人心的一天。"',
},
], ],
tags: ['金融科技', '数字化转型', '数据中台'], results: [
image: '/images/cases/bank.jpg', { 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', id: 'case-2',
title: '智能制造示范工厂项目', title: '制造企业技术资源规划咨询项目',
client: '某大型制造企业', client: '某大型制造企业',
industry: '智能制造', industry: '智能制造',
description: '为制造企业打造智能制造示范工厂,实现生产设备互联、生产过程可视化、质量追溯全覆盖,提升生产效率和产品质量。', description:
content: '为制造企业打造智能制造示范工厂,实现生产设备互联、生产过程可视化、质量追溯全覆盖,提升生产效率和产品质量。', '为某大型制造企业提供专业的技术咨询与资源规划服务,帮助客户全面梳理现有技术资源,制定合理的资源配置方案,优化技术架构,减少不必要的成本支出,提升整体运营效率。',
results: [ challenge:
{ label: '生产效率', value: '提升40%' }, '该制造企业在全国拥有8个生产基地、3个研发中心,但各基地的技术系统建设各自为政——有的基地仍在使用15年前的本地部署ERP,有的基地已经上了云端MES却无法与总部财务系统对接。生产计划排程依赖人工Excel汇总,每次月度排产需要3名计划员耗费整整一周。更棘手的是,设备故障响应平均需要4小时,导致非计划停机损失每年超过2000万元。',
{ label: '设备利用率', value: '提升35%' }, solution:
{ label: '不良品率', value: '降低50%' }, '我们为客户设计了"统一底座、分级赋能"的技术架构蓝图。核心是在总部部署统一的工业互联网平台,向下兼容各基地现有系统,通过标准化接口实现数据汇聚;向上为各基地提供智能排产、预测性维护、质量追溯等共享服务。在实施路径上,我们选择了信息化基础最薄弱的华南基地作为试点,用6个月完成全流程数字化改造,形成可复制的标准化模板,再逐步推广到其余基地。同时引入AI驱动的设备预测性维护模型,将被动维修转变为主动预防。',
keyMoments: [
{
title: '华南基地试点:从质疑到标杆',
description:
'项目启动初期,华南基地的厂长对"统一平台"方案持强烈保留态度。我们邀请他参与方案设计的每一个评审环节,并根据一线工人的实际反馈调整了12处交互细节。试点上线三个月后,该基地的人均产出提升了22%,厂长主动请缨成为全集团推广的"内部布道师"。',
},
{
title: '预测性维护:避免了一场重大事故',
description:
'AI预测性维护系统上线仅两个月,就成功提前48小时预警了一台核心冲压设备的轴承疲劳问题。如果按以往的被动维修模式,这次故障将导致整条产线停工5天,损失约350万元。',
},
], ],
tags: ['智能制造', '工业互联网', 'IoT'], results: [
image: '/images/cases/manufacturing.jpg', { 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', id: 'case-3',
title: '企业数据中台建设项目', title: '政府单位数字化整体解决方案项目',
client: '某零售集团', client: '某市级政府单位',
industry: '企业数字化', industry: '政务服务',
description: '为零售集团建设企业级数据中台,整合多渠道数据资源,实现数据资产统一管理,支撑精准营销和智能决策。', description:
content: '为零售集团建设企业级数据中台,整合多渠道数据资源,实现数据资产统一管理,支撑精准营销和智能决策。', '为某市级政府单位提供数字化整体解决方案,涵盖业务流程优化、信息系统整合、在线服务平台建设等,有效提高办事效率,提升公共服务水平和群众满意度。',
results: [ challenge:
{ label: '数据整合效率', value: '提升80%' }, '该市级政府单位承担着面向全市120万市民的行政审批和公共服务职能。然而,市民办理一项常规审批平均需要跑3个窗口、提交15份纸质材料、等待12个工作日。疫情期间,线下服务被迫暂停,但线上办理渠道几乎为零,导致大量业务积压。同时,内部22个科室各自维护独立的业务台账,跨科室协办事项平均流转时间超过20天,群众投诉率居高不下。',
{ label: '决策响应时间', value: '缩短70%' }, solution:
{ label: '营销转化率', value: '提升25%' }, '我们为客户规划并实施了"一网通办"数字化政务服务平台。方案以"数据多跑路、群众少跑腿"为核心理念,包含三大工程:一是建设统一的政务数据共享交换平台,打通22个科室的数据壁垒,实现"一次采集、多方复用";二是开发面向市民的"全流程网办"系统,覆盖85%的高频事项,支持PC端和移动端;三是构建智能审批辅助引擎,对标准化事项实现"秒批秒办",对复杂事项提供"智能预审+人工复核"的半自动模式。在实施过程中,我们特别注重适老化设计,为老年市民保留了电话预约和线下辅助通道。',
keyMoments: [
{
title: '疫情期间紧急上线:72小时攻坚战',
description:
'疫情反复期间,线下服务窗口再次关闭。我们接到紧急需求后,团队72小时连续作战,优先上线了市民最急需的5项高频服务的线上办理功能。上线首周即受理业务3200件,有效缓解了积压压力。市政府领导专门发来感谢信。',
},
{
title: '适老化改造:让数字化不落下任何人',
description:
'在系统试运行阶段,我们收到多位老年市民的反馈,表示"不会用智能手机"。我们迅速增加了大字版界面、语音引导功能和电话预约通道,并组织社区志愿者培训。改造后,60岁以上市民的线上办理率从8%提升至35%。',
},
], ],
tags: ['数据中台', '大数据', '商业智能'], results: [
image: '/images/cases/retail.jpg', { 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',
}, },
]; ];
+27 -6
View File
@@ -1,11 +1,32 @@
import { CASES } from './cases';
export interface StatItem { export interface StatItem {
value: string; value: string;
label: string; label: string;
} }
export const STATS: StatItem[] = [ function calculateYearsOfExperience(): number {
{ value: '10+', label: '企业客户' }, const startYear = 2014;
{ value: '20+', label: '成功案例' }, const currentYear = new Date().getFullYear();
{ value: '30+', label: '项目交付' }, return currentYear - startYear;
{ value: '12+', label: '年行业经验' }, }
];
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();