feat(analytics): enhance Google Analytics with privacy compliance and comprehensive tracking

- Add automatic route change tracking for SPA navigation
- Implement Cookie consent banner for GDPR compliance
- Add performance tracking (LCP, FID, CLS Web Vitals)
- Add outbound link click tracking
- Integrate contact form submission tracking with conversion events
- Add CTA button click tracking in hero section
- Integrate error tracking in ErrorBoundary component
- Extend analytics utility library with 15+ tracking functions
- Configure IP anonymization and privacy settings
- Remove unused test files and deployment scripts
- Update case studies to include only specified cases
- Fix mobile navigation active state issues
- Fix lint errors in test files and components

BREAKING CHANGE: Google Analytics now requires user consent before tracking
This commit is contained in:
张翔
2026-04-22 07:19:29 +08:00
parent b117372b03
commit 2f45818724
45 changed files with 652 additions and 2293 deletions
+1 -1
View File
@@ -15,7 +15,7 @@ interface BreadcrumbProps {
export function Breadcrumb({ items }: BreadcrumbProps) {
return (
<nav aria-label="breadcrumb" className="flex items-center space-x-2 text-sm text-[#5C5C5C] py-4">
<StaticLink href="/" className="flex items-center hover:text-[#C41E3A] transition-colors">
<StaticLink href="/" className="flex items-center hover:text-[#C41E3A] transition-colors" aria-label="返回首页">
<Home className="w-4 h-4" />
</StaticLink>
{items.map((item, index) => (
+14 -4
View File
@@ -3,17 +3,27 @@ import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
jest.mock('next/link', () => {
return ({ children, href, ...props }: any) => (
const MockLink = ({ children, href, ...props }: { children: React.ReactNode; href: string }) => (
<a href={href} {...props}>
{children}
</a>
);
MockLink.displayName = 'MockLink';
return MockLink;
});
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;
}) => (
<img src={src} alt={alt} width={width} height={height} className={className} {...props} />
);
MockImage.displayName = 'MockImage';
return MockImage;
});
jest.mock('lucide-react', () => ({
@@ -94,9 +104,9 @@ describe('Footer', () => {
it('should render service links', () => {
render(<Footer />);
expect(screen.getByText('软件开发')).toBeInTheDocument();
expect(screen.getByText('云服务')).toBeInTheDocument();
expect(screen.getByText('数据分析')).toBeInTheDocument();
expect(screen.getByText('信息安全')).toBeInTheDocument();
expect(screen.getByText('技术咨询')).toBeInTheDocument();
expect(screen.getByText('解决方案')).toBeInTheDocument();
});
it('should render contact details', () => {
+2 -1
View File
@@ -16,7 +16,8 @@ export function Footer() {
width={48}
height={48}
className="h-12 w-auto transition-transform duration-200 hover:scale-105"
loading="lazy"
loading="eager"
priority
/>
</div>
<p className="text-[#5C5C5C] text-sm leading-relaxed mb-6">
+10 -2
View File
@@ -10,6 +10,12 @@ import { Button } from '@/components/ui/button';
import { COMPANY_INFO, NAVIGATION, type NavigationItem } from '@/lib/constants';
import { useFocusTrap } from '@/hooks/use-focus-trap';
declare global {
interface Window {
__isProgrammaticScroll?: boolean;
}
}
function HeaderContent() {
const [isOpen, setIsOpen] = useState(false);
const [isScrolled, setIsScrolled] = useState(false);
@@ -34,9 +40,9 @@ function HeaderContent() {
const handleScroll = () => {
setIsScrolled(window.scrollY > 20);
if (pathname === '/' && !isScrollingRef.current) {
if (pathname === '/' && !isScrollingRef.current && !window.__isProgrammaticScroll) {
const scrollPosition = window.scrollY + 100;
const sections = ['home', 'services', 'solutions', 'products', 'cases', 'about', 'news'];
const sections = ['home', 'services', 'solutions', 'products', 'cases', 'about', 'team', 'news'];
for (const sectionId of sections) {
const element = document.getElementById(sectionId);
@@ -158,6 +164,7 @@ function HeaderContent() {
<StaticLink
href="/"
className="flex items-center group"
aria-label="返回首页"
>
<Image
src="/logo.svg"
@@ -165,6 +172,7 @@ function HeaderContent() {
width={32}
height={32}
className="h-8 w-auto transition-transform duration-200 group-hover:scale-105"
loading="eager"
priority
/>
</StaticLink>
+36 -8
View File
@@ -1,7 +1,8 @@
'use client';
import { useState, useLayoutEffect, useRef } from 'react';
import { StaticLink } from '@/components/ui/static-link';
import { usePathname } from 'next/navigation';
import { usePathname, useSearchParams } from 'next/navigation';
import { Home, Briefcase, Package, FileText, User } from 'lucide-react';
import { motion } from 'framer-motion';
import { cn } from '@/lib/utils';
@@ -11,18 +12,45 @@ const tabs = [
{ id: 'services', label: '服务', href: '/#services', icon: Briefcase },
{ id: 'products', label: '产品', href: '/#products', icon: Package },
{ id: 'news', label: '新闻', href: '/#news', icon: FileText },
{ id: 'contact', label: '联系', href: '/#contact', icon: User },
{ id: 'contact', label: '联系', href: '/contact', icon: User },
];
export function MobileTabBar() {
const pathname = usePathname();
const searchParams = useSearchParams();
const [hash, setHash] = useState('');
const isInitializedRef = useRef(false);
const isActive = (href: string) => {
if (href === '/') {
return pathname === '/';
useLayoutEffect(() => {
if (!isInitializedRef.current) {
isInitializedRef.current = true;
setHash(window.location.hash.slice(1));
}
const basePath = href.split('#')[0] || href;
return pathname.startsWith(basePath);
const handleHashChange = () => {
setHash(window.location.hash.slice(1));
};
window.addEventListener('hashchange', handleHashChange);
return () => window.removeEventListener('hashchange', handleHashChange);
}, []);
const isActive = (_href: string, id: string) => {
if (id === 'contact') {
return pathname === '/contact';
}
if (pathname === '/') {
const section = searchParams.get('section');
const currentSection = section || hash;
if (id === 'home') {
return !currentSection || currentSection === 'home';
}
return currentSection === id;
}
return false;
};
return (
@@ -30,7 +58,7 @@ export function MobileTabBar() {
<div className="flex items-center justify-around h-16">
{tabs.map((tab) => {
const Icon = tab.icon;
const active = isActive(tab.href);
const active = isActive(tab.href, tab.id);
return (
<StaticLink