-
页面访问明细(按 PV 排序)
-
加载中...
+
+
页面访问明细
+
+
+
+
+
+
加载中...
{{ error }}
暂无访问数据
@@ -102,6 +122,7 @@ import {
const DOMAIN_STORAGE_KEY = "cwd_admin_domain_filter";
const loading = ref(false);
+const listLoading = ref(false);
const error = ref("");
const overview = ref({
totalPv: 0,
@@ -109,6 +130,7 @@ const overview = ref({
last30Days: [],
});
const items = ref([]);
+const visitTab = ref<"pv" | "latest">("pv");
const storedDomain =
typeof window !== "undefined"
@@ -183,14 +205,23 @@ function extractDomain(source: string | null | undefined): string | null {
}
}
+function getVisitOrderParam(): "pv" | "latest" | undefined {
+ if (visitTab.value === "latest") {
+ return "latest";
+ }
+ return undefined;
+}
+
async function loadData() {
loading.value = true;
+ listLoading.value = true;
error.value = "";
try {
const domain = domainFilter.value || undefined;
+ const order = getVisitOrderParam();
const [overviewRes, pagesRes] = await Promise.all([
fetchVisitOverview(domain),
- fetchVisitPages(domain),
+ fetchVisitPages(domain, order),
]);
overview.value = {
totalPv: overviewRes.totalPv,
@@ -223,6 +254,7 @@ async function loadData() {
showToast(msg, "error");
} finally {
loading.value = false;
+ listLoading.value = false;
await nextTick();
if (!error.value && last30Days.value.length > 0) {
renderChart();
@@ -232,6 +264,31 @@ async function loadData() {
}
}
+async function loadVisitPagesOnly() {
+ listLoading.value = true;
+ error.value = "";
+ try {
+ const domain = domainFilter.value || undefined;
+ const order = getVisitOrderParam();
+ const pagesRes = await fetchVisitPages(domain, order);
+ items.value = pagesRes.items || [];
+ } catch (e: any) {
+ const msg = e.message || "加载访问统计数据失败";
+ error.value = msg;
+ showToast(msg, "error");
+ } finally {
+ listLoading.value = false;
+ }
+}
+
+function changeVisitTab(tab: "pv" | "latest") {
+ if (visitTab.value === tab) {
+ return;
+ }
+ visitTab.value = tab;
+ loadVisitPagesOnly();
+}
+
function renderChart() {
const el = chartEl.value;
if (!el) {
@@ -364,6 +421,14 @@ watch(
font-size: 16px;
}
+.card-title-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ margin-bottom: 12px;
+}
+
.page-hint {
font-size: 14px;
color: #57606a;
@@ -476,10 +541,6 @@ watch(
}
}
-.chart-wrapper {
- margin-top: 4px;
-}
-
.chart {
width: 100%;
height: 260px;
@@ -508,4 +569,29 @@ watch(
background-color: #d1242f;
color: #ffffff;
}
+
+.visit-tabs {
+ display: inline-flex;
+ border-radius: 999px;
+ border: 1px solid #d0d7de;
+ overflow: hidden;
+}
+
+.visit-tab {
+ padding: 4px 10px;
+ font-size: 12px;
+ border: none;
+ background-color: #ffffff;
+ color: #57606a;
+ cursor: pointer;
+}
+
+.visit-tab + .visit-tab {
+ border-left: 1px solid #d0d7de;
+}
+
+.visit-tab-active {
+ background-color: #0969da;
+ color: #ffffff;
+}
diff --git a/cwd-admin/src/views/LayoutView.vue b/cwd-admin/src/views/LayoutView.vue
index 5a0901f..b6fc856 100644
--- a/cwd-admin/src/views/LayoutView.vue
+++ b/cwd-admin/src/views/LayoutView.vue
@@ -94,7 +94,7 @@
:class="{ active: isRouteActive('data') }"
@click="goData"
>
- 数据管理
+ 数据迁移
diff --git a/cwd-admin/src/views/SettingsView.vue b/cwd-admin/src/views/SettingsView.vue
index 30461ec..639232e 100644
--- a/cwd-admin/src/views/SettingsView.vue
+++ b/cwd-admin/src/views/SettingsView.vue
@@ -556,7 +556,7 @@ onMounted(() => {
display: flex;
flex-direction: column;
gap: 12px;
- max-width: 520px;
+ max-width: 620px;
}
.page-title {
diff --git a/cwd-api/src/api/admin/visitAnalytics.ts b/cwd-api/src/api/admin/visitAnalytics.ts
index d6a4a0f..41dc9d3 100644
--- a/cwd-api/src/api/admin/visitAnalytics.ts
+++ b/cwd-api/src/api/admin/visitAnalytics.ts
@@ -148,14 +148,23 @@ export const getVisitPages = async (c: Context<{ Bindings: Bindings }>) => {
try {
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';
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();
- const { results } = await c.env.CWD_DB.prepare(
- 'SELECT post_slug, post_title, post_url, pv, last_visit_at FROM page_stats ORDER BY pv DESC, last_visit_at DESC'
- ).all<{
+ 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<{
post_slug: string;
post_title: string | null;
post_url: string | null;