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 @@
@@ -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
})