feat(analytics): 添加按最新访问排序功能
在访问统计页面增加按最新访问排序的选项,并优化加载状态管理 修改相关API以支持排序参数,同时调整页面布局和样式
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="app-root">
|
<div class="app-root">
|
||||||
<router-view />
|
<router-view />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts"></script>
|
<script setup lang="ts"></script>
|
||||||
@@ -11,13 +11,12 @@ html,
|
|||||||
body,
|
body,
|
||||||
#app,
|
#app,
|
||||||
.app-root {
|
.app-root {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||||
background-color: #f5f5f5;
|
background-color: #f5f5f5;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -250,11 +250,14 @@ export function fetchVisitOverview(domain?: string): Promise<VisitOverviewRespon
|
|||||||
return get<VisitOverviewResponse>(url);
|
return get<VisitOverviewResponse>(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchVisitPages(domain?: string): Promise<VisitPagesResponse> {
|
export function fetchVisitPages(domain?: string, order?: 'pv' | 'latest'): Promise<VisitPagesResponse> {
|
||||||
const searchParams = new URLSearchParams();
|
const searchParams = new URLSearchParams();
|
||||||
if (domain) {
|
if (domain) {
|
||||||
searchParams.set('domain', domain);
|
searchParams.set('domain', domain);
|
||||||
}
|
}
|
||||||
|
if (order) {
|
||||||
|
searchParams.set('order', order);
|
||||||
|
}
|
||||||
const query = searchParams.toString();
|
const query = searchParams.toString();
|
||||||
const url = query ? `/admin/analytics/pages?${query}` : '/admin/analytics/pages';
|
const url = query ? `/admin/analytics/pages?${query}` : '/admin/analytics/pages';
|
||||||
return get<VisitPagesResponse>(url);
|
return get<VisitPagesResponse>(url);
|
||||||
|
|||||||
@@ -49,8 +49,28 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h3 class="card-title">页面访问明细(按 PV 排序)</h3>
|
<div class="card-title-row">
|
||||||
<div v-if="loading" class="page-hint">加载中...</div>
|
<h3 class="card-title">页面访问明细</h3>
|
||||||
|
<div class="visit-tabs">
|
||||||
|
<button
|
||||||
|
class="visit-tab"
|
||||||
|
:class="{ 'visit-tab-active': visitTab === 'pv' }"
|
||||||
|
type="button"
|
||||||
|
@click="changeVisitTab('pv')"
|
||||||
|
>
|
||||||
|
按 PV 排序
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="visit-tab"
|
||||||
|
:class="{ 'visit-tab-active': visitTab === 'latest' }"
|
||||||
|
type="button"
|
||||||
|
@click="changeVisitTab('latest')"
|
||||||
|
>
|
||||||
|
最新访问
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="listLoading" class="page-hint">加载中...</div>
|
||||||
<div v-else-if="error" class="page-error">{{ error }}</div>
|
<div v-else-if="error" class="page-error">{{ error }}</div>
|
||||||
<div v-else-if="items.length === 0" class="page-hint">暂无访问数据</div>
|
<div v-else-if="items.length === 0" class="page-hint">暂无访问数据</div>
|
||||||
<div v-else class="domain-table-wrapper">
|
<div v-else class="domain-table-wrapper">
|
||||||
@@ -102,6 +122,7 @@ import {
|
|||||||
const DOMAIN_STORAGE_KEY = "cwd_admin_domain_filter";
|
const DOMAIN_STORAGE_KEY = "cwd_admin_domain_filter";
|
||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
const listLoading = ref(false);
|
||||||
const error = ref("");
|
const error = ref("");
|
||||||
const overview = ref<VisitOverviewResponse>({
|
const overview = ref<VisitOverviewResponse>({
|
||||||
totalPv: 0,
|
totalPv: 0,
|
||||||
@@ -109,6 +130,7 @@ const overview = ref<VisitOverviewResponse>({
|
|||||||
last30Days: [],
|
last30Days: [],
|
||||||
});
|
});
|
||||||
const items = ref<VisitPageItem[]>([]);
|
const items = ref<VisitPageItem[]>([]);
|
||||||
|
const visitTab = ref<"pv" | "latest">("pv");
|
||||||
|
|
||||||
const storedDomain =
|
const storedDomain =
|
||||||
typeof window !== "undefined"
|
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() {
|
async function loadData() {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
|
listLoading.value = true;
|
||||||
error.value = "";
|
error.value = "";
|
||||||
try {
|
try {
|
||||||
const domain = domainFilter.value || undefined;
|
const domain = domainFilter.value || undefined;
|
||||||
|
const order = getVisitOrderParam();
|
||||||
const [overviewRes, pagesRes] = await Promise.all([
|
const [overviewRes, pagesRes] = await Promise.all([
|
||||||
fetchVisitOverview(domain),
|
fetchVisitOverview(domain),
|
||||||
fetchVisitPages(domain),
|
fetchVisitPages(domain, order),
|
||||||
]);
|
]);
|
||||||
overview.value = {
|
overview.value = {
|
||||||
totalPv: overviewRes.totalPv,
|
totalPv: overviewRes.totalPv,
|
||||||
@@ -223,6 +254,7 @@ async function loadData() {
|
|||||||
showToast(msg, "error");
|
showToast(msg, "error");
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
|
listLoading.value = false;
|
||||||
await nextTick();
|
await nextTick();
|
||||||
if (!error.value && last30Days.value.length > 0) {
|
if (!error.value && last30Days.value.length > 0) {
|
||||||
renderChart();
|
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() {
|
function renderChart() {
|
||||||
const el = chartEl.value;
|
const el = chartEl.value;
|
||||||
if (!el) {
|
if (!el) {
|
||||||
@@ -364,6 +421,14 @@ watch(
|
|||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.card-title-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.page-hint {
|
.page-hint {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #57606a;
|
color: #57606a;
|
||||||
@@ -476,10 +541,6 @@ watch(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-wrapper {
|
|
||||||
margin-top: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart {
|
.chart {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 260px;
|
height: 260px;
|
||||||
@@ -508,4 +569,29 @@ watch(
|
|||||||
background-color: #d1242f;
|
background-color: #d1242f;
|
||||||
color: #ffffff;
|
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;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -94,7 +94,7 @@
|
|||||||
:class="{ active: isRouteActive('data') }"
|
:class="{ active: isRouteActive('data') }"
|
||||||
@click="goData"
|
@click="goData"
|
||||||
>
|
>
|
||||||
数据管理
|
数据迁移
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@@ -556,7 +556,7 @@ onMounted(() => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
max-width: 520px;
|
max-width: 620px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-title {
|
.page-title {
|
||||||
|
|||||||
@@ -148,14 +148,23 @@ export const getVisitPages = async (c: Context<{ Bindings: Bindings }>) => {
|
|||||||
try {
|
try {
|
||||||
const rawDomain = c.req.query('domain') || '';
|
const rawDomain = c.req.query('domain') || '';
|
||||||
const domainFilter = rawDomain.trim().toLowerCase();
|
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(
|
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)'
|
'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();
|
).run();
|
||||||
|
|
||||||
const { results } = await c.env.CWD_DB.prepare(
|
let sql =
|
||||||
'SELECT post_slug, post_title, post_url, pv, last_visit_at FROM page_stats ORDER BY pv DESC, last_visit_at DESC'
|
'SELECT post_slug, post_title, post_url, pv, last_visit_at FROM page_stats ORDER BY pv DESC, last_visit_at DESC';
|
||||||
).all<{
|
|
||||||
|
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_slug: string;
|
||||||
post_title: string | null;
|
post_title: string | null;
|
||||||
post_url: string | null;
|
post_url: string | null;
|
||||||
|
|||||||
Reference in New Issue
Block a user