feat(sidebar/widget): add 51LA visitor statistics sidebar widget - #435
feat(sidebar/widget): add 51LA visitor statistics sidebar widget#435jacksen168sub wants to merge 7 commits into
Conversation
feat(侧边栏/组件): 新增51LA访问统计侧边栏组件 This commit fully implements the 51LA visitor statistics sidebar widget feature. New Features: - **LaStats widget**: Add new sidebar widget component for 51LA analytics, supports displaying online users, today UV/PV, yesterday UV/PV, monthly and total views - **Type definition**: Add `lastats` widget type in `src/types/sidebarConfig.ts` - **I18n support**: Add multi-language translation entries for 51LA stats across all supported languages - **Config preset**: Pre-configure the LaStats widget in default sidebar layout config 本提交完整实现了51LA访问统计侧边栏组件功能: 新增功能: - **LaStats组件**:新增51LA分析侧边栏组件,支持展示当前在线人数、今日访客/浏览量、昨日访客/浏览量、本月和总浏览量 - **类型定义**:在`src/types/sidebarConfig.ts`中新增`lastats`组件类型 - **国际化支持**:为所有支持语言添加51LA统计相关的多语言翻译条目 - **配置预设**:在默认侧边栏布局配置中预启用LaStats组件
✅ Deploy Preview for demo-firefly ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
Reviewer's Guide添加一个新的 51LA 访客统计侧边栏小部件(LaStats),将其接入侧边栏配置和组件映射,并扩展分析配置与 i18n 键/翻译,以支持可配置的数据源以及新统计展示的本地化标签。 LaStats 小部件数据加载时序图sequenceDiagram
actor User
participant Browser
participant LaStatsWidget as LaStats_astro
participant AnalyticsConfig as analyticsConfig
participant QuoteJs as La51_quote_js
participant CustomApi as statsApi
User ->> Browser: load sidebar page
Browser ->> LaStatsWidget: render with la51Id, la51StatsDataSource, la51StatsApiUrl
LaStatsWidget ->> AnalyticsConfig: read la51Analytics.Id
AnalyticsConfig -->> LaStatsWidget: la51Id
LaStatsWidget ->> Browser: inline script
Browser ->> LaStatsWidget: updateLaStats()
alt la51StatsDataSource === "api"
LaStatsWidget ->> CustomApi: fetch(statsApiUrl)
CustomApi -->> LaStatsWidget: JSON { success, data }
LaStatsWidget ->> LaStatsWidget: setStatValues([...])
else la51StatsDataSource === "quotejs"
LaStatsWidget ->> QuoteJs: fetch("https://v6-widget.51.la/v6/" + la51Id + "/quote.js")
QuoteJs -->> LaStatsWidget: quote.js text
LaStatsWidget ->> LaStatsWidget: updateViaQuoteJs()
LaStatsWidget ->> LaStatsWidget: setStatValues([...])
end
Browser ->> Browser: render values into [data-stat-id] elements
Browser ->> LaStatsWidget: on swup:contentReplaced
LaStatsWidget ->> Browser: setTimeout(updateLaStats, 100)
LaStats 小部件在侧边栏集成的流程图flowchart LR
sidebarLayoutConfig -->|type lastats| SideBar_astro
sidebarLayoutConfig --> analyticsConfig
SideBar_astro -->|componentMap.lastats| LaStats_astro
LaStats_astro --> analyticsConfig
LaStats_astro --> i18n
i18n --> I18nLanguages
LaStats_astro -->|stats display| SidebarDOM
文件级变更
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
Original review guide in EnglishReviewer's GuideAdds a new 51LA visitor statistics sidebar widget (LaStats), wires it into the sidebar configuration and component map, and extends analytics configuration and i18n keys/translations to support configurable data sources and localized labels for the new stats display. Sequence diagram for LaStats widget data loadingsequenceDiagram
actor User
participant Browser
participant LaStatsWidget as LaStats_astro
participant AnalyticsConfig as analyticsConfig
participant QuoteJs as La51_quote_js
participant CustomApi as statsApi
User ->> Browser: load sidebar page
Browser ->> LaStatsWidget: render with la51Id, la51StatsDataSource, la51StatsApiUrl
LaStatsWidget ->> AnalyticsConfig: read la51Analytics.Id
AnalyticsConfig -->> LaStatsWidget: la51Id
LaStatsWidget ->> Browser: inline script
Browser ->> LaStatsWidget: updateLaStats()
alt la51StatsDataSource === "api"
LaStatsWidget ->> CustomApi: fetch(statsApiUrl)
CustomApi -->> LaStatsWidget: JSON { success, data }
LaStatsWidget ->> LaStatsWidget: setStatValues([...])
else la51StatsDataSource === "quotejs"
LaStatsWidget ->> QuoteJs: fetch("https://v6-widget.51.la/v6/" + la51Id + "/quote.js")
QuoteJs -->> LaStatsWidget: quote.js text
LaStatsWidget ->> LaStatsWidget: updateViaQuoteJs()
LaStatsWidget ->> LaStatsWidget: setStatValues([...])
end
Browser ->> Browser: render values into [data-stat-id] elements
Browser ->> LaStatsWidget: on swup:contentReplaced
LaStatsWidget ->> Browser: setTimeout(updateLaStats, 100)
Flow diagram for LaStats widget integration in sidebarflowchart LR
sidebarLayoutConfig -->|type lastats| SideBar_astro
sidebarLayoutConfig --> analyticsConfig
SideBar_astro -->|componentMap.lastats| LaStats_astro
LaStats_astro --> analyticsConfig
LaStats_astro --> i18n
i18n --> I18nLanguages
LaStats_astro -->|stats display| SidebarDOM
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - 我发现了 1 个问题,并留下了一些整体性的反馈:
- 在
LaStats.astro中,INDEX_MAP重复了stats数组中已经定义的 ID,这有导致两者不同步的风险;建议直接从stats衍生出这个映射(例如const INDEX_MAP = stats.map(s => s.id)),这样就只有一个可信来源。 - 用于解析
quote.js的正则(r.innerHTML = "..."以及<p><span>...</span><span>...</span></p>这一模式)相当严格,假设不存在额外的空白字符/属性,也假设引文不会改变;你可能需要适当放宽它们(例如允许空白和非贪婪匹配,或者在提取出的 HTML 上使用 DOM 解析器),以便在上游标记略有变化时更具鲁棒性。
供 AI 代理使用的提示词
Please address the comments from this code review:
## Overall Comments
- 在 `LaStats.astro` 中,`INDEX_MAP` 重复了 `stats` 数组中已经定义的 ID,这有导致两者不同步的风险;建议直接从 `stats` 衍生出这个映射(例如 `const INDEX_MAP = stats.map(s => s.id)`),这样就只有一个可信来源。
- 用于解析 `quote.js` 的正则(`r.innerHTML = "..."` 以及 `<p><span>...</span><span>...</span></p>` 这一模式)相当严格,假设不存在额外的空白字符/属性,也假设引文不会改变;你可能需要适当放宽它们(例如允许空白和非贪婪匹配,或者在提取出的 HTML 上使用 DOM 解析器),以便在上游标记略有变化时更具鲁棒性。
## Individual Comments
### Comment 1
<location path="src/components/widget/LaStats.astro" line_range="110-114" />
<code_context>
+
+ 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();
+ });
+ }
</code_context>
<issue_to_address>
**issue (bug_risk):** 在调用 toLocaleString 之前,对非数值或意外的值进行保护,以避免运行时错误。
如果 `m[2]` 缺失或为非数值(例如由于格式变更),`parseInt` 会返回 `NaN`,而此时调用 `val.toLocaleString()` 会抛出异常。建议使用 `!Number.isNaN(val)` 做保护检查,在解析值无效时跳过更新,或使用安全的回退值(例如 `'-'`)。
</issue_to_address>帮我变得更有用!请在每条评论上点 👍 或 👎,我会根据你的反馈改进后续评审。
Original comment in English
Hey - I've found 1 issue, and left some high level feedback:
- In
LaStats.astro,INDEX_MAPduplicates the IDs already defined in thestatsarray, which risks them getting out of sync; consider deriving the mapping directly fromstats(e.g.,const INDEX_MAP = stats.map(s => s.id)) so there's a single source of truth. - The regexes used to parse
quote.js(r.innerHTML = "..."and the<p><span>...</span><span>...</span></p>pattern) are quite strict and assume no extra whitespace/attributes or quote changes; you might want to loosen them slightly (e.g., allow whitespace and non-greedy matches, or use a DOM parser on the extracted HTML) to be more robust against minor upstream markup changes.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- In `LaStats.astro`, `INDEX_MAP` duplicates the IDs already defined in the `stats` array, which risks them getting out of sync; consider deriving the mapping directly from `stats` (e.g., `const INDEX_MAP = stats.map(s => s.id)`) so there's a single source of truth.
- The regexes used to parse `quote.js` (`r.innerHTML = "..."` and the `<p><span>...</span><span>...</span></p>` pattern) are quite strict and assume no extra whitespace/attributes or quote changes; you might want to loosen them slightly (e.g., allow whitespace and non-greedy matches, or use a DOM parser on the extracted HTML) to be more robust against minor upstream markup changes.
## Individual Comments
### Comment 1
<location path="src/components/widget/LaStats.astro" line_range="110-114" />
<code_context>
+
+ 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();
+ });
+ }
</code_context>
<issue_to_address>
**issue (bug_risk):** Guard against non-numeric or unexpected values before calling toLocaleString to avoid runtime errors.
If `m[2]` is missing or non-numeric (e.g. due to a format change), `parseInt` will yield `NaN` and `val.toLocaleString()` will throw. Consider guarding with `!Number.isNaN(val)` and either skipping the update or using a safe fallback (e.g. `'-'`) when the parsed value is invalid.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
…aStats.astro fix(components/widget): 修复因自动合并冲突导致的 LaStats.astro 组件模板语法错误 The automatic merge resolution introduced a misplaced closing curly brace in the LaStats.astro component template, resulting in invalid template syntax. This fix restores the correct structure and resolves the compilation error. 修复因自动合并冲突导致的 LaStats.astro 组件模板语法错误。 合并过程中引入了一个位置错误的闭合大括号,导致模板语法无效。
There was a problem hiding this comment.
Hey - 我这边发现了两个问题,并且有一些整体性的反馈:
quote.js的解析逻辑依赖非常具体的字符串模式(例如r.innerHTML = "..."以及固定的<p><span>标记结构),一旦 51LA 修改实现,这里就会在没有任何提示的情况下失效;建议增加一层小的抽象(配合注释和后备 UI 状态),既方便后续修改,也更容易在解析失败时被发现。- 每次触发
swup:contentReplaced事件都会重新向 51LA 发起一次请求;可以考虑加一个带短 TTL 的内存缓存,既能减少不必要的网络请求、避免配额/限流问题,又能在合理范围内保持统计数据的实时性。
给 AI Agent 的提示
Please address the comments from this code review:
## Overall Comments
- The parsing logic for `quote.js` relies on very specific string patterns (e.g. `r.innerHTML = "..."` and fixed `<p><span>` markup), which will break silently if 51LA changes their implementation; consider adding a small abstraction (with comments and a fallback UI state) to make this easier to update or detect when parsing fails.
- Each `swup:contentReplaced` event triggers a new fetch to 51LA; adding a simple in-memory cache with a short TTL would reduce unnecessary network calls and avoid rate/quotas issues while still keeping the stats reasonably fresh.
## Individual Comments
### Comment 1
<location path="src/components/widget/LaStats.astro" line_range="102-103" />
<code_context>
+ return res.text();
+ })
+ .then(function(text) {
+ var match = text.match(/r\.innerHTML\s*=\s*"([^"]+)"/);
+ if (!match) return;
+
+ var html = match[1];
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Parsing `quote.js` via a very specific regex on `r.innerHTML` is brittle to upstream implementation changes.
It currently relies on 51LA’s script using `r.innerHTML="..."` with that exact variable name, double quotes, and assignment shape. Any variation (e.g. single quotes, different variable, or spacing change) will break extraction and silently stop updating stats. Please make the regex more tolerant to these variations, and/or log when `match` is falsy so failures are visible.
Suggested implementation:
```
.then(function(text) {
// Match any innerHTML assignment, allowing for different variable names,
// single or double quotes, and flexible whitespace/newlines.
var match = text.match(/innerHTML\s*=\s*(["'])([\s\S]*?)\1/);
if (!match) {
console.warn("51.LA stats: failed to extract innerHTML from quote.js", { url: url, textSample: text.slice(0, 200) });
return;
}
var html = match[2];
```
The updated regex `/innerHTML\s*=\s*(["'])([\s\S]*?)\1/` now:
- Ignores the specific variable name (`r`) and focuses on any `innerHTML` assignment.
- Supports both single and double quotes.
- Handles arbitrary spacing and newlines between `=` and the assigned string.
The new `console.warn` ensures that if extraction fails, there is a visible log with the URL and a small sample of the response body to aid debugging. No other changes are required if this is the only place that parses `quote.js`.
</issue_to_address>
### Comment 2
<location path="src/components/widget/LaStats.astro" line_range="91-100" />
<code_context>
+ 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;
+
+ 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) {
</code_context>
<issue_to_address>
**suggestion (bug_risk):** The `stats` definition and `INDEX_MAP` need to stay manually in sync, which is easy to break over time.
Because the mapping lives partly in `stats` and partly in `INDEX_MAP`, any reordering or additions can silently desync labels and values. It would be safer to derive `INDEX_MAP` from a single source of truth (for example, store the 51LA index on each `stats` entry and generate the map from that).
Suggested implementation:
```
// 通过 stats 配置派生出“51LA 索引 → statId”的映射,避免手动维护两份列表
// 期望在上方有形如:
// const stats = [
// { id: "la-online", /* ... */, laIndex: 0 },
// { id: "la-today-uv", /* ... */, laIndex: 1 },
// ...
// ];
var INDEX_MAP = (function() {
var map = [];
if (Array.isArray(stats)) {
stats.forEach(function(stat) {
if (
stat &&
typeof stat.laIndex === "number" &&
stat.laIndex >= 0
) {
map[stat.laIndex] = stat.id;
}
});
}
return map;
})();
```
```
var idx = parseInt(m[1], 10);
var val = parseInt(m[2], 10);
var statId = INDEX_MAP[idx];
if (typeof statId === "string") {
var elements = document.querySelectorAll('[data-stat-id="' + statId + '"]');
```
To fully implement the “single source of truth” suggestion, you should also:
1. Ensure there is a `stats` array defined earlier in `LaStats.astro` (or imported into it) that is used to render the stats UI.
2. Add a `laIndex` numeric field to each relevant `stats` entry corresponding to the 51LA index:
- Example:
- `la-online` → `laIndex: 0`
- `la-today-uv` → `laIndex: 1`
- `la-today-pv` → `laIndex: 2`
- `la-yesterday-uv` → `laIndex: 3`
- `la-yesterday-pv` → `laIndex: 4`
- `la-month-pv` → `laIndex: 5`
- `la-total-pv` → `laIndex: 6`
3. If some `stats` entries are not backed by 51LA, omit `laIndex` on those; the derived `INDEX_MAP` will only include entries where `laIndex` is set.
4. Remove any old hard-coded arrays or mappings that duplicate this index information elsewhere to keep `stats` as the single source of truth.
</issue_to_address>帮我变得更有用!请在每条评论上点 👍 或 👎,我会根据你的反馈改进后续的 Review。
Original comment in English
Hey - I've found 2 issues, and left some high level feedback:
- The parsing logic for
quote.jsrelies on very specific string patterns (e.g.r.innerHTML = "..."and fixed<p><span>markup), which will break silently if 51LA changes their implementation; consider adding a small abstraction (with comments and a fallback UI state) to make this easier to update or detect when parsing fails. - Each
swup:contentReplacedevent triggers a new fetch to 51LA; adding a simple in-memory cache with a short TTL would reduce unnecessary network calls and avoid rate/quotas issues while still keeping the stats reasonably fresh.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The parsing logic for `quote.js` relies on very specific string patterns (e.g. `r.innerHTML = "..."` and fixed `<p><span>` markup), which will break silently if 51LA changes their implementation; consider adding a small abstraction (with comments and a fallback UI state) to make this easier to update or detect when parsing fails.
- Each `swup:contentReplaced` event triggers a new fetch to 51LA; adding a simple in-memory cache with a short TTL would reduce unnecessary network calls and avoid rate/quotas issues while still keeping the stats reasonably fresh.
## Individual Comments
### Comment 1
<location path="src/components/widget/LaStats.astro" line_range="102-103" />
<code_context>
+ return res.text();
+ })
+ .then(function(text) {
+ var match = text.match(/r\.innerHTML\s*=\s*"([^"]+)"/);
+ if (!match) return;
+
+ var html = match[1];
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Parsing `quote.js` via a very specific regex on `r.innerHTML` is brittle to upstream implementation changes.
It currently relies on 51LA’s script using `r.innerHTML="..."` with that exact variable name, double quotes, and assignment shape. Any variation (e.g. single quotes, different variable, or spacing change) will break extraction and silently stop updating stats. Please make the regex more tolerant to these variations, and/or log when `match` is falsy so failures are visible.
Suggested implementation:
```
.then(function(text) {
// Match any innerHTML assignment, allowing for different variable names,
// single or double quotes, and flexible whitespace/newlines.
var match = text.match(/innerHTML\s*=\s*(["'])([\s\S]*?)\1/);
if (!match) {
console.warn("51.LA stats: failed to extract innerHTML from quote.js", { url: url, textSample: text.slice(0, 200) });
return;
}
var html = match[2];
```
The updated regex `/innerHTML\s*=\s*(["'])([\s\S]*?)\1/` now:
- Ignores the specific variable name (`r`) and focuses on any `innerHTML` assignment.
- Supports both single and double quotes.
- Handles arbitrary spacing and newlines between `=` and the assigned string.
The new `console.warn` ensures that if extraction fails, there is a visible log with the URL and a small sample of the response body to aid debugging. No other changes are required if this is the only place that parses `quote.js`.
</issue_to_address>
### Comment 2
<location path="src/components/widget/LaStats.astro" line_range="91-100" />
<code_context>
+ 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;
+
+ 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) {
</code_context>
<issue_to_address>
**suggestion (bug_risk):** The `stats` definition and `INDEX_MAP` need to stay manually in sync, which is easy to break over time.
Because the mapping lives partly in `stats` and partly in `INDEX_MAP`, any reordering or additions can silently desync labels and values. It would be safer to derive `INDEX_MAP` from a single source of truth (for example, store the 51LA index on each `stats` entry and generate the map from that).
Suggested implementation:
```
// 通过 stats 配置派生出“51LA 索引 → statId”的映射,避免手动维护两份列表
// 期望在上方有形如:
// const stats = [
// { id: "la-online", /* ... */, laIndex: 0 },
// { id: "la-today-uv", /* ... */, laIndex: 1 },
// ...
// ];
var INDEX_MAP = (function() {
var map = [];
if (Array.isArray(stats)) {
stats.forEach(function(stat) {
if (
stat &&
typeof stat.laIndex === "number" &&
stat.laIndex >= 0
) {
map[stat.laIndex] = stat.id;
}
});
}
return map;
})();
```
```
var idx = parseInt(m[1], 10);
var val = parseInt(m[2], 10);
var statId = INDEX_MAP[idx];
if (typeof statId === "string") {
var elements = document.querySelectorAll('[data-stat-id="' + statId + '"]');
```
To fully implement the “single source of truth” suggestion, you should also:
1. Ensure there is a `stats` array defined earlier in `LaStats.astro` (or imported into it) that is used to render the stats UI.
2. Add a `laIndex` numeric field to each relevant `stats` entry corresponding to the 51LA index:
- Example:
- `la-online` → `laIndex: 0`
- `la-today-uv` → `laIndex: 1`
- `la-today-pv` → `laIndex: 2`
- `la-yesterday-uv` → `laIndex: 3`
- `la-yesterday-pv` → `laIndex: 4`
- `la-month-pv` → `laIndex: 5`
- `la-total-pv` → `laIndex: 6`
3. If some `stats` entries are not backed by 51LA, omit `laIndex` on those; the derived `INDEX_MAP` will only include entries where `laIndex` is set.
4. Remove any old hard-coded arrays or mappings that duplicate this index information elsewhere to keep `stats` as the single source of truth.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| var match = text.match(/r\.innerHTML\s*=\s*"([^"]+)"/); | ||
| if (!match) return; |
There was a problem hiding this comment.
suggestion (bug_risk): 通过对 r.innerHTML 使用过于具体的正则来解析 quote.js,对上游实现变更非常脆弱。
当前实现依赖 51LA 的脚本使用形如 r.innerHTML="..." 的写法,并且假定变量名、双引号以及赋值结构都完全一致。任何变化(例如改用单引号、更换变量名,或只是空格格式的调整)都会导致提取失败,并在无提示的情况下停止更新统计数据。请让正则对这些变体更具容错能力,并且在 match 为 falsy 时输出日志,方便发现失败。
建议实现:
.then(function(text) {
// Match any innerHTML assignment, allowing for different variable names,
// single or double quotes, and flexible whitespace/newlines.
var match = text.match(/innerHTML\s*=\s*(["'])([\s\S]*?)\1/);
if (!match) {
console.warn("51.LA stats: failed to extract innerHTML from quote.js", { url: url, textSample: text.slice(0, 200) });
return;
}
var html = match[2];
更新后的正则 /innerHTML\s*=\s*(["'])([\s\S]*?)\1/:
- 不再依赖特定变量名(如
r),而是聚焦任意innerHTML赋值语句; - 同时支持单引号和双引号;
- 能处理
=与被赋值字符串之间任意空白和换行。
新增的 console.warn 能确保在提取失败时,控制台会有可见日志,并附带 URL 和响应体前 200 个字符,方便调试。如果这里只在该处解析 quote.js,则无需做其他修改。
Original comment in English
suggestion (bug_risk): Parsing quote.js via a very specific regex on r.innerHTML is brittle to upstream implementation changes.
It currently relies on 51LA’s script using r.innerHTML="..." with that exact variable name, double quotes, and assignment shape. Any variation (e.g. single quotes, different variable, or spacing change) will break extraction and silently stop updating stats. Please make the regex more tolerant to these variations, and/or log when match is falsy so failures are visible.
Suggested implementation:
.then(function(text) {
// Match any innerHTML assignment, allowing for different variable names,
// single or double quotes, and flexible whitespace/newlines.
var match = text.match(/innerHTML\s*=\s*(["'])([\s\S]*?)\1/);
if (!match) {
console.warn("51.LA stats: failed to extract innerHTML from quote.js", { url: url, textSample: text.slice(0, 200) });
return;
}
var html = match[2];
The updated regex /innerHTML\s*=\s*(["'])([\s\S]*?)\1/ now:
- Ignores the specific variable name (
r) and focuses on anyinnerHTMLassignment. - Supports both single and double quotes.
- Handles arbitrary spacing and newlines between
=and the assigned string.
The new console.warn ensures that if extraction fails, there is a visible log with the URL and a small sample of the response body to aid debugging. No other changes are required if this is the only place that parses quote.js.
| 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(); | ||
| }) |
There was a problem hiding this comment.
suggestion (bug_risk): 目前的 stats 定义和 INDEX_MAP 需要手动保持同步,随着时间推移很容易被改坏。
由于映射关系一部分在 stats 中,一部分在 INDEX_MAP 中,任何重排或新增项都可能在没有明显迹象的情况下导致标签与数值错位。更安全的做法是从单一信息源派生出 INDEX_MAP(例如在每个 stats 条目上存 51LA 的索引,然后据此生成映射)。
建议实现:
// 通过 stats 配置派生出“51LA 索引 → statId”的映射,避免手动维护两份列表
// 期望在上方有形如:
// const stats = [
// { id: "la-online", /* ... */, laIndex: 0 },
// { id: "la-today-uv", /* ... */, laIndex: 1 },
// ...
// ];
var INDEX_MAP = (function() {
var map = [];
if (Array.isArray(stats)) {
stats.forEach(function(stat) {
if (
stat &&
typeof stat.laIndex === "number" &&
stat.laIndex >= 0
) {
map[stat.laIndex] = stat.id;
}
});
}
return map;
})();
var idx = parseInt(m[1], 10);
var val = parseInt(m[2], 10);
var statId = INDEX_MAP[idx];
if (typeof statId === "string") {
var elements = document.querySelectorAll('[data-stat-id="' + statId + '"]');
要完整落地这个“单一信息源”的建议,还需要:
- 确保在
LaStats.astro文件中更前面(或从外部导入)定义了一个stats数组,用于渲染统计信息的 UI; - 在每个与 51LA 对应的
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: 6
- 示例:
- 如果某些
stats项并非由 51LA 提供数据,则不要为其设置laIndex;派生出的INDEX_MAP只会包含设了laIndex的条目。 - 删除其他地方任何硬编码的数组或重复的索引映射信息,确保
stats是唯一的“单一信息源”。
Original comment in English
suggestion (bug_risk): The stats definition and INDEX_MAP need to stay manually in sync, which is easy to break over time.
Because the mapping lives partly in stats and partly in INDEX_MAP, any reordering or additions can silently desync labels and values. It would be safer to derive INDEX_MAP from a single source of truth (for example, store the 51LA index on each stats entry and generate the map from that).
Suggested implementation:
// 通过 stats 配置派生出“51LA 索引 → statId”的映射,避免手动维护两份列表
// 期望在上方有形如:
// const stats = [
// { id: "la-online", /* ... */, laIndex: 0 },
// { id: "la-today-uv", /* ... */, laIndex: 1 },
// ...
// ];
var INDEX_MAP = (function() {
var map = [];
if (Array.isArray(stats)) {
stats.forEach(function(stat) {
if (
stat &&
typeof stat.laIndex === "number" &&
stat.laIndex >= 0
) {
map[stat.laIndex] = stat.id;
}
});
}
return map;
})();
var idx = parseInt(m[1], 10);
var val = parseInt(m[2], 10);
var statId = INDEX_MAP[idx];
if (typeof statId === "string") {
var elements = document.querySelectorAll('[data-stat-id="' + statId + '"]');
To fully implement the “single source of truth” suggestion, you should also:
- Ensure there is a
statsarray defined earlier inLaStats.astro(or imported into it) that is used to render the stats UI. - Add a
laIndexnumeric field to each relevantstatsentry corresponding to the 51LA index:- Example:
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: 6
- Example:
- If some
statsentries are not backed by 51LA, omitlaIndexon those; the derivedINDEX_MAPwill only include entries wherelaIndexis set. - Remove any old hard-coded arrays or mappings that duplicate this index information elsewhere to keep
statsas the single source of truth.
feat(analytics/widget): 为51LA统计挂件添加可配置的数据源 This commit adds support for multiple data sources for the LA stats widget: - Add `statsDataSource` and `statsApiUrl` fields to AnalyticsConfig type - Set default config values for the new fields - Rework the widget logic to support both quote.js built-in source and custom API source 该提交为51LA统计挂件添加了多数据源支持: - 向AnalyticsConfig类型新增`statsDataSource`和`statsApiUrl`配置项 - 为新配置项设置默认值 - 重构挂件逻辑,同时支持官方quote.js数据源和自定义API数据源
…t sending duplicate requests and the compilation problem caused by adding Korean without adaptation fix(组件/国际化):修复了51LA访客统计组件发送重复请求的问题,以及添加韩语未适配导致的编译问题 This commit adds full support for 51LA visitor statistics widget: 1. **Add Korean translations**: Add all required i18n keys for 51LA stats in `src/i18n/languages/ko.ts` 2. **Fix duplicate requests**: Add global initialization lock to prevent multiple widget instantiations 3. **Optimize fetch code**: Simplify the fetch URL inline writing 本次提交完整实现51LA访客统计功能: 1. **添加韩语国际化支持**:在`src/i18n/languages/ko.ts`中新增所有51LA统计相关的多语言键值 2. **修复重复请求问题**:添加全局初始化锁,避免多侧边栏实例重复加载组件 3. **优化请求代码**:简化fetch请求的URL编写方式
There was a problem hiding this comment.
Hey - 我发现了 1 个问题,并且有一些整体性的反馈:
- 当
statsDataSource设置为api但statsApiUrl为空时,建议不要只记录错误日志,可以考虑回退到quotejs的解析路径,这样在最小配置下组件仍然可以正常工作。 updateViaQuoteJs的逻辑假设quote.js的结构是稳定不变的;在解析失败时,更安全的做法可能是显式清空/隐藏组件或展示一个兜底状态,而不是静默地保留占位内容。
给 AI 代理的提示词
Please address the comments from this code review:
## Overall Comments
- 当 `statsDataSource` 设置为 `api` 但 `statsApiUrl` 为空时,建议不要只记录错误日志,可以考虑回退到 `quotejs` 的解析路径,这样在最小配置下组件仍然可以正常工作。
- `updateViaQuoteJs` 的逻辑假设 `quote.js` 的结构是稳定不变的;在解析失败时,更安全的做法可能是显式清空/隐藏组件或展示一个兜底状态,而不是静默地保留占位内容。
## Individual Comments
### Comment 1
<location path="src/components/widget/LaStats.astro" line_range="98-107" />
<code_context>
+ var STAT_IDS = ["la-online", "la-today-uv", "la-today-pv", "la-yesterday-uv", "la-yesterday-pv", "la-month-pv", "la-total-pv"];
</code_context>
<issue_to_address>
**suggestion:** 硬编码的 STAT_IDS 数组很容易与上面的统计定义失去同步。
由于 `stats` 和 `STAT_IDS` 必须保持相同的顺序和长度,任何对 `stats` 的修改(添加/删除/重新排序)都需要在这里手动更新,这很容易出错。可以考虑从 `stats` 推导出 `STAT_IDS`(例如通过 `define:vars` 传入 ID),或者从渲染后的 DOM 中推导,这样度量 ID 和排序就只有一个可信来源。
建议实现:
```
function setStatValues(values) {
// 从渲染后的 DOM 中推导 STAT_IDS,使其顺序和标记保持同步
var allStatEls = document.querySelectorAll('[data-stat-id]');
var statIds = [];
allStatEls.forEach(function(el) {
var id = el.getAttribute('data-stat-id');
if (id && statIds.indexOf(id) === -1) {
statIds.push(id);
}
});
for (var i = 0; i < values.length && i < statIds.length; i++) {
var els = document.querySelectorAll('[data-stat-id="' + statIds[i] + '"]');
els.forEach(function(el) { el.textContent = values[i].toLocaleString(); });
}
}
```
如果统计项的顺序不仅仅由 DOM 顺序决定,可以考虑:
1. 通过 `define:vars`(例如 `la51StatIds`)从父级传入一个有序的统计 ID 列表,并用它来代替从 DOM 推导。
2. 在定义 `data-stat-id` 属性的标记附近添加一小段内联注释,说明它们在 DOM 中的顺序会决定此脚本中 stats 数组的映射关系。
</issue_to_address>帮我变得更有用!请在每条评论上点击 👍 或 👎,我会根据反馈改进后续评审。
Original comment in English
Hey - I've found 1 issue, and left some high level feedback:
- When
statsDataSourceis set toapibutstatsApiUrlis empty, consider falling back to thequotejsparsing path instead of just logging an error so the widget still works with a minimal configuration. - The
updateViaQuoteJslogic assumes thequote.jsstructure stays stable; it might be safer to handle parse failures by explicitly clearing/hiding the widget or showing a fallback state instead of silently leaving placeholders.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- When `statsDataSource` is set to `api` but `statsApiUrl` is empty, consider falling back to the `quotejs` parsing path instead of just logging an error so the widget still works with a minimal configuration.
- The `updateViaQuoteJs` logic assumes the `quote.js` structure stays stable; it might be safer to handle parse failures by explicitly clearing/hiding the widget or showing a fallback state instead of silently leaving placeholders.
## Individual Comments
### Comment 1
<location path="src/components/widget/LaStats.astro" line_range="98-107" />
<code_context>
+ var STAT_IDS = ["la-online", "la-today-uv", "la-today-pv", "la-yesterday-uv", "la-yesterday-pv", "la-month-pv", "la-total-pv"];
</code_context>
<issue_to_address>
**suggestion:** The hardcoded STAT_IDS array can easily get out of sync with the stats definition above.
Because `stats` and `STAT_IDS` must stay the same order and length, any change to `stats` (add/remove/reorder) also requires a manual update here, which is error‑prone. Consider deriving `STAT_IDS` from `stats` (e.g., passing IDs via `define:vars`) or from the rendered DOM so there’s a single source of truth for metric IDs and ordering.
Suggested implementation:
```
function setStatValues(values) {
// Derive STAT_IDS from the rendered DOM so ordering stays in sync with the markup
var allStatEls = document.querySelectorAll('[data-stat-id]');
var statIds = [];
allStatEls.forEach(function(el) {
var id = el.getAttribute('data-stat-id');
if (id && statIds.indexOf(id) === -1) {
statIds.push(id);
}
});
for (var i = 0; i < values.length && i < statIds.length; i++) {
var els = document.querySelectorAll('[data-stat-id="' + statIds[i] + '"]');
els.forEach(function(el) { el.textContent = values[i].toLocaleString(); });
}
}
```
If the stats ordering is not purely determined by DOM order, consider:
1. Passing an ordered list of stat IDs from the parent via `define:vars` (e.g. `la51StatIds`) and using that instead of deriving from the DOM.
2. Adding a small inline comment near the markup where `data-stat-id` attributes are defined, documenting that their DOM order determines the mapping to the stats array used by this script.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| 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() { |
There was a problem hiding this comment.
suggestion: 硬编码的 STAT_IDS 数组很容易与上面的统计定义失去同步。
由于 stats 和 STAT_IDS 必须保持相同的顺序和长度,任何对 stats 的修改(添加/删除/重新排序)都需要在这里手动更新,这很容易出错。可以考虑从 stats 推导出 STAT_IDS(例如通过 define:vars 传入 ID),或者从渲染后的 DOM 中推导,这样度量 ID 和排序就只有一个可信来源。
建议实现:
function setStatValues(values) {
// 从渲染后的 DOM 中推导 STAT_IDS,使其顺序和标记保持同步
var allStatEls = document.querySelectorAll('[data-stat-id]');
var statIds = [];
allStatEls.forEach(function(el) {
var id = el.getAttribute('data-stat-id');
if (id && statIds.indexOf(id) === -1) {
statIds.push(id);
}
});
for (var i = 0; i < values.length && i < statIds.length; i++) {
var els = document.querySelectorAll('[data-stat-id="' + statIds[i] + '"]');
els.forEach(function(el) { el.textContent = values[i].toLocaleString(); });
}
}
如果统计项的顺序不仅仅由 DOM 顺序决定,可以考虑:
- 通过
define:vars(例如la51StatIds)从父级传入一个有序的统计 ID 列表,并用它来代替从 DOM 推导。 - 在定义
data-stat-id属性的标记附近添加一小段内联注释,说明它们在 DOM 中的顺序会决定此脚本中 stats 数组的映射关系。
Original comment in English
suggestion: The hardcoded STAT_IDS array can easily get out of sync with the stats definition above.
Because stats and STAT_IDS must stay the same order and length, any change to stats (add/remove/reorder) also requires a manual update here, which is error‑prone. Consider deriving STAT_IDS from stats (e.g., passing IDs via define:vars) or from the rendered DOM so there’s a single source of truth for metric IDs and ordering.
Suggested implementation:
function setStatValues(values) {
// Derive STAT_IDS from the rendered DOM so ordering stays in sync with the markup
var allStatEls = document.querySelectorAll('[data-stat-id]');
var statIds = [];
allStatEls.forEach(function(el) {
var id = el.getAttribute('data-stat-id');
if (id && statIds.indexOf(id) === -1) {
statIds.push(id);
}
});
for (var i = 0; i < values.length && i < statIds.length; i++) {
var els = document.querySelectorAll('[data-stat-id="' + statIds[i] + '"]');
els.forEach(function(el) { el.textContent = values[i].toLocaleString(); });
}
}
If the stats ordering is not purely determined by DOM order, consider:
- Passing an ordered list of stat IDs from the parent via
define:vars(e.g.la51StatIds) and using that instead of deriving from the DOM. - Adding a small inline comment near the markup where
data-stat-idattributes are defined, documenting that their DOM order determines the mapping to the stats array used by this script.
Type of change
Checklist
Related Issue
Changes
src/components/widget/LaStats.astro,支持展示当前在线人数、今日访客/浏览量、昨日访客/浏览量、本月浏览量及总浏览量src/types/sidebarConfig.ts中新增lastats组件类型src/i18n/i18nKey.ts及各语言文件)src/config/sidebarConfig.ts中预启用 LaStats 组件src/components/layout/SideBar.astro中引入 LaStats 组件How To Test
Screenshots (if applicable)
Additional Notes
Q: 读取js里的数据来展示, 为什么使用这种看上去很笨的做法?
A: 51la 官方API是收费的, 但是它们提供的官方数据挂件是不要钱的。还有样式是写死的, 数据是硬编码在js里。所有才用了这个笨方法, 暂未想到更好更实惠的办法来实现。(可选也更推荐cf自建第三方api)
Q: 这样做安全吗?
A: 做了基础的正则匹配来锁定数据内容+parseInt, 可排除较为简单的攻击手段来防止被污染时插入广告/恶意脚本
Q: 加入该数据展示组件有什么益处?
A: 好玩(bushi), 是方便直接登入站点就能查看数据
涉及 10 个文件,共 214 行新增代码,无删除行。
Summary by Sourcery v1
在侧边栏中添加新的 51LA 访客统计挂件,并将其集成到默认布局中,提供完整的国际化(i18n)支持。
新功能:
改进:
lastats挂件类型,并将其映射到侧边栏组件注册表中。Summary by Sourcery v2
改进:
lastats挂件类型,并在默认侧边栏布局中启用它。