fix(seo): 修复页面标题公司名重复与品牌名繁简体不一致问题

- 新增 COMPANY_INFO.displayName 属性用于页面标题和SEO元数据
- 统一所有页面 metadata 使用 displayName(简体"睿新致远")
- 视觉展示元素保留 shortName(繁体"睿新致遠"配合青柳隷書字体)
- 修复关于/联系/团队页面标题中公司名重复出现的问题
- 修复新闻ID从数字改为SEO友好slug
- 更新结构化数据使用完整公司名
- 修复ESLint报错:引号转义、组件displayName、any类型替换
This commit is contained in:
张翔
2026-05-03 09:15:14 +08:00
parent f0657ce9f4
commit 1bf22a8f95
29 changed files with 183 additions and 76 deletions
+4 -4
View File
@@ -73,7 +73,7 @@ export function AboutClient() {
transition={{ duration: 0.5 }} transition={{ duration: 0.5 }}
className="p-8 rounded-xl border border-[#E5E5E5]" className="p-8 rounded-xl border border-[#E5E5E5]"
> >
<h2 className="text-2xl font-bold text-[#1C1C1C] mb-6"> </h2> <h2 className="text-2xl font-bold text-[#1C1C1C] mb-6"> {COMPANY_INFO.shortName}</h2>
<p className="text-xl font-semibold text-[#1C1C1C] mb-4"></p> <p className="text-xl font-semibold text-[#1C1C1C] mb-4"></p>
<p className="text-[#595959] mb-6 leading-relaxed"></p> <p className="text-[#595959] mb-6 leading-relaxed"></p>
@@ -86,10 +86,10 @@ export function AboutClient() {
<div> <div>
<h3 className="text-lg font-semibold text-[#1C1C1C] mb-3"></h3> <h3 className="text-lg font-semibold text-[#1C1C1C] mb-3"></h3>
<p className="text-[#595959] mb-2 leading-relaxed">"项目交付"</p> <p className="text-[#595959] mb-2 leading-relaxed">&ldquo;&rdquo;</p>
<p className="text-[#595959] mb-2 leading-relaxed"></p> <p className="text-[#595959] mb-2 leading-relaxed"></p>
<p className="text-[#595959] mb-2 leading-relaxed"></p> <p className="text-[#595959] mb-2 leading-relaxed"></p>
<p className="text-[#595959] mt-3 leading-relaxed">"项目是否按时交付"</p> <p className="text-[#595959] mt-3 leading-relaxed">&ldquo;&rdquo;</p>
</div> </div>
</motion.div> </motion.div>
@@ -113,7 +113,7 @@ export function AboutClient() {
</li> </li>
<li className="flex items-start gap-3"> <li className="flex items-start gap-3">
<span className="text-[#C41E3A] font-bold"></span> <span className="text-[#C41E3A] font-bold"></span>
<span className="text-[#595959]">"一锤子买卖"</span> <span className="text-[#595959]">&ldquo;&rdquo;</span>
</li> </li>
</ul> </ul>
<p className="text-[#1C1C1C] leading-relaxed font-medium"> <p className="text-[#1C1C1C] leading-relaxed font-medium">
+31 -21
View File
@@ -2,40 +2,42 @@ import { describe, it, expect, jest } from '@jest/globals';
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom'; import '@testing-library/jest-dom';
type MotionProps = { children: React.ReactNode; className?: string; [key: string]: unknown };
jest.mock('framer-motion', () => ({ jest.mock('framer-motion', () => ({
motion: { motion: {
div: ({ children, className, ...props }: any) => ( div: ({ children, className, ...props }: MotionProps) => (
<div className={className} {...props}> <div className={className} {...props}>
{children} {children}
</div> </div>
), ),
section: ({ children, className, ...props }: any) => ( section: ({ children, className, ...props }: MotionProps) => (
<section className={className} {...props}> <section className={className} {...props}>
{children} {children}
</section> </section>
), ),
span: ({ children, className, ...props }: any) => ( span: ({ children, className, ...props }: MotionProps) => (
<span className={className} {...props}> <span className={className} {...props}>
{children} {children}
</span> </span>
), ),
h1: ({ children, className, ...props }: any) => ( h1: ({ children, className, ...props }: MotionProps) => (
<h1 className={className} {...props}> <h1 className={className} {...props}>
{children} {children}
</h1> </h1>
), ),
h2: ({ children, className, ...props }: any) => ( h2: ({ children, className, ...props }: MotionProps) => (
<h2 className={className} {...props}> <h2 className={className} {...props}>
{children} {children}
</h2> </h2>
), ),
}, },
AnimatePresence: ({ children }: any) => <>{children}</>, AnimatePresence: ({ children }: { children: React.ReactNode }) => <>{children}</>,
useInView: () => [null, true], useInView: () => [null, true],
})); }));
jest.mock('next/link', () => { jest.mock('next/link', () => {
const MockLink = ({ children, href, ...props }: any) => ( const MockLink = ({ children, href, ...props }: { children: React.ReactNode; href: string; [key: string]: unknown }) => (
<a href={href} {...props}> <a href={href} {...props}>
{children} {children}
</a> </a>
@@ -54,40 +56,48 @@ jest.mock('lucide-react', () => ({
Phone: () => <span data-testid="phone-icon" />, Phone: () => <span data-testid="phone-icon" />,
})); }));
jest.mock('@/components/ui/card', () => ({ jest.mock('@/components/ui/card', () => {
Card: ({ children, className, ...props }: any) => ( const Card = ({ children, className, ...props }: { children: React.ReactNode; className?: string; [key: string]: unknown }) => (
<div className={className} {...props}> <div className={className} {...props}>
{children} {children}
</div> </div>
), );
CardContent: ({ children, className, ...props }: any) => ( const CardContent = ({ children, className, ...props }: { children: React.ReactNode; className?: string; [key: string]: unknown }) => (
<div className={className} {...props}> <div className={className} {...props}>
{children} {children}
</div> </div>
), );
})); Card.displayName = 'Card';
CardContent.displayName = 'CardContent';
return { Card, CardContent };
});
jest.mock('@/components/ui/page-header', () => ({ jest.mock('@/components/ui/page-header', () => {
PageHeader: ({ title, description }: any) => ( const PageHeader = ({ title, description }: { title: string; description?: string }) => (
<header> <header>
<h1>{title}</h1> <h1>{title}</h1>
<p>{description}</p> <p>{description}</p>
</header> </header>
), );
})); PageHeader.displayName = 'PageHeader';
return { PageHeader };
});
jest.mock('@/components/ui/flip-clock', () => ({ jest.mock('@/components/ui/flip-clock', () => {
FlipClock: ({ years, months, days }: any) => ( const FlipClock = ({ years, months, days }: { years: number; months: number; days: number }) => (
<div data-testid="flip-clock"> <div data-testid="flip-clock">
{years} {months} {days} {years} {months} {days}
</div> </div>
), );
})); FlipClock.displayName = 'FlipClock';
return { FlipClock };
});
jest.mock('@/lib/constants', () => ({ jest.mock('@/lib/constants', () => ({
COMPANY_INFO: { COMPANY_INFO: {
name: '四川睿新致远科技有限公司', name: '四川睿新致远科技有限公司',
shortName: '睿新致遠', shortName: '睿新致遠',
displayName: '睿新致远',
description: '以智慧连接数字趋势,以伙伴身份陪您成长', description: '以智慧连接数字趋势,以伙伴身份陪您成长',
address: '四川省成都市龙泉驿区', address: '四川省成都市龙泉驿区',
email: 'contact@ruixin.com', email: 'contact@ruixin.com',
+1 -1
View File
@@ -2,7 +2,7 @@ import { COMPANY_INFO } from '@/lib/constants';
import { AboutClient } from './client'; import { AboutClient } from './client';
export const metadata = { export const metadata = {
title: `关于我们 - ${COMPANY_INFO.name}`, title: `关于我们 - ${COMPANY_INFO.displayName}`,
description: `了解${COMPANY_INFO.name}的品牌故事。我们不只是技术供应商,更是您数字化转型的成长伙伴。以智慧连接数字趋势,以伙伴身份陪您成长。`, description: `了解${COMPANY_INFO.name}的品牌故事。我们不只是技术供应商,更是您数字化转型的成长伙伴。以智慧连接数字趋势,以伙伴身份陪您成长。`,
}; };
+3 -1
View File
@@ -1,5 +1,7 @@
import { COMPANY_INFO } from '@/lib/constants';
export const metadata = { export const metadata = {
title: '联系我们 - 四川睿新致远科技有限公司', title: `联系我们 - ${COMPANY_INFO.displayName}`,
description: '无论您有任何问题或合作意向,我们都很乐意与您交流', description: '无论您有任何问题或合作意向,我们都很乐意与您交流',
}; };
+2 -2
View File
@@ -1,5 +1,5 @@
import { notFound } from 'next/navigation'; import { notFound } from 'next/navigation';
import { NEWS } from '@/lib/constants'; import { NEWS, COMPANY_INFO } from '@/lib/constants';
import { NewsDetailClient } from './NewsDetailClient'; import { NewsDetailClient } from './NewsDetailClient';
export async function generateStaticParams() { export async function generateStaticParams() {
@@ -19,7 +19,7 @@ export async function generateMetadata({ params }: { params: Promise<{ slug: str
} }
return { return {
title: `${news.title} - 睿新致远`, title: `${news.title} - ${COMPANY_INFO.displayName}`,
description: news.excerpt, description: news.excerpt,
}; };
} }
+2 -2
View File
@@ -2,8 +2,8 @@ import { Metadata } from 'next';
import { COMPANY_INFO } from '@/lib/constants'; import { COMPANY_INFO } from '@/lib/constants';
export const metadata: Metadata = { export const metadata: Metadata = {
title: `新闻动态 - ${COMPANY_INFO.shortName}`, title: `新闻动态 - ${COMPANY_INFO.displayName}`,
description: `了解${COMPANY_INFO.shortName}最新动态,把握行业发展脉搏。`, description: `了解${COMPANY_INFO.displayName}最新动态,把握行业发展脉搏。`,
}; };
export default function NewsLayout({ children }: { children: React.ReactNode }) { export default function NewsLayout({ children }: { children: React.ReactNode }) {
+2 -2
View File
@@ -2,7 +2,7 @@
import { useState, useMemo, ChangeEvent } from 'react'; import { useState, useMemo, ChangeEvent } from 'react';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { NEWS } from '@/lib/constants'; import { NEWS, COMPANY_INFO } from '@/lib/constants';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
@@ -65,7 +65,7 @@ export default function NewsListPage() {
</h1> </h1>
<p className="text-lg text-[#595959] leading-relaxed"> <p className="text-lg text-[#595959] leading-relaxed">
{COMPANY_INFO.displayName}
</p> </p>
</motion.div> </motion.div>
</div> </div>
+2 -2
View File
@@ -1,6 +1,6 @@
import { notFound } from 'next/navigation'; import { notFound } from 'next/navigation';
import { StaticLink } from '@/components/ui/static-link'; import { StaticLink } from '@/components/ui/static-link';
import { PRODUCTS } from '@/lib/constants'; import { PRODUCTS, COMPANY_INFO } from '@/lib/constants';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { PageNav } from '@/components/layout/page-nav'; import { PageNav } from '@/components/layout/page-nav';
import { CheckCircle2, Zap, Target, Layers, ArrowRight } from 'lucide-react'; import { CheckCircle2, Zap, Target, Layers, ArrowRight } from 'lucide-react';
@@ -20,7 +20,7 @@ export async function generateMetadata({ params }: { params: Promise<{ id: strin
} }
return { return {
title: `${product.title} - 睿新致远`, title: `${product.title} - ${COMPANY_INFO.displayName}`,
description: product.description, description: product.description,
}; };
} }
+2 -2
View File
@@ -2,8 +2,8 @@ import { Metadata } from 'next';
import { COMPANY_INFO } from '@/lib/constants'; import { COMPANY_INFO } from '@/lib/constants';
export const metadata: Metadata = { export const metadata: Metadata = {
title: `产品 - ${COMPANY_INFO.shortName}`, title: `产品 - ${COMPANY_INFO.displayName}`,
description: `自主研发的企业级产品,助力企业高效运营,实现数字化转型。${COMPANY_INFO.shortName}提供ERP、CRM、BI、CMS等产品。`, description: `自主研发的企业级产品,助力企业高效运营,实现数字化转型。${COMPANY_INFO.displayName}提供ERP、CRM、BI、CMS等产品。`,
}; };
export default function ProductsLayout({ children }: { children: React.ReactNode }) { export default function ProductsLayout({ children }: { children: React.ReactNode }) {
+2 -2
View File
@@ -1,6 +1,6 @@
import { Metadata } from 'next'; import { Metadata } from 'next';
import { notFound } from 'next/navigation'; import { notFound } from 'next/navigation';
import { SERVICES } from '@/lib/constants'; import { SERVICES, COMPANY_INFO } from '@/lib/constants';
import { ServiceDetailClient } from './client'; import { ServiceDetailClient } from './client';
export async function generateStaticParams() { export async function generateStaticParams() {
@@ -20,7 +20,7 @@ export async function generateMetadata({ params }: { params: Promise<{ id: strin
} }
return { return {
title: `${service.title} - 睿新致远`, title: `${service.title} - ${COMPANY_INFO.displayName}`,
description: service.description, description: service.description,
}; };
} }
+2 -2
View File
@@ -2,8 +2,8 @@ import { Metadata } from 'next';
import { COMPANY_INFO } from '@/lib/constants'; import { COMPANY_INFO } from '@/lib/constants';
export const metadata: Metadata = { export const metadata: Metadata = {
title: `服务 - ${COMPANY_INFO.shortName}`, title: `服务 - ${COMPANY_INFO.displayName}`,
description: `专业技术团队,为您提供全方位的数字化解决方案。${COMPANY_INFO.shortName}涵盖软件开发、数据分析、咨询服务等核心业务。`, description: `专业技术团队,为您提供全方位的数字化解决方案。${COMPANY_INFO.displayName}涵盖软件开发、数据分析、咨询服务等核心业务。`,
}; };
export default function ServicesLayout({ children }: { children: React.ReactNode }) { export default function ServicesLayout({ children }: { children: React.ReactNode }) {
+2 -1
View File
@@ -2,6 +2,7 @@ import { Metadata } from 'next';
import { notFound } from 'next/navigation'; import { notFound } from 'next/navigation';
import { SOLUTIONS } from '@/lib/constants/solutions'; import { SOLUTIONS } from '@/lib/constants/solutions';
import { PRODUCTS } from '@/lib/constants/products'; import { PRODUCTS } from '@/lib/constants/products';
import { COMPANY_INFO } from '@/lib/constants';
import { SolutionDetailClient } from './client'; import { SolutionDetailClient } from './client';
export async function generateStaticParams() { export async function generateStaticParams() {
@@ -19,7 +20,7 @@ export async function generateMetadata({ params }: { params: Promise<{ id: strin
} }
return { return {
title: `${solution.title} - 睿新致远`, title: `${solution.title} - ${COMPANY_INFO.displayName}`,
description: solution.description, description: solution.description,
}; };
} }
+3 -1
View File
@@ -1,7 +1,9 @@
import { Metadata } from 'next'; import { Metadata } from 'next';
import { COMPANY_INFO } from '@/lib/constants';
export const metadata: Metadata = { export const metadata: Metadata = {
title: '解决方案 - 睿新致远', title: `解决方案 - ${COMPANY_INFO.displayName}`,
description: '三种角色,一种身份——您的成长伙伴', description: '三种角色,一种身份——您的成长伙伴',
}; };
+4 -1
View File
@@ -20,6 +20,7 @@ const modules = [
values: ['行业趋势洞察报告', '数字化转型成熟度评估', '个性化实施路径规划'], values: ['行业趋势洞察报告', '数字化转型成熟度评估', '个性化实施路径规划'],
cta: '预约一次免费诊断', cta: '预约一次免费诊断',
ctaVariant: 'default' as const, ctaVariant: 'default' as const,
ctaHref: '/contact',
}, },
{ {
icon: Cpu, icon: Cpu,
@@ -33,6 +34,7 @@ const modules = [
values: ['业务场景深度调研', '技术方案定制开发', '敏捷交付快速迭代'], values: ['业务场景深度调研', '技术方案定制开发', '敏捷交付快速迭代'],
cta: '了解技术方案', cta: '了解技术方案',
ctaVariant: 'outline' as const, ctaVariant: 'outline' as const,
ctaHref: '/products',
}, },
{ {
icon: Users, icon: Users,
@@ -46,6 +48,7 @@ const modules = [
values: ['专属客户成功经理', '季度业务复盘会', '7×24小时响应通道'], values: ['专属客户成功经理', '季度业务复盘会', '7×24小时响应通道'],
cta: '了解陪跑服务', cta: '了解陪跑服务',
ctaVariant: 'default' as const, ctaVariant: 'default' as const,
ctaHref: '/services',
}, },
]; ];
@@ -127,7 +130,7 @@ export default function SolutionsPage() {
className={module.ctaVariant === 'default' ? 'bg-[#C41E3A] hover:bg-[#A01830] text-white' : 'border-[#C41E3A] text-[#C41E3A] hover:bg-[#C41E3A] hover:text-white'} className={module.ctaVariant === 'default' ? 'bg-[#C41E3A] hover:bg-[#A01830] text-white' : 'border-[#C41E3A] text-[#C41E3A] hover:bg-[#C41E3A] hover:text-white'}
asChild asChild
> >
<StaticLink href="/contact"> <StaticLink href={module.ctaHref}>
{module.cta} {module.cta}
<ArrowRight className="ml-2 w-4 h-4" /> <ArrowRight className="ml-2 w-4 h-4" />
</StaticLink> </StaticLink>
+1 -1
View File
@@ -2,7 +2,7 @@ import { COMPANY_INFO } from '@/lib/constants';
import { TeamClient } from './client'; import { TeamClient } from './client';
export const metadata = { export const metadata = {
title: `核心团队 - ${COMPANY_INFO.name}`, title: `核心团队 - ${COMPANY_INFO.displayName}`,
description: `了解${COMPANY_INFO.name}的核心团队。我们的团队成员拥有丰富的行业经验和技术专长,致力于为客户提供专业的数字化转型服务。`, description: `了解${COMPANY_INFO.name}的核心团队。我们的团队成员拥有丰富的行业经验和技术专长,致力于为客户提供专业的数字化转型服务。`,
}; };
+2 -1
View File
@@ -3,6 +3,7 @@
import { StaticLink } from '@/components/ui/static-link'; import { StaticLink } from '@/components/ui/static-link';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Home, ArrowLeft, Search } from 'lucide-react'; import { Home, ArrowLeft, Search } from 'lucide-react';
import { COMPANY_INFO } from '@/lib/constants';
export default function NotFound() { export default function NotFound() {
return ( return (
@@ -62,7 +63,7 @@ export default function NotFound() {
</div> </div>
<div className="text-left"> <div className="text-left">
<div className="font-semibold text-[#1C1C1C]"></div> <div className="font-semibold text-[#1C1C1C]"></div>
<div className="text-sm text-[#5C5C5C]"></div> <div className="text-sm text-[#5C5C5C]">{COMPANY_INFO.displayName}</div>
</div> </div>
</StaticLink> </StaticLink>
+3 -2
View File
@@ -1,8 +1,9 @@
import { Metadata } from 'next'; import { Metadata } from 'next';
import { COMPANY_INFO } from '@/lib/constants';
export const metadata: Metadata = { export const metadata: Metadata = {
title: '隐私政策 - 睿新致远', title: `隐私政策 - ${COMPANY_INFO.displayName}`,
description: '四川睿新致远科技有限公司隐私政策', description: `${COMPANY_INFO.name}隐私政策`,
}; };
export default function PrivacyPolicyPage() { export default function PrivacyPolicyPage() {
+3 -2
View File
@@ -1,8 +1,9 @@
import { Metadata } from 'next'; import { Metadata } from 'next';
import { COMPANY_INFO } from '@/lib/constants';
export const metadata: Metadata = { export const metadata: Metadata = {
title: '服务条款 - 睿新致远', title: `服务条款 - ${COMPANY_INFO.displayName}`,
description: '四川睿新致远科技有限公司服务条款', description: `${COMPANY_INFO.name}服务条款`,
}; };
export default function TermsOfServicePage() { export default function TermsOfServicePage() {
+30 -2
View File
@@ -1,6 +1,6 @@
'use client'; 'use client';
import { useState, useEffect, useCallback } from 'react'; import { useState, useEffect, useCallback, useRef } from 'react';
import { import {
updateConsentDetailed, updateConsentDetailed,
trackButtonClick, trackButtonClick,
@@ -13,19 +13,37 @@ import { motion, AnimatePresence } from 'framer-motion';
const LEGACY_CONSENT_KEY = 'ga_consent'; const LEGACY_CONSENT_KEY = 'ga_consent';
let hasConsentBeenHandled = false;
function getInitialShowConsent(): boolean {
if (typeof window === 'undefined') {return false;}
if (hasConsentBeenHandled) {return false;}
const stored = getStoredPreferences();
if (stored) {return false;}
const legacyConsent = localStorage.getItem(LEGACY_CONSENT_KEY);
if (legacyConsent) {return false;}
return false;
}
export function CookieConsent() { export function CookieConsent() {
const [showConsent, setShowConsent] = useState(false); const [showConsent, setShowConsent] = useState(getInitialShowConsent);
const [showSettings, setShowSettings] = useState(false); const [showSettings, setShowSettings] = useState(false);
const [isAnimating, setIsAnimating] = useState(false); const [isAnimating, setIsAnimating] = useState(false);
const [preferences, setPreferences] = useState<CookiePreferences>(getDefaultPreferences()); const [preferences, setPreferences] = useState<CookiePreferences>(getDefaultPreferences());
const consentCheckedRef = useRef(false);
useEffect(() => { useEffect(() => {
if (consentCheckedRef.current) {return;}
consentCheckedRef.current = true;
const stored = getStoredPreferences(); const stored = getStoredPreferences();
if (stored) { if (stored) {
hasConsentBeenHandled = true;
updateConsentDetailed(stored); updateConsentDetailed(stored);
} else { } else {
const legacyConsent = localStorage.getItem(LEGACY_CONSENT_KEY); const legacyConsent = localStorage.getItem(LEGACY_CONSENT_KEY);
if (legacyConsent) { if (legacyConsent) {
hasConsentBeenHandled = true;
const migratedPrefs: CookiePreferences = { const migratedPrefs: CookiePreferences = {
necessary: true, necessary: true,
analytics: legacyConsent === 'granted', analytics: legacyConsent === 'granted',
@@ -45,8 +63,18 @@ export function CookieConsent() {
return undefined; return undefined;
}, []); }, []);
useEffect(() => {
const handleOpenSettings = () => {
setShowSettings(true);
setShowConsent(true);
};
window.addEventListener('open-cookie-settings', handleOpenSettings);
return () => window.removeEventListener('open-cookie-settings', handleOpenSettings);
}, []);
const handleSavePreferences = useCallback((prefs: CookiePreferences) => { const handleSavePreferences = useCallback((prefs: CookiePreferences) => {
setIsAnimating(true); setIsAnimating(true);
hasConsentBeenHandled = true;
const finalPrefs = { ...prefs, timestamp: Date.now() }; const finalPrefs = { ...prefs, timestamp: Date.now() };
storePreferences(finalPrefs); storePreferences(finalPrefs);
updateConsentDetailed(finalPrefs); updateConsentDetailed(finalPrefs);
+2
View File
@@ -35,6 +35,8 @@ jest.mock('lucide-react', () => ({
jest.mock('@/lib/constants', () => ({ jest.mock('@/lib/constants', () => ({
COMPANY_INFO: { COMPANY_INFO: {
name: '四川睿新致远科技有限公司', name: '四川睿新致远科技有限公司',
shortName: '睿新致遠',
displayName: '睿新致远',
description: '以智慧连接数字趋势,以伙伴身份陪您成长', description: '以智慧连接数字趋势,以伙伴身份陪您成长',
email: 'contact@novalon.cn', email: 'contact@novalon.cn',
phone: '028-88888888', phone: '028-88888888',
+15 -8
View File
@@ -15,28 +15,32 @@ jest.mock('next/navigation', () => ({
})); }));
jest.mock('next/link', () => { jest.mock('next/link', () => {
return ({ children, href, onClick, ...props }: any) => ( const MockLink = ({ children, href, onClick, ...props }: { children: React.ReactNode; href: string; onClick?: () => void; [key: string]: unknown }) => (
<a href={href} onClick={onClick} {...props}> <a href={href} onClick={onClick} {...props}>
{children} {children}
</a> </a>
); );
MockLink.displayName = 'MockLink';
return MockLink;
}); });
jest.mock('next/image', () => { jest.mock('next/image', () => {
return ({ src, alt, width, height, className, ...props }: any) => ( const MockImage = ({ src, alt, width, height, className, ...props }: { src: string; alt: string; width: number; height: number; className?: string; [key: string]: unknown }) => (
<img src={src} alt={alt} width={width} height={height} className={className} {...props} /> <img src={src} alt={alt} width={width} height={height} className={className} {...props} />
); );
MockImage.displayName = 'MockImage';
return MockImage;
}); });
jest.mock('framer-motion', () => ({ jest.mock('framer-motion', () => ({
motion: { motion: {
div: ({ children, className, ...props }: any) => ( div: ({ children, className, ...props }: { children: React.ReactNode; className?: string; [key: string]: unknown }) => (
<div className={className} {...props}> <div className={className} {...props}>
{children} {children}
</div> </div>
), ),
}, },
AnimatePresence: ({ children }: any) => <>{children}</>, AnimatePresence: ({ children }: { children: React.ReactNode }) => <>{children}</>,
})); }));
jest.mock('lucide-react', () => ({ jest.mock('lucide-react', () => ({
@@ -44,18 +48,21 @@ jest.mock('lucide-react', () => ({
X: () => <span data-testid="x-icon" />, X: () => <span data-testid="x-icon" />,
})); }));
jest.mock('@/components/ui/button', () => ({ jest.mock('@/components/ui/button', () => {
Button: ({ children, className, asChild, ...props }: any) => ( const MockButton = ({ children, className, ...props }: { children: React.ReactNode; className?: string; [key: string]: unknown }) => (
<button className={className} {...props}> <button className={className} {...props}>
{children} {children}
</button> </button>
), );
})); MockButton.displayName = 'MockButton';
return { Button: MockButton };
});
jest.mock('@/lib/constants', () => ({ jest.mock('@/lib/constants', () => ({
COMPANY_INFO: { COMPANY_INFO: {
name: '四川睿新致远科技有限公司', name: '四川睿新致远科技有限公司',
shortName: '睿新致遠', shortName: '睿新致遠',
displayName: '睿新致远',
}, },
NAVIGATION: [ NAVIGATION: [
{ id: 'home', label: '首页', href: '/' }, { id: 'home', label: '首页', href: '/' },
+6 -2
View File
@@ -108,7 +108,11 @@ function HeaderContent() {
label={item.label} label={item.label}
items={MEGA_DROPDOWN_DATA[item.dropdownKey!] ?? []} items={MEGA_DROPDOWN_DATA[item.dropdownKey!] ?? []}
isOpen={openDropdown === item.id} isOpen={openDropdown === item.id}
onToggle={() => setOpenDropdown(openDropdown === item.id ? null : item.id)} onToggle={() => {
setOpenDropdown((prev) => prev === item.id ? null : item.id);
}}
onOpen={() => setOpenDropdown(item.id)}
onClose={() => setOpenDropdown((prev) => prev === item.id ? null : prev)}
/> />
) : ( ) : (
<StaticLink <StaticLink
@@ -224,7 +228,7 @@ function HeaderContent() {
size="lg" size="lg"
> >
<StaticLink href="/contact" onClick={() => setIsOpen(false)}> <StaticLink href="/contact" onClick={() => setIsOpen(false)}>
</StaticLink> </StaticLink>
</Button> </Button>
</div> </div>
+41 -2
View File
@@ -11,10 +11,15 @@ interface MegaDropdownProps {
items: MegaDropdownItem[]; items: MegaDropdownItem[];
isOpen: boolean; isOpen: boolean;
onToggle: () => void; onToggle: () => void;
onOpen?: () => void;
onClose?: () => void;
} }
export function MegaDropdown({ label, items, isOpen, onToggle }: MegaDropdownProps) { const HOVER_DELAY = 150;
export function MegaDropdown({ label, items, isOpen, onToggle, onOpen, onClose }: MegaDropdownProps) {
const dropdownRef = useRef<HTMLDivElement>(null); const dropdownRef = useRef<HTMLDivElement>(null);
const hoverTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
useEffect(() => { useEffect(() => {
function handleClickOutside(event: MouseEvent) { function handleClickOutside(event: MouseEvent) {
@@ -26,8 +31,42 @@ export function MegaDropdown({ label, items, isOpen, onToggle }: MegaDropdownPro
return () => document.removeEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside);
}, [isOpen, onToggle]); }, [isOpen, onToggle]);
useEffect(() => {
return () => {
if (hoverTimeoutRef.current) {
clearTimeout(hoverTimeoutRef.current);
}
};
}, []);
const handleMouseEnter = () => {
if (hoverTimeoutRef.current) {
clearTimeout(hoverTimeoutRef.current);
hoverTimeoutRef.current = null;
}
if (!isOpen) {
if (onOpen) {
onOpen();
} else {
onToggle();
}
}
};
const handleMouseLeave = () => {
hoverTimeoutRef.current = setTimeout(() => {
if (isOpen) {
if (onClose) {
onClose();
} else {
onToggle();
}
}
}, HOVER_DELAY);
};
return ( return (
<div ref={dropdownRef} className="relative"> <div ref={dropdownRef} className="relative" onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
<button <button
onClick={onToggle} onClick={onToggle}
className={` className={`
+2 -1
View File
@@ -4,6 +4,7 @@ import { motion } from 'framer-motion';
import { StaticLink } from '@/components/ui/static-link'; import { StaticLink } from '@/components/ui/static-link';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { ArrowRight } from 'lucide-react'; import { ArrowRight } from 'lucide-react';
import { COMPANY_INFO } from '@/lib/constants';
interface CTASectionProps { interface CTASectionProps {
title?: string; title?: string;
@@ -16,7 +17,7 @@ interface CTASectionProps {
export function CTASection({ export function CTASection({
title = '开启您的数字化转型之旅', title = '开启您的数字化转型之旅',
description = '与睿新致遠一起,让技术成为您业务增长的核心引擎', description = `${COMPANY_INFO.shortName}一起,让技术成为您业务增长的核心引擎`,
primaryLabel = '立即咨询', primaryLabel = '立即咨询',
primaryHref = '/contact', primaryHref = '/contact',
secondaryLabel = '了解方案', secondaryLabel = '了解方案',
+3 -2
View File
@@ -1,6 +1,7 @@
import { render } from '@testing-library/react'; import { render } from '@testing-library/react';
import '@testing-library/jest-dom'; import '@testing-library/jest-dom';
import { OrganizationSchema, WebsiteSchema } from './structured-data'; import { OrganizationSchema, WebsiteSchema } from './structured-data';
import { COMPANY_INFO } from '@/lib/constants';
describe('StructuredData', () => { describe('StructuredData', () => {
beforeEach(() => { beforeEach(() => {
@@ -18,7 +19,7 @@ describe('StructuredData', () => {
const { container } = render(<OrganizationSchema />); const { container } = render(<OrganizationSchema />);
const script = container.querySelector('script[type="application/ld+json"]'); const script = container.querySelector('script[type="application/ld+json"]');
const schema = JSON.parse(script?.textContent || '{}'); const schema = JSON.parse(script?.textContent || '{}');
expect(schema.name).toBe('四川睿新致远科技有限公司'); expect(schema.name).toBe(COMPANY_INFO.name);
}); });
it('should contain organization type', () => { it('should contain organization type', () => {
@@ -54,7 +55,7 @@ describe('StructuredData', () => {
const { container } = render(<WebsiteSchema />); const { container } = render(<WebsiteSchema />);
const script = container.querySelector('script[type="application/ld+json"]'); const script = container.querySelector('script[type="application/ld+json"]');
const schema = JSON.parse(script?.textContent || '{}'); const schema = JSON.parse(script?.textContent || '{}');
expect(schema.name).toBe('四川睿新致远科技有限公司'); expect(schema.name).toBe(COMPANY_INFO.name);
}); });
}); });
}); });
+5 -3
View File
@@ -1,8 +1,10 @@
import { COMPANY_INFO } from '@/lib/constants';
export function OrganizationSchema() { export function OrganizationSchema() {
const schema = { const schema = {
"@context": "https://schema.org", "@context": "https://schema.org",
"@type": "Organization", "@type": "Organization",
"name": "四川睿新致远科技有限公司", "name": COMPANY_INFO.name,
"alternateName": "诺瓦隆", "alternateName": "诺瓦隆",
"url": "https://www.novalon.cn", "url": "https://www.novalon.cn",
"logo": "https://www.novalon.cn/logo.svg", "logo": "https://www.novalon.cn/logo.svg",
@@ -32,7 +34,7 @@ export function WebsiteSchema() {
const schema = { const schema = {
"@context": "https://schema.org", "@context": "https://schema.org",
"@type": "WebSite", "@type": "WebSite",
"name": "四川睿新致远科技有限公司", "name": COMPANY_INFO.name,
"url": "https://www.novalon.cn", "url": "https://www.novalon.cn",
"potentialAction": { "potentialAction": {
"@type": "SearchAction", "@type": "SearchAction",
@@ -56,7 +58,7 @@ export function ServiceSchema() {
"serviceType": "企业数字化转型服务", "serviceType": "企业数字化转型服务",
"provider": { "provider": {
"@type": "Organization", "@type": "Organization",
"name": "四川睿新致远科技有限公司" "name": COMPANY_INFO.name
}, },
"description": "提供软件开发、云计算、数据分析、信息安全等一站式数字化转型解决方案", "description": "提供软件开发、云计算、数据分析、信息安全等一站式数字化转型解决方案",
"areaServed": { "areaServed": {
+4 -3
View File
@@ -2,6 +2,7 @@
import { useRef, useState, useCallback, type ReactNode } from 'react'; import { useRef, useState, useCallback, type ReactNode } from 'react';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { StaticLink } from '@/components/ui/static-link';
interface InkGlowCardProps { interface InkGlowCardProps {
children: ReactNode; children: ReactNode;
@@ -33,7 +34,7 @@ export function InkGlowCard({
const [isHovered, setIsHovered] = useState(false); const [isHovered, setIsHovered] = useState(false);
const handleMouseMove = useCallback((e: React.MouseEvent) => { const handleMouseMove = useCallback((e: React.MouseEvent) => {
if (!cardRef.current) return; if (!cardRef.current) {return;}
const rect = cardRef.current.getBoundingClientRect(); const rect = cardRef.current.getBoundingClientRect();
setMousePos({ setMousePos({
x: e.clientX - rect.left, x: e.clientX - rect.left,
@@ -72,7 +73,7 @@ export function InkGlowCard({
} as React.CSSProperties} } as React.CSSProperties}
> >
{href ? ( {href ? (
<a <StaticLink
href={href} href={href}
className="relative block rounded-2xl bg-white overflow-hidden transition-all duration-500" className="relative block rounded-2xl bg-white overflow-hidden transition-all duration-500"
style={{ style={{
@@ -86,7 +87,7 @@ export function InkGlowCard({
onMouseLeave={() => setIsHovered(false)} onMouseLeave={() => setIsHovered(false)}
> >
{content} {content}
</a> </StaticLink>
) : ( ) : (
<div <div
className="relative rounded-2xl bg-white overflow-hidden transition-all duration-500" className="relative rounded-2xl bg-white overflow-hidden transition-all duration-500"
+1
View File
@@ -1,6 +1,7 @@
export const COMPANY_INFO = { export const COMPANY_INFO = {
name: '四川睿新致远科技有限公司', name: '四川睿新致远科技有限公司',
shortName: '睿新致遠', shortName: '睿新致遠',
displayName: '睿新致远',
slogan: '智连未来,成长伙伴', slogan: '智连未来,成长伙伴',
description: '以智慧连接数字趋势,以伙伴身份陪您成长——您的数字化转型同行者', description: '以智慧连接数字趋势,以伙伴身份陪您成长——您的数字化转型同行者',
founded: '2026', founded: '2026',
+2 -2
View File
@@ -12,7 +12,7 @@ export interface NewsItem {
export const NEWS: NewsItem[] = [ export const NEWS: NewsItem[] = [
{ {
id: '1', id: 'company-founded',
title: '四川睿新致远科技有限公司正式成立', title: '四川睿新致远科技有限公司正式成立',
excerpt: '2026年1月15日,四川睿新致远科技有限公司在成都龙泉驿区正式成立,标志着公司在科技创新领域迈出了坚实的第一步。', excerpt: '2026年1月15日,四川睿新致远科技有限公司在成都龙泉驿区正式成立,标志着公司在科技创新领域迈出了坚实的第一步。',
date: '2026-01-15', date: '2026-01-15',
@@ -27,7 +27,7 @@ export const NEWS: NewsItem[] = [
公司正积极拓展业务合作,业务范围涵盖软件开发、云服务、数据分析、信息安全等多个领域。`, 公司正积极拓展业务合作,业务范围涵盖软件开发、云服务、数据分析、信息安全等多个领域。`,
}, },
{ {
id: '2', id: 'digital-transformation-solution',
title: '公司推出企业数字化转型解决方案', title: '公司推出企业数字化转型解决方案',
excerpt: '针对中小企业数字化转型需求,公司推出一站式数字化转型解决方案,帮助企业快速实现数字化升级。', excerpt: '针对中小企业数字化转型需求,公司推出一站式数字化转型解决方案,帮助企业快速实现数字化升级。',
date: '2026-02-20', date: '2026-02-20',