Skip to content
2 changes: 2 additions & 0 deletions src/components/layout/SideBar.astro
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Announcement from "@/components/widget/Announcement.astro";
import Calendar from "@/components/widget/Calendar.astro";
import Categories from "@/components/widget/Categories.astro";
import Dynamic from "@/components/widget/Dynamic.astro";
import LaStats from "@/components/widget/LaStats.astro";
import Music from "@/components/widget/Music.astro";
import Profile from "@/components/widget/Profile.astro";
import SidebarTOC from "@/components/widget/SidebarTOC.astro";
Expand Down Expand Up @@ -49,6 +50,7 @@ const componentMap = {
sidebarToc: SidebarTOC,
advertisement: Advertisement,
stats: SiteStats,
lastats: LaStats,
calendar: Calendar,
music: Music,
siteInfo: SiteInfo,
Expand Down
161 changes: 161 additions & 0 deletions src/components/widget/LaStats.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
---
import { Icon } from "astro-icon/components";
import WidgetLayout from "@/components/common/WidgetLayout.astro";
import { analyticsConfig } from "@/config";
import I18nKey from "@/i18n/i18nKey";
import { i18n } from "@/i18n/translation";
import type { WidgetComponentConfig } from "@/types/config";

interface Props {
class?: string;
style?: string;
widgetConfig?: WidgetComponentConfig;
}

const { class: className, style, widgetConfig } = Astro.props;

const showTitle = widgetConfig?.showTitle !== false;

const la51Id = analyticsConfig?.la51Analytics?.Id || "";
const la51StatsDataSource =
analyticsConfig?.la51Analytics?.statsDataSource || "quotejs";
const la51StatsApiUrl = analyticsConfig?.la51Analytics?.statsApiUrl || "";

// 51LA 统计指标定义
const stats = [
{
icon: "material-symbols:person-outline",
label: i18n(I18nKey.laStatsOnline),
id: "la-online",
},
{
icon: "material-symbols:calendar-today-outline",
label: i18n(I18nKey.laStatsTodayUV),
id: "la-today-uv",
},
{
icon: "material-symbols:visibility-outline",
label: i18n(I18nKey.laStatsTodayPV),
id: "la-today-pv",
},
{
icon: "material-symbols:calendar-month-outline",
label: i18n(I18nKey.laStatsYesterdayUV),
id: "la-yesterday-uv",
},
{
icon: "material-symbols:visibility-outline",
label: i18n(I18nKey.laStatsYesterdayPV),
id: "la-yesterday-pv",
},
{
icon: "material-symbols:calendar-clock-outline",
label: i18n(I18nKey.laStatsMonthPV),
id: "la-month-pv",
},
{
icon: "mingcute:chart-line-line",
label: i18n(I18nKey.laStatsTotalPV),
id: "la-total-pv",
},
];
---

{la51Id && (
<WidgetLayout name={i18n(I18nKey.laStats)} showTitle={showTitle} id="la-stats" class={className} style={style}>
<div class="flex flex-col gap-2">
{stats.map((stat) => (
<div class="flex items-center justify-between px-3 py-1.5">
<div class="flex items-center gap-2.5">
<div class="text-(--primary) text-xl">
<Icon is:inline name={stat.icon} />
</div>
<span class="text-neutral-700 dark:text-neutral-300 font-medium text-sm">
{stat.label}
</span>
</div>
<span
class="text-base font-bold text-neutral-900 dark:text-neutral-100"
data-stat-id={stat.id}>
-
</span>
</div>
))}
</div>
</WidgetLayout>
)}

<script is:inline define:vars={{ la51Id, la51StatsDataSource, la51StatsApiUrl }}>
(function() {
// 验证 ID 格式:仅允许字母数字字符,防止 URL 注入
if (!la51Id || !/^[A-Za-z0-9]+$/.test(la51Id)) return;

// 全局去重:多侧边栏实例共享同一份逻辑,只初始化一次
var ns = window.__laStats || (window.__laStats = { inited: false });
if (ns.inited) return;
ns.inited = true;

var STAT_IDS = ["la-online", "la-today-uv", "la-today-pv", "la-yesterday-uv", "la-yesterday-pv", "la-month-pv", "la-total-pv"];

function setStatValues(values) {
for (var i = 0; i < values.length && i < STAT_IDS.length; i++) {
var els = document.querySelectorAll('[data-stat-id="' + STAT_IDS[i] + '"]');
els.forEach(function(el) { el.textContent = values[i].toLocaleString(); });
}
}

function updateViaQuoteJs() {
fetch("https://v6-widget.51.la/v6/" + la51Id + "/quote.js")
.then(function(res) { if (!res.ok) throw new Error("HTTP " + res.status); return res.text(); })
.then(function(text) {
var match = text.match(/r\.innerHTML\s*=\s*"([^"]+)"/);
if (!match) return;
var html = match[1];
var pRegex = /<p><span>(\d+)<\/span><span>(\d+)<\/span><\/p>/g;
var map = {};
var m;
while ((m = pRegex.exec(html)) !== null) map[parseInt(m[1], 10)] = parseInt(m[2], 10);
var values = [];
for (var i = 0; i < STAT_IDS.length; i++) values.push(map[i] || 0);
setStatValues(values);
})
.catch(function(err) { console.error("LA Stats (quotejs) error:", err); });
}

function updateViaApi() {
if (!la51StatsApiUrl) { console.error("LA Stats: statsApiUrl is empty"); return; }
var url = la51StatsApiUrl.indexOf("{id}") !== -1
? la51StatsApiUrl.replace("{id}", la51Id)
: la51StatsApiUrl.replace(/\/+$/, "") + "/" + la51Id;
fetch(url)
.then(function(res) { if (!res.ok) throw new Error("HTTP " + res.status); return res.json(); })
.then(function(json) {
if (!json.success || !json.data) return;
var d = json.data;
setStatValues([
d.recentActive || 0,
(d.today && d.today.visitors) || 0,
(d.today && d.today.views) || 0,
(d.yesterday && d.yesterday.visitors) || 0,
(d.yesterday && d.yesterday.views) || 0,
d.monthViews || 0,
d.totalViews || 0
]);
})
.catch(function(err) { console.error("LA Stats (api) error:", err); });
}

function updateLaStats() {
if (la51StatsDataSource === "api") {
updateViaApi();
} else {
updateViaQuoteJs();
}
}

updateLaStats();
document.addEventListener("swup:contentReplaced", function() {
setTimeout(updateLaStats, 100);
});
})();
</script>
9 changes: 9 additions & 0 deletions src/config/analyticsConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,14 @@ export const analyticsConfig: AnalyticsConfig = {
hashMode: false,
// 是否开启网站录屏功能
screenRecord: true,
// 数据挂件数据源:"quotejs" 从官方 quote.js 解析,"api" 从第三方 API 获取 JSON
statsDataSource: "quotejs",
// 第三方 API 地址模板,仅 statsDataSource 为 "api" 时生效
// 自建API参考: https://jishuzhan.net/article/1997120577757315073
// 使用 {id} 占位符代表 51la 统计 ID,也可省略则自动追加 /{id}
// 示例:"https://51la.api.trustdev.org/{id}"
// "https://your-worker.workers.dev?id={id}"
// "https://51la.api.trustdev.org" (自动追加 /{id})
statsApiUrl: "",
},
};
18 changes: 18 additions & 0 deletions src/config/sidebarConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,16 @@ export const sidebarLayoutConfig: SidebarLayoutConfig = {
// 是否在文章详情页显示
showOnPostPage: false,
},
{
// 组件类型:51LA访问统计组件
type: "lastats",
// 是否启用该组件
enable: true,
// 组件位置
position: "top",
// 是否在文章详情页显示
showOnPostPage: true,
},
{
// 组件类型:站点信息组件
type: "siteInfo",
Expand Down Expand Up @@ -303,6 +313,14 @@ export const sidebarLayoutConfig: SidebarLayoutConfig = {
// 是否在文章详情页显示
showOnPostPage: true,
},
{
// 组件类型:51LA访问统计组件
type: "lastats",
// 是否启用该组件
enable: true,
// 是否在文章详情页显示
showOnPostPage: true,
},
{
// 组件类型:站点信息组件
type: "siteInfo",
Expand Down
10 changes: 10 additions & 0 deletions src/i18n/i18nKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,16 @@ enum I18nKey {
siteStatsDays = "siteStatsDays",
today = "today",

// 51LA 访问统计
laStats = "laStats",
laStatsOnline = "laStatsOnline",
laStatsTodayUV = "laStatsTodayUV",
laStatsTodayPV = "laStatsTodayPV",
laStatsYesterdayUV = "laStatsYesterdayUV",
laStatsYesterdayPV = "laStatsYesterdayPV",
laStatsMonthPV = "laStatsMonthPV",
laStatsTotalPV = "laStatsTotalPV",

// 站点信息
siteInfo = "siteInfo",
siteInfoBuildTime = "siteInfoBuildTime",
Expand Down
10 changes: 10 additions & 0 deletions src/i18n/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,16 @@ export const en: Translation = {
[Key.siteStatsDays]: "{days} days",
[Key.today]: "Today",

// 51LA Visitor Statistics
[Key.laStats]: "Visitor Statistics",
[Key.laStatsOnline]: "Online Now",
[Key.laStatsTodayUV]: "Today Visitors",
[Key.laStatsTodayPV]: "Today Views",
[Key.laStatsYesterdayUV]: "Yesterday Visitors",
[Key.laStatsYesterdayPV]: "Yesterday Views",
[Key.laStatsMonthPV]: "This Month Views",
[Key.laStatsTotalPV]: "Total Views",

// Site Info
[Key.siteInfo]: "Site Info",
[Key.siteInfoBuildTime]: "Build Time",
Expand Down
10 changes: 10 additions & 0 deletions src/i18n/languages/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,16 @@ export const ja: Translation = {
[Key.siteStatsDays]: "{days} 日",
[Key.today]: "今日",

// 51LA アクセス統計
[Key.laStats]: "アクセス統計",
[Key.laStatsOnline]: "現在オンライン",
[Key.laStatsTodayUV]: "本日の訪問者",
[Key.laStatsTodayPV]: "本日の閲覧数",
[Key.laStatsYesterdayUV]: "昨日の訪問者",
[Key.laStatsYesterdayPV]: "昨日の閲覧数",
[Key.laStatsMonthPV]: "今月の閲覧数",
[Key.laStatsTotalPV]: "累計閲覧数",

// サイト情報
[Key.siteInfo]: "サイト情報",
[Key.siteInfoBuildTime]: "ビルド日時",
Expand Down
10 changes: 10 additions & 0 deletions src/i18n/languages/ko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,16 @@ export const ko: Translation = {
[Key.siteStatsDays]: "{days}일",
[Key.today]: "오늘",

// 51LA Visitor Statistics
[Key.laStats]: "방문자 통계",
[Key.laStatsOnline]: "현재 온라인",
[Key.laStatsTodayUV]: "오늘 방문자",
[Key.laStatsTodayPV]: "오늘 조회수",
[Key.laStatsYesterdayUV]: "어제 방문자",
[Key.laStatsYesterdayPV]: "어제 조회수",
[Key.laStatsMonthPV]: "이번 달 조회수",
[Key.laStatsTotalPV]: "총 조회수",

// Site Info
[Key.siteInfo]: "사이트 정보",
[Key.siteInfoBuildTime]: "빌드 시각",
Expand Down
10 changes: 10 additions & 0 deletions src/i18n/languages/ru.ts
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,16 @@ export const ru: Translation = {
[Key.siteStatsDays]: "{days} дней",
[Key.today]: "Сегодня",

// 51LA Статистика посещений
[Key.laStats]: "Статистика посещений",
[Key.laStatsOnline]: "Сейчас онлайн",
[Key.laStatsTodayUV]: "Посетители сегодня",
[Key.laStatsTodayPV]: "Просмотры сегодня",
[Key.laStatsYesterdayUV]: "Посетители вчера",
[Key.laStatsYesterdayPV]: "Просмотры вчера",
[Key.laStatsMonthPV]: "Просмотры за месяц",
[Key.laStatsTotalPV]: "Всего просмотров",

// Информация о сайте
[Key.siteInfo]: "Информация о сайте",
[Key.siteInfoBuildTime]: "Время сборки",
Expand Down
10 changes: 10 additions & 0 deletions src/i18n/languages/zh_CN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,16 @@ export const zh_CN: Translation = {
[Key.siteStatsDays]: "{days} 天",
[Key.today]: "今天",

// 51LA 访问统计
[Key.laStats]: "访问统计",
[Key.laStatsOnline]: "当前在线",
[Key.laStatsTodayUV]: "今日访客",
[Key.laStatsTodayPV]: "今日浏览",
[Key.laStatsYesterdayUV]: "昨日访客",
[Key.laStatsYesterdayPV]: "昨日浏览",
[Key.laStatsMonthPV]: "本月浏览",
[Key.laStatsTotalPV]: "累计浏览",

// 站点信息
[Key.siteInfo]: "站点信息",
[Key.siteInfoBuildTime]: "构建时间",
Expand Down
10 changes: 10 additions & 0 deletions src/i18n/languages/zh_TW.ts
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,16 @@ export const zh_TW: Translation = {
[Key.siteStatsDays]: "{days} 天",
[Key.today]: "今天",

// 51LA 訪問統計
[Key.laStats]: "訪問統計",
[Key.laStatsOnline]: "當前在線",
[Key.laStatsTodayUV]: "今日訪客",
[Key.laStatsTodayPV]: "今日瀏覽",
[Key.laStatsYesterdayUV]: "昨日訪客",
[Key.laStatsYesterdayPV]: "昨日瀏覽",
[Key.laStatsMonthPV]: "本月瀏覽",
[Key.laStatsTotalPV]: "累計瀏覽",

// 站點資訊
[Key.siteInfo]: "站點資訊",
[Key.siteInfoBuildTime]: "構建時間",
Expand Down
2 changes: 2 additions & 0 deletions src/types/analyticsConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,7 @@ export type AnalyticsConfig = {
autoTrack?: boolean; // 开启事件分析功能,默认 true
hashMode?: boolean; // 单页面应用统计(Vue/React 等),默认 false
screenRecord?: boolean; // 开启网站录屏功能,默认 true
statsDataSource?: "quotejs" | "api"; // 数据挂件数据源:"quotejs" 从官方 quote.js 解析,"api" 从第三方 API 获取 JSON
statsApiUrl?: string; // 第三方 API 地址模板,仅 statsDataSource 为 "api" 时生效,使用 {id} 占位符代表 Id
};
};
1 change: 1 addition & 0 deletions src/types/sidebarConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export type WidgetComponentType =
| "sidebarToc"
| "advertisement"
| "stats"
| "lastats"
| "calendar"
| "music"
| "siteInfo"
Expand Down
Loading