refactor(project): 全面清理项目代码并重命名项目 #19
+1
-1
@@ -1 +1 @@
|
||||
NEXT_PUBLIC_GA_ID=G-XXXXXXXXXX
|
||||
NEXT_PUBLIC_GA_MEASUREMENT_ID=G-XXXXXXXXXX
|
||||
|
||||
+15
-11
@@ -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 目录已上传"
|
||||
|
||||
|
||||
@@ -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容器..."
|
||||
|
||||
@@ -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`}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user