@composy/log-viewer-core 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/README.md +175 -37
  3. package/dist/index.js +614 -299
  4. package/dist/index.js.map +1 -1
  5. package/dist/index.min.js +548 -1
  6. package/dist/index.min.js.map +1 -0
  7. package/es/.dts-output-manifest.json +1 -0
  8. package/es/.dts.tsbuildinfo +1 -0
  9. package/es/core/EventEmitter.d.ts +0 -1
  10. package/es/core/EventEmitter.js +13 -9
  11. package/es/core/EventEmitter.js.map +1 -1
  12. package/es/core/LogViewer.d.ts +14 -11
  13. package/es/core/LogViewer.js +243 -64
  14. package/es/core/LogViewer.js.map +1 -1
  15. package/es/filters/FilterEngine.d.ts +0 -1
  16. package/es/filters/FilterEngine.js +31 -26
  17. package/es/filters/FilterEngine.js.map +1 -1
  18. package/es/index.d.ts +7 -7
  19. package/es/index.js +3 -3
  20. package/es/index.js.map +1 -1
  21. package/es/renderers/DiffRenderer.d.ts +0 -1
  22. package/es/renderers/DiffRenderer.js +13 -3
  23. package/es/renderers/DiffRenderer.js.map +1 -1
  24. package/es/renderers/TimelineRenderer.d.ts +7 -3
  25. package/es/renderers/TimelineRenderer.js +56 -22
  26. package/es/renderers/TimelineRenderer.js.map +1 -1
  27. package/es/styles/index.d.ts +1 -5
  28. package/es/styles/index.js +167 -131
  29. package/es/styles/index.js.map +1 -1
  30. package/es/types/index.d.ts +4 -1
  31. package/es/types/index.js +1 -1
  32. package/es/types/index.js.map +1 -1
  33. package/es/utils/index.d.ts +1 -2
  34. package/es/utils/index.js +54 -6
  35. package/es/utils/index.js.map +1 -1
  36. package/esm/core/EventEmitter.js +13 -9
  37. package/esm/core/EventEmitter.js.map +1 -1
  38. package/esm/core/LogViewer.js +243 -64
  39. package/esm/core/LogViewer.js.map +1 -1
  40. package/esm/filters/FilterEngine.js +31 -26
  41. package/esm/filters/FilterEngine.js.map +1 -1
  42. package/esm/index.js +2 -2
  43. package/esm/index.js.map +1 -1
  44. package/esm/renderers/DiffRenderer.js +13 -3
  45. package/esm/renderers/DiffRenderer.js.map +1 -1
  46. package/esm/renderers/TimelineRenderer.js +56 -22
  47. package/esm/renderers/TimelineRenderer.js.map +1 -1
  48. package/esm/styles/index.js +167 -131
  49. package/esm/styles/index.js.map +1 -1
  50. package/esm/types/index.js +1 -1
  51. package/esm/types/index.js.map +1 -1
  52. package/esm/utils/index.js +54 -6
  53. package/esm/utils/index.js.map +1 -1
  54. package/lib/core/EventEmitter.cjs +13 -9
  55. package/lib/core/EventEmitter.cjs.map +1 -1
  56. package/{esm/core/EventEmitter.d.ts → lib/core/EventEmitter.d.cts} +0 -1
  57. package/lib/core/LogViewer.cjs +247 -68
  58. package/lib/core/LogViewer.cjs.map +1 -1
  59. package/{esm/core/LogViewer.d.ts → lib/core/LogViewer.d.cts} +14 -11
  60. package/lib/filters/FilterEngine.cjs +31 -26
  61. package/lib/filters/FilterEngine.cjs.map +1 -1
  62. package/lib/filters/{FilterEngine.d.ts → FilterEngine.d.cts} +0 -1
  63. package/lib/index.cjs +3 -3
  64. package/lib/index.cjs.map +1 -1
  65. package/{esm/index.d.ts → lib/index.d.cts} +7 -7
  66. package/lib/renderers/DiffRenderer.cjs +13 -3
  67. package/lib/renderers/DiffRenderer.cjs.map +1 -1
  68. package/lib/renderers/{DiffRenderer.d.ts → DiffRenderer.d.cts} +0 -1
  69. package/lib/renderers/TimelineRenderer.cjs +55 -21
  70. package/lib/renderers/TimelineRenderer.cjs.map +1 -1
  71. package/lib/renderers/{TimelineRenderer.d.ts → TimelineRenderer.d.cts} +7 -3
  72. package/lib/styles/index.cjs +167 -131
  73. package/lib/styles/index.cjs.map +1 -1
  74. package/{esm/styles/index.d.ts → lib/styles/index.d.cts} +1 -5
  75. package/lib/types/index.cjs +1 -1
  76. package/lib/types/index.cjs.map +1 -1
  77. package/{esm/types/index.d.ts → lib/types/index.d.cts} +4 -1
  78. package/lib/utils/index.cjs +54 -6
  79. package/lib/utils/index.cjs.map +1 -1
  80. package/{esm/utils/index.d.ts → lib/utils/index.d.cts} +1 -2
  81. package/package.json +15 -9
  82. package/es/core/EventEmitter.d.ts.map +0 -1
  83. package/es/core/LogViewer.d.ts.map +0 -1
  84. package/es/filters/FilterEngine.d.ts.map +0 -1
  85. package/es/index.d.ts.map +0 -1
  86. package/es/renderers/DiffRenderer.d.ts.map +0 -1
  87. package/es/renderers/TimelineRenderer.d.ts.map +0 -1
  88. package/es/styles/index.d.ts.map +0 -1
  89. package/es/types/index.d.ts.map +0 -1
  90. package/es/utils/index.d.ts.map +0 -1
  91. package/esm/core/EventEmitter.d.ts.map +0 -1
  92. package/esm/core/LogViewer.d.ts.map +0 -1
  93. package/esm/filters/FilterEngine.d.ts +0 -26
  94. package/esm/filters/FilterEngine.d.ts.map +0 -1
  95. package/esm/index.d.ts.map +0 -1
  96. package/esm/renderers/DiffRenderer.d.ts +0 -18
  97. package/esm/renderers/DiffRenderer.d.ts.map +0 -1
  98. package/esm/renderers/TimelineRenderer.d.ts +0 -22
  99. package/esm/renderers/TimelineRenderer.d.ts.map +0 -1
  100. package/esm/styles/index.d.ts.map +0 -1
  101. package/esm/types/index.d.ts.map +0 -1
  102. package/esm/utils/index.d.ts.map +0 -1
  103. package/lib/core/EventEmitter.d.ts +0 -11
  104. package/lib/core/LogViewer.d.ts +0 -46
  105. package/lib/index.d.ts +0 -26
  106. package/lib/styles/index.d.ts +0 -9
  107. package/lib/types/index.d.ts +0 -113
  108. package/lib/utils/index.d.ts +0 -18
@@ -1 +1 @@
1
- {"version":3,"file":"LogViewer.cjs","names":[],"sources":["../../src/core/LogViewer.ts"],"sourcesContent":["/**\n * LogViewer 核心引擎\n */\n\nimport type {\n LogEntry,\n LogViewerConfig,\n LogViewerInstance,\n FilterConfig,\n PaginationConfig,\n ThemeMode,\n} from '../types'\nimport { EventEmitter } from './EventEmitter'\nimport { FilterEngine } from '../filters/FilterEngine'\nimport { TimelineRenderer } from '../renderers/TimelineRenderer'\nimport { DiffRenderer } from '../renderers/DiffRenderer'\nimport { injectStyles } from '../styles'\nimport { deepClone } from '../utils'\n\n/**\n * LogViewer - Log Viewer class\n */\nexport class LogViewer implements LogViewerInstance {\n private root: HTMLElement\n private emitter = new EventEmitter()\n private filterEngine = new FilterEngine()\n private timelineRenderer = new TimelineRenderer()\n private diffRenderer = new DiffRenderer()\n\n private data: LogEntry[] = []\n private filteredData: LogEntry[] = []\n private filter: FilterConfig = {}\n private pagination: PaginationConfig = { page: 1, pageSize: 20 }\n private theme: ThemeMode = 'light'\n private expandedIds = new Set<string>()\n private config: LogViewerConfig\n\n private timelineEl: HTMLElement | null = null\n private paginationEl: HTMLElement | null = null\n private destroyed = false\n\n constructor(config: LogViewerConfig) {\n this.config = config\n\n // Resolve container\n const container =\n typeof config.container === 'string'\n ? document.querySelector<HTMLElement>(config.container)\n : config.container\n if (!container) throw new Error(`[LogViewer] Container not found: ${config.container}`)\n\n // Inject global styles\n injectStyles()\n\n // Create root\n this.root = document.createElement('div')\n this.root.className = 'lv-root'\n container.appendChild(this.root)\n\n // Apply config\n this.theme = config.theme || 'light'\n this.root.setAttribute('data-theme', this.theme)\n\n if (config.pageSize) this.pagination.pageSize = config.pageSize\n if (config.pagination) Object.assign(this.pagination, config.pagination)\n if (config.filter) this.filter = deepClone(config.filter)\n\n // Load data\n if (config.data) {\n this.data = [...config.data]\n }\n\n this.applyFilterAndRender()\n this.bindEvents()\n }\n\n // ===== Public API =====\n\n getData(): LogEntry[] {\n return [...this.data]\n }\n\n setData(entries: LogEntry[]): void {\n this.data = [...entries]\n this.pagination.page = 1\n this.applyFilterAndRender()\n this.emitter.emit('data:change', this.data)\n }\n\n appendData(entries: LogEntry[]): void {\n this.data.push(...entries)\n this.applyFilterAndRender()\n this.emitter.emit('data:change', this.data)\n }\n\n getFilter(): FilterConfig {\n return deepClone(this.filter)\n }\n\n setFilter(filter: FilterConfig): void {\n this.filter = deepClone(filter)\n this.pagination.page = 1\n this.applyFilterAndRender()\n this.emitter.emit('filter:change', this.filter)\n this.config.onFilterChange?.(this.filter)\n }\n\n resetFilter(): void {\n this.filter = {}\n this.pagination.page = 1\n this.applyFilterAndRender()\n this.emitter.emit('filter:change', this.filter)\n }\n\n getTheme(): ThemeMode {\n return this.theme\n }\n\n setTheme(theme: ThemeMode): void {\n this.theme = theme\n this.root.setAttribute('data-theme', theme)\n this.emitter.emit('theme:change', theme)\n this.config.onThemeChange?.(theme)\n }\n\n expandEntry(id: string): void {\n this.expandedIds.add(id)\n const entry = this.data.find((e) => e.id === id)\n if (entry) this.emitter.emit('entry:expand', entry)\n this.renderTimeline()\n }\n\n collapseEntry(id: string): void {\n this.expandedIds.delete(id)\n const entry = this.data.find((e) => e.id === id)\n if (entry) this.emitter.emit('entry:collapse', entry)\n this.renderTimeline()\n }\n\n collapseAll(): void {\n this.expandedIds.clear()\n this.renderTimeline()\n }\n\n goToPage(page: number): void {\n const totalPages = Math.ceil(this.filteredData.length / this.pagination.pageSize)\n if (page < 1 || page > totalPages) return\n this.pagination.page = page\n this.emitter.emit('page:change', page)\n this.renderTimeline()\n }\n\n getPagination(): PaginationConfig {\n return {\n ...this.pagination,\n total: this.filteredData.length,\n }\n }\n\n on(event: string, callback: (...args: unknown[]) => void): () => void {\n return this.emitter.on(event, callback)\n }\n\n refresh(): void {\n this.applyFilterAndRender()\n }\n\n destroy(): void {\n if (this.destroyed) return\n this.destroyed = true\n this.emitter.removeAllListeners()\n this.root.remove()\n }\n\n // ===== Internal =====\n\n private applyFilterAndRender(): void {\n this.filteredData = this.filterEngine.filter(this.data, this.filter)\n this.filteredData.sort(\n (a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime(),\n )\n this.pagination.total = this.filteredData.length\n this.renderTimeline()\n }\n\n private getPageData(): LogEntry[] {\n const { page, pageSize } = this.pagination\n const start = (page - 1) * pageSize\n return this.filteredData.slice(start, start + pageSize)\n }\n\n private renderTimeline(): void {\n if (this.destroyed) return\n\n const pageData = this.getPageData()\n\n // Timeline\n const newTimeline = this.timelineRenderer.render(pageData, this.expandedIds)\n\n // Render detail panels for expanded items\n for (const entry of pageData) {\n if (this.expandedIds.has(entry.id)) {\n const itemEl = newTimeline.querySelector(`[data-id=\"${entry.id}\"] .lv-timeline-card`)\n if (itemEl) {\n itemEl.appendChild(this.diffRenderer.renderDetail(entry))\n }\n }\n }\n\n if (this.timelineEl) {\n this.root.removeChild(this.timelineEl)\n }\n this.timelineEl = newTimeline\n this.root.appendChild(this.timelineEl)\n\n // Pagination\n const newPagination = this.timelineRenderer.renderPagination(\n this.pagination.page,\n this.pagination.pageSize,\n this.filteredData.length,\n )\n if (this.paginationEl) {\n this.root.removeChild(this.paginationEl)\n }\n this.paginationEl = newPagination\n this.root.appendChild(this.paginationEl)\n }\n\n private bindEvents(): void {\n this.root.addEventListener('click', (e) => {\n const target = e.target as HTMLElement\n\n // Timeline card click\n const card = target.closest('.lv-timeline-card') as HTMLElement | null\n if (card) {\n const item = card.closest('.lv-timeline-item') as HTMLElement | null\n const id = item?.getAttribute('data-id')\n if (!id) return\n\n const entry = this.filteredData.find((en) => en.id === id)\n if (!entry) return\n\n // Click on expand hint\n if (target.classList.contains('lv-timeline-expand-hint')) {\n if (this.expandedIds.has(id)) {\n this.collapseEntry(id)\n } else {\n this.expandEntry(id)\n }\n return\n }\n\n // Click on card itself\n this.emitter.emit('entry:click', entry)\n this.config.onEntryClick?.(entry)\n }\n\n // Pagination click\n const pageBtn = target.closest('.lv-pagination-btn') as HTMLButtonElement | null\n if (pageBtn && !pageBtn.disabled) {\n const page = parseInt(pageBtn.getAttribute('data-page') || '1', 10)\n this.goToPage(page)\n }\n })\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"LogViewer.cjs","names":[],"sources":["../../src/core/LogViewer.ts"],"sourcesContent":["/**\n * LogViewer 核心引擎\n */\n\nimport type {\n ActionType,\n FilterConfig,\n LogEntry,\n LogViewerConfig,\n LogViewerInstance,\n PaginationConfig,\n ThemeMode,\n} from '../types'\nimport { ACTION_LABELS } from '../types'\nimport { EventEmitter } from './EventEmitter'\nimport { FilterEngine } from '../filters/FilterEngine'\nimport { DiffRenderer } from '../renderers/DiffRenderer'\nimport { TimelineRenderer } from '../renderers/TimelineRenderer'\nimport { injectStyles } from '../styles'\nimport { deepClone } from '../utils'\n\nconst DEFAULT_PAGE_SIZE = 20\nconst MAX_PAGE_SIZE = 1000\n\nfunction normalizePageSize(\n value: unknown,\n fallback = DEFAULT_PAGE_SIZE\n): number {\n const pageSize = Number(value)\n if (!Number.isFinite(pageSize) || pageSize <= 0) return fallback\n return Math.min(MAX_PAGE_SIZE, Math.max(1, Math.floor(pageSize)))\n}\n\nfunction normalizePage(value: unknown): number {\n const page = Number(value)\n if (!Number.isFinite(page) || page < 1) return 1\n return Math.floor(page)\n}\n\nfunction cloneEntries(entries: readonly LogEntry[] | undefined): LogEntry[] {\n if (entries === undefined) return []\n if (!Array.isArray(entries))\n throw new TypeError('[LogViewer] data must be an array')\n\n return entries.map((entry, index) => {\n if (!entry || typeof entry !== 'object') {\n throw new TypeError(`[LogViewer] data[${index}] must be an object`)\n }\n\n const cloned = deepClone(entry)\n const id = String(cloned.id ?? '').trim()\n if (!id)\n throw new TypeError(\n `[LogViewer] data[${index}].id must be a non-empty string`\n )\n\n return {\n ...cloned,\n id,\n user: String(cloned.user ?? ''),\n module: String(cloned.module ?? ''),\n changes: Array.isArray(cloned.changes) ? cloned.changes : undefined,\n }\n })\n}\n\nfunction isActionType(value: string): value is ActionType {\n return Object.keys(ACTION_LABELS).includes(value)\n}\n\nfunction sourceEntries(payload: unknown): LogEntry[] {\n if (Array.isArray(payload)) return payload as LogEntry[]\n if (!payload || typeof payload !== 'object') {\n throw new TypeError('[LogViewer] dataSource response must be an array')\n }\n\n const record = payload as Record<string, unknown>\n for (const key of ['data', 'logs', 'items', 'entries']) {\n if (Array.isArray(record[key])) return record[key] as LogEntry[]\n }\n throw new TypeError('[LogViewer] dataSource response must contain an array')\n}\n\n/** LogViewer - framework-neutral audit log viewer. */\nexport class LogViewer implements LogViewerInstance {\n private readonly root: HTMLElement\n private readonly emitter = new EventEmitter()\n private readonly filterEngine = new FilterEngine()\n private readonly timelineRenderer = new TimelineRenderer()\n private readonly diffRenderer = new DiffRenderer()\n private readonly config: LogViewerConfig\n\n private data: LogEntry[] = []\n private filteredData: LogEntry[] = []\n private filter: FilterConfig = {}\n private pagination: PaginationConfig = {\n page: 1,\n pageSize: DEFAULT_PAGE_SIZE,\n }\n private theme: ThemeMode = 'light'\n private expandedIds = new Set<string>()\n private filterEl: HTMLElement | null = null\n private timelineEl: HTMLElement | null = null\n private paginationEl: HTMLElement | null = null\n private destroyed = false\n private requestId = 0\n private abortController: AbortController | null = null\n\n constructor(config: LogViewerConfig) {\n if (typeof document === 'undefined') {\n throw new Error('[LogViewer] A browser document is required')\n }\n\n const container =\n typeof config.container === 'string'\n ? document.querySelector<HTMLElement>(config.container)\n : config.container\n if (!container)\n throw new Error(\n `[LogViewer] Container not found: ${String(config.container)}`\n )\n\n this.config = { ...config }\n this.theme = config.theme === 'dark' ? 'dark' : 'light'\n this.pagination = {\n page: normalizePage(config.pagination?.page),\n pageSize: normalizePageSize(\n config.pageSize ?? config.pagination?.pageSize\n ),\n }\n this.filter = deepClone(config.filter || {})\n this.data = cloneEntries(config.data)\n\n injectStyles()\n this.root = document.createElement('div')\n this.root.className = 'lv-root'\n this.root.setAttribute('data-theme', this.theme)\n container.appendChild(this.root)\n\n this.bindEvents()\n this.applyFilterAndRender()\n\n if (config.dataSource) {\n void this.loadData().catch(() => undefined)\n }\n }\n\n getData(): LogEntry[] {\n return deepClone(this.data)\n }\n\n setData(entries: LogEntry[]): void {\n if (this.destroyed) return\n this.data = cloneEntries(entries)\n this.pagination.page = 1\n this.expandedIds.clear()\n this.applyFilterAndRender()\n this.emitter.emit('data:change', this.getData())\n }\n\n appendData(entries: LogEntry[]): void {\n if (this.destroyed) return\n this.data.push(...cloneEntries(entries))\n this.applyFilterAndRender()\n this.emitter.emit('data:change', this.getData())\n }\n\n getFilter(): FilterConfig {\n return deepClone(this.filter)\n }\n\n setFilter(filter: FilterConfig): void {\n if (this.destroyed) return\n this.filter = deepClone(filter || {})\n this.pagination.page = 1\n this.applyFilterAndRender()\n const current = this.getFilter()\n this.emitter.emit('filter:change', current)\n this.config.onFilterChange?.(current)\n }\n\n resetFilter(): void {\n if (this.destroyed) return\n this.setFilter({})\n }\n\n getTheme(): ThemeMode {\n return this.theme\n }\n\n setTheme(theme: ThemeMode): void {\n if (this.destroyed) return\n if (theme !== 'light' && theme !== 'dark') {\n throw new TypeError('[LogViewer] theme must be light or dark')\n }\n if (this.theme === theme) return\n this.theme = theme\n this.root.setAttribute('data-theme', theme)\n this.emitter.emit('theme:change', theme)\n this.config.onThemeChange?.(theme)\n }\n\n expandEntry(id: string): void {\n if (this.destroyed || this.expandedIds.has(id)) return\n this.expandedIds.add(id)\n const entry = this.data.find(item => item.id === id)\n if (entry) this.emitter.emit('entry:expand', deepClone(entry))\n this.renderTimeline()\n }\n\n collapseEntry(id: string): void {\n if (this.destroyed || !this.expandedIds.has(id)) return\n this.expandedIds.delete(id)\n const entry = this.data.find(item => item.id === id)\n if (entry) this.emitter.emit('entry:collapse', deepClone(entry))\n this.renderTimeline()\n }\n\n collapseAll(): void {\n if (this.destroyed || this.expandedIds.size === 0) return\n this.expandedIds.clear()\n this.renderTimeline()\n }\n\n goToPage(page: number): void {\n if (this.destroyed) return\n const totalPages = Math.max(\n 1,\n Math.ceil(this.filteredData.length / this.pagination.pageSize)\n )\n const nextPage = Math.min(totalPages, Math.max(1, normalizePage(page)))\n if (nextPage === this.pagination.page) return\n this.pagination.page = nextPage\n this.emitter.emit('page:change', nextPage)\n this.renderTimeline()\n }\n\n getPagination(): PaginationConfig {\n return {\n ...this.pagination,\n total: this.filteredData.length,\n }\n }\n\n on(event: string, callback: (...args: unknown[]) => void): () => void {\n return this.emitter.on(event, callback)\n }\n\n refresh(): void {\n if (!this.destroyed) this.applyFilterAndRender()\n }\n\n async loadData(): Promise<void> {\n if (this.destroyed || !this.config.dataSource) return\n\n const currentRequest = ++this.requestId\n this.abortController?.abort()\n this.abortController =\n typeof AbortController !== 'undefined' ? new AbortController() : null\n\n try {\n let payload: unknown\n if (typeof this.config.dataSource === 'function') {\n payload = await this.config.dataSource()\n } else {\n if (typeof fetch !== 'function')\n throw new Error('[LogViewer] fetch is unavailable')\n const response = await fetch(this.config.dataSource, {\n signal: this.abortController?.signal,\n })\n if (!response.ok) {\n throw new Error(\n `[LogViewer] dataSource request failed: ${response.status}`\n )\n }\n payload = await response.json()\n }\n\n if (this.destroyed || currentRequest !== this.requestId) return\n this.setData(sourceEntries(payload))\n } catch (error) {\n if (\n this.destroyed ||\n (typeof DOMException !== 'undefined' &&\n error instanceof DOMException &&\n error.name === 'AbortError')\n ) {\n return\n }\n this.config.onError?.(error)\n this.emitter.emit('error', error)\n throw error\n }\n }\n\n destroy(): void {\n if (this.destroyed) return\n this.destroyed = true\n this.requestId++\n this.abortController?.abort()\n this.abortController = null\n this.emitter.removeAllListeners()\n this.expandedIds.clear()\n this.root.replaceChildren()\n this.root.remove()\n }\n\n private applyFilterAndRender(): void {\n this.filteredData = this.filterEngine.filter(this.data, this.filter)\n this.filteredData = this.filteredData\n .map((entry, index) => ({ entry, index }))\n .sort((a, b) => {\n const left = new Date(a.entry.timestamp).getTime()\n const right = new Date(b.entry.timestamp).getTime()\n const leftTime = Number.isFinite(left) ? left : Number.MIN_SAFE_INTEGER\n const rightTime = Number.isFinite(right)\n ? right\n : Number.MIN_SAFE_INTEGER\n return rightTime - leftTime || a.index - b.index\n })\n .map(({ entry }) => entry)\n\n const totalPages = Math.max(\n 1,\n Math.ceil(this.filteredData.length / this.pagination.pageSize)\n )\n this.pagination.page = Math.min(this.pagination.page, totalPages)\n this.pagination.total = this.filteredData.length\n this.renderTimeline()\n }\n\n private getPageData(): LogEntry[] {\n const { page, pageSize } = this.pagination\n const start = (page - 1) * pageSize\n return this.filteredData.slice(start, start + pageSize)\n }\n\n private renderFilterBar(): HTMLElement | null {\n if (this.config.showFilter === false) return null\n\n const filter = document.createElement('div')\n filter.className = 'lv-filter'\n filter.setAttribute('role', 'search')\n\n const addInput = (\n field: 'user' | 'module' | 'ip' | 'keyword',\n label: string\n ) => {\n const item = document.createElement('label')\n item.className = 'lv-filter-item'\n const text = document.createElement('span')\n text.className = 'lv-filter-label'\n text.textContent = label\n const input = document.createElement('input')\n input.className = 'lv-filter-input'\n input.type = 'search'\n input.value = String(this.filter[field] || '')\n input.setAttribute('data-filter-field', field)\n input.setAttribute('aria-label', label)\n item.append(text, input)\n filter.appendChild(item)\n }\n\n addInput('keyword', this.config.locale === 'en-US' ? 'Keyword' : '关键词')\n addInput('user', this.config.locale === 'en-US' ? 'User' : '用户')\n addInput('module', this.config.locale === 'en-US' ? 'Module' : '模块')\n addInput('ip', 'IP')\n\n const actionItem = document.createElement('label')\n actionItem.className = 'lv-filter-item'\n const actionLabel = document.createElement('span')\n actionLabel.className = 'lv-filter-label'\n actionLabel.textContent = this.config.locale === 'en-US' ? 'Action' : '操作'\n const action = document.createElement('select')\n action.className = 'lv-filter-select'\n action.setAttribute('data-filter-field', 'action')\n action.setAttribute('aria-label', actionLabel.textContent)\n const allOption = document.createElement('option')\n allOption.value = ''\n allOption.textContent = this.config.locale === 'en-US' ? 'All' : '全部'\n action.appendChild(allOption)\n for (const [value, label] of Object.entries(ACTION_LABELS)) {\n const option = document.createElement('option')\n option.value = value\n option.textContent = label\n action.appendChild(option)\n }\n const selectedAction = Array.isArray(this.filter.action)\n ? ''\n : this.filter.action || ''\n action.value = selectedAction\n actionItem.append(actionLabel, action)\n filter.appendChild(actionItem)\n\n const reset = document.createElement('button')\n reset.type = 'button'\n reset.className = 'lv-filter-btn'\n reset.setAttribute('data-filter-reset', 'true')\n reset.textContent = this.config.locale === 'en-US' ? 'Reset' : '重置'\n filter.appendChild(reset)\n return filter\n }\n\n private updateFilterField(field: string, value: string): void {\n const next = this.getFilter()\n if (!value.trim()) {\n delete next[field as keyof FilterConfig]\n } else if (field === 'action' && isActionType(value)) {\n next.action = value\n } else if (field !== 'action') {\n next[field as 'user' | 'module' | 'ip' | 'keyword'] = value\n }\n this.setFilter(next)\n }\n\n private renderTimeline(): void {\n if (this.destroyed) return\n\n const pageData = this.getPageData()\n const timeline = this.timelineRenderer.render(pageData, this.expandedIds, {\n showDiff: this.config.showDiff,\n showAnomalyBadge: this.config.showAnomalyBadge,\n locale: this.config.locale,\n })\n\n if (this.config.showDiff !== false) {\n for (const entry of pageData) {\n if (!this.expandedIds.has(entry.id)) continue\n const item = [...timeline.children].find(\n child => child.getAttribute('data-id') === entry.id\n )\n const card = item?.querySelector('.lv-timeline-card')\n card?.appendChild(this.diffRenderer.renderDetail(entry))\n }\n }\n\n this.filterEl?.remove()\n this.timelineEl?.remove()\n this.paginationEl?.remove()\n\n this.filterEl = this.renderFilterBar()\n this.timelineEl = timeline\n this.paginationEl = this.timelineRenderer.renderPagination(\n this.pagination.page,\n this.pagination.pageSize,\n this.filteredData.length\n )\n\n if (this.filterEl) this.root.appendChild(this.filterEl)\n this.root.append(this.timelineEl, this.paginationEl)\n }\n\n private bindEvents(): void {\n this.root.addEventListener('change', event => {\n const target = event.target\n if (\n !(target instanceof HTMLInputElement) &&\n !(target instanceof HTMLSelectElement)\n )\n return\n const field = target.getAttribute('data-filter-field')\n if (field) this.updateFilterField(field, target.value)\n })\n\n this.root.addEventListener('click', event => {\n const target = event.target\n if (!(target instanceof Element)) return\n\n const reset = target.closest('[data-filter-reset]')\n if (reset) {\n this.resetFilter()\n return\n }\n\n const hint = target.closest('.lv-timeline-expand-hint')\n const item =\n hint?.closest('.lv-timeline-item') ||\n target.closest('.lv-timeline-item')\n const id = item?.getAttribute('data-id')\n if (hint && id) {\n if (this.expandedIds.has(id)) this.collapseEntry(id)\n else this.expandEntry(id)\n return\n }\n\n const pageButton = target.closest(\n '.lv-pagination-btn'\n ) as HTMLButtonElement | null\n if (pageButton && !pageButton.disabled) {\n const page = Number(pageButton.getAttribute('data-page'))\n if (Number.isFinite(page)) this.goToPage(page)\n return\n }\n\n const card = target.closest('.lv-timeline-card')\n if (!card || !id) return\n const entry = this.filteredData.find(candidate => candidate.id === id)\n if (entry) {\n const snapshot = deepClone(entry)\n this.emitter.emit('entry:click', snapshot)\n this.config.onEntryClick?.(snapshot)\n }\n })\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAqBA,MAAM,oBAAoB;AAC1B,MAAM,gBAAgB;AAEtB,SAAS,kBACP,OACA,WAAW,mBACH;CACR,MAAM,WAAW,OAAO,KAAK;CAC7B,IAAI,CAAC,OAAO,SAAS,QAAQ,KAAK,YAAY,GAAG,OAAO;CACxD,OAAO,KAAK,IAAI,eAAe,KAAK,IAAI,GAAG,KAAK,MAAM,QAAQ,CAAC,CAAC;AAClE;AAEA,SAAS,cAAc,OAAwB;CAC7C,MAAM,OAAO,OAAO,KAAK;CACzB,IAAI,CAAC,OAAO,SAAS,IAAI,KAAK,OAAO,GAAG,OAAO;CAC/C,OAAO,KAAK,MAAM,IAAI;AACxB;AAEA,SAAS,aAAa,SAAsD;CAC1E,IAAI,YAAY,KAAA,GAAW,OAAO,CAAC;CACnC,IAAI,CAAC,MAAM,QAAQ,OAAO,GACxB,MAAM,IAAI,UAAU,mCAAmC;CAEzD,OAAO,QAAQ,KAAK,OAAO,UAAU;EACnC,IAAI,CAAC,SAAS,OAAO,UAAU,UAC7B,MAAM,IAAI,UAAU,oBAAoB,MAAK,oBAAqB;EAGpE,MAAM,SAAS,gBAAA,UAAU,KAAK;EAC9B,MAAM,KAAK,OAAO,OAAO,MAAM,EAAE,CAAA,CAAE,KAAK;EACxC,IAAI,CAAC,IACH,MAAM,IAAI,UACR,oBAAoB,MAAK,gCAC3B;EAEF,OAAO;GACL,GAAG;GACH;GACA,MAAM,OAAO,OAAO,QAAQ,EAAE;GAC9B,QAAQ,OAAO,OAAO,UAAU,EAAE;GAClC,SAAS,MAAM,QAAQ,OAAO,OAAO,IAAI,OAAO,UAAU,KAAA;EAC5D;CACF,CAAC;AACH;AAEA,SAAS,aAAa,OAAoC;CACxD,OAAO,OAAO,KAAK,cAAA,aAAa,CAAA,CAAE,SAAS,KAAK;AAClD;AAEA,SAAS,cAAc,SAA8B;CACnD,IAAI,MAAM,QAAQ,OAAO,GAAG,OAAO;CACnC,IAAI,CAAC,WAAW,OAAO,YAAY,UACjC,MAAM,IAAI,UAAU,kDAAkD;CAGxE,MAAM,SAAS;CACf,KAAA,MAAW,OAAO;EAAC;EAAQ;EAAQ;EAAS;CAAS,GACnD,IAAI,MAAM,QAAQ,OAAO,IAAI,GAAG,OAAO,OAAO;CAEhD,MAAM,IAAI,UAAU,uDAAuD;AAC7E;AAGO,IAAM,YAAN,MAA6C;CAwBlD,YAAY,QAAyB;EAtBrC,KAAiB,UAAU,IAAI,qBAAA,aAAa;EAC5C,KAAiB,eAAe,IAAI,qBAAA,aAAa;EACjD,KAAiB,mBAAmB,IAAI,yBAAA,iBAAiB;EACzD,KAAiB,eAAe,IAAI,qBAAA,aAAa;EAGjD,KAAQ,OAAmB,CAAC;EAC5B,KAAQ,eAA2B,CAAC;EACpC,KAAQ,SAAuB,CAAC;EAChC,KAAQ,aAA+B;GACrC,MAAM;GACN,UAAU;EACZ;EACA,KAAQ,QAAmB;EAC3B,KAAQ,8BAAc,IAAI,IAAY;EACtC,KAAQ,WAA+B;EACvC,KAAQ,aAAiC;EACzC,KAAQ,eAAmC;EAC3C,KAAQ,YAAY;EACpB,KAAQ,YAAY;EACpB,KAAQ,kBAA0C;EAGhD,IAAI,OAAO,aAAa,aACtB,MAAM,IAAI,MAAM,4CAA4C;EAG9D,MAAM,YACJ,OAAO,OAAO,cAAc,WACxB,SAAS,cAA2B,OAAO,SAAS,IACpD,OAAO;EACb,IAAI,CAAC,WACH,MAAM,IAAI,MACR,oCAAoC,OAAO,OAAO,SAAS,GAC7D;EAEF,KAAK,SAAS,EAAE,GAAG,OAAO;EAC1B,KAAK,QAAQ,OAAO,UAAU,SAAS,SAAS;EAChD,KAAK,aAAa;GAChB,MAAM,cAAc,OAAO,YAAY,IAAI;GAC3C,UAAU,kBACR,OAAO,YAAY,OAAO,YAAY,QACxC;EACF;EACA,KAAK,SAAS,gBAAA,UAAU,OAAO,UAAU,CAAC,CAAC;EAC3C,KAAK,OAAO,aAAa,OAAO,IAAI;EAEpC,gBAAA,aAAa;EACb,KAAK,OAAO,SAAS,cAAc,KAAK;EACxC,KAAK,KAAK,YAAY;EACtB,KAAK,KAAK,aAAa,cAAc,KAAK,KAAK;EAC/C,UAAU,YAAY,KAAK,IAAI;EAE/B,KAAK,WAAW;EAChB,KAAK,qBAAqB;EAE1B,IAAI,OAAO,YACT,KAAU,SAAS,CAAA,CAAE,YAAY,KAAA,CAAS;CAE9C;CAEA,UAAsB;EACpB,OAAO,gBAAA,UAAU,KAAK,IAAI;CAC5B;CAEA,QAAQ,SAA2B;EACjC,IAAI,KAAK,WAAW;EACpB,KAAK,OAAO,aAAa,OAAO;EAChC,KAAK,WAAW,OAAO;EACvB,KAAK,YAAY,MAAM;EACvB,KAAK,qBAAqB;EAC1B,KAAK,QAAQ,KAAK,eAAe,KAAK,QAAQ,CAAC;CACjD;CAEA,WAAW,SAA2B;EACpC,IAAI,KAAK,WAAW;EACpB,KAAK,KAAK,KAAK,GAAG,aAAa,OAAO,CAAC;EACvC,KAAK,qBAAqB;EAC1B,KAAK,QAAQ,KAAK,eAAe,KAAK,QAAQ,CAAC;CACjD;CAEA,YAA0B;EACxB,OAAO,gBAAA,UAAU,KAAK,MAAM;CAC9B;CAEA,UAAU,QAA4B;EACpC,IAAI,KAAK,WAAW;EACpB,KAAK,SAAS,gBAAA,UAAU,UAAU,CAAC,CAAC;EACpC,KAAK,WAAW,OAAO;EACvB,KAAK,qBAAqB;EAC1B,MAAM,UAAU,KAAK,UAAU;EAC/B,KAAK,QAAQ,KAAK,iBAAiB,OAAO;EAC1C,KAAK,OAAO,iBAAiB,OAAO;CACtC;CAEA,cAAoB;EAClB,IAAI,KAAK,WAAW;EACpB,KAAK,UAAU,CAAC,CAAC;CACnB;CAEA,WAAsB;EACpB,OAAO,KAAK;CACd;CAEA,SAAS,OAAwB;EAC/B,IAAI,KAAK,WAAW;EACpB,IAAI,UAAU,WAAW,UAAU,QACjC,MAAM,IAAI,UAAU,yCAAyC;EAE/D,IAAI,KAAK,UAAU,OAAO;EAC1B,KAAK,QAAQ;EACb,KAAK,KAAK,aAAa,cAAc,KAAK;EAC1C,KAAK,QAAQ,KAAK,gBAAgB,KAAK;EACvC,KAAK,OAAO,gBAAgB,KAAK;CACnC;CAEA,YAAY,IAAkB;EAC5B,IAAI,KAAK,aAAa,KAAK,YAAY,IAAI,EAAE,GAAG;EAChD,KAAK,YAAY,IAAI,EAAE;EACvB,MAAM,QAAQ,KAAK,KAAK,MAAK,SAAQ,KAAK,OAAO,EAAE;EACnD,IAAI,OAAO,KAAK,QAAQ,KAAK,gBAAgB,gBAAA,UAAU,KAAK,CAAC;EAC7D,KAAK,eAAe;CACtB;CAEA,cAAc,IAAkB;EAC9B,IAAI,KAAK,aAAa,CAAC,KAAK,YAAY,IAAI,EAAE,GAAG;EACjD,KAAK,YAAY,OAAO,EAAE;EAC1B,MAAM,QAAQ,KAAK,KAAK,MAAK,SAAQ,KAAK,OAAO,EAAE;EACnD,IAAI,OAAO,KAAK,QAAQ,KAAK,kBAAkB,gBAAA,UAAU,KAAK,CAAC;EAC/D,KAAK,eAAe;CACtB;CAEA,cAAoB;EAClB,IAAI,KAAK,aAAa,KAAK,YAAY,SAAS,GAAG;EACnD,KAAK,YAAY,MAAM;EACvB,KAAK,eAAe;CACtB;CAEA,SAAS,MAAoB;EAC3B,IAAI,KAAK,WAAW;EACpB,MAAM,aAAa,KAAK,IACtB,GACA,KAAK,KAAK,KAAK,aAAa,SAAS,KAAK,WAAW,QAAQ,CAC/D;EACA,MAAM,WAAW,KAAK,IAAI,YAAY,KAAK,IAAI,GAAG,cAAc,IAAI,CAAC,CAAC;EACtE,IAAI,aAAa,KAAK,WAAW,MAAM;EACvC,KAAK,WAAW,OAAO;EACvB,KAAK,QAAQ,KAAK,eAAe,QAAQ;EACzC,KAAK,eAAe;CACtB;CAEA,gBAAkC;EAChC,OAAO;GACL,GAAG,KAAK;GACR,OAAO,KAAK,aAAa;EAC3B;CACF;CAEA,GAAG,OAAe,UAAoD;EACpE,OAAO,KAAK,QAAQ,GAAG,OAAO,QAAQ;CACxC;CAEA,UAAgB;EACd,IAAI,CAAC,KAAK,WAAW,KAAK,qBAAqB;CACjD;CAEA,MAAM,WAA0B;EAC9B,IAAI,KAAK,aAAa,CAAC,KAAK,OAAO,YAAY;EAE/C,MAAM,iBAAiB,EAAE,KAAK;EAC9B,KAAK,iBAAiB,MAAM;EAC5B,KAAK,kBACH,OAAO,oBAAoB,cAAc,IAAI,gBAAgB,IAAI;EAEnE,IAAI;GACF,IAAI;GACJ,IAAI,OAAO,KAAK,OAAO,eAAe,YACpC,UAAU,MAAM,KAAK,OAAO,WAAW;QAClC;IACL,IAAI,OAAO,UAAU,YACnB,MAAM,IAAI,MAAM,kCAAkC;IACpD,MAAM,WAAW,MAAM,MAAM,KAAK,OAAO,YAAY,EACnD,QAAQ,KAAK,iBAAiB,OAChC,CAAC;IACD,IAAI,CAAC,SAAS,IACZ,MAAM,IAAI,MACR,0CAA0C,SAAS,QACrD;IAEF,UAAU,MAAM,SAAS,KAAK;GAChC;GAEA,IAAI,KAAK,aAAa,mBAAmB,KAAK,WAAW;GACzD,KAAK,QAAQ,cAAc,OAAO,CAAC;EACrC,SAAS,OAAO;GACd,IACE,KAAK,aACJ,OAAO,iBAAiB,eACvB,iBAAiB,gBACjB,MAAM,SAAS,cAEjB;GAEF,KAAK,OAAO,UAAU,KAAK;GAC3B,KAAK,QAAQ,KAAK,SAAS,KAAK;GAChC,MAAM;EACR;CACF;CAEA,UAAgB;EACd,IAAI,KAAK,WAAW;EACpB,KAAK,YAAY;EACjB,KAAK;EACL,KAAK,iBAAiB,MAAM;EAC5B,KAAK,kBAAkB;EACvB,KAAK,QAAQ,mBAAmB;EAChC,KAAK,YAAY,MAAM;EACvB,KAAK,KAAK,gBAAgB;EAC1B,KAAK,KAAK,OAAO;CACnB;CAEQ,uBAA6B;EACnC,KAAK,eAAe,KAAK,aAAa,OAAO,KAAK,MAAM,KAAK,MAAM;EACnE,KAAK,eAAe,KAAK,aACtB,KAAK,OAAO,WAAW;GAAE;GAAO;EAAM,EAAE,CAAA,CACxC,MAAM,GAAG,MAAM;GACd,MAAM,OAAO,IAAI,KAAK,EAAE,MAAM,SAAS,CAAA,CAAE,QAAQ;GACjD,MAAM,QAAQ,IAAI,KAAK,EAAE,MAAM,SAAS,CAAA,CAAE,QAAQ;GAClD,MAAM,WAAW,OAAO,SAAS,IAAI,IAAI,OAAO,OAAO;GAIvD,QAHkB,OAAO,SAAS,KAAK,IACnC,QACA,OAAO,oBACQ,YAAY,EAAE,QAAQ,EAAE;EAC7C,CAAC,CAAA,CACA,KAAK,EAAE,YAAY,KAAK;EAE3B,MAAM,aAAa,KAAK,IACtB,GACA,KAAK,KAAK,KAAK,aAAa,SAAS,KAAK,WAAW,QAAQ,CAC/D;EACA,KAAK,WAAW,OAAO,KAAK,IAAI,KAAK,WAAW,MAAM,UAAU;EAChE,KAAK,WAAW,QAAQ,KAAK,aAAa;EAC1C,KAAK,eAAe;CACtB;CAEQ,cAA0B;EAChC,MAAM,EAAE,MAAM,aAAa,KAAK;EAChC,MAAM,SAAS,OAAO,KAAK;EAC3B,OAAO,KAAK,aAAa,MAAM,OAAO,QAAQ,QAAQ;CACxD;CAEQ,kBAAsC;EAC5C,IAAI,KAAK,OAAO,eAAe,OAAO,OAAO;EAE7C,MAAM,SAAS,SAAS,cAAc,KAAK;EAC3C,OAAO,YAAY;EACnB,OAAO,aAAa,QAAQ,QAAQ;EAEpC,MAAM,YACJ,OACA,UACG;GACH,MAAM,OAAO,SAAS,cAAc,OAAO;GAC3C,KAAK,YAAY;GACjB,MAAM,OAAO,SAAS,cAAc,MAAM;GAC1C,KAAK,YAAY;GACjB,KAAK,cAAc;GACnB,MAAM,QAAQ,SAAS,cAAc,OAAO;GAC5C,MAAM,YAAY;GAClB,MAAM,OAAO;GACb,MAAM,QAAQ,OAAO,KAAK,OAAO,UAAU,EAAE;GAC7C,MAAM,aAAa,qBAAqB,KAAK;GAC7C,MAAM,aAAa,cAAc,KAAK;GACtC,KAAK,OAAO,MAAM,KAAK;GACvB,OAAO,YAAY,IAAI;EACzB;EAEA,SAAS,WAAW,KAAK,OAAO,WAAW,UAAU,YAAY,KAAK;EACtE,SAAS,QAAQ,KAAK,OAAO,WAAW,UAAU,SAAS,IAAI;EAC/D,SAAS,UAAU,KAAK,OAAO,WAAW,UAAU,WAAW,IAAI;EACnE,SAAS,MAAM,IAAI;EAEnB,MAAM,aAAa,SAAS,cAAc,OAAO;EACjD,WAAW,YAAY;EACvB,MAAM,cAAc,SAAS,cAAc,MAAM;EACjD,YAAY,YAAY;EACxB,YAAY,cAAc,KAAK,OAAO,WAAW,UAAU,WAAW;EACtE,MAAM,SAAS,SAAS,cAAc,QAAQ;EAC9C,OAAO,YAAY;EACnB,OAAO,aAAa,qBAAqB,QAAQ;EACjD,OAAO,aAAa,cAAc,YAAY,WAAW;EACzD,MAAM,YAAY,SAAS,cAAc,QAAQ;EACjD,UAAU,QAAQ;EAClB,UAAU,cAAc,KAAK,OAAO,WAAW,UAAU,QAAQ;EACjE,OAAO,YAAY,SAAS;EAC5B,KAAA,MAAW,CAAC,OAAO,UAAU,OAAO,QAAQ,cAAA,aAAa,GAAG;GAC1D,MAAM,SAAS,SAAS,cAAc,QAAQ;GAC9C,OAAO,QAAQ;GACf,OAAO,cAAc;GACrB,OAAO,YAAY,MAAM;EAC3B;EAIA,OAAO,QAHgB,MAAM,QAAQ,KAAK,OAAO,MAAM,IACnD,KACA,KAAK,OAAO,UAAU;EAE1B,WAAW,OAAO,aAAa,MAAM;EACrC,OAAO,YAAY,UAAU;EAE7B,MAAM,QAAQ,SAAS,cAAc,QAAQ;EAC7C,MAAM,OAAO;EACb,MAAM,YAAY;EAClB,MAAM,aAAa,qBAAqB,MAAM;EAC9C,MAAM,cAAc,KAAK,OAAO,WAAW,UAAU,UAAU;EAC/D,OAAO,YAAY,KAAK;EACxB,OAAO;CACT;CAEQ,kBAAkB,OAAe,OAAqB;EAC5D,MAAM,OAAO,KAAK,UAAU;EAC5B,IAAI,CAAC,MAAM,KAAK,GACd,OAAO,KAAK;OACd,IAAW,UAAU,YAAY,aAAa,KAAK,GACjD,KAAK,SAAS;OAChB,IAAW,UAAU,UACnB,KAAK,SAAiD;EAExD,KAAK,UAAU,IAAI;CACrB;CAEQ,iBAAuB;EAC7B,IAAI,KAAK,WAAW;EAEpB,MAAM,WAAW,KAAK,YAAY;EAClC,MAAM,WAAW,KAAK,iBAAiB,OAAO,UAAU,KAAK,aAAa;GACxE,UAAU,KAAK,OAAO;GACtB,kBAAkB,KAAK,OAAO;GAC9B,QAAQ,KAAK,OAAO;EACtB,CAAC;EAED,IAAI,KAAK,OAAO,aAAa,OAC3B,KAAA,MAAW,SAAS,UAAU;GAC5B,IAAI,CAAC,KAAK,YAAY,IAAI,MAAM,EAAE,GAAG;GAKrC,CAJa,CAAC,GAAG,SAAS,QAAQ,CAAA,CAAE,MAClC,UAAS,MAAM,aAAa,SAAS,MAAM,MAAM,EAEtC,CAAA,EAAM,cAAc,mBAAmB,EAAA,EAC9C,YAAY,KAAK,aAAa,aAAa,KAAK,CAAC;EACzD;EAGF,KAAK,UAAU,OAAO;EACtB,KAAK,YAAY,OAAO;EACxB,KAAK,cAAc,OAAO;EAE1B,KAAK,WAAW,KAAK,gBAAgB;EACrC,KAAK,aAAa;EAClB,KAAK,eAAe,KAAK,iBAAiB,iBACxC,KAAK,WAAW,MAChB,KAAK,WAAW,UAChB,KAAK,aAAa,MACpB;EAEA,IAAI,KAAK,UAAU,KAAK,KAAK,YAAY,KAAK,QAAQ;EACtD,KAAK,KAAK,OAAO,KAAK,YAAY,KAAK,YAAY;CACrD;CAEQ,aAAmB;EACzB,KAAK,KAAK,iBAAiB,WAAU,UAAS;GAC5C,MAAM,SAAS,MAAM;GACrB,IACE,EAAE,kBAAkB,qBACpB,EAAE,kBAAkB,oBAEpB;GACF,MAAM,QAAQ,OAAO,aAAa,mBAAmB;GACrD,IAAI,OAAO,KAAK,kBAAkB,OAAO,OAAO,KAAK;EACvD,CAAC;EAED,KAAK,KAAK,iBAAiB,UAAS,UAAS;GAC3C,MAAM,SAAS,MAAM;GACrB,IAAI,EAAE,kBAAkB,UAAU;GAGlC,IADc,OAAO,QAAQ,qBACzB,GAAO;IACT,KAAK,YAAY;IACjB;GACF;GAEA,MAAM,OAAO,OAAO,QAAQ,0BAA0B;GAItD,MAAM,MAFJ,MAAM,QAAQ,mBAAmB,KACjC,OAAO,QAAQ,mBAAmB,EAAA,EACnB,aAAa,SAAS;GACvC,IAAI,QAAQ,IAAI;IACd,IAAI,KAAK,YAAY,IAAI,EAAE,GAAG,KAAK,cAAc,EAAE;SAC9C,KAAK,YAAY,EAAE;IACxB;GACF;GAEA,MAAM,aAAa,OAAO,QACxB,oBACF;GACA,IAAI,cAAc,CAAC,WAAW,UAAU;IACtC,MAAM,OAAO,OAAO,WAAW,aAAa,WAAW,CAAC;IACxD,IAAI,OAAO,SAAS,IAAI,GAAG,KAAK,SAAS,IAAI;IAC7C;GACF;GAGA,IAAI,CADS,OAAO,QAAQ,mBACvB,KAAQ,CAAC,IAAI;GAClB,MAAM,QAAQ,KAAK,aAAa,MAAK,cAAa,UAAU,OAAO,EAAE;GACrE,IAAI,OAAO;IACT,MAAM,WAAW,gBAAA,UAAU,KAAK;IAChC,KAAK,QAAQ,KAAK,eAAe,QAAQ;IACzC,KAAK,OAAO,eAAe,QAAQ;GACrC;EACF,CAAC;CACH;AACF"}
@@ -1,26 +1,27 @@
1
1
  /**
2
2
  * LogViewer 核心引擎
3
3
  */
4
- import type { LogEntry, LogViewerConfig, LogViewerInstance, FilterConfig, PaginationConfig, ThemeMode } from '../types';
5
- /**
6
- * LogViewer - Log Viewer class
7
- */
4
+ import type { FilterConfig, LogEntry, LogViewerConfig, LogViewerInstance, PaginationConfig, ThemeMode } from '../types';
5
+ /** LogViewer - framework-neutral audit log viewer. */
8
6
  export declare class LogViewer implements LogViewerInstance {
9
- private root;
10
- private emitter;
11
- private filterEngine;
12
- private timelineRenderer;
13
- private diffRenderer;
7
+ private readonly root;
8
+ private readonly emitter;
9
+ private readonly filterEngine;
10
+ private readonly timelineRenderer;
11
+ private readonly diffRenderer;
12
+ private readonly config;
14
13
  private data;
15
14
  private filteredData;
16
15
  private filter;
17
16
  private pagination;
18
17
  private theme;
19
18
  private expandedIds;
20
- private config;
19
+ private filterEl;
21
20
  private timelineEl;
22
21
  private paginationEl;
23
22
  private destroyed;
23
+ private requestId;
24
+ private abortController;
24
25
  constructor(config: LogViewerConfig);
25
26
  getData(): LogEntry[];
26
27
  setData(entries: LogEntry[]): void;
@@ -37,10 +38,12 @@ export declare class LogViewer implements LogViewerInstance {
37
38
  getPagination(): PaginationConfig;
38
39
  on(event: string, callback: (...args: unknown[]) => void): () => void;
39
40
  refresh(): void;
41
+ loadData(): Promise<void>;
40
42
  destroy(): void;
41
43
  private applyFilterAndRender;
42
44
  private getPageData;
45
+ private renderFilterBar;
46
+ private updateFilterField;
43
47
  private renderTimeline;
44
48
  private bindEvents;
45
49
  }
46
- //# sourceMappingURL=LogViewer.d.ts.map
@@ -2,7 +2,7 @@
2
2
  * ***********************************
3
3
  * @composy/log-viewer-core v0.0.1 *
4
4
  * Built with rollup *
5
- * Build time: 2024-08-17 11:33:32 *
5
+ * Build time: 2024-10-12 17:29:20 *
6
6
  * Build mode: production *
7
7
  * Minified: No *
8
8
  * ***********************************
@@ -14,39 +14,44 @@ var FilterEngine = class {
14
14
  * 对日志条目进行多维筛选
15
15
  */
16
16
  filter(entries, config) {
17
- let result = entries;
18
- if (config.user) {
19
- const keyword = config.user.toLowerCase();
20
- result = result.filter((e) => e.user.toLowerCase().includes(keyword) || e.userId && e.userId.toLowerCase().includes(keyword));
17
+ let result = Array.isArray(entries) ? entries.slice() : [];
18
+ const normalizedConfig = config || {};
19
+ if (normalizedConfig.user?.trim()) {
20
+ const keyword = normalizedConfig.user.trim().toLowerCase();
21
+ result = result.filter((e) => e.user?.toLowerCase().includes(keyword) || e.userId?.toLowerCase().includes(keyword));
21
22
  }
22
- if (config.action) {
23
- const actions = Array.isArray(config.action) ? config.action : [config.action];
24
- result = result.filter((e) => actions.includes(e.action));
23
+ if (normalizedConfig.action) {
24
+ const actions = Array.isArray(normalizedConfig.action) ? normalizedConfig.action : [normalizedConfig.action];
25
+ if (actions.length > 0) result = result.filter((e) => actions.includes(e.action));
25
26
  }
26
- if (config.module) {
27
- const mod = config.module.toLowerCase();
28
- result = result.filter((e) => e.module.toLowerCase().includes(mod));
27
+ if (normalizedConfig.module?.trim()) {
28
+ const mod = normalizedConfig.module.trim().toLowerCase();
29
+ result = result.filter((e) => e.module?.toLowerCase().includes(mod));
29
30
  }
30
- if (config.ip) {
31
- const ip = config.ip.toLowerCase();
32
- result = result.filter((e) => e.ip && e.ip.toLowerCase().includes(ip));
31
+ if (normalizedConfig.ip?.trim()) {
32
+ const ip = normalizedConfig.ip.trim().toLowerCase();
33
+ result = result.filter((e) => e.ip?.toLowerCase().includes(ip));
33
34
  }
34
- if (config.timeRange) {
35
- const [start, end] = config.timeRange;
35
+ if (normalizedConfig.timeRange) {
36
+ const [start, end] = normalizedConfig.timeRange;
36
37
  const startTime = new Date(start).getTime();
37
38
  const endTime = new Date(end).getTime();
38
- result = result.filter((e) => {
39
- const t = new Date(e.timestamp).getTime();
40
- return t >= startTime && t <= endTime;
41
- });
39
+ if (Number.isFinite(startTime) && Number.isFinite(endTime)) {
40
+ const lower = Math.min(startTime, endTime);
41
+ const upper = Math.max(startTime, endTime);
42
+ result = result.filter((e) => {
43
+ const t = new Date(e.timestamp).getTime();
44
+ return Number.isFinite(t) && t >= lower && t <= upper;
45
+ });
46
+ }
42
47
  }
43
- if (config.keyword) {
44
- const kw = config.keyword.toLowerCase();
48
+ if (normalizedConfig.keyword?.trim()) {
49
+ const kw = normalizedConfig.keyword.trim().toLowerCase();
45
50
  result = result.filter((e) => {
46
51
  return [
47
- e.user,
52
+ e.user || "",
48
53
  e.action,
49
- e.module,
54
+ e.module || "",
50
55
  e.detail || "",
51
56
  e.ip || "",
52
57
  e.id
@@ -59,13 +64,13 @@ var FilterEngine = class {
59
64
  * 获取所有唯一操作人
60
65
  */
61
66
  getUniqueUsers(entries) {
62
- return [...new Set(entries.map((e) => e.user))].sort();
67
+ return [...new Set(entries.map((e) => e.user).filter(Boolean))].sort();
63
68
  }
64
69
  /**
65
70
  * 获取所有唯一模块
66
71
  */
67
72
  getUniqueModules(entries) {
68
- return [...new Set(entries.map((e) => e.module))].sort();
73
+ return [...new Set(entries.map((e) => e.module).filter(Boolean))].sort();
69
74
  }
70
75
  /**
71
76
  * 获取所有唯一操作类型
@@ -1 +1 @@
1
- {"version":3,"file":"FilterEngine.cjs","names":[],"sources":["../../src/filters/FilterEngine.ts"],"sourcesContent":["/**\n * 筛选引擎 - 支持按操作人/操作类型/时间范围/模块/IP/关键词筛选\n */\n\nimport type { LogEntry, FilterConfig } from '../types'\n\n/**\n * FilterEngine - Filter Engine class\n */\nexport class FilterEngine {\n /**\n * 对日志条目进行多维筛选\n */\n filter(entries: LogEntry[], config: FilterConfig): LogEntry[] {\n let result = entries\n\n // 按操作人筛选\n if (config.user) {\n const keyword = config.user.toLowerCase()\n result = result.filter(\n (e) =>\n e.user.toLowerCase().includes(keyword) ||\n (e.userId && e.userId.toLowerCase().includes(keyword)),\n )\n }\n\n // 按操作类型筛选\n if (config.action) {\n const actions = Array.isArray(config.action) ? config.action : [config.action]\n result = result.filter((e) => actions.includes(e.action))\n }\n\n // 按模块筛选\n if (config.module) {\n const mod = config.module.toLowerCase()\n result = result.filter((e) => e.module.toLowerCase().includes(mod))\n }\n\n // 按 IP 筛选\n if (config.ip) {\n const ip = config.ip.toLowerCase()\n result = result.filter((e) => e.ip && e.ip.toLowerCase().includes(ip))\n }\n\n // 按时间范围筛选\n if (config.timeRange) {\n const [start, end] = config.timeRange\n const startTime = new Date(start).getTime()\n const endTime = new Date(end).getTime()\n result = result.filter((e) => {\n const t = new Date(e.timestamp).getTime()\n return t >= startTime && t <= endTime\n })\n }\n\n // 按关键词全文搜索\n if (config.keyword) {\n const kw = config.keyword.toLowerCase()\n result = result.filter((e) => {\n const texts = [\n e.user,\n e.action,\n e.module,\n e.detail || '',\n e.ip || '',\n e.id,\n ]\n return texts.some((t) => t.toLowerCase().includes(kw))\n })\n }\n\n return result\n }\n\n /**\n * 获取所有唯一操作人\n */\n getUniqueUsers(entries: LogEntry[]): string[] {\n return [...new Set(entries.map((e) => e.user))].sort()\n }\n\n /**\n * 获取所有唯一模块\n */\n getUniqueModules(entries: LogEntry[]): string[] {\n return [...new Set(entries.map((e) => e.module))].sort()\n }\n\n /**\n * 获取所有唯一操作类型\n */\n getUniqueActions(entries: LogEntry[]): string[] {\n return [...new Set(entries.map((e) => e.action))].sort()\n }\n}\n"],"mappings":";;;;;;;;;;;AASO,IAAM,eAAN,MAAmB;;;;CAIxB,OAAO,SAAqB,QAAkC;EAC5D,IAAI,SAAS;EAGb,IAAI,OAAO,MAAM;GACf,MAAM,UAAU,OAAO,KAAK,YAAY;GACxC,SAAS,OAAO,QACb,MACC,EAAE,KAAK,YAAY,CAAA,CAAE,SAAS,OAAO,KACpC,EAAE,UAAU,EAAE,OAAO,YAAY,CAAA,CAAE,SAAS,OAAO,CACxD;EACF;EAGA,IAAI,OAAO,QAAQ;GACjB,MAAM,UAAU,MAAM,QAAQ,OAAO,MAAM,IAAI,OAAO,SAAS,CAAC,OAAO,MAAM;GAC7E,SAAS,OAAO,QAAQ,MAAM,QAAQ,SAAS,EAAE,MAAM,CAAC;EAC1D;EAGA,IAAI,OAAO,QAAQ;GACjB,MAAM,MAAM,OAAO,OAAO,YAAY;GACtC,SAAS,OAAO,QAAQ,MAAM,EAAE,OAAO,YAAY,CAAA,CAAE,SAAS,GAAG,CAAC;EACpE;EAGA,IAAI,OAAO,IAAI;GACb,MAAM,KAAK,OAAO,GAAG,YAAY;GACjC,SAAS,OAAO,QAAQ,MAAM,EAAE,MAAM,EAAE,GAAG,YAAY,CAAA,CAAE,SAAS,EAAE,CAAC;EACvE;EAGA,IAAI,OAAO,WAAW;GACpB,MAAM,CAAC,OAAO,OAAO,OAAO;GAC5B,MAAM,YAAY,IAAI,KAAK,KAAK,CAAA,CAAE,QAAQ;GAC1C,MAAM,UAAU,IAAI,KAAK,GAAG,CAAA,CAAE,QAAQ;GACtC,SAAS,OAAO,QAAQ,MAAM;IAC5B,MAAM,IAAI,IAAI,KAAK,EAAE,SAAS,CAAA,CAAE,QAAQ;IACxC,OAAO,KAAK,aAAa,KAAK;GAChC,CAAC;EACH;EAGA,IAAI,OAAO,SAAS;GAClB,MAAM,KAAK,OAAO,QAAQ,YAAY;GACtC,SAAS,OAAO,QAAQ,MAAM;IAS5B,OAAO;KAPL,EAAE;KACF,EAAE;KACF,EAAE;KACF,EAAE,UAAU;KACZ,EAAE,MAAM;KACR,EAAE;IAEG,CAAA,CAAM,MAAM,MAAM,EAAE,YAAY,CAAA,CAAE,SAAS,EAAE,CAAC;GACvD,CAAC;EACH;EAEA,OAAO;CACT;;;;CAKA,eAAe,SAA+B;EAC5C,OAAO,CAAC,GAAG,IAAI,IAAI,QAAQ,KAAK,MAAM,EAAE,IAAI,CAAC,CAAC,CAAA,CAAE,KAAK;CACvD;;;;CAKA,iBAAiB,SAA+B;EAC9C,OAAO,CAAC,GAAG,IAAI,IAAI,QAAQ,KAAK,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA,CAAE,KAAK;CACzD;;;;CAKA,iBAAiB,SAA+B;EAC9C,OAAO,CAAC,GAAG,IAAI,IAAI,QAAQ,KAAK,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA,CAAE,KAAK;CACzD;AACF"}
1
+ {"version":3,"file":"FilterEngine.cjs","names":[],"sources":["../../src/filters/FilterEngine.ts"],"sourcesContent":["/**\r\n * 筛选引擎 - 支持按操作人/操作类型/时间范围/模块/IP/关键词筛选\r\n */\r\n\r\nimport type { LogEntry, FilterConfig } from '../types'\r\n\r\n/**\r\n * FilterEngine - Filter Engine class\r\n */\r\nexport class FilterEngine {\r\n /**\r\n * 对日志条目进行多维筛选\r\n */\r\n filter(entries: LogEntry[], config: FilterConfig): LogEntry[] {\r\n let result = Array.isArray(entries) ? entries.slice() : []\r\n const normalizedConfig = config || {}\r\n\r\n // 按操作人筛选\r\n if (normalizedConfig.user?.trim()) {\r\n const keyword = normalizedConfig.user.trim().toLowerCase()\r\n result = result.filter(\r\n e =>\r\n e.user?.toLowerCase().includes(keyword) ||\r\n e.userId?.toLowerCase().includes(keyword)\r\n )\r\n }\r\n\r\n // 按操作类型筛选\r\n if (normalizedConfig.action) {\r\n const actions = Array.isArray(normalizedConfig.action)\r\n ? normalizedConfig.action\r\n : [normalizedConfig.action]\r\n if (actions.length > 0)\r\n result = result.filter(e => actions.includes(e.action))\r\n }\r\n\r\n // 按模块筛选\r\n if (normalizedConfig.module?.trim()) {\r\n const mod = normalizedConfig.module.trim().toLowerCase()\r\n result = result.filter(e => e.module?.toLowerCase().includes(mod))\r\n }\r\n\r\n // 按 IP 筛选\r\n if (normalizedConfig.ip?.trim()) {\r\n const ip = normalizedConfig.ip.trim().toLowerCase()\r\n result = result.filter(e => e.ip?.toLowerCase().includes(ip))\r\n }\r\n\r\n // 按时间范围筛选\r\n if (normalizedConfig.timeRange) {\r\n const [start, end] = normalizedConfig.timeRange\r\n const startTime = new Date(start).getTime()\r\n const endTime = new Date(end).getTime()\r\n if (Number.isFinite(startTime) && Number.isFinite(endTime)) {\r\n const lower = Math.min(startTime, endTime)\r\n const upper = Math.max(startTime, endTime)\r\n result = result.filter(e => {\r\n const t = new Date(e.timestamp).getTime()\r\n return Number.isFinite(t) && t >= lower && t <= upper\r\n })\r\n }\r\n }\r\n\r\n // 按关键词全文搜索\r\n if (normalizedConfig.keyword?.trim()) {\r\n const kw = normalizedConfig.keyword.trim().toLowerCase()\r\n result = result.filter(e => {\r\n const texts = [\r\n e.user || '',\r\n e.action,\r\n e.module || '',\r\n e.detail || '',\r\n e.ip || '',\r\n e.id,\r\n ]\r\n return texts.some(t => t.toLowerCase().includes(kw))\r\n })\r\n }\r\n\r\n return result\r\n }\r\n\r\n /**\r\n * 获取所有唯一操作人\r\n */\r\n getUniqueUsers(entries: LogEntry[]): string[] {\r\n return [...new Set(entries.map(e => e.user).filter(Boolean))].sort()\r\n }\r\n\r\n /**\r\n * 获取所有唯一模块\r\n */\r\n getUniqueModules(entries: LogEntry[]): string[] {\r\n return [...new Set(entries.map(e => e.module).filter(Boolean))].sort()\r\n }\r\n\r\n /**\r\n * 获取所有唯一操作类型\r\n */\r\n getUniqueActions(entries: LogEntry[]): string[] {\r\n return [...new Set(entries.map(e => e.action))].sort()\r\n }\r\n}\r\n"],"mappings":";;;;;;;;;;;AASO,IAAM,eAAN,MAAmB;;;;CAIxB,OAAO,SAAqB,QAAkC;EAC5D,IAAI,SAAS,MAAM,QAAQ,OAAO,IAAI,QAAQ,MAAM,IAAI,CAAC;EACzD,MAAM,mBAAmB,UAAU,CAAC;EAGpC,IAAI,iBAAiB,MAAM,KAAK,GAAG;GACjC,MAAM,UAAU,iBAAiB,KAAK,KAAK,CAAA,CAAE,YAAY;GACzD,SAAS,OAAO,QACd,MACE,EAAE,MAAM,YAAY,CAAA,CAAE,SAAS,OAAO,KACtC,EAAE,QAAQ,YAAY,CAAA,CAAE,SAAS,OAAO,CAC5C;EACF;EAGA,IAAI,iBAAiB,QAAQ;GAC3B,MAAM,UAAU,MAAM,QAAQ,iBAAiB,MAAM,IACjD,iBAAiB,SACjB,CAAC,iBAAiB,MAAM;GAC5B,IAAI,QAAQ,SAAS,GACnB,SAAS,OAAO,QAAO,MAAK,QAAQ,SAAS,EAAE,MAAM,CAAC;EAC1D;EAGA,IAAI,iBAAiB,QAAQ,KAAK,GAAG;GACnC,MAAM,MAAM,iBAAiB,OAAO,KAAK,CAAA,CAAE,YAAY;GACvD,SAAS,OAAO,QAAO,MAAK,EAAE,QAAQ,YAAY,CAAA,CAAE,SAAS,GAAG,CAAC;EACnE;EAGA,IAAI,iBAAiB,IAAI,KAAK,GAAG;GAC/B,MAAM,KAAK,iBAAiB,GAAG,KAAK,CAAA,CAAE,YAAY;GAClD,SAAS,OAAO,QAAO,MAAK,EAAE,IAAI,YAAY,CAAA,CAAE,SAAS,EAAE,CAAC;EAC9D;EAGA,IAAI,iBAAiB,WAAW;GAC9B,MAAM,CAAC,OAAO,OAAO,iBAAiB;GACtC,MAAM,YAAY,IAAI,KAAK,KAAK,CAAA,CAAE,QAAQ;GAC1C,MAAM,UAAU,IAAI,KAAK,GAAG,CAAA,CAAE,QAAQ;GACtC,IAAI,OAAO,SAAS,SAAS,KAAK,OAAO,SAAS,OAAO,GAAG;IAC1D,MAAM,QAAQ,KAAK,IAAI,WAAW,OAAO;IACzC,MAAM,QAAQ,KAAK,IAAI,WAAW,OAAO;IACzC,SAAS,OAAO,QAAO,MAAK;KAC1B,MAAM,IAAI,IAAI,KAAK,EAAE,SAAS,CAAA,CAAE,QAAQ;KACxC,OAAO,OAAO,SAAS,CAAC,KAAK,KAAK,SAAS,KAAK;IAClD,CAAC;GACH;EACF;EAGA,IAAI,iBAAiB,SAAS,KAAK,GAAG;GACpC,MAAM,KAAK,iBAAiB,QAAQ,KAAK,CAAA,CAAE,YAAY;GACvD,SAAS,OAAO,QAAO,MAAK;IAS1B,OAAO;KAPL,EAAE,QAAQ;KACV,EAAE;KACF,EAAE,UAAU;KACZ,EAAE,UAAU;KACZ,EAAE,MAAM;KACR,EAAE;IAEG,CAAA,CAAM,MAAK,MAAK,EAAE,YAAY,CAAA,CAAE,SAAS,EAAE,CAAC;GACrD,CAAC;EACH;EAEA,OAAO;CACT;;;;CAKA,eAAe,SAA+B;EAC5C,OAAO,CAAC,GAAG,IAAI,IAAI,QAAQ,KAAI,MAAK,EAAE,IAAI,CAAA,CAAE,OAAO,OAAO,CAAC,CAAC,CAAA,CAAE,KAAK;CACrE;;;;CAKA,iBAAiB,SAA+B;EAC9C,OAAO,CAAC,GAAG,IAAI,IAAI,QAAQ,KAAI,MAAK,EAAE,MAAM,CAAA,CAAE,OAAO,OAAO,CAAC,CAAC,CAAA,CAAE,KAAK;CACvE;;;;CAKA,iBAAiB,SAA+B;EAC9C,OAAO,CAAC,GAAG,IAAI,IAAI,QAAQ,KAAI,MAAK,EAAE,MAAM,CAAC,CAAC,CAAA,CAAE,KAAK;CACvD;AACF"}
@@ -23,4 +23,3 @@ export declare class FilterEngine {
23
23
  */
24
24
  getUniqueActions(entries: LogEntry[]): string[];
25
25
  }
26
- //# sourceMappingURL=FilterEngine.d.ts.map
package/lib/index.cjs CHANGED
@@ -2,18 +2,18 @@
2
2
  * ***********************************
3
3
  * @composy/log-viewer-core v0.0.1 *
4
4
  * Built with rollup *
5
- * Build time: 2024-08-17 11:33:32 *
5
+ * Build time: 2024-10-12 17:29:20 *
6
6
  * Build mode: production *
7
7
  * Minified: No *
8
8
  * ***********************************
9
9
  */
10
10
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
11
11
  const require_EventEmitter = require("./core/EventEmitter.cjs");
12
- const require_FilterEngine = require("./filters/FilterEngine.cjs");
13
12
  const require_index = require("./types/index.cjs");
13
+ const require_FilterEngine = require("./filters/FilterEngine.cjs");
14
14
  const require_index$1 = require("./utils/index.cjs");
15
- const require_TimelineRenderer = require("./renderers/TimelineRenderer.cjs");
16
15
  const require_DiffRenderer = require("./renderers/DiffRenderer.cjs");
16
+ const require_TimelineRenderer = require("./renderers/TimelineRenderer.cjs");
17
17
  const require_index$2 = require("./styles/index.cjs");
18
18
  const require_LogViewer = require("./core/LogViewer.cjs");
19
19
  //#region src/index.ts
package/lib/index.cjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["LogViewer"],"sources":["../src/index.ts"],"sourcesContent":["/**\n * @composy/log-viewer-core\n * 审计日志查看器 - 操作日志可视化与追溯\n */\n\nexport const VERSION = '0.1.0'\n\n// 类型\nexport type {\n ActionType,\n ThemeMode,\n AnomalyType,\n DiffEntry,\n LogEntry,\n FilterConfig,\n PaginationConfig,\n LogViewerConfig,\n LogViewerInstance,\n LogViewerEvents,\n} from './types'\nexport { ACTION_LABELS, ACTION_COLORS } from './types'\n\n// 核心\nexport { LogViewer } from './core/LogViewer'\nexport { EventEmitter } from './core/EventEmitter'\n\n// 筛选\nexport { FilterEngine } from './filters/FilterEngine'\n\n// 渲染器\nexport { TimelineRenderer } from './renderers/TimelineRenderer'\nexport { DiffRenderer } from './renderers/DiffRenderer'\n\n// 工具\nexport { formatDate, formatRelativeTime, escapeHtml, debounce, uid, formatValue, deepClone } from './utils'\n\n// 样式\nexport { injectStyles } from './styles'\n\n/**\n * 创建 LogViewer 实例 - 工厂函数\n */\nimport type { LogViewerConfig } from './types'\nimport { LogViewer } from './core/LogViewer'\n\n/**\n * createLogViewer - create Log Viewer function\n * @param config - 参数说明\n * @returns 返回值说明\n */\nexport function createLogViewer(config: LogViewerConfig): LogViewer {\n return new LogViewer(config)\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAKO,MAAM,UAAU;AA6ChB,SAAS,gBAAgB,QAAoC;CAClE,OAAO,IAAIA,kBAAAA,UAAU,MAAM;AAC7B"}
1
+ {"version":3,"file":"index.cjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["/**\r\n * @composy/log-viewer-core\r\n * 审计日志查看器 - 操作日志可视化与追溯\r\n */\r\n\r\n/**\r\n * 创建 LogViewer 实例 - 工厂函数\r\n */\r\nimport type { LogViewerConfig } from './types'\r\nimport { LogViewer } from './core/LogViewer'\r\n\r\nexport const VERSION = '0.1.0'\r\n\r\n// 类型\r\nexport type {\r\n ActionType,\r\n ThemeMode,\r\n AnomalyType,\r\n DiffEntry,\r\n LogEntry,\r\n FilterConfig,\r\n PaginationConfig,\r\n LogViewerConfig,\r\n LogViewerInstance,\r\n LogViewerEvents,\r\n} from './types'\r\nexport { ACTION_LABELS, ACTION_COLORS } from './types'\r\n\r\n// 核心\r\nexport { LogViewer } from './core/LogViewer'\r\nexport { EventEmitter } from './core/EventEmitter'\r\n\r\n// 筛选\r\nexport { FilterEngine } from './filters/FilterEngine'\r\n\r\n// 渲染器\r\nexport { TimelineRenderer } from './renderers/TimelineRenderer'\r\nexport type { TimelineRendererOptions } from './renderers/TimelineRenderer'\r\nexport { DiffRenderer } from './renderers/DiffRenderer'\r\n\r\n// 工具\r\nexport {\r\n formatDate,\r\n formatRelativeTime,\r\n escapeHtml,\r\n debounce,\r\n uid,\r\n formatValue,\r\n deepClone,\r\n} from './utils'\r\n\r\n// 样式\r\nexport { injectStyles } from './styles'\r\n\r\n/**\r\n * createLogViewer - create Log Viewer function\r\n * @param config - 参数说明\r\n * @returns 返回值说明\r\n */\r\nexport function createLogViewer(config: LogViewerConfig): LogViewer {\r\n return new LogViewer(config)\r\n}\r\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAWO,MAAM,UAAU;AAgDhB,SAAS,gBAAgB,QAAoC;CAClE,OAAO,IAAI,kBAAA,UAAU,MAAM;AAC7B"}
@@ -2,6 +2,11 @@
2
2
  * @composy/log-viewer-core
3
3
  * 审计日志查看器 - 操作日志可视化与追溯
4
4
  */
5
+ /**
6
+ * 创建 LogViewer 实例 - 工厂函数
7
+ */
8
+ import type { LogViewerConfig } from './types';
9
+ import { LogViewer } from './core/LogViewer';
5
10
  export declare const VERSION = "0.1.0";
6
11
  export type { ActionType, ThemeMode, AnomalyType, DiffEntry, LogEntry, FilterConfig, PaginationConfig, LogViewerConfig, LogViewerInstance, LogViewerEvents, } from './types';
7
12
  export { ACTION_LABELS, ACTION_COLORS } from './types';
@@ -9,18 +14,13 @@ export { LogViewer } from './core/LogViewer';
9
14
  export { EventEmitter } from './core/EventEmitter';
10
15
  export { FilterEngine } from './filters/FilterEngine';
11
16
  export { TimelineRenderer } from './renderers/TimelineRenderer';
17
+ export type { TimelineRendererOptions } from './renderers/TimelineRenderer';
12
18
  export { DiffRenderer } from './renderers/DiffRenderer';
13
- export { formatDate, formatRelativeTime, escapeHtml, debounce, uid, formatValue, deepClone } from './utils';
19
+ export { formatDate, formatRelativeTime, escapeHtml, debounce, uid, formatValue, deepClone, } from './utils';
14
20
  export { injectStyles } from './styles';
15
- /**
16
- * 创建 LogViewer 实例 - 工厂函数
17
- */
18
- import type { LogViewerConfig } from './types';
19
- import { LogViewer } from './core/LogViewer';
20
21
  /**
21
22
  * createLogViewer - create Log Viewer function
22
23
  * @param config - 参数说明
23
24
  * @returns 返回值说明
24
25
  */
25
26
  export declare function createLogViewer(config: LogViewerConfig): LogViewer;
26
- //# sourceMappingURL=index.d.ts.map
@@ -2,7 +2,7 @@
2
2
  * ***********************************
3
3
  * @composy/log-viewer-core v0.0.1 *
4
4
  * Built with rollup *
5
- * Build time: 2024-08-17 11:33:32 *
5
+ * Build time: 2024-10-12 17:29:20 *
6
6
  * Build mode: production *
7
7
  * Minified: No *
8
8
  * ***********************************
@@ -21,7 +21,7 @@ var DiffRenderer = class {
21
21
  if (entry.metadata && Object.keys(entry.metadata).length > 0) {
22
22
  const meta = document.createElement("div");
23
23
  meta.className = "lv-metadata";
24
- meta.textContent = JSON.stringify(entry.metadata, null, 2);
24
+ meta.textContent = require_index.formatValue(entry.metadata);
25
25
  container.appendChild(meta);
26
26
  }
27
27
  return container;
@@ -33,7 +33,17 @@ var DiffRenderer = class {
33
33
  const table = document.createElement("table");
34
34
  table.className = "lv-diff-table";
35
35
  const thead = document.createElement("thead");
36
- thead.innerHTML = `<tr><th>\u5B57\u6BB5</th><th>\u53D8\u66F4\u524D</th><th>\u53D8\u66F4\u540E</th></tr>`;
36
+ const headerRow = document.createElement("tr");
37
+ for (const label of [
38
+ "字段",
39
+ "变更前",
40
+ "变更后"
41
+ ]) {
42
+ const th = document.createElement("th");
43
+ th.textContent = label;
44
+ headerRow.appendChild(th);
45
+ }
46
+ thead.appendChild(headerRow);
37
47
  table.appendChild(thead);
38
48
  const tbody = document.createElement("tbody");
39
49
  for (const change of changes) {
@@ -1 +1 @@
1
- {"version":3,"file":"DiffRenderer.cjs","names":[],"sources":["../../src/renderers/DiffRenderer.ts"],"sourcesContent":["/**\n * 变更对比渲染器 - 渲染字段变更前后的 Diff 表格\n */\n\nimport type { DiffEntry, LogEntry } from '../types'\nimport { escapeHtml, formatValue } from '../utils'\n\n/**\n * DiffRenderer - Diff Renderer class\n */\nexport class DiffRenderer {\n /**\n * 渲染详情面板(包含 diff 表格和 metadata)\n */\n renderDetail(entry: LogEntry): HTMLElement {\n const container = document.createElement('div')\n container.className = 'lv-detail'\n\n if (entry.changes && entry.changes.length > 0) {\n container.appendChild(this.renderDiffTable(entry.changes))\n }\n\n if (entry.metadata && Object.keys(entry.metadata).length > 0) {\n const meta = document.createElement('div')\n meta.className = 'lv-metadata'\n meta.textContent = JSON.stringify(entry.metadata, null, 2)\n container.appendChild(meta)\n }\n\n return container\n }\n\n /**\n * 渲染 Diff 表格\n */\n renderDiffTable(changes: DiffEntry[]): HTMLElement {\n const table = document.createElement('table')\n table.className = 'lv-diff-table'\n\n // Header\n const thead = document.createElement('thead')\n thead.innerHTML = `<tr><th>字段</th><th>变更前</th><th>变更后</th></tr>`\n table.appendChild(thead)\n\n // Body\n const tbody = document.createElement('tbody')\n for (const change of changes) {\n const tr = document.createElement('tr')\n\n const tdField = document.createElement('td')\n tdField.textContent = change.label || change.field\n tr.appendChild(tdField)\n\n const tdOld = document.createElement('td')\n tdOld.className = 'lv-diff-old'\n tdOld.textContent = formatValue(change.oldValue)\n tr.appendChild(tdOld)\n\n const tdNew = document.createElement('td')\n tdNew.className = 'lv-diff-new'\n tdNew.textContent = formatValue(change.newValue)\n tr.appendChild(tdNew)\n\n tbody.appendChild(tr)\n }\n table.appendChild(tbody)\n\n return table\n }\n}\n"],"mappings":";;;;;;;;;;;;AAUO,IAAM,eAAN,MAAmB;;;;CAIxB,aAAa,OAA8B;EACzC,MAAM,YAAY,SAAS,cAAc,KAAK;EAC9C,UAAU,YAAY;EAEtB,IAAI,MAAM,WAAW,MAAM,QAAQ,SAAS,GAC1C,UAAU,YAAY,KAAK,gBAAgB,MAAM,OAAO,CAAC;EAG3D,IAAI,MAAM,YAAY,OAAO,KAAK,MAAM,QAAQ,CAAA,CAAE,SAAS,GAAG;GAC5D,MAAM,OAAO,SAAS,cAAc,KAAK;GACzC,KAAK,YAAY;GACjB,KAAK,cAAc,KAAK,UAAU,MAAM,UAAU,MAAM,CAAC;GACzD,UAAU,YAAY,IAAI;EAC5B;EAEA,OAAO;CACT;;;;CAKA,gBAAgB,SAAmC;EACjD,MAAM,QAAQ,SAAS,cAAc,OAAO;EAC5C,MAAM,YAAY;EAGlB,MAAM,QAAQ,SAAS,cAAc,OAAO;EAC5C,MAAM,YAAY;EAClB,MAAM,YAAY,KAAK;EAGvB,MAAM,QAAQ,SAAS,cAAc,OAAO;EAC5C,KAAA,MAAW,UAAU,SAAS;GAC5B,MAAM,KAAK,SAAS,cAAc,IAAI;GAEtC,MAAM,UAAU,SAAS,cAAc,IAAI;GAC3C,QAAQ,cAAc,OAAO,SAAS,OAAO;GAC7C,GAAG,YAAY,OAAO;GAEtB,MAAM,QAAQ,SAAS,cAAc,IAAI;GACzC,MAAM,YAAY;GAClB,MAAM,cAAc,cAAA,YAAY,OAAO,QAAQ;GAC/C,GAAG,YAAY,KAAK;GAEpB,MAAM,QAAQ,SAAS,cAAc,IAAI;GACzC,MAAM,YAAY;GAClB,MAAM,cAAc,cAAA,YAAY,OAAO,QAAQ;GAC/C,GAAG,YAAY,KAAK;GAEpB,MAAM,YAAY,EAAE;EACtB;EACA,MAAM,YAAY,KAAK;EAEvB,OAAO;CACT;AACF"}
1
+ {"version":3,"file":"DiffRenderer.cjs","names":[],"sources":["../../src/renderers/DiffRenderer.ts"],"sourcesContent":["/**\r\n * 变更对比渲染器 - 渲染字段变更前后的 Diff 表格\r\n */\r\n\r\nimport type { DiffEntry, LogEntry } from '../types'\r\nimport { formatValue } from '../utils'\r\n\r\n/**\r\n * DiffRenderer - Diff Renderer class\r\n */\r\nexport class DiffRenderer {\r\n /**\r\n * 渲染详情面板(包含 diff 表格和 metadata)\r\n */\r\n renderDetail(entry: LogEntry): HTMLElement {\r\n const container = document.createElement('div')\r\n container.className = 'lv-detail'\r\n\r\n if (entry.changes && entry.changes.length > 0) {\r\n container.appendChild(this.renderDiffTable(entry.changes))\r\n }\r\n\r\n if (entry.metadata && Object.keys(entry.metadata).length > 0) {\r\n const meta = document.createElement('div')\r\n meta.className = 'lv-metadata'\r\n meta.textContent = formatValue(entry.metadata)\r\n container.appendChild(meta)\r\n }\r\n\r\n return container\r\n }\r\n\r\n /**\r\n * 渲染 Diff 表格\r\n */\r\n renderDiffTable(changes: DiffEntry[]): HTMLElement {\r\n const table = document.createElement('table')\r\n table.className = 'lv-diff-table'\r\n\r\n // Header\r\n const thead = document.createElement('thead')\r\n const headerRow = document.createElement('tr')\r\n for (const label of ['字段', '变更前', '变更后']) {\r\n const th = document.createElement('th')\r\n th.textContent = label\r\n headerRow.appendChild(th)\r\n }\r\n thead.appendChild(headerRow)\r\n table.appendChild(thead)\r\n\r\n // Body\r\n const tbody = document.createElement('tbody')\r\n for (const change of changes) {\r\n const tr = document.createElement('tr')\r\n\r\n const tdField = document.createElement('td')\r\n tdField.textContent = change.label || change.field\r\n tr.appendChild(tdField)\r\n\r\n const tdOld = document.createElement('td')\r\n tdOld.className = 'lv-diff-old'\r\n tdOld.textContent = formatValue(change.oldValue)\r\n tr.appendChild(tdOld)\r\n\r\n const tdNew = document.createElement('td')\r\n tdNew.className = 'lv-diff-new'\r\n tdNew.textContent = formatValue(change.newValue)\r\n tr.appendChild(tdNew)\r\n\r\n tbody.appendChild(tr)\r\n }\r\n table.appendChild(tbody)\r\n\r\n return table\r\n }\r\n}\r\n"],"mappings":";;;;;;;;;;;;AAUO,IAAM,eAAN,MAAmB;;;;CAIxB,aAAa,OAA8B;EACzC,MAAM,YAAY,SAAS,cAAc,KAAK;EAC9C,UAAU,YAAY;EAEtB,IAAI,MAAM,WAAW,MAAM,QAAQ,SAAS,GAC1C,UAAU,YAAY,KAAK,gBAAgB,MAAM,OAAO,CAAC;EAG3D,IAAI,MAAM,YAAY,OAAO,KAAK,MAAM,QAAQ,CAAA,CAAE,SAAS,GAAG;GAC5D,MAAM,OAAO,SAAS,cAAc,KAAK;GACzC,KAAK,YAAY;GACjB,KAAK,cAAc,cAAA,YAAY,MAAM,QAAQ;GAC7C,UAAU,YAAY,IAAI;EAC5B;EAEA,OAAO;CACT;;;;CAKA,gBAAgB,SAAmC;EACjD,MAAM,QAAQ,SAAS,cAAc,OAAO;EAC5C,MAAM,YAAY;EAGlB,MAAM,QAAQ,SAAS,cAAc,OAAO;EAC5C,MAAM,YAAY,SAAS,cAAc,IAAI;EAC7C,KAAA,MAAW,SAAS;GAAC;GAAM;GAAO;EAAK,GAAG;GACxC,MAAM,KAAK,SAAS,cAAc,IAAI;GACtC,GAAG,cAAc;GACjB,UAAU,YAAY,EAAE;EAC1B;EACA,MAAM,YAAY,SAAS;EAC3B,MAAM,YAAY,KAAK;EAGvB,MAAM,QAAQ,SAAS,cAAc,OAAO;EAC5C,KAAA,MAAW,UAAU,SAAS;GAC5B,MAAM,KAAK,SAAS,cAAc,IAAI;GAEtC,MAAM,UAAU,SAAS,cAAc,IAAI;GAC3C,QAAQ,cAAc,OAAO,SAAS,OAAO;GAC7C,GAAG,YAAY,OAAO;GAEtB,MAAM,QAAQ,SAAS,cAAc,IAAI;GACzC,MAAM,YAAY;GAClB,MAAM,cAAc,cAAA,YAAY,OAAO,QAAQ;GAC/C,GAAG,YAAY,KAAK;GAEpB,MAAM,QAAQ,SAAS,cAAc,IAAI;GACzC,MAAM,YAAY;GAClB,MAAM,cAAc,cAAA,YAAY,OAAO,QAAQ;GAC/C,GAAG,YAAY,KAAK;GAEpB,MAAM,YAAY,EAAE;EACtB;EACA,MAAM,YAAY,KAAK;EAEvB,OAAO;CACT;AACF"}
@@ -15,4 +15,3 @@ export declare class DiffRenderer {
15
15
  */
16
16
  renderDiffTable(changes: DiffEntry[]): HTMLElement;
17
17
  }
18
- //# sourceMappingURL=DiffRenderer.d.ts.map
@@ -2,7 +2,7 @@
2
2
  * ***********************************
3
3
  * @composy/log-viewer-core v0.0.1 *
4
4
  * Built with rollup *
5
- * Build time: 2024-08-17 11:33:32 *
5
+ * Build time: 2024-10-12 17:29:20 *
6
6
  * Build mode: production *
7
7
  * Minified: No *
8
8
  * ***********************************
@@ -17,29 +17,53 @@ const ANOMALY_LABELS = {
17
17
  "high-frequency": "高频操作",
18
18
  none: ""
19
19
  };
20
+ const ANOMALY_LABELS_EN = {
21
+ "off-hours": "Off hours",
22
+ sensitive: "Sensitive",
23
+ "high-frequency": "High frequency",
24
+ none: ""
25
+ };
26
+ const ACTION_LABELS_EN = {
27
+ create: "Create",
28
+ update: "Update",
29
+ delete: "Delete",
30
+ query: "Query",
31
+ login: "Login",
32
+ logout: "Logout",
33
+ export: "Export",
34
+ import: "Import",
35
+ approve: "Approve",
36
+ reject: "Reject",
37
+ other: "Other"
38
+ };
20
39
  var TimelineRenderer = class {
21
40
  /**
22
41
  * 渲染时间线
23
42
  */
24
- render(entries, expandedIds) {
43
+ render(entries, expandedIds, options = {}) {
25
44
  const container = document.createElement("div");
26
45
  container.className = "lv-timeline";
27
46
  if (entries.length === 0) {
28
- container.innerHTML = `<div class="lv-empty"><div class="lv-empty-icon">--</div><div>\u6682\u65E0\u65E5\u5FD7\u8BB0\u5F55</div></div>`;
47
+ const emptyIcon = document.createElement("div");
48
+ emptyIcon.className = "lv-empty-icon";
49
+ emptyIcon.textContent = "--";
50
+ const emptyText = document.createElement("div");
51
+ emptyText.textContent = options.locale === "en-US" ? "No log entries" : "暂无日志记录";
52
+ container.append(emptyIcon, emptyText);
29
53
  container.className = "";
30
54
  return container;
31
55
  }
32
- for (const entry of entries) container.appendChild(this.renderItem(entry, expandedIds.has(entry.id)));
56
+ for (const entry of entries) container.appendChild(this.renderItem(entry, expandedIds.has(entry.id), options));
33
57
  return container;
34
58
  }
35
59
  /**
36
60
  * 渲染单条时间线条目
37
61
  */
38
- renderItem(entry, expanded) {
62
+ renderItem(entry, expanded, options = {}) {
39
63
  const item = document.createElement("div");
40
64
  item.className = "lv-timeline-item";
41
65
  item.setAttribute("data-id", entry.id);
42
- const isAnomaly = entry.anomaly && entry.anomaly !== "none";
66
+ const isAnomaly = options.showAnomalyBadge !== false && entry.anomaly && entry.anomaly !== "none";
43
67
  const dot = document.createElement("div");
44
68
  dot.className = `lv-timeline-dot${isAnomaly ? " lv-timeline-dot--anomaly" : ""}`;
45
69
  item.appendChild(dot);
@@ -56,9 +80,8 @@ var TimelineRenderer = class {
56
80
  user.textContent = entry.user;
57
81
  header.appendChild(user);
58
82
  const actionTag = document.createElement("span");
59
- actionTag.className = "lv-action-tag";
60
- actionTag.textContent = require_index.ACTION_LABELS[entry.action] || entry.action;
61
- actionTag.style.backgroundColor = require_index.ACTION_COLORS[entry.action] || "#8c8c8c";
83
+ actionTag.className = `lv-action-tag lv-action-tag--${Object.keys(require_index.ACTION_LABELS).includes(entry.action) ? entry.action : "other"}`;
84
+ actionTag.textContent = (options.locale === "en-US" ? ACTION_LABELS_EN[entry.action] : require_index.ACTION_LABELS[entry.action]) || entry.action;
62
85
  header.appendChild(actionTag);
63
86
  const moduleBadge = document.createElement("span");
64
87
  moduleBadge.className = "lv-timeline-module";
@@ -70,10 +93,10 @@ var TimelineRenderer = class {
70
93
  ip.textContent = entry.ip;
71
94
  header.appendChild(ip);
72
95
  }
73
- if (isAnomaly) {
96
+ if (options.showAnomalyBadge !== false && isAnomaly) {
74
97
  const badge = document.createElement("span");
75
98
  badge.className = "lv-anomaly-badge";
76
- badge.textContent = ANOMALY_LABELS[entry.anomaly];
99
+ badge.textContent = (options.locale === "en-US" ? ANOMALY_LABELS_EN[entry.anomaly] : ANOMALY_LABELS)[entry.anomaly] || entry.anomaly;
77
100
  header.appendChild(badge);
78
101
  }
79
102
  card.appendChild(header);
@@ -83,9 +106,11 @@ var TimelineRenderer = class {
83
106
  detail.textContent = entry.detail;
84
107
  card.appendChild(detail);
85
108
  }
86
- if (entry.changes && entry.changes.length > 0) {
87
- const hint = document.createElement("div");
109
+ if (options.showDiff !== false && entry.changes && entry.changes.length > 0) {
110
+ const hint = document.createElement("button");
111
+ hint.type = "button";
88
112
  hint.className = "lv-timeline-expand-hint";
113
+ hint.setAttribute("aria-expanded", String(expanded));
89
114
  hint.textContent = expanded ? "[ 收起变更详情 ]" : `[ \u67E5\u770B\u53D8\u66F4\u8BE6\u60C5 (${entry.changes.length} \u4E2A\u5B57\u6BB5) ]`;
90
115
  card.appendChild(hint);
91
116
  }
@@ -98,34 +123,43 @@ var TimelineRenderer = class {
98
123
  renderPagination(page, pageSize, total) {
99
124
  const container = document.createElement("div");
100
125
  container.className = "lv-pagination";
101
- const totalPages = Math.ceil(total / pageSize);
126
+ const safePageSize = Number.isFinite(pageSize) && pageSize > 0 ? Math.floor(pageSize) : 1;
127
+ const safeTotal = Number.isFinite(total) && total > 0 ? Math.floor(total) : 0;
128
+ const safePage = Number.isFinite(page) && page > 0 ? Math.floor(page) : 1;
129
+ const totalPages = Math.ceil(safeTotal / safePageSize);
102
130
  if (totalPages <= 1) return container;
103
131
  const prev = document.createElement("button");
132
+ prev.type = "button";
104
133
  prev.className = "lv-pagination-btn";
105
134
  prev.textContent = "<";
106
- prev.disabled = page <= 1;
107
- prev.setAttribute("data-page", String(page - 1));
135
+ prev.disabled = safePage <= 1;
136
+ prev.setAttribute("aria-label", "Previous page");
137
+ prev.setAttribute("data-page", String(safePage - 1));
108
138
  container.appendChild(prev);
109
139
  const maxVisible = 7;
110
- let startPage = Math.max(1, page - Math.floor(maxVisible / 2));
140
+ let startPage = Math.max(1, safePage - Math.floor(maxVisible / 2));
111
141
  const endPage = Math.min(totalPages, startPage + maxVisible - 1);
112
142
  if (endPage - startPage < maxVisible - 1) startPage = Math.max(1, endPage - maxVisible + 1);
113
143
  for (let i = startPage; i <= endPage; i++) {
114
144
  const btn = document.createElement("button");
115
- btn.className = `lv-pagination-btn${i === page ? " lv-pagination-btn--active" : ""}`;
145
+ btn.type = "button";
146
+ btn.className = `lv-pagination-btn${i === safePage ? " lv-pagination-btn--active" : ""}`;
116
147
  btn.textContent = String(i);
117
148
  btn.setAttribute("data-page", String(i));
149
+ if (i === safePage) btn.setAttribute("aria-current", "page");
118
150
  container.appendChild(btn);
119
151
  }
120
152
  const next = document.createElement("button");
153
+ next.type = "button";
121
154
  next.className = "lv-pagination-btn";
122
155
  next.textContent = ">";
123
- next.disabled = page >= totalPages;
124
- next.setAttribute("data-page", String(page + 1));
156
+ next.disabled = safePage >= totalPages;
157
+ next.setAttribute("aria-label", "Next page");
158
+ next.setAttribute("data-page", String(safePage + 1));
125
159
  container.appendChild(next);
126
160
  const info = document.createElement("span");
127
161
  info.className = "lv-pagination-info";
128
- info.textContent = `\u5171 ${total} \u6761`;
162
+ info.textContent = `\u5171 ${safeTotal} \u6761`;
129
163
  container.appendChild(info);
130
164
  return container;
131
165
  }
@@ -1 +1 @@
1
- {"version":3,"file":"TimelineRenderer.cjs","names":[],"sources":["../../src/renderers/TimelineRenderer.ts"],"sourcesContent":["/**\n * 时间线渲染器 - 将日志条目渲染为时间线 DOM\n */\n\nimport type { LogEntry, AnomalyType } from '../types'\nimport { ACTION_LABELS, ACTION_COLORS } from '../types'\nimport { formatDate, escapeHtml } from '../utils'\n\nconst ANOMALY_LABELS: Record<AnomalyType, string> = {\n 'off-hours': '非工作时间',\n sensitive: '敏感操作',\n 'high-frequency': '高频操作',\n none: '',\n}\n\n/**\n * TimelineRenderer - Timeline Renderer class\n */\nexport class TimelineRenderer {\n /**\n * 渲染时间线\n */\n render(entries: LogEntry[], expandedIds: Set<string>): HTMLElement {\n const container = document.createElement('div')\n container.className = 'lv-timeline'\n\n if (entries.length === 0) {\n container.innerHTML = `<div class=\"lv-empty\"><div class=\"lv-empty-icon\">--</div><div>暂无日志记录</div></div>`\n container.className = ''\n return container\n }\n\n for (const entry of entries) {\n container.appendChild(this.renderItem(entry, expandedIds.has(entry.id)))\n }\n\n return container\n }\n\n /**\n * 渲染单条时间线条目\n */\n renderItem(entry: LogEntry, expanded: boolean): HTMLElement {\n const item = document.createElement('div')\n item.className = 'lv-timeline-item'\n item.setAttribute('data-id', entry.id)\n\n const isAnomaly = entry.anomaly && entry.anomaly !== 'none'\n\n // Dot\n const dot = document.createElement('div')\n dot.className = `lv-timeline-dot${isAnomaly ? ' lv-timeline-dot--anomaly' : ''}`\n item.appendChild(dot)\n\n // Card\n const card = document.createElement('div')\n card.className = `lv-timeline-card${isAnomaly ? ' lv-timeline-card--anomaly' : ''}`\n\n // Header\n const header = document.createElement('div')\n header.className = 'lv-timeline-header'\n\n const time = document.createElement('span')\n time.className = 'lv-timeline-time'\n time.textContent = formatDate(entry.timestamp)\n header.appendChild(time)\n\n const user = document.createElement('span')\n user.className = 'lv-timeline-user'\n user.textContent = entry.user\n header.appendChild(user)\n\n const actionTag = document.createElement('span')\n actionTag.className = 'lv-action-tag'\n actionTag.textContent = ACTION_LABELS[entry.action] || entry.action\n actionTag.style.backgroundColor = ACTION_COLORS[entry.action] || '#8c8c8c'\n header.appendChild(actionTag)\n\n const moduleBadge = document.createElement('span')\n moduleBadge.className = 'lv-timeline-module'\n moduleBadge.textContent = entry.module\n header.appendChild(moduleBadge)\n\n if (entry.ip) {\n const ip = document.createElement('span')\n ip.className = 'lv-timeline-ip'\n ip.textContent = entry.ip\n header.appendChild(ip)\n }\n\n if (isAnomaly) {\n const badge = document.createElement('span')\n badge.className = 'lv-anomaly-badge'\n badge.textContent = ANOMALY_LABELS[entry.anomaly!]\n header.appendChild(badge)\n }\n\n card.appendChild(header)\n\n // Detail text\n if (entry.detail) {\n const detail = document.createElement('div')\n detail.className = 'lv-timeline-detail'\n detail.textContent = entry.detail\n card.appendChild(detail)\n }\n\n // Expand hint\n if (entry.changes && entry.changes.length > 0) {\n const hint = document.createElement('div')\n hint.className = 'lv-timeline-expand-hint'\n hint.textContent = expanded ? '[ 收起变更详情 ]' : `[ 查看变更详情 (${entry.changes.length} 个字段) ]`\n card.appendChild(hint)\n }\n\n item.appendChild(card)\n\n return item\n }\n\n /**\n * 渲染分页\n */\n renderPagination(page: number, pageSize: number, total: number): HTMLElement {\n const container = document.createElement('div')\n container.className = 'lv-pagination'\n\n const totalPages = Math.ceil(total / pageSize)\n if (totalPages <= 1) return container\n\n // Prev\n const prev = document.createElement('button')\n prev.className = 'lv-pagination-btn'\n prev.textContent = '<'\n prev.disabled = page <= 1\n prev.setAttribute('data-page', String(page - 1))\n container.appendChild(prev)\n\n // Page buttons\n const maxVisible = 7\n let startPage = Math.max(1, page - Math.floor(maxVisible / 2))\n const endPage = Math.min(totalPages, startPage + maxVisible - 1)\n if (endPage - startPage < maxVisible - 1) {\n startPage = Math.max(1, endPage - maxVisible + 1)\n }\n\n for (let i = startPage; i <= endPage; i++) {\n const btn = document.createElement('button')\n btn.className = `lv-pagination-btn${i === page ? ' lv-pagination-btn--active' : ''}`\n btn.textContent = String(i)\n btn.setAttribute('data-page', String(i))\n container.appendChild(btn)\n }\n\n // Next\n const next = document.createElement('button')\n next.className = 'lv-pagination-btn'\n next.textContent = '>'\n next.disabled = page >= totalPages\n next.setAttribute('data-page', String(page + 1))\n container.appendChild(next)\n\n // Info\n const info = document.createElement('span')\n info.className = 'lv-pagination-info'\n info.textContent = `共 ${total} 条`\n container.appendChild(info)\n\n return container\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"TimelineRenderer.cjs","names":[],"sources":["../../src/renderers/TimelineRenderer.ts"],"sourcesContent":["/**\r\n * 时间线渲染器 - 将日志条目渲染为时间线 DOM\r\n */\r\n\r\nimport type { LogEntry, AnomalyType } from '../types'\r\nimport { ACTION_LABELS } from '../types'\r\nimport { formatDate } from '../utils'\r\n\r\nexport interface TimelineRendererOptions {\r\n showDiff?: boolean\r\n showAnomalyBadge?: boolean\r\n locale?: 'zh-CN' | 'en-US'\r\n}\r\n\r\nconst ANOMALY_LABELS: Record<AnomalyType, string> = {\r\n 'off-hours': '非工作时间',\r\n sensitive: '敏感操作',\r\n 'high-frequency': '高频操作',\r\n none: '',\r\n}\r\n\r\nconst ANOMALY_LABELS_EN: Record<AnomalyType, string> = {\r\n 'off-hours': 'Off hours',\r\n sensitive: 'Sensitive',\r\n 'high-frequency': 'High frequency',\r\n none: '',\r\n}\r\n\r\nconst ACTION_LABELS_EN: Record<string, string> = {\r\n create: 'Create',\r\n update: 'Update',\r\n delete: 'Delete',\r\n query: 'Query',\r\n login: 'Login',\r\n logout: 'Logout',\r\n export: 'Export',\r\n import: 'Import',\r\n approve: 'Approve',\r\n reject: 'Reject',\r\n other: 'Other',\r\n}\r\n\r\n/**\r\n * TimelineRenderer - Timeline Renderer class\r\n */\r\nexport class TimelineRenderer {\r\n /**\r\n * 渲染时间线\r\n */\r\n render(\r\n entries: LogEntry[],\r\n expandedIds: Set<string>,\r\n options: TimelineRendererOptions = {}\r\n ): HTMLElement {\r\n const container = document.createElement('div')\r\n container.className = 'lv-timeline'\r\n\r\n if (entries.length === 0) {\r\n const emptyIcon = document.createElement('div')\r\n emptyIcon.className = 'lv-empty-icon'\r\n emptyIcon.textContent = '--'\r\n const emptyText = document.createElement('div')\r\n emptyText.textContent =\r\n options.locale === 'en-US' ? 'No log entries' : '暂无日志记录'\r\n container.append(emptyIcon, emptyText)\r\n container.className = ''\r\n return container\r\n }\r\n\r\n for (const entry of entries) {\r\n container.appendChild(\r\n this.renderItem(entry, expandedIds.has(entry.id), options)\r\n )\r\n }\r\n\r\n return container\r\n }\r\n\r\n /**\r\n * 渲染单条时间线条目\r\n */\r\n renderItem(\r\n entry: LogEntry,\r\n expanded: boolean,\r\n options: TimelineRendererOptions = {}\r\n ): HTMLElement {\r\n const item = document.createElement('div')\r\n item.className = 'lv-timeline-item'\r\n item.setAttribute('data-id', entry.id)\r\n\r\n const isAnomaly =\r\n options.showAnomalyBadge !== false &&\r\n entry.anomaly &&\r\n entry.anomaly !== 'none'\r\n\r\n // Dot\r\n const dot = document.createElement('div')\r\n dot.className = `lv-timeline-dot${isAnomaly ? ' lv-timeline-dot--anomaly' : ''}`\r\n item.appendChild(dot)\r\n\r\n // Card\r\n const card = document.createElement('div')\r\n card.className = `lv-timeline-card${isAnomaly ? ' lv-timeline-card--anomaly' : ''}`\r\n\r\n // Header\r\n const header = document.createElement('div')\r\n header.className = 'lv-timeline-header'\r\n\r\n const time = document.createElement('span')\r\n time.className = 'lv-timeline-time'\r\n time.textContent = formatDate(entry.timestamp)\r\n header.appendChild(time)\r\n\r\n const user = document.createElement('span')\r\n user.className = 'lv-timeline-user'\r\n user.textContent = entry.user\r\n header.appendChild(user)\r\n\r\n const actionTag = document.createElement('span')\r\n const actionKey = Object.keys(ACTION_LABELS).includes(entry.action)\r\n ? entry.action\r\n : 'other'\r\n actionTag.className = `lv-action-tag lv-action-tag--${actionKey}`\r\n actionTag.textContent =\r\n (options.locale === 'en-US'\r\n ? ACTION_LABELS_EN[entry.action]\r\n : ACTION_LABELS[entry.action]) || entry.action\r\n header.appendChild(actionTag)\r\n\r\n const moduleBadge = document.createElement('span')\r\n moduleBadge.className = 'lv-timeline-module'\r\n moduleBadge.textContent = entry.module\r\n header.appendChild(moduleBadge)\r\n\r\n if (entry.ip) {\r\n const ip = document.createElement('span')\r\n ip.className = 'lv-timeline-ip'\r\n ip.textContent = entry.ip\r\n header.appendChild(ip)\r\n }\r\n\r\n if (options.showAnomalyBadge !== false && isAnomaly) {\r\n const badge = document.createElement('span')\r\n badge.className = 'lv-anomaly-badge'\r\n badge.textContent =\r\n (options.locale === 'en-US'\r\n ? ANOMALY_LABELS_EN[entry.anomaly!]\r\n : ANOMALY_LABELS)[entry.anomaly!] || entry.anomaly\r\n header.appendChild(badge)\r\n }\r\n\r\n card.appendChild(header)\r\n\r\n // Detail text\r\n if (entry.detail) {\r\n const detail = document.createElement('div')\r\n detail.className = 'lv-timeline-detail'\r\n detail.textContent = entry.detail\r\n card.appendChild(detail)\r\n }\r\n\r\n // Expand hint\r\n if (\r\n options.showDiff !== false &&\r\n entry.changes &&\r\n entry.changes.length > 0\r\n ) {\r\n const hint = document.createElement('button')\r\n hint.type = 'button'\r\n hint.className = 'lv-timeline-expand-hint'\r\n hint.setAttribute('aria-expanded', String(expanded))\r\n hint.textContent = expanded\r\n ? '[ 收起变更详情 ]'\r\n : `[ 查看变更详情 (${entry.changes.length} 个字段) ]`\r\n card.appendChild(hint)\r\n }\r\n\r\n item.appendChild(card)\r\n\r\n return item\r\n }\r\n\r\n /**\r\n * 渲染分页\r\n */\r\n renderPagination(page: number, pageSize: number, total: number): HTMLElement {\r\n const container = document.createElement('div')\r\n container.className = 'lv-pagination'\r\n\r\n const safePageSize =\r\n Number.isFinite(pageSize) && pageSize > 0 ? Math.floor(pageSize) : 1\r\n const safeTotal =\r\n Number.isFinite(total) && total > 0 ? Math.floor(total) : 0\r\n const safePage = Number.isFinite(page) && page > 0 ? Math.floor(page) : 1\r\n const totalPages = Math.ceil(safeTotal / safePageSize)\r\n if (totalPages <= 1) return container\r\n\r\n // Prev\r\n const prev = document.createElement('button')\r\n prev.type = 'button'\r\n prev.className = 'lv-pagination-btn'\r\n prev.textContent = '<'\r\n prev.disabled = safePage <= 1\r\n prev.setAttribute('aria-label', 'Previous page')\r\n prev.setAttribute('data-page', String(safePage - 1))\r\n container.appendChild(prev)\r\n\r\n // Page buttons\r\n const maxVisible = 7\r\n let startPage = Math.max(1, safePage - Math.floor(maxVisible / 2))\r\n const endPage = Math.min(totalPages, startPage + maxVisible - 1)\r\n if (endPage - startPage < maxVisible - 1) {\r\n startPage = Math.max(1, endPage - maxVisible + 1)\r\n }\r\n\r\n for (let i = startPage; i <= endPage; i++) {\r\n const btn = document.createElement('button')\r\n btn.type = 'button'\r\n btn.className = `lv-pagination-btn${i === safePage ? ' lv-pagination-btn--active' : ''}`\r\n btn.textContent = String(i)\r\n btn.setAttribute('data-page', String(i))\r\n if (i === safePage) btn.setAttribute('aria-current', 'page')\r\n container.appendChild(btn)\r\n }\r\n\r\n // Next\r\n const next = document.createElement('button')\r\n next.type = 'button'\r\n next.className = 'lv-pagination-btn'\r\n next.textContent = '>'\r\n next.disabled = safePage >= totalPages\r\n next.setAttribute('aria-label', 'Next page')\r\n next.setAttribute('data-page', String(safePage + 1))\r\n container.appendChild(next)\r\n\r\n // Info\r\n const info = document.createElement('span')\r\n info.className = 'lv-pagination-info'\r\n info.textContent = `共 ${safeTotal} 条`\r\n container.appendChild(info)\r\n\r\n return container\r\n }\r\n}\r\n"],"mappings":"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"}