From 95f246fa36c1e49c684d425b96e04272d5f13582 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E7=BF=94?= Date: Wed, 22 Apr 2026 22:09:38 +0800 Subject: [PATCH] =?UTF-8?q?feat(analytics):=20=E5=A2=9E=E5=BC=BA=20Google?= =?UTF-8?q?=20Analytics=20=E9=9A=90=E7=A7=81=E5=90=88=E8=A7=84=E4=B8=8E?= =?UTF-8?q?=E8=BF=BD=E8=B8=AA=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复 allow_google_signals 配置为 false,禁用跨设备追踪 - 升级 Cookie 同意组件,支持三级偏好控制(必要/分析/营销) - 新增滚动深度追踪组件,追踪 25%/50%/75%/100% 里程碑 - 更新隐私政策,新增 Cookie 和网站分析工具章节 - 新增细化同意管理函数,支持 PIPL 合规 --- src/app/layout.tsx | 2 + src/app/privacy/page.tsx | 84 +++++- src/components/analytics/CookieConsent.tsx | 263 ++++++++++++++---- src/components/analytics/GoogleAnalytics.tsx | 2 +- .../analytics/ScrollDepthTracker.tsx | 42 +++ src/lib/analytics.ts | 48 ++++ 6 files changed, 387 insertions(+), 54 deletions(-) create mode 100644 src/components/analytics/ScrollDepthTracker.tsx diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 8aaf629..2d1548e 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -8,6 +8,7 @@ import { GoogleAnalytics } from "@/components/analytics/GoogleAnalytics"; import { CookieConsent } from "@/components/analytics/CookieConsent"; import { PerformanceTracker } from "@/components/analytics/PerformanceTracker"; import { OutboundLinkTracker } from "@/components/analytics/OutboundLinkTracker"; +import { ScrollDepthTracker } from "@/components/analytics/ScrollDepthTracker"; import { OrganizationSchema, WebsiteSchema } from "@/components/seo/structured-data"; import { MobileTabBar } from "@/components/layout/mobile-tab-bar"; import { ErrorBoundary } from "@/components/ui/error-boundary"; @@ -141,6 +142,7 @@ export default function RootLayout({ + {children} diff --git a/src/app/privacy/page.tsx b/src/app/privacy/page.tsx index 0c4fa01..e74f829 100644 --- a/src/app/privacy/page.tsx +++ b/src/app/privacy/page.tsx @@ -138,13 +138,95 @@ export default function PrivacyPolicyPage() {
-

七、如何联系我们

+

七、Cookie 和网站分析工具

+ +

7.1 Cookie 使用说明

+

+ 我们使用 Cookie 和类似技术来提供、保护和改进我们的服务。Cookie 是存储在您设备上的小型文本文件,帮助我们识别您的设备、记住您的偏好设置。 +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Cookie 类型用途持续时间是否必需
必要 Cookie网站基本功能运行会话期间
分析 Cookie了解网站使用情况,改进服务14个月
营销 Cookie个性化广告(当前未使用)-
+
+ +

7.2 Google Analytics 使用说明

+

+ 我们使用 Google Analytics 4(由 Google LLC 提供)分析网站使用情况,帮助我们了解访客如何使用网站,从而改进用户体验。 +

+

+ 收集的数据包括: +

+
    +
  • 访问的页面和停留时间
  • +
  • 设备类型、浏览器类型
  • +
  • 地理位置(国家/城市级别,IP 地址已匿名化)
  • +
  • 访问来源(直接访问、搜索引擎、外部链接)
  • +
+

+ 我们已采取的保护措施: +

+
    +
  • IP 地址匿名化
  • +
  • 数据保留期限设为 14 个月
  • +
  • 禁用广告个性化功能
  • +
  • 禁用 Google 信号(不进行跨设备追踪)
  • +
  • 不与 Google 其他服务共享数据用于广告目的
  • +
+ +

7.3 您的选择

+
    +
  • 您可以在首次访问时选择接受或拒绝分析 Cookie
  • +
  • 您可以点击页面右下角的“Cookie 设置”按钮随时更改偏好
  • +
  • 您可以通过浏览器设置删除或阻止 Cookie(可能影响网站功能)
  • +
+ +

7.4 数据删除请求

+

+ 如您希望删除我们持有的您的个人数据,或撤回您的同意,请通过以下方式联系我们: +

+
    +
  • 隐私邮箱:privacy@novalon.cn
  • +
  • 联系地址:中国四川省成都市龙泉驿区幸福路12号
  • +
+
+ +
+

八、如何联系我们

如果您对本隐私政策有任何疑问、意见或建议,或需要行使您的权利,请通过以下方式与我们联系:

  • 公司名称:四川睿新致远科技有限公司
  • 联系邮箱:contact@novalon.cn
  • +
  • 隐私邮箱:privacy@novalon.cn
  • 联系地址:中国四川省成都市龙泉驿区幸福路12号
diff --git a/src/components/analytics/CookieConsent.tsx b/src/components/analytics/CookieConsent.tsx index a8e399a..7e06037 100644 --- a/src/components/analytics/CookieConsent.tsx +++ b/src/components/analytics/CookieConsent.tsx @@ -1,48 +1,91 @@ 'use client'; -import { useState, useEffect } from 'react'; -import { updateConsent, trackButtonClick } from '@/lib/analytics'; +import { useState, useEffect, useCallback } from 'react'; +import { + updateConsentDetailed, + trackButtonClick, + CookiePreferences, + getStoredPreferences, + storePreferences, + getDefaultPreferences, +} from '@/lib/analytics'; import { motion, AnimatePresence } from 'framer-motion'; -const CONSENT_KEY = 'ga_consent'; +const LEGACY_CONSENT_KEY = 'ga_consent'; export function CookieConsent() { const [showConsent, setShowConsent] = useState(false); + const [showSettings, setShowSettings] = useState(false); const [isAnimating, setIsAnimating] = useState(false); + const [preferences, setPreferences] = useState(getDefaultPreferences()); useEffect(() => { - const consent = localStorage.getItem(CONSENT_KEY); - if (!consent) { - const timer = setTimeout(() => { - setShowConsent(true); - }, 2000); - return () => clearTimeout(timer); - } else if (consent === 'granted') { - updateConsent(true); + const stored = getStoredPreferences(); + if (stored) { + updateConsentDetailed(stored); + } else { + const legacyConsent = localStorage.getItem(LEGACY_CONSENT_KEY); + if (legacyConsent) { + const migratedPrefs: CookiePreferences = { + necessary: true, + analytics: legacyConsent === 'granted', + marketing: false, + timestamp: Date.now(), + }; + storePreferences(migratedPrefs); + updateConsentDetailed(migratedPrefs); + localStorage.removeItem(LEGACY_CONSENT_KEY); + } else { + const timer = setTimeout(() => { + setShowConsent(true); + }, 2000); + return () => clearTimeout(timer); + } } return undefined; }, []); - const handleAccept = () => { + const handleSavePreferences = useCallback((prefs: CookiePreferences) => { setIsAnimating(true); - localStorage.setItem(CONSENT_KEY, 'granted'); - updateConsent(true); - trackButtonClick('accept_cookies', 'consent_banner'); + const finalPrefs = { ...prefs, timestamp: Date.now() }; + storePreferences(finalPrefs); + updateConsentDetailed(finalPrefs); + trackButtonClick('save_cookie_preferences', 'consent_banner'); setTimeout(() => { setShowConsent(false); + setShowSettings(false); setIsAnimating(false); }, 300); + }, []); + + const handleAcceptAll = () => { + const allAccepted: CookiePreferences = { + necessary: true, + analytics: true, + marketing: false, + timestamp: Date.now(), + }; + handleSavePreferences(allAccepted); + trackButtonClick('accept_all_cookies', 'consent_banner'); }; - const handleDecline = () => { - setIsAnimating(true); - localStorage.setItem(CONSENT_KEY, 'denied'); - updateConsent(false); - trackButtonClick('decline_cookies', 'consent_banner'); - setTimeout(() => { - setShowConsent(false); - setIsAnimating(false); - }, 300); + const handleRejectAll = () => { + const allRejected: CookiePreferences = { + necessary: true, + analytics: false, + marketing: false, + timestamp: Date.now(), + }; + handleSavePreferences(allRejected); + trackButtonClick('reject_all_cookies', 'consent_banner'); + }; + + const handleTogglePreference = (key: 'analytics' | 'marketing') => { + setPreferences((prev) => ({ ...prev, [key]: !prev[key] })); + }; + + const handleSaveCustom = () => { + handleSavePreferences(preferences); }; return ( @@ -56,40 +99,156 @@ export function CookieConsent() { className="fixed bottom-16 md:bottom-0 left-0 right-0 z-[9998] bg-white border-t border-gray-200 shadow-lg" >
-
-
-

- 我们使用 Cookie 和类似技术来改善您的体验、分析网站流量并提供个性化内容。 - 继续使用即表示您同意我们的{' '} - +

+
+ + + +
-
- - + ) : ( +
+
+

Cookie 偏好设置

+ +
+ +
+
+ +
+
+ 必要 Cookie + 始终启用 +
+

+ 网站正常运行所必需,无法禁用 +

+
+
+ +
+ handleTogglePreference('analytics')} + className="mt-1 h-4 w-4 rounded border-gray-300 text-[#C41E3A] focus:ring-[#C41E3A] cursor-pointer" + aria-label="分析 Cookie" + /> +
+ 分析 Cookie +

+ 帮助我们了解访客如何使用网站,改进用户体验 +

+
+
+ +
+ handleTogglePreference('marketing')} + className="mt-1 h-4 w-4 rounded border-gray-300 text-[#C41E3A] focus:ring-[#C41E3A] cursor-pointer" + aria-label="营销 Cookie" + /> +
+ 营销 Cookie +

+ 用于个性化广告(当前未使用) +

+
+
+
+ +
+ + +
-
+ )}
)} ); } + +export function CookieSettingsButton() { + const [isVisible] = useState(() => { + if (typeof window === 'undefined') {return false;} + return !!getStoredPreferences(); + }); + + if (!isVisible) {return null;} + + return ( + + ); +} diff --git a/src/components/analytics/GoogleAnalytics.tsx b/src/components/analytics/GoogleAnalytics.tsx index 0f83e9a..4fed77b 100644 --- a/src/components/analytics/GoogleAnalytics.tsx +++ b/src/components/analytics/GoogleAnalytics.tsx @@ -48,7 +48,7 @@ function GoogleAnalyticsContent() { gtag('config', '${GA_MEASUREMENT_ID}', { send_page_view: false, anonymize_ip: true, - allow_google_signals: true, + allow_google_signals: false, allow_ad_personalization_signals: false, cookie_flags: 'SameSite=None;Secure' }); diff --git a/src/components/analytics/ScrollDepthTracker.tsx b/src/components/analytics/ScrollDepthTracker.tsx new file mode 100644 index 0000000..a7b879c --- /dev/null +++ b/src/components/analytics/ScrollDepthTracker.tsx @@ -0,0 +1,42 @@ +'use client'; + +import { useEffect, useRef, useCallback } from 'react'; +import { usePathname } from 'next/navigation'; +import { trackScrollDepth } from '@/lib/analytics'; + +const MILESTONES = [25, 50, 75, 100] as const; + +export function ScrollDepthTracker() { + const trackedRef = useRef>(new Set()); + const pathname = usePathname(); + + const handleScroll = useCallback(() => { + const scrollTop = window.scrollY; + const docHeight = document.documentElement.scrollHeight - window.innerHeight; + + if (docHeight <= 0) { + return; + } + + const scrollPercent = Math.round((scrollTop / docHeight) * 100); + + MILESTONES.forEach((milestone) => { + if (scrollPercent >= milestone && !trackedRef.current.has(milestone)) { + trackedRef.current.add(milestone); + trackScrollDepth(milestone); + } + }); + }, []); + + useEffect(() => { + trackedRef.current = new Set(); + + window.addEventListener('scroll', handleScroll, { passive: true }); + + return () => { + window.removeEventListener('scroll', handleScroll); + }; + }, [pathname, handleScroll]); + + return null; +} diff --git a/src/lib/analytics.ts b/src/lib/analytics.ts index b36a7e7..2709eaa 100644 --- a/src/lib/analytics.ts +++ b/src/lib/analytics.ts @@ -149,6 +149,13 @@ export const trackProductView = (productId: string, productName: string) => { } }; +export interface CookiePreferences { + necessary: boolean; + analytics: boolean; + marketing: boolean; + timestamp: number; +} + export const updateConsent = (granted: boolean) => { if (typeof window !== 'undefined' && window.gtag) { window.gtag('consent', 'update', { @@ -157,3 +164,44 @@ export const updateConsent = (granted: boolean) => { }); } }; + +export const updateConsentDetailed = (preferences: CookiePreferences) => { + if (typeof window !== 'undefined' && window.gtag) { + window.gtag('consent', 'update', { + analytics_storage: preferences.analytics ? 'granted' : 'denied', + ad_storage: preferences.marketing ? 'granted' : 'denied', + functionality_storage: 'granted', + personalization_storage: preferences.marketing ? 'granted' : 'denied', + security_storage: 'granted', + }); + } +}; + +export const getStoredPreferences = (): CookiePreferences | null => { + if (typeof window === 'undefined') { + return null; + } + + try { + const stored = localStorage.getItem('cookie_preferences'); + if (stored) { + return JSON.parse(stored) as CookiePreferences; + } + } catch { + return null; + } + return null; +}; + +export const storePreferences = (preferences: CookiePreferences) => { + if (typeof window !== 'undefined') { + localStorage.setItem('cookie_preferences', JSON.stringify(preferences)); + } +}; + +export const getDefaultPreferences = (): CookiePreferences => ({ + necessary: true, + analytics: false, + marketing: false, + timestamp: Date.now(), +}); -- 2.52.0