feat(logs): 分页显示总页数(新增 /logs/stats,走 index/stats 只读索引)

用户反馈:底部只有「上一页 第 1 页 下一页」,看不到末尾/总页数。

原因:日志结果是 Loki 游标分页(cursor = 纳秒时间戳),本身不返回总数,
前端只能知道"有没有下一页",算不出总页数。

解法:新增 POST /api/v1/logs/stats
  → 调 Loki /loki/api/v1/index/stats(只读 TSDB 索引里的 chunk 元数据)
  → 返回 {streams, chunks, bytes, entries}
  → 前端用 entries / limit 得到总页数
实测成本极低:1h 51ms、24h 30ms(3.28 亿条 / 44.2 GB)。

改动:
1. backend: services.LokiStatsFor + controllers.LogsController.Stats + 路由 /logs/stats
2. frontend: logsApi.stats;查询成功后拉取总数(复用同一 AbortController,
   失败静默不影响日志展示);计算 totalPages 传给结果面板
3. LogResultsPanel: 有总数时显示「第 N / M 页」,否则维持「第 N 页」
4. i18n: 新增 results.pageOfTotal(中英)

镜像:lghost/lokistack:latest + v0.1 = sha256:41d6576ec232
This commit is contained in:
opsx committed 2026-10-28 16:02:35 +00:00
1 parent de376a1284
commit 5d530036e6
8 files changed
+88 -5

No files matched your search

@@ -22,9 +22,9 @@ function levelBadgeClass(level: string) {
// 只保留原始日志视图;表格/折线/流图/饼图等可视化已迁至「日志分析」页(LogDetailSection)
export function LogResultsPanel({ entries, searched, hasSource, pageIndex, hasNextPage, loading, loadingStartedAt, query, terms, onQueryChange, onApplyDomain, onShowContext, onSearch, onNextPage, onPreviousPage }: {
export function LogResultsPanel({ entries, searched, hasSource, pageIndex, hasNextPage, totalPages, loading, loadingStartedAt, query, terms, onQueryChange, onApplyDomain, onShowContext, onSearch, onNextPage, onPreviousPage }: {
entries: LogEntry[]; searched: boolean; hasSource: boolean
pageIndex: number; hasNextPage: boolean; loading: boolean; loadingStartedAt: number | null; query: string; terms: string[]
pageIndex: number; hasNextPage: boolean; totalPages?: number; loading: boolean; loadingStartedAt: number | null; query: string; terms: string[]
onQueryChange: (query: string) => void; onApplyDomain: (domain: string) => void; onShowContext: (row: LogEntry) => void; onSearch: () => void; onNextPage: () => void; onPreviousPage: () => void
}) {
const { t } = useTranslation('logs')
@@ -80,7 +80,11 @@ export function LogResultsPanel({ entries, searched, hasSource, pageIndex, hasNe
</div>}
{searched && entries.length > 0 && <div className="mt-3 flex items-center gap-2">
<Button variant="outline" disabled={loading || pageIndex === 0} onClick={onPreviousPage}>{t('results.prevPage')}</Button>
<span className="text-xs tabular-nums text-muted-foreground">{t('results.pageOf', { page: pageIndex + 1 })}</span>
<span className="text-xs tabular-nums text-muted-foreground">
{totalPages && totalPages > 1
? t('results.pageOfTotal', { page: pageIndex + 1, total: totalPages })
: t('results.pageOf', { page: pageIndex + 1 })}
</span>
<Button variant="outline" disabled={loading || !hasNextPage} onClick={onNextPage}>{t('results.nextPage')}</Button>
</div>}
</section>