diff --git a/cwd-admin/src/api/admin.ts b/cwd-admin/src/api/admin.ts index 2e38dc6..b20cd63 100644 --- a/cwd-admin/src/api/admin.ts +++ b/cwd-admin/src/api/admin.ts @@ -111,6 +111,8 @@ export type VisitPageItem = { export type VisitPagesResponse = { items: VisitPageItem[]; + itemsByPv?: VisitPageItem[]; + itemsByLatest?: VisitPageItem[]; }; export type DomainListResponse = { diff --git a/cwd-admin/src/views/AnalyticsVisitView/index.vue b/cwd-admin/src/views/AnalyticsVisitView/index.vue index fe2b9db..6cb5504 100644 --- a/cwd-admin/src/views/AnalyticsVisitView/index.vue +++ b/cwd-admin/src/views/AnalyticsVisitView/index.vue @@ -150,12 +150,12 @@
- {{ item.postSlug }} + {{ item.postUrl }} -
@@ -259,24 +259,13 @@ const monthPercentageChange = computed(() => { return calculateChange(overview.value.monthPv, overview.value.lastMonthPv || 0); }); -const rawItems = ref([]); +const itemsByPv = ref([]); +const itemsByLatest = ref([]); const items = computed(() => { - const list = rawItems.value.slice(); - list.sort((a, b) => { - const aLast = getLastVisitAtTs(a.lastVisitAt); - const bLast = getLastVisitAtTs(b.lastVisitAt); - if (visitTab.value === "latest") { - if (bLast !== aLast) { - return bLast - aLast; - } - return b.pv - a.pv; - } - if (b.pv !== a.pv) { - return b.pv - a.pv; - } - return bLast - aLast; - }); - return list; + if (visitTab.value === "latest") { + return itemsByLatest.value; + } + return itemsByPv.value; }); const visitTab = ref<"pv" | "latest">("pv"); const visitTabStorageKey = "cwd-analytics-visit-tab"; @@ -353,11 +342,11 @@ function extractDomain(source: string | null | undefined): string | null { } } -function getVisitOrderParam(): "pv" | "latest" | undefined { +function getVisitOrderParam(): "pv" | "latest" { if (visitTab.value === "latest") { return "latest"; } - return undefined; + return "pv"; } function filterLikeStatsByDomain(list: LikeStatsItem[], domain: string | undefined): LikeStatsItem[] { @@ -457,8 +446,36 @@ async function loadData() { }; const likeItemsRaw = Array.isArray(likeStatsRes.items) ? likeStatsRes.items : []; likeStatsItems.value = filterLikeStatsByDomain(likeItemsRaw, domain); - const pageItems = Array.isArray(pagesRes.items) ? pagesRes.items : []; - rawItems.value = pageItems; + const pageItemsByPv = Array.isArray(pagesRes.itemsByPv) + ? pagesRes.itemsByPv + : []; + const pageItemsByLatest = Array.isArray(pagesRes.itemsByLatest) + ? pagesRes.itemsByLatest + : []; + if (pageItemsByPv.length === 0 && pageItemsByLatest.length === 0) { + const baseItems = Array.isArray(pagesRes.items) ? pagesRes.items : []; + const sortedByPv = baseItems.slice().sort((a, b) => { + if (b.pv !== a.pv) { + return b.pv - a.pv; + } + const aLast = getLastVisitAtTs(a.lastVisitAt); + const bLast = getLastVisitAtTs(b.lastVisitAt); + return bLast - aLast; + }); + const sortedByLatest = baseItems.slice().sort((a, b) => { + const aLast = getLastVisitAtTs(a.lastVisitAt); + const bLast = getLastVisitAtTs(b.lastVisitAt); + if (bLast !== aLast) { + return bLast - aLast; + } + return b.pv - a.pv; + }); + itemsByPv.value = sortedByPv; + itemsByLatest.value = sortedByLatest; + } else { + itemsByPv.value = pageItemsByPv; + itemsByLatest.value = pageItemsByLatest; + } last30Days.value = Array.isArray(overviewRes.last30Days) ? overviewRes.last30Days : []; diff --git a/cwd-api/src/api/admin/visitAnalytics.ts b/cwd-api/src/api/admin/visitAnalytics.ts index 6828ed4..e0ffe1d 100644 --- a/cwd-api/src/api/admin/visitAnalytics.ts +++ b/cwd-api/src/api/admin/visitAnalytics.ts @@ -263,22 +263,15 @@ export const getVisitPages = async (c: Context<{ Bindings: Bindings }>) => { const rawDomain = c.req.query('domain') || ''; const domainFilter = rawDomain.trim().toLowerCase(); const rawOrder = c.req.query('order') || ''; - const order = rawOrder.trim().toLowerCase(); - const isLatest = order === 'latest'; + const order = rawOrder.trim().toLowerCase() === 'latest' ? 'latest' : 'pv'; 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 INTEGER, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL)' ).run(); - let sql = - 'SELECT post_slug, post_title, post_url, pv, last_visit_at FROM page_stats ORDER BY pv DESC, last_visit_at DESC'; - - if (isLatest) { - sql = - 'SELECT post_slug, post_title, post_url, pv, last_visit_at FROM page_stats ORDER BY last_visit_at DESC, pv DESC'; - } - - const { results } = await c.env.CWD_DB.prepare(sql).all<{ + 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; @@ -307,9 +300,44 @@ export const getVisitPages = async (c: Context<{ Bindings: Bindings }>) => { }); } - items = items.slice(0, 20); + const itemsByPv = items + .slice() + .sort((a, b) => { + if (b.pv !== a.pv) { + return b.pv - a.pv; + } + const aLast = a.lastVisitAt ?? 0; + const bLast = b.lastVisitAt ?? 0; + return bLast - aLast; + }) + .slice(0, 20); - return c.json({ items }); + const itemsByLatest = items + .slice() + .sort((a, b) => { + const aLast = a.lastVisitAt ?? 0; + const bLast = b.lastVisitAt ?? 0; + if (bLast !== aLast) { + return bLast - aLast; + } + return b.pv - a.pv; + }) + .slice(0, 20); + + const response = + order === 'latest' + ? { + items: itemsByLatest, + itemsByPv, + itemsByLatest + } + : { + items: itemsByPv, + itemsByPv, + itemsByLatest + }; + + return c.json(response); } catch (e: any) { return c.json( { message: e.message || '获取页面访问统计失败' }, diff --git a/docs/.vitepress/configs/nav.js b/docs/.vitepress/configs/nav.js index cc5ab2e..e52aa19 100644 --- a/docs/.vitepress/configs/nav.js +++ b/docs/.vitepress/configs/nav.js @@ -2,5 +2,6 @@ export default [ { text: '首页', link: '/' }, { text: '快速开始', link: '/guide/getting-started' }, { text: 'API 文档', link: '/api/overview' }, + { text: '常见问题', link: '/common-problems' }, { text: '管理后台', link: 'https://cwd.zishu.me' }, ]; diff --git a/docs/.vitepress/configs/sidebar.js b/docs/.vitepress/configs/sidebar.js index b5c09a5..1607015 100644 --- a/docs/.vitepress/configs/sidebar.js +++ b/docs/.vitepress/configs/sidebar.js @@ -20,7 +20,14 @@ export const rootSidebar = [ { text: '数据管理', link: '/function/data-migration' }, ], }, + { + text: '配置', + items: [ + { text: '站点隔离', link: '/config/site-isolation' }, + ], + }, { text: '反馈', link: '/guide/feedback' }, + { text: '常见问题', link: '/common-problems' }, ]; export const apiSidebar = [ diff --git a/docs/api/admin/analytics.md b/docs/api/admin/analytics.md index 2c9dc84..676303b 100644 --- a/docs/api/admin/analytics.md +++ b/docs/api/admin/analytics.md @@ -124,13 +124,24 @@ GET /admin/analytics/pages "postUrl": "https://example.com/blog/hello-world", "pv": 100, "lastVisitAt": 1737593600000 - }, + } + ], + "itemsByPv": [ { - "postSlug": "https://example.com/about", - "postTitle": "关于我", - "postUrl": "https://example.com/about", - "pv": 50, - "lastVisitAt": 1737593500000 + "postSlug": "https://example.com/blog/hello-world", + "postTitle": "Hello World", + "postUrl": "https://example.com/blog/hello-world", + "pv": 100, + "lastVisitAt": 1737593600000 + } + ], + "itemsByLatest": [ + { + "postSlug": "https://example.com/blog/hello-world", + "postTitle": "Hello World", + "postUrl": "https://example.com/blog/hello-world", + "pv": 100, + "lastVisitAt": 1737593600000 } ] } @@ -138,13 +149,16 @@ GET /admin/analytics/pages 字段说明: -| 字段名 | 类型 | 说明 | -| ------------- | ------ | -------------------------- | -| `postSlug` | string | 文章唯一标识符 | -| `postTitle` | string \| null | 文章标题 | -| `postUrl` | string \| null | 文章 URL | -| `pv` | number | 访问量(PV) | -| `lastVisitAt` | number \| null | 最后访问时间戳(毫秒) | +| 字段名 | 类型 | 说明 | +| ------------------- | ------ | ----------------------------------------- | +| `items` | Array | 根据 `order` 参数返回的主列表 | +| `itemsByPv` | Array | 按 `order=pv` 规则排序后的前 20 条数据 | +| `itemsByLatest` | Array | 按 `order=latest` 规则排序后的前 20 条数据 | +| `postSlug` | string | 文章唯一标识符 | +| `postTitle` | string \| null | 文章标题 | +| `postUrl` | string \| null | 文章 URL | +| `pv` | number | 访问量(PV) | +| `lastVisitAt` | number \| null | 最后访问时间戳(毫秒) | **错误响应** diff --git a/docs/common-problems.md b/docs/common-problems.md new file mode 100644 index 0000000..7565430 --- /dev/null +++ b/docs/common-problems.md @@ -0,0 +1,16 @@ +# 常见问题 + +## 1. 为什么设置完 siteId 后,评论区没有显示评论数据? + +因为设置了 siteId 后,接口会根据 siteId 来查询数据库带有你设置的 siteId 的评论数据。所以如果设置了 siteId,旧数据就有可能不显示,需要手动去 Cloudflare D1 控制台 Query 运行 SQL 语句来更新数据。 + +- `abc`: 你要设置的 siteId +- `example.com`: 查找包含指定域名的评论数据 + +```sql +UPDATE Comment +SET site_id = 'abc' +WHERE post_url LIKE '%example.com%'; +``` + +运行以上 SQL 语句后,评论区就会显示带有 `abc` siteId 的评论数据了。 diff --git a/docs/config/site-isolation.md b/docs/config/site-isolation.md new file mode 100644 index 0000000..aaef78f --- /dev/null +++ b/docs/config/site-isolation.md @@ -0,0 +1,9 @@ +# 站点隔离 + +配置支持站点数据隔离。 + +通过前端实例调用时进行配置: + +``` + +``` \ No newline at end of file diff --git a/docs/guide/frontend-config.md b/docs/guide/frontend-config.md index 0badccd..5f0f431 100644 --- a/docs/guide/frontend-config.md +++ b/docs/guide/frontend-config.md @@ -28,6 +28,7 @@ CWD 评论组件采用 **Shadow DOM** 技术构建,基于独立根节点渲染 const comments = new CWDComments({ el: '#comments', apiBaseUrl: 'https://your-api.example.com', // 换成你的 API 地址 + siteId: 'your-site-id', // 换成你的站点 ID(关键词),比如 blog }); comments.mount(); diff --git a/docs/index.md b/docs/index.md index e8fec46..351e49b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -14,6 +14,9 @@ hero: - theme: alt text: Github link: https://github.com/anghunk/cwd + - theme: alt + text: 常见问题 + link: /common-problems features: - icon: ⚡️ diff --git a/docs/widget/src/core/api.js b/docs/widget/src/core/api.js index 873799a..c099c9a 100644 --- a/docs/widget/src/core/api.js +++ b/docs/widget/src/core/api.js @@ -128,7 +128,7 @@ export function createApiClient(config) { 'Content-Type': 'application/json' }, body: JSON.stringify({ - postSlug: config.postSlug, + postSlug: config.postUrl || config.postSlug, postTitle: config.postTitle, postUrl: config.postUrl })