feat(数据分析): 添加域名过滤功能并持久化存储

- 在评论、统计和访问分析页面添加域名过滤功能
- 使用localStorage持久化存储用户选择的域名过滤条件
- 新增30天访问趋势图表展示
- 优化API接口支持按域名过滤数据
- 添加每日访问统计记录功能
This commit is contained in:
anghunk
2026-01-21 22:35:59 +08:00
parent 73eaa975fe
commit 5d2cf49d96
7 changed files with 574 additions and 81 deletions

View File

@@ -4,6 +4,10 @@ import type { Bindings } from '../../bindings';
type VisitOverview = {
totalPv: number;
totalPages: number;
last30Days: {
date: string;
total: number;
}[];
};
type VisitPageItem = {
@@ -14,24 +18,116 @@ type VisitPageItem = {
lastVisitAt: string | null;
};
function extractDomain(source: string | null | undefined): string | null {
if (!source) {
return null;
}
const value = source.trim();
if (!value) {
return null;
}
if (!/^https?:\/\//i.test(value)) {
return null;
}
try {
const url = new URL(value);
return url.hostname.toLowerCase();
} catch {
return null;
}
}
export const getVisitOverview = async (
c: Context<{ Bindings: Bindings }>
) => {
try {
const rawDomain = c.req.query('domain') || '';
const domainFilter = rawDomain.trim().toLowerCase();
await c.env.CWD_DB.prepare(
'CREATE TABLE IF NOT EXISTS page_stats (id INTEGER PRIMARY KEY AUTOINCREMENT, post_slug TEXT UNIQUE NOT NULL, post_title TEXT, post_url TEXT, pv INTEGER NOT NULL DEFAULT 0, last_visit_at TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL)'
).run();
const row = await c.env.CWD_DB.prepare(
'SELECT COUNT(*) as totalPages, COALESCE(SUM(pv), 0) as totalPv FROM page_stats'
).first<{
totalPages: number | null;
totalPv: number | null;
await c.env.CWD_DB.prepare(
'CREATE TABLE IF NOT EXISTS page_visit_daily (id INTEGER PRIMARY KEY AUTOINCREMENT, date TEXT NOT NULL, domain TEXT, count INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL, updated_at TEXT NOT NULL)'
).run();
const { results } = await c.env.CWD_DB.prepare(
'SELECT post_slug, post_title, post_url, pv, last_visit_at FROM page_stats'
).all<{
post_slug: string;
post_title: string | null;
post_url: string | null;
pv: number;
last_visit_at: string | null;
}>();
let totalPv = 0;
let totalPages = 0;
for (const row of results) {
const domain =
extractDomain(row.post_url) ||
extractDomain(row.post_slug) ||
null;
if (domainFilter && domain !== domainFilter) {
continue;
}
totalPv += row.pv || 0;
totalPages += 1;
}
const now = new Date();
const thirtyDaysAgo = new Date(now.getTime() - 29 * 24 * 60 * 60 * 1000);
const startDate = thirtyDaysAgo.toISOString().slice(0, 10);
let dailySql =
'SELECT date, domain, count FROM page_visit_daily WHERE date >= ?';
const params: string[] = [startDate];
if (domainFilter) {
dailySql += ' AND domain = ?';
params.push(domainFilter);
}
const { results: dailyRows } = await c.env.CWD_DB.prepare(dailySql)
.bind(...params)
.all<{
date: string;
domain: string | null;
count: number;
}>();
const dailyMap = new Map<string, number>();
for (const row of dailyRows) {
if (!row || !row.date) {
continue;
}
const key = row.date;
const value = row.count || 0;
dailyMap.set(key, (dailyMap.get(key) || 0) + value);
}
const last30Days: { date: string; total: number }[] = [];
for (let i = 29; i >= 0; i--) {
const d = new Date(now.getTime() - i * 24 * 60 * 60 * 1000);
const year = d.getUTCFullYear();
const month = String(d.getUTCMonth() + 1).padStart(2, '0');
const day = String(d.getUTCDate()).padStart(2, '0');
const key = `${year}-${month}-${day}`;
last30Days.push({
date: key,
total: dailyMap.get(key) || 0
});
}
const data: VisitOverview = {
totalPv: row?.totalPv || 0,
totalPages: row?.totalPages || 0
totalPv,
totalPages,
last30Days
};
return c.json(data);
@@ -45,6 +141,9 @@ export const getVisitOverview = async (
export const getVisitPages = async (c: Context<{ Bindings: Bindings }>) => {
try {
const rawDomain = c.req.query('domain') || '';
const domainFilter = rawDomain.trim().toLowerCase();
await c.env.CWD_DB.prepare(
'CREATE TABLE IF NOT EXISTS page_stats (id INTEGER PRIMARY KEY AUTOINCREMENT, post_slug TEXT UNIQUE NOT NULL, post_title TEXT, post_url TEXT, pv INTEGER NOT NULL DEFAULT 0, last_visit_at TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL)'
).run();
@@ -59,13 +158,26 @@ export const getVisitPages = async (c: Context<{ Bindings: Bindings }>) => {
last_visit_at: string | null;
}>();
const items: VisitPageItem[] = results.map((row) => ({
postSlug: row.post_slug,
postTitle: row.post_title,
postUrl: row.post_url,
pv: row.pv || 0,
lastVisitAt: row.last_visit_at
}));
const items: VisitPageItem[] = [];
for (const row of results) {
const domain =
extractDomain(row.post_url) ||
extractDomain(row.post_slug) ||
null;
if (domainFilter && domain !== domainFilter) {
continue;
}
items.push({
postSlug: row.post_slug,
postTitle: row.post_title,
postUrl: row.post_url,
pv: row.pv || 0,
lastVisitAt: row.last_visit_at
});
}
return c.json({ items });
} catch (e: any) {
@@ -75,4 +187,3 @@ export const getVisitPages = async (c: Context<{ Bindings: Bindings }>) => {
);
}
};