feat: 集成 Cloudflare Web Analytics 流量统计与图表展示
This commit is contained in:
1 parent
24b35694ed
commit
02dc3c155f
7 files changed
+411
-7
No files matched your search
@@ -12,7 +12,18 @@ export interface TrafficScope {
|
|||||||
error?: string;
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AudienceAnalytics {
|
||||||
|
scopeLabel: string;
|
||||||
|
days: number;
|
||||||
|
pageViews: number | null;
|
||||||
|
uniqueVisitors: number | null;
|
||||||
|
last7Days: { pageViews: number; uniqueVisitors: number } | null;
|
||||||
|
daily: { date: string; pageViews: number | null; uniqueVisitors: number | null }[];
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface AnalyticsData {
|
export interface AnalyticsData {
|
||||||
|
audience?: AudienceAnalytics;
|
||||||
scopes: TrafficScope[];
|
scopes: TrafficScope[];
|
||||||
d1: { sum: { rowsRead: number; rowsWritten: number } }[];
|
d1: { sum: { rowsRead: number; rowsWritten: number } }[];
|
||||||
r2: { max: { payloadSize: number } }[];
|
r2: { max: { payloadSize: number } }[];
|
||||||
|
|||||||
@@ -7,6 +7,8 @@
|
|||||||
let loading = $state(true);
|
let loading = $state(true);
|
||||||
let error = $state('');
|
let error = $state('');
|
||||||
let chartCanvas = $state<HTMLCanvasElement>();
|
let chartCanvas = $state<HTMLCanvasElement>();
|
||||||
|
let audienceCanvas = $state<HTMLCanvasElement>();
|
||||||
|
let audience = $derived(data?.audience);
|
||||||
let scopes = $derived(
|
let scopes = $derived(
|
||||||
(data?.scopes || []).map((scope) => ({ ...scope, summary: summarizeTraffic(scope.rows) }))
|
(data?.scopes || []).map((scope) => ({ ...scope, summary: summarizeTraffic(scope.rows) }))
|
||||||
);
|
);
|
||||||
@@ -170,6 +172,90 @@
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
const canvas = audienceCanvas;
|
||||||
|
const current = audience;
|
||||||
|
if (!canvas || !current || current.error || !current.daily.length) return;
|
||||||
|
let cancelled = false;
|
||||||
|
let chart: import('chart.js').Chart | undefined;
|
||||||
|
const rows = [...current.daily].sort((a, b) => a.date.localeCompare(b.date));
|
||||||
|
const labels = rows.map((row) => row.date);
|
||||||
|
void import('chart.js/auto')
|
||||||
|
.then(({ default: Chart }) => {
|
||||||
|
if (cancelled) return;
|
||||||
|
chart = new Chart(canvas, {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: labels.map((date) => date.substring(5)),
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: '页面浏览量 (PV)',
|
||||||
|
data: rows.map((row) => row.pageViews),
|
||||||
|
borderColor: '#333',
|
||||||
|
backgroundColor: 'rgba(0,0,0,0.05)',
|
||||||
|
tension: 0.3,
|
||||||
|
fill: true,
|
||||||
|
yAxisID: 'y'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '每日独立访客 (UV)',
|
||||||
|
data: rows.map((row) => row.uniqueVisitors),
|
||||||
|
borderColor: '#2196F3',
|
||||||
|
backgroundColor: 'rgba(33, 150, 243, 0.1)',
|
||||||
|
tension: 0.3,
|
||||||
|
fill: true,
|
||||||
|
yAxisID: 'y1'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
interaction: {
|
||||||
|
mode: 'index',
|
||||||
|
intersect: false
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
tooltip: {
|
||||||
|
mode: 'index',
|
||||||
|
intersect: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
ticks: {
|
||||||
|
maxRotation: 45,
|
||||||
|
maxTicksLimit: 15 // 最多显示 15 个刻度,避免太拥挤
|
||||||
|
}
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
type: 'linear',
|
||||||
|
display: true,
|
||||||
|
position: 'left',
|
||||||
|
beginAtZero: true
|
||||||
|
},
|
||||||
|
y1: {
|
||||||
|
type: 'linear',
|
||||||
|
display: true,
|
||||||
|
position: 'right',
|
||||||
|
beginAtZero: true,
|
||||||
|
grid: {
|
||||||
|
drawOnChartArea: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
/* 图表加载失败时仍保留数值指标。 */
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
chart?.destroy();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
function formatBytes(bytes: number) {
|
function formatBytes(bytes: number) {
|
||||||
if (!bytes) return '0 B';
|
if (!bytes) return '0 B';
|
||||||
const index = Math.min(4, Math.floor(Math.log(bytes) / Math.log(1024)));
|
const index = Math.min(4, Math.floor(Math.log(bytes) / Math.log(1024)));
|
||||||
@@ -194,10 +280,6 @@
|
|||||||
<div class="error-state">
|
<div class="error-state">
|
||||||
{error}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
{:else if scopes.length === 0}
|
|
||||||
<div class="empty-state">
|
|
||||||
{data?.errorMsg || '没有获取到有效的流量数据,请检查 Cloudflare 配置。'}
|
|
||||||
</div>
|
|
||||||
{:else}
|
{:else}
|
||||||
<div
|
<div
|
||||||
class="section-title"
|
class="section-title"
|
||||||
@@ -205,6 +287,11 @@
|
|||||||
>
|
>
|
||||||
项目访问流量
|
项目访问流量
|
||||||
</div>
|
</div>
|
||||||
|
{#if scopes.length === 0}
|
||||||
|
<div class="empty-state">
|
||||||
|
{data?.errorMsg || '没有获取到有效的流量数据,请检查 Cloudflare 配置。'}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div class="dashboard-grid">
|
<div class="dashboard-grid">
|
||||||
<div class="chart-container">
|
<div class="chart-container">
|
||||||
@@ -320,6 +407,133 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="section-title">页面浏览与独立访客 · 近 30 天趋势</div>
|
||||||
|
<div class="info-alert">
|
||||||
|
{audience?.scopeLabel || 'PV、UV 数据源待确认:已撤除站内采集,不使用本地数据表。'}
|
||||||
|
</div>
|
||||||
|
{#if audience?.error}<div class="error-state">{audience.error}</div>{/if}
|
||||||
|
<div class="dashboard-grid">
|
||||||
|
<div class="chart-container">
|
||||||
|
<div class="canvas-wrapper">
|
||||||
|
<canvas bind:this={audienceCanvas} aria-label="PV 与每日 UV 趋势图"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="core-metrics">
|
||||||
|
<div class="metric-item">
|
||||||
|
<div class="metric-title">
|
||||||
|
<span class="metric-icon" style="color: #2196F3; background: rgba(33,150,243,0.1)">
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="14"
|
||||||
|
height="14"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
fill="none"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle
|
||||||
|
cx="12"
|
||||||
|
cy="12"
|
||||||
|
r="3"
|
||||||
|
></circle></svg
|
||||||
|
>
|
||||||
|
</span>
|
||||||
|
页面浏览量 (PV)
|
||||||
|
</div>
|
||||||
|
<div class="metric-value" style="color: #2196F3">
|
||||||
|
{audience?.pageViews == null || audience.error ? '—' : formatNumber(audience.pageViews)}
|
||||||
|
</div>
|
||||||
|
<div class="metric-subtitle" title={audience?.scopeLabel}>
|
||||||
|
近 {audience?.days ?? 30} 天累计浏览
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="metric-item">
|
||||||
|
<div class="metric-title">
|
||||||
|
<span class="metric-icon">
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="14"
|
||||||
|
height="14"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
fill="none"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path><circle
|
||||||
|
cx="12"
|
||||||
|
cy="7"
|
||||||
|
r="4"
|
||||||
|
></circle></svg
|
||||||
|
>
|
||||||
|
</span>
|
||||||
|
独立访客 (UV)
|
||||||
|
</div>
|
||||||
|
<div class="metric-value">
|
||||||
|
{audience?.uniqueVisitors == null || audience.error
|
||||||
|
? '—'
|
||||||
|
: formatNumber(audience.uniqueVisitors)}
|
||||||
|
</div>
|
||||||
|
<div class="metric-subtitle" title={audience?.scopeLabel}>近 30 天去重访客</div>
|
||||||
|
</div>
|
||||||
|
<div class="metric-item">
|
||||||
|
<div class="metric-title">
|
||||||
|
<span class="metric-icon" style="color: #2196F3; background: rgba(33,150,243,0.1)">
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="14"
|
||||||
|
height="14"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
fill="none"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle
|
||||||
|
cx="12"
|
||||||
|
cy="12"
|
||||||
|
r="3"
|
||||||
|
></circle></svg
|
||||||
|
>
|
||||||
|
</span>
|
||||||
|
页面浏览量 (PV)
|
||||||
|
</div>
|
||||||
|
<div class="metric-value" style="color: #2196F3">
|
||||||
|
{audience?.last7Days?.pageViews == null || audience.error
|
||||||
|
? '—'
|
||||||
|
: formatNumber(audience.last7Days.pageViews)}
|
||||||
|
</div>
|
||||||
|
<div class="metric-subtitle" title={audience?.scopeLabel}>近 7 天累计浏览</div>
|
||||||
|
</div>
|
||||||
|
<div class="metric-item">
|
||||||
|
<div class="metric-title">
|
||||||
|
<span class="metric-icon">
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="14"
|
||||||
|
height="14"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
fill="none"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path><circle
|
||||||
|
cx="12"
|
||||||
|
cy="7"
|
||||||
|
r="4"
|
||||||
|
></circle></svg
|
||||||
|
>
|
||||||
|
</span>
|
||||||
|
独立访客 (UV)
|
||||||
|
</div>
|
||||||
|
<div class="metric-value">
|
||||||
|
{audience?.last7Days?.uniqueVisitors == null || audience.error
|
||||||
|
? '—'
|
||||||
|
: formatNumber(audience.last7Days.uniqueVisitors)}
|
||||||
|
</div>
|
||||||
|
<div class="metric-subtitle" title={audience?.scopeLabel}>近 7 天去重访客</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="section-title">Cloudflare 资源配额</div>
|
<div class="section-title">Cloudflare 资源配额</div>
|
||||||
{#if data?.errorMsg}<div class="error-state">{data.errorMsg}</div>{/if}
|
{#if data?.errorMsg}<div class="error-state">{data.errorMsg}</div>{/if}
|
||||||
{#if missingEnvVars}
|
{#if missingEnvVars}
|
||||||
|
|||||||
@@ -150,7 +150,8 @@ async function fetchTraffic(
|
|||||||
|
|
||||||
export async function fetchAnalytics(
|
export async function fetchAnalytics(
|
||||||
platform: App.Platform | undefined,
|
platform: App.Platform | undefined,
|
||||||
assetsDomain?: string
|
assetsDomain?: string,
|
||||||
|
webAnalyticsToken?: string
|
||||||
): Promise<AnalyticsData> {
|
): Promise<AnalyticsData> {
|
||||||
const env = platform?.env as AnalyticsEnv | undefined;
|
const env = platform?.env as AnalyticsEnv | undefined;
|
||||||
const token = env?.CF_API_TOKEN;
|
const token = env?.CF_API_TOKEN;
|
||||||
@@ -174,6 +175,22 @@ export async function fetchAnalytics(
|
|||||||
fetchTraffic(token, zoneId, '博客', env?.CF_DOMAIN, now),
|
fetchTraffic(token, zoneId, '博客', env?.CF_DOMAIN, now),
|
||||||
fetchTraffic(token, env?.CF_ASSETS_ZONE_ID || zoneId, 'R2 附件', assetsDomain, now)
|
fetchTraffic(token, env?.CF_ASSETS_ZONE_ID || zoneId, 'R2 附件', assetsDomain, now)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
let audiencePromise: Promise<AnalyticsData['audience']> = Promise.resolve(undefined);
|
||||||
|
if (accountId && webAnalyticsToken) {
|
||||||
|
audiencePromise = fetchAudience(token, accountId, webAnalyticsToken, now);
|
||||||
|
} else if (!webAnalyticsToken) {
|
||||||
|
audiencePromise = Promise.resolve({
|
||||||
|
scopeLabel: 'Web Analytics (未配置 Token)',
|
||||||
|
days: 30,
|
||||||
|
pageViews: null,
|
||||||
|
uniqueVisitors: null,
|
||||||
|
last7Days: null,
|
||||||
|
daily: [],
|
||||||
|
error: '请在上方配置 Web Analytics Token / Site Tag 以拉取数据。'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
let accountQuery = '';
|
let accountQuery = '';
|
||||||
if (accountId) {
|
if (accountId) {
|
||||||
let d1Query = '';
|
let d1Query = '';
|
||||||
@@ -235,5 +252,103 @@ export async function fetchAnalytics(
|
|||||||
result.errorMsg = `账户资源统计不可用:${cause instanceof Error ? cause.message : '查询失败'}`;
|
result.errorMsg = `账户资源统计不可用:${cause instanceof Error ? cause.message : '查询失败'}`;
|
||||||
}
|
}
|
||||||
result.scopes = await trafficPromise;
|
result.scopes = await trafficPromise;
|
||||||
|
result.audience = await audiencePromise;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function fetchAudience(
|
||||||
|
token: string,
|
||||||
|
accountId: string,
|
||||||
|
siteTag: string,
|
||||||
|
now: Date
|
||||||
|
): Promise<AnalyticsData['audience']> {
|
||||||
|
const thirtyDaysAgo = new Date(now.getTime() - 30 * 86400000).toISOString().split('T')[0];
|
||||||
|
try {
|
||||||
|
const data = await graphql<{
|
||||||
|
viewer: {
|
||||||
|
accounts: {
|
||||||
|
rumPagesAdaptiveGroups: {
|
||||||
|
count: number;
|
||||||
|
sum: { visits: number };
|
||||||
|
dimensions: { date: string };
|
||||||
|
}[];
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
}>(
|
||||||
|
token,
|
||||||
|
`
|
||||||
|
query WebAnalytics($account: string, $site: string, $date: string) {
|
||||||
|
viewer {
|
||||||
|
accounts(filter: { accountTag: $account }) {
|
||||||
|
rumPagesAdaptiveGroups(
|
||||||
|
limit: 30,
|
||||||
|
filter: { siteTag: $site, datetime_geq: $date }
|
||||||
|
orderBy: [date_ASC]
|
||||||
|
) {
|
||||||
|
count
|
||||||
|
sum {
|
||||||
|
visits
|
||||||
|
}
|
||||||
|
dimensions {
|
||||||
|
date
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
{
|
||||||
|
account: accountId,
|
||||||
|
site: siteTag,
|
||||||
|
date: `${thirtyDaysAgo}T00:00:00Z`
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const rows = data.viewer.accounts[0]?.rumPagesAdaptiveGroups;
|
||||||
|
if (!rows) throw new Error('未返回数据,请检查 Token、Account ID 和 Site Tag 是否正确并具有权限');
|
||||||
|
|
||||||
|
let totalPageViews = 0;
|
||||||
|
let totalUniqueVisitors = 0;
|
||||||
|
let last7DaysPV = 0;
|
||||||
|
let last7DaysUV = 0;
|
||||||
|
const sevenDaysAgoStr = new Date(now.getTime() - 7 * 86400000).toISOString().split('T')[0];
|
||||||
|
const dailyMap = new Map(rows.map((row) => [row.dimensions.date, row]));
|
||||||
|
|
||||||
|
const daily = Array.from({ length: 30 }, (_, index) => {
|
||||||
|
const date = new Date(now.getTime() - (29 - index) * 86400000).toISOString().split('T')[0];
|
||||||
|
const row = dailyMap.get(date);
|
||||||
|
const pv = row ? row.count : 0;
|
||||||
|
const uv = row ? row.sum.visits : 0;
|
||||||
|
|
||||||
|
totalPageViews += pv;
|
||||||
|
totalUniqueVisitors += uv;
|
||||||
|
if (date >= sevenDaysAgoStr) {
|
||||||
|
last7DaysPV += pv;
|
||||||
|
last7DaysUV += uv;
|
||||||
|
}
|
||||||
|
return { date, pageViews: pv, uniqueVisitors: uv };
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
scopeLabel: 'Web Analytics (RUM)',
|
||||||
|
days: 30,
|
||||||
|
pageViews: totalPageViews,
|
||||||
|
uniqueVisitors: totalUniqueVisitors,
|
||||||
|
last7Days: {
|
||||||
|
pageViews: last7DaysPV,
|
||||||
|
uniqueVisitors: last7DaysUV
|
||||||
|
},
|
||||||
|
daily
|
||||||
|
};
|
||||||
|
} catch (cause) {
|
||||||
|
console.error('Web Analytics query failed:', cause);
|
||||||
|
return {
|
||||||
|
scopeLabel: 'Web Analytics (RUM)',
|
||||||
|
days: 30,
|
||||||
|
pageViews: null,
|
||||||
|
uniqueVisitors: null,
|
||||||
|
last7Days: null,
|
||||||
|
daily: [],
|
||||||
|
error: cause instanceof Error ? cause.message : 'Web Analytics 查询失败'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,7 +18,8 @@ export const load: LayoutServerLoad = async ({ platform }) => {
|
|||||||
description: 'A high-performance blog powered by Cloudflare D1, R2, and KV.',
|
description: 'A high-performance blog powered by Cloudflare D1, R2, and KV.',
|
||||||
avatar_enabled: 'true',
|
avatar_enabled: 'true',
|
||||||
avatar_url: '',
|
avatar_url: '',
|
||||||
launch_date: ''
|
launch_date: '',
|
||||||
|
web_analytics_token: ''
|
||||||
};
|
};
|
||||||
|
|
||||||
if (db) {
|
if (db) {
|
||||||
|
|||||||
@@ -49,6 +49,13 @@
|
|||||||
>{$page.data.site?.title || 'BlogFlare'} - {$page.data.site?.description ||
|
>{$page.data.site?.title || 'BlogFlare'} - {$page.data.site?.description ||
|
||||||
'A Pure Cloudflare Blog'}</title
|
'A Pure Cloudflare Blog'}</title
|
||||||
>
|
>
|
||||||
|
{#if $page.data.site?.web_analytics_token}
|
||||||
|
<script
|
||||||
|
defer
|
||||||
|
src="https://static.cloudflareinsights.com/beacon.min.js"
|
||||||
|
data-cf-beacon={JSON.stringify({ token: $page.data.site.web_analytics_token })}
|
||||||
|
></script>
|
||||||
|
{/if}
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
{#if $page.url.pathname.startsWith('/admin')}
|
{#if $page.url.pathname.startsWith('/admin')}
|
||||||
|
|||||||
@@ -25,11 +25,37 @@ export const load: PageServerLoad = async ({ locals, platform }) => {
|
|||||||
platformEnv: db ? 'Cloudflare D1 (Connected)' : 'Local Mock',
|
platformEnv: db ? 'Cloudflare D1 (Connected)' : 'Local Mock',
|
||||||
r2Env: r2 ? 'Cloudflare R2 (Connected)' : 'Local Mock',
|
r2Env: r2 ? 'Cloudflare R2 (Connected)' : 'Local Mock',
|
||||||
settings,
|
settings,
|
||||||
analytics: fetchAnalytics(platform, settings['r2.public_domain']) // Streaming promise
|
analytics: fetchAnalytics(
|
||||||
|
platform,
|
||||||
|
settings['r2.public_domain'],
|
||||||
|
settings['site.web_analytics_token']
|
||||||
|
) // Streaming promise
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const actions: Actions = {
|
export const actions: Actions = {
|
||||||
|
updateWebAnalytics: async ({ request, platform }) => {
|
||||||
|
const db = platform?.env?.DB;
|
||||||
|
if (!db) return fail(500, { error: '数据库未连接' });
|
||||||
|
|
||||||
|
const data = await request.formData();
|
||||||
|
const token = data.get('web_analytics_token')?.toString().trim() || '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
await db
|
||||||
|
.prepare(
|
||||||
|
`INSERT INTO settings (key, value, updated_at)
|
||||||
|
VALUES (?, ?, CURRENT_TIMESTAMP)
|
||||||
|
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = CURRENT_TIMESTAMP`
|
||||||
|
)
|
||||||
|
.bind('site.web_analytics_token', token)
|
||||||
|
.run();
|
||||||
|
return { success: true };
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('Failed to update web analytics token', err);
|
||||||
|
return fail(500, { error: '保存统计设置失败' });
|
||||||
|
}
|
||||||
|
},
|
||||||
updateUploads: async ({ request, platform }) => {
|
updateUploads: async ({ request, platform }) => {
|
||||||
const db = platform?.env?.DB;
|
const db = platform?.env?.DB;
|
||||||
if (!db) return fail(500, { error: '数据库未连接' });
|
if (!db) return fail(500, { error: '数据库未连接' });
|
||||||
|
|||||||
@@ -361,6 +361,36 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
{:else if activeTab === 'analytics'}
|
{:else if activeTab === 'analytics'}
|
||||||
|
<div class="settings-card full-width" style="margin-bottom: 25px;">
|
||||||
|
<h3>Web Analytics (流量统计)</h3>
|
||||||
|
<p class="desc">
|
||||||
|
配置 Cloudflare Web Analytics 站点 Token。配置后博客将自动采集 PV、UV。如果您通过 Cloudflare Pages 部署并在后台一键开启了统计,只需在这里填写对应生成的 Site Tag 即可拉取数据展示。
|
||||||
|
</p>
|
||||||
|
<form
|
||||||
|
method="POST"
|
||||||
|
action="?/updateWebAnalytics"
|
||||||
|
use:enhance={() => {
|
||||||
|
return async ({ update }) => {
|
||||||
|
update({ reset: false });
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
class="alias-form"
|
||||||
|
>
|
||||||
|
<div class="form-group" style="max-width: 500px;">
|
||||||
|
<label for="web_analytics_token">Web Analytics Token / Site Tag</label>
|
||||||
|
<TextInput
|
||||||
|
type="text"
|
||||||
|
id="web_analytics_token"
|
||||||
|
name="web_analytics_token"
|
||||||
|
value={data.settings['site.web_analytics_token'] || ''}
|
||||||
|
placeholder="留空则禁用数据拉取和自动注入"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="form-actions" style="justify-content: flex-start;">
|
||||||
|
<Button type="submit" variant="primary">保存统计设置</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
<AnalyticsPanel analyticsPromise={data.analytics} />
|
<AnalyticsPanel analyticsPromise={data.analytics} />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in new issue
Block a user