refactor(project): 全面清理项目代码并重命名项目 #18

Merged
zhangxiang merged 9 commits from refactor/google-analytics into dev 2026-04-29 21:48:39 +08:00
8 changed files with 157 additions and 115 deletions
Showing only changes of commit fb888a673f - Show all commits
+1 -1
View File
@@ -1 +1 @@
NEXT_PUBLIC_GA_ID=G-XXXXXXXXXX
NEXT_PUBLIC_GA_MEASUREMENT_ID=G-XXXXXXXXXX
+15 -11
View File
@@ -51,6 +51,20 @@ fi
DIST_SIZE=$(du -sh "$DIST_DIR" | cut -f1)
echo "✅ dist 目录大小: $DIST_SIZE"
echo ""
echo "📋 步骤1.1: 验证构建产物..."
if [ -f "$DIST_DIR/index.html" ]; then
if grep -q "googletagmanager.com" "$DIST_DIR/index.html"; then
GA_ID=$(grep -oP 'id=G-[A-Z0-9]+' "$DIST_DIR/index.html" | head -1 | sed 's/id=//')
echo "✅ GA 脚本已嵌入: $GA_ID"
else
echo "⚠️ 未检测到 GA 脚本,请检查 .env.production 中的 NEXT_PUBLIC_GA_MEASUREMENT_ID"
fi
else
echo "❌ index.html 不存在,构建可能失败"
exit 1
fi
echo ""
echo "📋 步骤2: 验证SSH连接..."
if ! ssh -o ConnectTimeout=5 "$SERVER_USER@$SERVER_IP" exit; then
@@ -79,17 +93,7 @@ echo ""
echo "📋 步骤4: 上传 dist 目录..."
ssh "$SERVER_USER@$SERVER_IP" "mkdir -p '$DEPLOY_ROOT/$STATIC_DIR'"
# 检查dist目录结构并正确处理Next.js静态文件
if [ -d "$DIST_DIR/server/app" ]; then
echo "🔧 检测到Next.js静态导出结构,正在处理HTML文件..."
# 复制HTML文件到根目录
rsync -avz --delete "$DIST_DIR/server/app/" "$SERVER_USER@$SERVER_IP:$DEPLOY_ROOT/$STATIC_DIR/"
# 复制其他静态资源
rsync -avz --delete --exclude='server/app' "$DIST_DIR/" "$SERVER_USER@$SERVER_IP:$DEPLOY_ROOT/$STATIC_DIR/"
else
# 标准静态文件结构
rsync -avz --delete "$DIST_DIR/" "$SERVER_USER@$SERVER_IP:$DEPLOY_ROOT/$STATIC_DIR/"
fi
rsync -avz --delete "$DIST_DIR/" "$SERVER_USER@$SERVER_IP:$DEPLOY_ROOT/$STATIC_DIR/"
echo "✅ dist 目录已上传"
+1 -1
View File
@@ -102,7 +102,7 @@ if [ ! -f .env ]; then
echo "📝 创建.env文件..."
cp .env.example .env
echo "⚠️ 请编辑.env文件,填入正确的环境变量"
echo "⚠️ 可选配置: NEXT_PUBLIC_GA_ID"
echo "⚠️ 可选配置: NEXT_PUBLIC_GA_MEASUREMENT_ID"
fi
echo "🐳 启动Docker容器..."
+42
View File
@@ -15,6 +15,8 @@ import { ErrorBoundary } from "@/components/ui/error-boundary";
import { ScrollProgress } from "@/components/ui/scroll-progress";
import { BackToTop } from "@/components/ui/back-to-top";
const GA_MEASUREMENT_ID = process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID || '';
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
@@ -127,6 +129,46 @@ export default function RootLayout({
<link rel="apple-touch-icon" href="/favicon.svg" />
<OrganizationSchema />
<WebsiteSchema />
{GA_MEASUREMENT_ID && (
<>
<script
dangerouslySetInnerHTML={{
__html: `
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('consent', 'default', {
'analytics_storage': 'denied',
'ad_storage': 'denied',
'ad_user_data': 'denied',
'ad_personalization': 'denied',
'functionality_storage': 'granted',
'security_storage': 'granted',
'wait_for_update': 3000
});
gtag('js', new Date());
`,
}}
/>
<script
async
src={`https://www.googletagmanager.com/gtag/js?id=${GA_MEASUREMENT_ID}`}
/>
<script
dangerouslySetInnerHTML={{
__html: `
gtag('config', '${GA_MEASUREMENT_ID}', {
send_page_view: false,
anonymize_ip: true,
cookie_domain: 'auto',
cookie_flags: 'SameSite=None;Secure'
});
`,
}}
/>
</>
)}
</head>
<body
className={`${geistSans.variable} ${geistMono.variable} ${notoSansSC.variable} ${maShanZheng.variable} ${aoyagiReisho.variable} font-sans antialiased`}
+7 -1
View File
@@ -4,6 +4,7 @@ import { useState, useEffect, useCallback } from 'react';
import {
updateConsentDetailed,
trackButtonClick,
trackPageView,
CookiePreferences,
getStoredPreferences,
storePreferences,
@@ -38,7 +39,7 @@ export function CookieConsent() {
} else {
const timer = setTimeout(() => {
setShowConsent(true);
}, 2000);
}, 500);
return () => clearTimeout(timer);
}
}
@@ -51,6 +52,11 @@ export function CookieConsent() {
storePreferences(finalPrefs);
updateConsentDetailed(finalPrefs);
trackButtonClick('save_cookie_preferences', 'consent_banner');
if (prefs.analytics) {
setTimeout(() => {
trackPageView(document.title, window.location.pathname);
}, 100);
}
setTimeout(() => {
setShowConsent(false);
setShowSettings(false);
+10 -42
View File
@@ -1,8 +1,8 @@
'use client';
import Script from 'next/script';
import { usePathname, useSearchParams } from 'next/navigation';
import { useEffect, Suspense } from 'react';
import { hasAnalyticsConsent } from '@/lib/analytics';
const GA_MEASUREMENT_ID = process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID || '';
@@ -12,50 +12,18 @@ function GoogleAnalyticsContent() {
useEffect(() => {
if (!GA_MEASUREMENT_ID || typeof window === 'undefined') {return;}
if (!hasAnalyticsConsent()) {return;}
const url = pathname + (searchParams.toString() ? `?${searchParams.toString()}` : '');
if (window.gtag) {
window.gtag('config', GA_MEASUREMENT_ID, {
page_path: url,
page_title: document.title,
page_location: window.location.origin + url,
});
}
window.gtag('event', 'page_view', {
page_title: document.title,
page_location: window.location.origin + url,
page_path: url,
});
}, [pathname, searchParams]);
if (!GA_MEASUREMENT_ID) {return null;}
return (
<>
<Script
src={`https://www.googletagmanager.com/gtag/js?id=${GA_MEASUREMENT_ID}`}
strategy="afterInteractive"
/>
<Script id="google-analytics" strategy="afterInteractive">
{`
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
// 默认禁用存储,等待用户同意
gtag('consent', 'default', {
'analytics_storage': 'denied',
'ad_storage': 'denied',
'wait_for_update': 500
});
gtag('config', '${GA_MEASUREMENT_ID}', {
send_page_view: false,
anonymize_ip: true,
allow_google_signals: false,
allow_ad_personalization_signals: false,
cookie_flags: 'SameSite=None;Secure'
});
`}
</Script>
</>
);
return null;
}
export function GoogleAnalytics() {
+53 -47
View File
@@ -6,63 +6,69 @@ import { trackPerformance } from '@/lib/analytics';
export function PerformanceTracker() {
useEffect(() => {
if (typeof window === 'undefined') {return;}
if (!('PerformanceObserver' in window)) {return;}
const reportWebVitals = (): (() => void) | undefined => {
if ('PerformanceObserver' in window) {
const lcpObserver = new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1];
if (lastEntry) {
trackPerformance('LCP', lastEntry.startTime);
}
});
const observers: PerformanceObserver[] = [];
const fidObserver = new PerformanceObserver((list) => {
const entries = list.getEntries();
const firstEntry = entries[0];
if (firstEntry && 'processingStart' in firstEntry) {
const fidEntry = firstEntry as PerformanceEventTiming;
trackPerformance('FID', fidEntry.processingStart - fidEntry.startTime);
}
});
const clsObserver = new PerformanceObserver((list) => {
let clsValue = 0;
for (const entry of list.getEntries()) {
if ('value' in entry && !(entry as LayoutShift).hadRecentInput) {
clsValue += (entry as LayoutShift).value;
}
}
if (clsValue > 0) {
trackPerformance('CLS', clsValue * 1000);
}
});
try {
lcpObserver.observe({ type: 'largest-contentful-paint', buffered: true });
fidObserver.observe({ type: 'first-input', buffered: true });
clsObserver.observe({ type: 'layout-shift', buffered: true });
} catch {
// Observer not supported
}
return () => {
lcpObserver.disconnect();
fidObserver.disconnect();
clsObserver.disconnect();
};
const lcpObserver = new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1];
if (lastEntry) {
trackPerformance('LCP', lastEntry.startTime);
}
return undefined;
};
});
const cleanup = reportWebVitals();
return cleanup;
const inpObserver = new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1];
if (lastEntry && 'duration' in lastEntry) {
trackPerformance('INP', (lastEntry as PerformanceEventTiming).duration);
}
});
const clsObserver = new PerformanceObserver((list) => {
let clsValue = 0;
for (const entry of list.getEntries()) {
if ('value' in entry && !(entry as LayoutShift).hadRecentInput) {
clsValue += (entry as LayoutShift).value;
}
}
if (clsValue > 0) {
trackPerformance('CLS', clsValue * 1000);
}
});
try {
lcpObserver.observe({ type: 'largest-contentful-paint', buffered: true });
observers.push(lcpObserver);
} catch {
// LCP observer not supported
}
try {
inpObserver.observe({ type: 'event', buffered: true });
observers.push(inpObserver);
} catch {
// INP observer not supported
}
try {
clsObserver.observe({ type: 'layout-shift', buffered: true });
observers.push(clsObserver);
} catch {
// CLS observer not supported
}
return () => {
observers.forEach((o) => o.disconnect());
};
}, []);
return null;
}
interface PerformanceEventTiming extends PerformanceEntry {
duration: number;
processingStart: number;
startTime: number;
}
+28 -12
View File
@@ -10,14 +10,6 @@ declare global {
}
}
export const pageview = (url: string) => {
if (typeof window !== 'undefined' && window.gtag && GA_MEASUREMENT_ID) {
window.gtag('config', GA_MEASUREMENT_ID, {
page_path: url,
});
}
};
export const event = (action: string, category: string, label?: string, value?: number) => {
if (typeof window !== 'undefined' && window.gtag && GA_MEASUREMENT_ID) {
window.gtag('event', action, {
@@ -28,6 +20,30 @@ export const event = (action: string, category: string, label?: string, value?:
}
};
export const hasAnalyticsConsent = (): boolean => {
if (typeof window === 'undefined') {return false;}
try {
const stored = localStorage.getItem('cookie_preferences');
if (stored) {
const prefs = JSON.parse(stored) as CookiePreferences;
return prefs.analytics === true;
}
} catch {
return false;
}
return false;
};
export const trackPageView = (pageTitle: string, pagePath: string) => {
if (typeof window !== 'undefined' && window.gtag && GA_MEASUREMENT_ID) {
window.gtag('event', 'page_view', {
page_title: pageTitle,
page_location: window.location.origin + pagePath,
page_path: pagePath,
});
}
};
export const trackContactForm = (formData: Record<string, string>) => {
event('generate_lead', 'engagement', 'contact_form_submission');
@@ -44,10 +60,6 @@ export const trackButtonClick = (buttonName: string, location: string) => {
event('click', 'button', `${location}_${buttonName}`);
};
export const trackPageView = (pageTitle: string, _pagePath: string) => {
event('page_view', 'navigation', pageTitle);
};
export const trackConversion = (conversionName: string, value?: number) => {
if (typeof window !== 'undefined' && window.gtag && GA_MEASUREMENT_ID) {
window.gtag('event', 'conversion', {
@@ -161,6 +173,8 @@ export const updateConsent = (granted: boolean) => {
window.gtag('consent', 'update', {
analytics_storage: granted ? 'granted' : 'denied',
ad_storage: 'denied',
ad_user_data: 'denied',
ad_personalization: 'denied',
});
}
};
@@ -170,6 +184,8 @@ export const updateConsentDetailed = (preferences: CookiePreferences) => {
window.gtag('consent', 'update', {
analytics_storage: preferences.analytics ? 'granted' : 'denied',
ad_storage: preferences.marketing ? 'granted' : 'denied',
ad_user_data: preferences.marketing ? 'granted' : 'denied',
ad_personalization: preferences.marketing ? 'granted' : 'denied',
functionality_storage: 'granted',
personalization_storage: preferences.marketing ? 'granted' : 'denied',
security_storage: 'granted',