feat(analytics): 添加点赞统计功能并修复页面URL处理
- 在admin.ts中添加LikeStats相关类型定义和API接口 - 在AnalyticsVisitView.vue中添加点赞排行榜展示 - 修改CWDComments.js中postUrl的生成逻辑,使用origin+pathname代替href
This commit is contained in:
@@ -111,6 +111,17 @@ export type DomainListResponse = {
|
|||||||
domains: string[];
|
domains: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type LikeStatsItem = {
|
||||||
|
pageSlug: string;
|
||||||
|
pageTitle: string | null;
|
||||||
|
pageUrl: string | null;
|
||||||
|
likes: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type LikeStatsResponse = {
|
||||||
|
items: LikeStatsItem[];
|
||||||
|
};
|
||||||
|
|
||||||
export async function loginAdmin(name: string, password: string): Promise<string> {
|
export async function loginAdmin(name: string, password: string): Promise<string> {
|
||||||
const res = await post<AdminLoginResponse>('/admin/login', { name, password });
|
const res = await post<AdminLoginResponse>('/admin/login', { name, password });
|
||||||
const key = res.data.key;
|
const key = res.data.key;
|
||||||
@@ -273,3 +284,7 @@ export function fetchVisitPages(domain?: string, order?: 'pv' | 'latest'): Promi
|
|||||||
export function fetchDomainList(): Promise<DomainListResponse> {
|
export function fetchDomainList(): Promise<DomainListResponse> {
|
||||||
return get<DomainListResponse>('/admin/stats/domains');
|
return get<DomainListResponse>('/admin/stats/domains');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function fetchLikeStats(): Promise<LikeStatsResponse> {
|
||||||
|
return get<LikeStatsResponse>('/admin/likes/stats');
|
||||||
|
}
|
||||||
|
|||||||
@@ -95,12 +95,55 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="domain-cell domain-cell-url">
|
<div class="domain-cell domain-cell-url">
|
||||||
<a
|
<a
|
||||||
v-if="item.postUrl"
|
v-if="item.postSlug"
|
||||||
:href="item.postUrl"
|
:href="item.postSlug"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
{{ item.postUrl }}
|
{{ item.postSlug }}
|
||||||
|
</a>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3 class="card-title">点赞页面排行榜</h3>
|
||||||
|
<div v-if="loading" class="page-hint">加载中...</div>
|
||||||
|
<div v-else-if="error" class="page-error">{{ error }}</div>
|
||||||
|
<div v-else-if="likeStatsItems.length === 0" class="page-hint">暂无点赞数据</div>
|
||||||
|
<div v-else class="domain-table-wrapper">
|
||||||
|
<div class="domain-table">
|
||||||
|
<div class="domain-table-header">
|
||||||
|
<div class="domain-cell domain-cell-rank">排名</div>
|
||||||
|
<div class="domain-cell domain-cell-title">页面标题</div>
|
||||||
|
<div class="domain-cell domain-cell-like">点赞数</div>
|
||||||
|
<div class="domain-cell domain-cell-url">页面地址</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-for="(item, index) in likeStatsItems"
|
||||||
|
:key="item.pageSlug"
|
||||||
|
class="domain-table-row"
|
||||||
|
>
|
||||||
|
<div class="domain-cell domain-cell-rank">
|
||||||
|
{{ index + 1 }}
|
||||||
|
</div>
|
||||||
|
<div class="domain-cell domain-cell-title">
|
||||||
|
{{ item.pageTitle || item.pageSlug }}
|
||||||
|
</div>
|
||||||
|
<div class="domain-cell domain-cell-like">
|
||||||
|
{{ item.likes }}
|
||||||
|
</div>
|
||||||
|
<div class="domain-cell domain-cell-url">
|
||||||
|
<a
|
||||||
|
v-if="item.pageSlug"
|
||||||
|
:href="item.pageSlug"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
{{ item.pageSlug }}
|
||||||
</a>
|
</a>
|
||||||
<span v-else>-</span>
|
<span v-else>-</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -120,6 +163,8 @@ import {
|
|||||||
fetchVisitPages,
|
fetchVisitPages,
|
||||||
type VisitOverviewResponse,
|
type VisitOverviewResponse,
|
||||||
type VisitPageItem,
|
type VisitPageItem,
|
||||||
|
fetchLikeStats,
|
||||||
|
type LikeStatsItem,
|
||||||
} from "../api/admin";
|
} from "../api/admin";
|
||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
@@ -133,6 +178,7 @@ const overview = ref<VisitOverviewResponse>({
|
|||||||
monthPv: 0,
|
monthPv: 0,
|
||||||
last30Days: [],
|
last30Days: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const items = ref<VisitPageItem[]>([]);
|
const items = ref<VisitPageItem[]>([]);
|
||||||
const visitTab = ref<"pv" | "latest">("pv");
|
const visitTab = ref<"pv" | "latest">("pv");
|
||||||
const visitTabStorageKey = "cwd-analytics-visit-tab";
|
const visitTabStorageKey = "cwd-analytics-visit-tab";
|
||||||
@@ -140,6 +186,7 @@ const visitTabStorageKey = "cwd-analytics-visit-tab";
|
|||||||
const injectedDomainFilter = inject<Ref<string> | null>("domainFilter", null);
|
const injectedDomainFilter = inject<Ref<string> | null>("domainFilter", null);
|
||||||
const domainFilter = injectedDomainFilter ?? ref("");
|
const domainFilter = injectedDomainFilter ?? ref("");
|
||||||
const last30Days = ref<{ date: string; total: number }[]>([]);
|
const last30Days = ref<{ date: string; total: number }[]>([]);
|
||||||
|
const likeStatsItems = ref<LikeStatsItem[]>([]);
|
||||||
|
|
||||||
const toastMessage = ref("");
|
const toastMessage = ref("");
|
||||||
const toastType = ref<"success" | "error">("success");
|
const toastType = ref<"success" | "error">("success");
|
||||||
@@ -213,6 +260,20 @@ function getVisitOrderParam(): "pv" | "latest" | undefined {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function filterLikeStatsByDomain(list: LikeStatsItem[], domain: string | undefined): LikeStatsItem[] {
|
||||||
|
if (!domain) {
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
return list.filter((item) => {
|
||||||
|
const source = item.pageUrl || item.pageSlug;
|
||||||
|
const d = extractDomain(source);
|
||||||
|
if (!d) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return d === domain;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function loadVisitTabFromStorage() {
|
function loadVisitTabFromStorage() {
|
||||||
if (typeof window === "undefined") {
|
if (typeof window === "undefined") {
|
||||||
return;
|
return;
|
||||||
@@ -243,9 +304,10 @@ async function loadData() {
|
|||||||
try {
|
try {
|
||||||
const domain = domainFilter.value || undefined;
|
const domain = domainFilter.value || undefined;
|
||||||
const order = getVisitOrderParam();
|
const order = getVisitOrderParam();
|
||||||
const [overviewRes, pagesRes] = await Promise.all([
|
const [overviewRes, pagesRes, likeStatsRes] = await Promise.all([
|
||||||
fetchVisitOverview(domain),
|
fetchVisitOverview(domain),
|
||||||
fetchVisitPages(domain, order),
|
fetchVisitPages(domain, order),
|
||||||
|
fetchLikeStats(),
|
||||||
]);
|
]);
|
||||||
overview.value = {
|
overview.value = {
|
||||||
totalPv: overviewRes.totalPv,
|
totalPv: overviewRes.totalPv,
|
||||||
@@ -257,7 +319,10 @@ async function loadData() {
|
|||||||
? overviewRes.last30Days
|
? overviewRes.last30Days
|
||||||
: [],
|
: [],
|
||||||
};
|
};
|
||||||
items.value = pagesRes.items || [];
|
const likeItemsRaw = Array.isArray(likeStatsRes.items) ? likeStatsRes.items : [];
|
||||||
|
likeStatsItems.value = filterLikeStatsByDomain(likeItemsRaw, domain);
|
||||||
|
const pageItems = Array.isArray(pagesRes.items) ? pagesRes.items : [];
|
||||||
|
items.value = pageItems;
|
||||||
last30Days.value = Array.isArray(overviewRes.last30Days)
|
last30Days.value = Array.isArray(overviewRes.last30Days)
|
||||||
? overviewRes.last30Days
|
? overviewRes.last30Days
|
||||||
: [];
|
: [];
|
||||||
@@ -282,7 +347,8 @@ async function loadVisitPagesOnly() {
|
|||||||
const domain = domainFilter.value || undefined;
|
const domain = domainFilter.value || undefined;
|
||||||
const order = getVisitOrderParam();
|
const order = getVisitOrderParam();
|
||||||
const pagesRes = await fetchVisitPages(domain, order);
|
const pagesRes = await fetchVisitPages(domain, order);
|
||||||
items.value = pagesRes.items || [];
|
const pageItems = Array.isArray(pagesRes.items) ? pagesRes.items : [];
|
||||||
|
items.value = pageItems;
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
const msg = e.message || "加载访问统计数据失败";
|
const msg = e.message || "加载访问统计数据失败";
|
||||||
error.value = msg;
|
error.value = msg;
|
||||||
@@ -514,6 +580,21 @@ watch(domainFilter, () => {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.domain-cell-rank {
|
||||||
|
flex: 0 0 60px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.domain-cell-like {
|
||||||
|
flex: 0 0 80px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.domain-cell-like-rate {
|
||||||
|
flex: 0 0 90px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
.domain-cell-time {
|
.domain-cell-time {
|
||||||
flex: 0 0 170px;
|
flex: 0 0 170px;
|
||||||
}
|
}
|
||||||
@@ -539,6 +620,18 @@ watch(domainFilter, () => {
|
|||||||
width: 100px;
|
width: 100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.domain-cell-rank {
|
||||||
|
width: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.domain-cell-like {
|
||||||
|
width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.domain-cell-like-rate {
|
||||||
|
width: 90px;
|
||||||
|
}
|
||||||
|
|
||||||
.domain-cell-time {
|
.domain-cell-time {
|
||||||
width: 150px;
|
width: 150px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export class CWDComments {
|
|||||||
* 以下字段由组件自动推导或从后端读取,无需通过 config 传入:
|
* 以下字段由组件自动推导或从后端读取,无需通过 config 传入:
|
||||||
* - postSlug:window.location.origin + window.location.pathname
|
* - postSlug:window.location.origin + window.location.pathname
|
||||||
* - postTitle:document.title 或 postSlug
|
* - postTitle:document.title 或 postSlug
|
||||||
* - postUrl:window.location.href
|
* - postUrl:window.location.origin + window.location.pathname
|
||||||
* - avatarPrefix/adminEmail/adminBadge:通过 /api/config/comments 接口获取
|
* - avatarPrefix/adminEmail/adminBadge:通过 /api/config/comments 接口获取
|
||||||
*/
|
*/
|
||||||
constructor(config) {
|
constructor(config) {
|
||||||
@@ -35,7 +35,7 @@ export class CWDComments {
|
|||||||
this.config.postTitle = document.title || this.config.postSlug;
|
this.config.postTitle = document.title || this.config.postSlug;
|
||||||
}
|
}
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
this.config.postUrl = window.location.href;
|
this.config.postUrl = window.location.origin + window.location.pathname;
|
||||||
}
|
}
|
||||||
this.hostElement = this._resolveElement(config.el);
|
this.hostElement = this._resolveElement(config.el);
|
||||||
this.shadowRoot = null;
|
this.shadowRoot = null;
|
||||||
@@ -522,7 +522,7 @@ export class CWDComments {
|
|||||||
this.config.postTitle = document.title || this.config.postSlug;
|
this.config.postTitle = document.title || this.config.postSlug;
|
||||||
}
|
}
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
this.config.postUrl = window.location.href;
|
this.config.postUrl = window.location.origin + window.location.pathname;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新主题
|
// 更新主题
|
||||||
|
|||||||
Reference in New Issue
Block a user