-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
feat(sidebar/widget): add 51LA visitor statistics sidebar widget #435
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 1 commit
1b6295a
a37a567
14a997b
8e6c21a
6037ca1
15ffa42
a0f59f6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,133 @@ | ||
| --- | ||
| 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 || ""; | ||
|
|
||
| // 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 }}> | ||
| (function() { | ||
| // 验证 ID 格式:仅允许字母数字字符,防止 URL 注入 | ||
| if (!la51Id || !/^[A-Za-z0-9]+$/.test(la51Id)) return; | ||
|
|
||
| // 索引到键名的映射,与 51LA quote.js 接口返回的 <p><span>索引</span><span>值</span></p> 格式对应 | ||
| var INDEX_MAP = ["la-online", "la-today-uv", "la-today-pv", "la-yesterday-uv", "la-yesterday-pv", "la-month-pv", "la-total-pv"]; | ||
|
|
||
| function updateLaStats() { | ||
| var url = "https://v6-widget.51.la/v6/" + la51Id + "/quote.js"; | ||
|
|
||
| fetch(url) | ||
| .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; | ||
|
Comment on lines
+111
to
+112
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion (bug_risk): 通过对 当前实现依赖 51LA 的脚本使用形如 建议实现: 更新后的正则
新增的 Original comment in Englishsuggestion (bug_risk): Parsing It currently relies on 51LA’s script using Suggested implementation: The updated regex
The new |
||
|
|
||
| var html = match[1]; | ||
| var pRegex = /<p><span>(\d+)<\/span><span>(\d+)<\/span><\/p>/g; | ||
| var m; | ||
|
|
||
| while ((m = pRegex.exec(html)) !== null) { | ||
| var idx = parseInt(m[1], 10); | ||
| var val = parseInt(m[2], 10); | ||
| if (idx >= 0 && idx < INDEX_MAP.length) { | ||
| var elements = document.querySelectorAll('[data-stat-id="' + INDEX_MAP[idx] + '"]'); | ||
| elements.forEach(function(el) { | ||
| el.textContent = val.toLocaleString(); | ||
|
sourcery-ai[bot] marked this conversation as resolved.
Outdated
|
||
| }); | ||
| } | ||
| } | ||
| }) | ||
| .catch(function(err) { | ||
| console.error("LA Stats load error:", err); | ||
| }); | ||
| } | ||
|
|
||
| // 页面加载时更新 | ||
| updateLaStats(); | ||
|
|
||
| // 页面切换时重新更新 | ||
| document.addEventListener("swup:contentReplaced", function() { | ||
| setTimeout(updateLaStats, 100); | ||
| }); | ||
| })(); | ||
| </script> | ||
| )} | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion (bug_risk): 目前的
stats定义和INDEX_MAP需要手动保持同步,随着时间推移很容易被改坏。由于映射关系一部分在
stats中,一部分在INDEX_MAP中,任何重排或新增项都可能在没有明显迹象的情况下导致标签与数值错位。更安全的做法是从单一信息源派生出INDEX_MAP(例如在每个stats条目上存 51LA 的索引,然后据此生成映射)。建议实现:
要完整落地这个“单一信息源”的建议,还需要:
LaStats.astro文件中更前面(或从外部导入)定义了一个stats数组,用于渲染统计信息的 UI;stats条目上增加一个数值型字段laIndex,与 51LA 的索引一一对应:la-online→laIndex: 0la-today-uv→laIndex: 1la-today-pv→laIndex: 2la-yesterday-uv→laIndex: 3la-yesterday-pv→laIndex: 4la-month-pv→laIndex: 5la-total-pv→laIndex: 6stats项并非由 51LA 提供数据,则不要为其设置laIndex;派生出的INDEX_MAP只会包含设了laIndex的条目。stats是唯一的“单一信息源”。Original comment in English
suggestion (bug_risk): The
statsdefinition andINDEX_MAPneed to stay manually in sync, which is easy to break over time.Because the mapping lives partly in
statsand partly inINDEX_MAP, any reordering or additions can silently desync labels and values. It would be safer to deriveINDEX_MAPfrom a single source of truth (for example, store the 51LA index on eachstatsentry and generate the map from that).Suggested implementation:
To fully implement the “single source of truth” suggestion, you should also:
statsarray defined earlier inLaStats.astro(or imported into it) that is used to render the stats UI.laIndexnumeric field to each relevantstatsentry corresponding to the 51LA index:la-online→laIndex: 0la-today-uv→laIndex: 1la-today-pv→laIndex: 2la-yesterday-uv→laIndex: 3la-yesterday-pv→laIndex: 4la-month-pv→laIndex: 5la-total-pv→laIndex: 6statsentries are not backed by 51LA, omitlaIndexon those; the derivedINDEX_MAPwill only include entries wherelaIndexis set.statsas the single source of truth.