@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
package/dist/index.js CHANGED
@@ -2,13 +2,13 @@
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
  (function(global, factory) {
11
- typeof exports === "object" && typeof module !== "undefined" ? factory(exports) : typeof define === "function" && define.amd ? define(["exports"], factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self, factory(global.LDesignLogViewer = {}));
11
+ typeof exports === "object" && typeof module !== "undefined" ? factory(exports) : typeof define === "function" && define.amd ? define(["exports"], factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self, factory(global.LDesignLogViewerCore = {}));
12
12
  })(this, function(exports) {
13
13
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
14
14
  //#region src/types/index.ts
@@ -45,21 +45,25 @@
45
45
  this.listeners = /* @__PURE__ */ new Map();
46
46
  }
47
47
  on(event, callback) {
48
+ if (!event || typeof callback !== "function") return () => void 0;
48
49
  if (!this.listeners.has(event)) this.listeners.set(event, /* @__PURE__ */ new Set());
49
50
  this.listeners.get(event).add(callback);
50
51
  return () => this.off(event, callback);
51
52
  }
52
53
  off(event, callback) {
53
- this.listeners.get(event)?.delete(callback);
54
+ const listeners = this.listeners.get(event);
55
+ if (!listeners) return;
56
+ listeners.delete(callback);
57
+ if (listeners.size === 0) this.listeners.delete(event);
54
58
  }
55
59
  emit(event, ...args) {
56
- this.listeners.get(event)?.forEach((cb) => {
57
- try {
58
- cb(...args);
59
- } catch (e) {
60
- console.error(`[LogViewer] Event handler error (${event}):`, e);
61
- }
62
- });
60
+ const listeners = this.listeners.get(event);
61
+ if (!listeners) return;
62
+ for (const cb of [...listeners]) try {
63
+ cb(...args);
64
+ } catch (e) {
65
+ console.error(`[LogViewer] Event handler error (${event}):`, e);
66
+ }
63
67
  }
64
68
  removeAllListeners(event) {
65
69
  if (event) this.listeners.delete(event);
@@ -73,39 +77,44 @@
73
77
  * 对日志条目进行多维筛选
74
78
  */
75
79
  filter(entries, config) {
76
- let result = entries;
77
- if (config.user) {
78
- const keyword = config.user.toLowerCase();
79
- result = result.filter((e) => e.user.toLowerCase().includes(keyword) || e.userId && e.userId.toLowerCase().includes(keyword));
80
+ let result = Array.isArray(entries) ? entries.slice() : [];
81
+ const normalizedConfig = config || {};
82
+ if (normalizedConfig.user?.trim()) {
83
+ const keyword = normalizedConfig.user.trim().toLowerCase();
84
+ result = result.filter((e) => e.user?.toLowerCase().includes(keyword) || e.userId?.toLowerCase().includes(keyword));
80
85
  }
81
- if (config.action) {
82
- const actions = Array.isArray(config.action) ? config.action : [config.action];
83
- result = result.filter((e) => actions.includes(e.action));
86
+ if (normalizedConfig.action) {
87
+ const actions = Array.isArray(normalizedConfig.action) ? normalizedConfig.action : [normalizedConfig.action];
88
+ if (actions.length > 0) result = result.filter((e) => actions.includes(e.action));
84
89
  }
85
- if (config.module) {
86
- const mod = config.module.toLowerCase();
87
- result = result.filter((e) => e.module.toLowerCase().includes(mod));
90
+ if (normalizedConfig.module?.trim()) {
91
+ const mod = normalizedConfig.module.trim().toLowerCase();
92
+ result = result.filter((e) => e.module?.toLowerCase().includes(mod));
88
93
  }
89
- if (config.ip) {
90
- const ip = config.ip.toLowerCase();
91
- result = result.filter((e) => e.ip && e.ip.toLowerCase().includes(ip));
94
+ if (normalizedConfig.ip?.trim()) {
95
+ const ip = normalizedConfig.ip.trim().toLowerCase();
96
+ result = result.filter((e) => e.ip?.toLowerCase().includes(ip));
92
97
  }
93
- if (config.timeRange) {
94
- const [start, end] = config.timeRange;
98
+ if (normalizedConfig.timeRange) {
99
+ const [start, end] = normalizedConfig.timeRange;
95
100
  const startTime = new Date(start).getTime();
96
101
  const endTime = new Date(end).getTime();
97
- result = result.filter((e) => {
98
- const t = new Date(e.timestamp).getTime();
99
- return t >= startTime && t <= endTime;
100
- });
102
+ if (Number.isFinite(startTime) && Number.isFinite(endTime)) {
103
+ const lower = Math.min(startTime, endTime);
104
+ const upper = Math.max(startTime, endTime);
105
+ result = result.filter((e) => {
106
+ const t = new Date(e.timestamp).getTime();
107
+ return Number.isFinite(t) && t >= lower && t <= upper;
108
+ });
109
+ }
101
110
  }
102
- if (config.keyword) {
103
- const kw = config.keyword.toLowerCase();
111
+ if (normalizedConfig.keyword?.trim()) {
112
+ const kw = normalizedConfig.keyword.trim().toLowerCase();
104
113
  result = result.filter((e) => {
105
114
  return [
106
- e.user,
115
+ e.user || "",
107
116
  e.action,
108
- e.module,
117
+ e.module || "",
109
118
  e.detail || "",
110
119
  e.ip || "",
111
120
  e.id
@@ -118,13 +127,13 @@
118
127
  * 获取所有唯一操作人
119
128
  */
120
129
  getUniqueUsers(entries) {
121
- return [...new Set(entries.map((e) => e.user))].sort();
130
+ return [...new Set(entries.map((e) => e.user).filter(Boolean))].sort();
122
131
  }
123
132
  /**
124
133
  * 获取所有唯一模块
125
134
  */
126
135
  getUniqueModules(entries) {
127
- return [...new Set(entries.map((e) => e.module))].sort();
136
+ return [...new Set(entries.map((e) => e.module).filter(Boolean))].sort();
128
137
  }
129
138
  /**
130
139
  * 获取所有唯一操作类型
@@ -137,11 +146,12 @@
137
146
  //#region src/utils/index.ts
138
147
  let _uid = 0;
139
148
  function uid(prefix = "lv") {
140
- return `${prefix}-${++_uid}-${Math.random().toString(36).slice(2, 8)}`;
149
+ const random = typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? crypto.randomUUID().slice(0, 8) : Math.random().toString(36).slice(2, 8);
150
+ return `${prefix}-${++_uid}-${random}`;
141
151
  }
142
152
  function formatDate(value, fmt = "YYYY-MM-DD HH:mm:ss") {
143
153
  const d = value instanceof Date ? value : new Date(value);
144
- if (isNaN(d.getTime())) return String(value);
154
+ if (Number.isNaN(d.getTime())) return String(value);
145
155
  const pad = (n) => String(n).padStart(2, "0");
146
156
  const year = String(d.getFullYear());
147
157
  const month = pad(d.getMonth() + 1);
@@ -154,6 +164,7 @@
154
164
  function formatRelativeTime(value) {
155
165
  const d = value instanceof Date ? value : new Date(value);
156
166
  const diff = Date.now() - d.getTime();
167
+ if (!Number.isFinite(diff) || diff < 0) return formatDate(d);
157
168
  if (diff < 6e4) return "刚刚";
158
169
  if (diff < 36e5) return `${Math.floor(diff / 6e4)} \u5206\u949F\u524D`;
159
170
  if (diff < 864e5) return `${Math.floor(diff / 36e5)} \u5C0F\u65F6\u524D`;
@@ -172,15 +183,58 @@
172
183
  }
173
184
  function debounce(fn, delay) {
174
185
  let timer = null;
186
+ const wait = Number.isFinite(delay) && delay > 0 ? delay : 0;
175
187
  return ((...args) => {
176
188
  if (timer) clearTimeout(timer);
177
- timer = setTimeout(() => fn(...args), delay);
189
+ timer = setTimeout(() => {
190
+ timer = null;
191
+ fn(...args);
192
+ }, wait);
178
193
  });
179
194
  }
180
195
  function deepClone(obj) {
181
196
  if (obj === null || typeof obj !== "object") return obj;
197
+ if (typeof structuredClone === "function") try {
198
+ return structuredClone(obj);
199
+ } catch {}
200
+ const seen = /* @__PURE__ */ new WeakMap();
201
+ const clone = (value) => {
202
+ if (value === null || typeof value !== "object") return value;
203
+ if (seen.has(value)) return seen.get(value);
204
+ if (value instanceof Date) return new Date(value.getTime());
205
+ if (value instanceof RegExp) return new RegExp(value.source, value.flags);
206
+ if (value instanceof Map) {
207
+ const result2 = /* @__PURE__ */ new Map();
208
+ seen.set(value, result2);
209
+ value.forEach((mapValue, key) => result2.set(clone(key), clone(mapValue)));
210
+ return result2;
211
+ }
212
+ if (value instanceof Set) {
213
+ const result2 = /* @__PURE__ */ new Set();
214
+ seen.set(value, result2);
215
+ value.forEach((setValue) => result2.add(clone(setValue)));
216
+ return result2;
217
+ }
218
+ if (Array.isArray(value)) {
219
+ const result2 = [];
220
+ seen.set(value, result2);
221
+ value.forEach((item) => result2.push(clone(item)));
222
+ return result2;
223
+ }
224
+ const result = Object.create(Object.getPrototypeOf(value));
225
+ seen.set(value, result);
226
+ for (const key of Reflect.ownKeys(value)) {
227
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
228
+ if (!descriptor || !("value" in descriptor)) continue;
229
+ Object.defineProperty(result, key, {
230
+ ...descriptor,
231
+ value: clone(descriptor.value)
232
+ });
233
+ }
234
+ return result;
235
+ };
182
236
  try {
183
- return JSON.parse(JSON.stringify(obj));
237
+ return clone(obj);
184
238
  } catch {
185
239
  return obj;
186
240
  }
@@ -188,13 +242,73 @@
188
242
  function formatValue(val) {
189
243
  if (val === null || val === void 0) return "-";
190
244
  if (typeof val === "object") try {
191
- return JSON.stringify(val, null, 2);
245
+ return JSON.stringify(val, (_key, value) => {
246
+ if (typeof value === "bigint") return `${value}n`;
247
+ return value;
248
+ }, 2);
192
249
  } catch {
193
250
  return String(val);
194
251
  }
195
252
  return String(val);
196
253
  }
197
254
  //#endregion
255
+ //#region src/renderers/DiffRenderer.ts
256
+ var DiffRenderer = class {
257
+ /**
258
+ * 渲染详情面板(包含 diff 表格和 metadata)
259
+ */
260
+ renderDetail(entry) {
261
+ const container = document.createElement("div");
262
+ container.className = "lv-detail";
263
+ if (entry.changes && entry.changes.length > 0) container.appendChild(this.renderDiffTable(entry.changes));
264
+ if (entry.metadata && Object.keys(entry.metadata).length > 0) {
265
+ const meta = document.createElement("div");
266
+ meta.className = "lv-metadata";
267
+ meta.textContent = formatValue(entry.metadata);
268
+ container.appendChild(meta);
269
+ }
270
+ return container;
271
+ }
272
+ /**
273
+ * 渲染 Diff 表格
274
+ */
275
+ renderDiffTable(changes) {
276
+ const table = document.createElement("table");
277
+ table.className = "lv-diff-table";
278
+ const thead = document.createElement("thead");
279
+ const headerRow = document.createElement("tr");
280
+ for (const label of [
281
+ "字段",
282
+ "变更前",
283
+ "变更后"
284
+ ]) {
285
+ const th = document.createElement("th");
286
+ th.textContent = label;
287
+ headerRow.appendChild(th);
288
+ }
289
+ thead.appendChild(headerRow);
290
+ table.appendChild(thead);
291
+ const tbody = document.createElement("tbody");
292
+ for (const change of changes) {
293
+ const tr = document.createElement("tr");
294
+ const tdField = document.createElement("td");
295
+ tdField.textContent = change.label || change.field;
296
+ tr.appendChild(tdField);
297
+ const tdOld = document.createElement("td");
298
+ tdOld.className = "lv-diff-old";
299
+ tdOld.textContent = formatValue(change.oldValue);
300
+ tr.appendChild(tdOld);
301
+ const tdNew = document.createElement("td");
302
+ tdNew.className = "lv-diff-new";
303
+ tdNew.textContent = formatValue(change.newValue);
304
+ tr.appendChild(tdNew);
305
+ tbody.appendChild(tr);
306
+ }
307
+ table.appendChild(tbody);
308
+ return table;
309
+ }
310
+ };
311
+ //#endregion
198
312
  //#region src/renderers/TimelineRenderer.ts
199
313
  const ANOMALY_LABELS = {
200
314
  "off-hours": "非工作时间",
@@ -202,29 +316,53 @@
202
316
  "high-frequency": "高频操作",
203
317
  none: ""
204
318
  };
319
+ const ANOMALY_LABELS_EN = {
320
+ "off-hours": "Off hours",
321
+ sensitive: "Sensitive",
322
+ "high-frequency": "High frequency",
323
+ none: ""
324
+ };
325
+ const ACTION_LABELS_EN = {
326
+ create: "Create",
327
+ update: "Update",
328
+ delete: "Delete",
329
+ query: "Query",
330
+ login: "Login",
331
+ logout: "Logout",
332
+ export: "Export",
333
+ import: "Import",
334
+ approve: "Approve",
335
+ reject: "Reject",
336
+ other: "Other"
337
+ };
205
338
  var TimelineRenderer = class {
206
339
  /**
207
340
  * 渲染时间线
208
341
  */
209
- render(entries, expandedIds) {
342
+ render(entries, expandedIds, options = {}) {
210
343
  const container = document.createElement("div");
211
344
  container.className = "lv-timeline";
212
345
  if (entries.length === 0) {
213
- container.innerHTML = `<div class="lv-empty"><div class="lv-empty-icon">--</div><div>\u6682\u65E0\u65E5\u5FD7\u8BB0\u5F55</div></div>`;
346
+ const emptyIcon = document.createElement("div");
347
+ emptyIcon.className = "lv-empty-icon";
348
+ emptyIcon.textContent = "--";
349
+ const emptyText = document.createElement("div");
350
+ emptyText.textContent = options.locale === "en-US" ? "No log entries" : "暂无日志记录";
351
+ container.append(emptyIcon, emptyText);
214
352
  container.className = "";
215
353
  return container;
216
354
  }
217
- for (const entry of entries) container.appendChild(this.renderItem(entry, expandedIds.has(entry.id)));
355
+ for (const entry of entries) container.appendChild(this.renderItem(entry, expandedIds.has(entry.id), options));
218
356
  return container;
219
357
  }
220
358
  /**
221
359
  * 渲染单条时间线条目
222
360
  */
223
- renderItem(entry, expanded) {
361
+ renderItem(entry, expanded, options = {}) {
224
362
  const item = document.createElement("div");
225
363
  item.className = "lv-timeline-item";
226
364
  item.setAttribute("data-id", entry.id);
227
- const isAnomaly = entry.anomaly && entry.anomaly !== "none";
365
+ const isAnomaly = options.showAnomalyBadge !== false && entry.anomaly && entry.anomaly !== "none";
228
366
  const dot = document.createElement("div");
229
367
  dot.className = `lv-timeline-dot${isAnomaly ? " lv-timeline-dot--anomaly" : ""}`;
230
368
  item.appendChild(dot);
@@ -241,9 +379,8 @@
241
379
  user.textContent = entry.user;
242
380
  header.appendChild(user);
243
381
  const actionTag = document.createElement("span");
244
- actionTag.className = "lv-action-tag";
245
- actionTag.textContent = ACTION_LABELS[entry.action] || entry.action;
246
- actionTag.style.backgroundColor = ACTION_COLORS[entry.action] || "#8c8c8c";
382
+ actionTag.className = `lv-action-tag lv-action-tag--${Object.keys(ACTION_LABELS).includes(entry.action) ? entry.action : "other"}`;
383
+ actionTag.textContent = (options.locale === "en-US" ? ACTION_LABELS_EN[entry.action] : ACTION_LABELS[entry.action]) || entry.action;
247
384
  header.appendChild(actionTag);
248
385
  const moduleBadge = document.createElement("span");
249
386
  moduleBadge.className = "lv-timeline-module";
@@ -255,10 +392,10 @@
255
392
  ip.textContent = entry.ip;
256
393
  header.appendChild(ip);
257
394
  }
258
- if (isAnomaly) {
395
+ if (options.showAnomalyBadge !== false && isAnomaly) {
259
396
  const badge = document.createElement("span");
260
397
  badge.className = "lv-anomaly-badge";
261
- badge.textContent = ANOMALY_LABELS[entry.anomaly];
398
+ badge.textContent = (options.locale === "en-US" ? ANOMALY_LABELS_EN[entry.anomaly] : ANOMALY_LABELS)[entry.anomaly] || entry.anomaly;
262
399
  header.appendChild(badge);
263
400
  }
264
401
  card.appendChild(header);
@@ -268,9 +405,11 @@
268
405
  detail.textContent = entry.detail;
269
406
  card.appendChild(detail);
270
407
  }
271
- if (entry.changes && entry.changes.length > 0) {
272
- const hint = document.createElement("div");
408
+ if (options.showDiff !== false && entry.changes && entry.changes.length > 0) {
409
+ const hint = document.createElement("button");
410
+ hint.type = "button";
273
411
  hint.className = "lv-timeline-expand-hint";
412
+ hint.setAttribute("aria-expanded", String(expanded));
274
413
  hint.textContent = expanded ? "[ 收起变更详情 ]" : `[ \u67E5\u770B\u53D8\u66F4\u8BE6\u60C5 (${entry.changes.length} \u4E2A\u5B57\u6BB5) ]`;
275
414
  card.appendChild(hint);
276
415
  }
@@ -283,86 +422,48 @@
283
422
  renderPagination(page, pageSize, total) {
284
423
  const container = document.createElement("div");
285
424
  container.className = "lv-pagination";
286
- const totalPages = Math.ceil(total / pageSize);
425
+ const safePageSize = Number.isFinite(pageSize) && pageSize > 0 ? Math.floor(pageSize) : 1;
426
+ const safeTotal = Number.isFinite(total) && total > 0 ? Math.floor(total) : 0;
427
+ const safePage = Number.isFinite(page) && page > 0 ? Math.floor(page) : 1;
428
+ const totalPages = Math.ceil(safeTotal / safePageSize);
287
429
  if (totalPages <= 1) return container;
288
430
  const prev = document.createElement("button");
431
+ prev.type = "button";
289
432
  prev.className = "lv-pagination-btn";
290
433
  prev.textContent = "<";
291
- prev.disabled = page <= 1;
292
- prev.setAttribute("data-page", String(page - 1));
434
+ prev.disabled = safePage <= 1;
435
+ prev.setAttribute("aria-label", "Previous page");
436
+ prev.setAttribute("data-page", String(safePage - 1));
293
437
  container.appendChild(prev);
294
438
  const maxVisible = 7;
295
- let startPage = Math.max(1, page - Math.floor(maxVisible / 2));
439
+ let startPage = Math.max(1, safePage - Math.floor(maxVisible / 2));
296
440
  const endPage = Math.min(totalPages, startPage + maxVisible - 1);
297
441
  if (endPage - startPage < maxVisible - 1) startPage = Math.max(1, endPage - maxVisible + 1);
298
442
  for (let i = startPage; i <= endPage; i++) {
299
443
  const btn = document.createElement("button");
300
- btn.className = `lv-pagination-btn${i === page ? " lv-pagination-btn--active" : ""}`;
444
+ btn.type = "button";
445
+ btn.className = `lv-pagination-btn${i === safePage ? " lv-pagination-btn--active" : ""}`;
301
446
  btn.textContent = String(i);
302
447
  btn.setAttribute("data-page", String(i));
448
+ if (i === safePage) btn.setAttribute("aria-current", "page");
303
449
  container.appendChild(btn);
304
450
  }
305
451
  const next = document.createElement("button");
452
+ next.type = "button";
306
453
  next.className = "lv-pagination-btn";
307
454
  next.textContent = ">";
308
- next.disabled = page >= totalPages;
309
- next.setAttribute("data-page", String(page + 1));
455
+ next.disabled = safePage >= totalPages;
456
+ next.setAttribute("aria-label", "Next page");
457
+ next.setAttribute("data-page", String(safePage + 1));
310
458
  container.appendChild(next);
311
459
  const info = document.createElement("span");
312
460
  info.className = "lv-pagination-info";
313
- info.textContent = `\u5171 ${total} \u6761`;
461
+ info.textContent = `\u5171 ${safeTotal} \u6761`;
314
462
  container.appendChild(info);
315
463
  return container;
316
464
  }
317
465
  };
318
466
  //#endregion
319
- //#region src/renderers/DiffRenderer.ts
320
- var DiffRenderer = class {
321
- /**
322
- * 渲染详情面板(包含 diff 表格和 metadata)
323
- */
324
- renderDetail(entry) {
325
- const container = document.createElement("div");
326
- container.className = "lv-detail";
327
- if (entry.changes && entry.changes.length > 0) container.appendChild(this.renderDiffTable(entry.changes));
328
- if (entry.metadata && Object.keys(entry.metadata).length > 0) {
329
- const meta = document.createElement("div");
330
- meta.className = "lv-metadata";
331
- meta.textContent = JSON.stringify(entry.metadata, null, 2);
332
- container.appendChild(meta);
333
- }
334
- return container;
335
- }
336
- /**
337
- * 渲染 Diff 表格
338
- */
339
- renderDiffTable(changes) {
340
- const table = document.createElement("table");
341
- table.className = "lv-diff-table";
342
- const thead = document.createElement("thead");
343
- thead.innerHTML = `<tr><th>\u5B57\u6BB5</th><th>\u53D8\u66F4\u524D</th><th>\u53D8\u66F4\u540E</th></tr>`;
344
- table.appendChild(thead);
345
- const tbody = document.createElement("tbody");
346
- for (const change of changes) {
347
- const tr = document.createElement("tr");
348
- const tdField = document.createElement("td");
349
- tdField.textContent = change.label || change.field;
350
- tr.appendChild(tdField);
351
- const tdOld = document.createElement("td");
352
- tdOld.className = "lv-diff-old";
353
- tdOld.textContent = formatValue(change.oldValue);
354
- tr.appendChild(tdOld);
355
- const tdNew = document.createElement("td");
356
- tdNew.className = "lv-diff-new";
357
- tdNew.textContent = formatValue(change.newValue);
358
- tr.appendChild(tdNew);
359
- tbody.appendChild(tr);
360
- }
361
- table.appendChild(tbody);
362
- return table;
363
- }
364
- };
365
- //#endregion
366
467
  //#region src/styles/index.ts
367
468
  const STYLE_ID = "ldesign-log-viewer-styles";
368
469
  function injectStyles() {
@@ -376,54 +477,54 @@
376
477
  const CSS_TEXT = `
377
478
  /* ===== CSS Variables ===== */
378
479
  .lv-root {
379
- --lv-bg: #ffffff;
380
- --lv-bg-secondary: #f7f8fa;
381
- --lv-bg-hover: #f0f3ff;
382
- --lv-border: #ebedf0;
383
- --lv-text: #1a1a2e;
384
- --lv-text-secondary: #666;
385
- --lv-text-muted: #999;
386
- --lv-primary: #4f6ef7;
387
- --lv-primary-light: #edf0ff;
388
- --lv-shadow: rgba(0, 0, 0, 0.04);
389
- --lv-timeline-line: #e0e0e0;
390
- --lv-timeline-dot: #4f6ef7;
391
- --lv-diff-add-bg: #f0fff4;
392
- --lv-diff-add-text: #22863a;
393
- --lv-diff-del-bg: #fff5f5;
394
- --lv-diff-del-text: #cb2431;
395
- --lv-anomaly-bg: #fff2e8;
396
- --lv-anomaly-border: #ffbb96;
397
- --lv-radius: 8px;
480
+ --lv-bg: var(--ld-white);
481
+ --lv-bg-secondary: var(--ld-bg-surface-soft);
482
+ --lv-bg-hover: var(--ld-primary-50);
483
+ --lv-border: var(--ld-border);
484
+ --lv-text: var(--ld-text-primary);
485
+ --lv-text-secondary: var(--ld-text-secondary);
486
+ --lv-text-muted: var(--ld-text-muted);
487
+ --lv-primary: var(--ld-primary-500);
488
+ --lv-primary-light: var(--ld-primary-50);
489
+ --lv-shadow: color-mix(in srgb, var(--ld-text-primary) 4%, transparent);
490
+ --lv-timeline-line: var(--ld-border);
491
+ --lv-timeline-dot: var(--ld-primary-500);
492
+ --lv-diff-add-bg: var(--ld-success-50);
493
+ --lv-diff-add-text: var(--ld-success-700);
494
+ --lv-diff-del-bg: var(--ld-danger-50);
495
+ --lv-diff-del-text: var(--ld-danger-700);
496
+ --lv-anomaly-bg: var(--ld-warning-50);
497
+ --lv-anomaly-border: var(--ld-warning-300);
498
+ --lv-radius: var(--ld-radius-md);
398
499
  --lv-font: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
399
500
  --lv-font-mono: 'Fira Code', 'SF Mono', Consolas, monospace;
400
501
  }
401
502
 
402
503
  .lv-root[data-theme="dark"] {
403
- --lv-bg: #1a1a2e;
404
- --lv-bg-secondary: #16213e;
405
- --lv-bg-hover: #1e2a4a;
406
- --lv-border: #2a2a4a;
407
- --lv-text: #e0e0e0;
408
- --lv-text-secondary: #a0a0b0;
409
- --lv-text-muted: #666680;
410
- --lv-primary: #6b85f9;
411
- --lv-primary-light: #252545;
412
- --lv-shadow: rgba(0, 0, 0, 0.2);
413
- --lv-timeline-line: #333355;
414
- --lv-timeline-dot: #6b85f9;
415
- --lv-diff-add-bg: #0d2818;
416
- --lv-diff-add-text: #85e89d;
417
- --lv-diff-del-bg: #2d1214;
418
- --lv-diff-del-text: #f97583;
419
- --lv-anomaly-bg: #2d1a0a;
420
- --lv-anomaly-border: #8b5e3c;
504
+ --lv-bg: var(--ld-bg-surface);
505
+ --lv-bg-secondary: var(--ld-bg-surface-soft);
506
+ --lv-bg-hover: var(--ld-primary-50);
507
+ --lv-border: var(--ld-border-strong);
508
+ --lv-text: var(--ld-text-primary);
509
+ --lv-text-secondary: var(--ld-text-secondary);
510
+ --lv-text-muted: var(--ld-text-muted);
511
+ --lv-primary: var(--ld-primary-400);
512
+ --lv-primary-light: var(--ld-primary-50);
513
+ --lv-shadow: color-mix(in srgb, var(--ld-text-primary) 20%, transparent);
514
+ --lv-timeline-line: var(--ld-border-strong);
515
+ --lv-timeline-dot: var(--ld-primary-400);
516
+ --lv-diff-add-bg: var(--ld-success-50);
517
+ --lv-diff-add-text: var(--ld-success-700);
518
+ --lv-diff-del-bg: var(--ld-danger-50);
519
+ --lv-diff-del-text: var(--ld-danger-700);
520
+ --lv-anomaly-bg: var(--ld-warning-50);
521
+ --lv-anomaly-border: var(--ld-warning-700);
421
522
  }
422
523
 
423
524
  /* ===== Root ===== */
424
525
  .lv-root {
425
526
  font-family: var(--lv-font);
426
- font-size: 14px;
527
+ font-size: var(--ld-px-14);
427
528
  color: var(--lv-text);
428
529
  background: var(--lv-bg);
429
530
  line-height: 1.6;
@@ -433,38 +534,38 @@
433
534
  .lv-filter {
434
535
  display: flex;
435
536
  flex-wrap: wrap;
436
- gap: 10px;
437
- padding: 16px;
537
+ gap: var(--ld-px-10);
538
+ padding: var(--ld-px-16);
438
539
  background: var(--lv-bg);
439
- border: 1px solid var(--lv-border);
540
+ border: var(--ld-px-1) solid var(--lv-border);
440
541
  border-radius: var(--lv-radius);
441
- margin-bottom: 16px;
542
+ margin-bottom: var(--ld-px-16);
442
543
  align-items: center;
443
544
  }
444
545
 
445
546
  .lv-filter-item {
446
547
  display: flex;
447
548
  align-items: center;
448
- gap: 6px;
449
- font-size: 13px;
549
+ gap: var(--ld-px-6);
550
+ font-size: var(--ld-px-13);
450
551
  }
451
552
 
452
553
  .lv-filter-label {
453
554
  color: var(--lv-text-secondary);
454
- font-size: 12px;
555
+ font-size: var(--ld-px-12);
455
556
  white-space: nowrap;
456
557
  }
457
558
 
458
559
  .lv-filter-input {
459
- padding: 5px 10px;
460
- border: 1px solid var(--lv-border);
461
- border-radius: 6px;
462
- font-size: 13px;
560
+ padding: var(--ld-px-5) var(--ld-px-10);
561
+ border: var(--ld-px-1) solid var(--lv-border);
562
+ border-radius: var(--ld-px-6);
563
+ font-size: var(--ld-px-13);
463
564
  background: var(--lv-bg);
464
565
  color: var(--lv-text);
465
566
  outline: none;
466
567
  transition: border-color 0.2s;
467
- min-width: 120px;
568
+ min-width: var(--ld-px-120);
468
569
  }
469
570
 
470
571
  .lv-filter-input:focus {
@@ -472,10 +573,10 @@
472
573
  }
473
574
 
474
575
  .lv-filter-select {
475
- padding: 5px 10px;
476
- border: 1px solid var(--lv-border);
477
- border-radius: 6px;
478
- font-size: 13px;
576
+ padding: var(--ld-px-5) var(--ld-px-10);
577
+ border: var(--ld-px-1) solid var(--lv-border);
578
+ border-radius: var(--ld-px-6);
579
+ font-size: var(--ld-px-13);
479
580
  background: var(--lv-bg);
480
581
  color: var(--lv-text);
481
582
  outline: none;
@@ -485,11 +586,11 @@
485
586
  .lv-filter-btn {
486
587
  display: inline-flex;
487
588
  align-items: center;
488
- gap: 4px;
489
- padding: 5px 14px;
490
- font-size: 13px;
491
- border: 1px solid var(--lv-border);
492
- border-radius: 6px;
589
+ gap: var(--ld-px-4);
590
+ padding: var(--ld-px-5) var(--ld-px-14);
591
+ font-size: var(--ld-px-13);
592
+ border: var(--ld-px-1) solid var(--lv-border);
593
+ border-radius: var(--ld-px-6);
493
594
  background: var(--lv-bg);
494
595
  color: var(--lv-text);
495
596
  cursor: pointer;
@@ -504,7 +605,7 @@
504
605
  .lv-filter-btn--primary {
505
606
  background: var(--lv-primary);
506
607
  border-color: var(--lv-primary);
507
- color: #fff;
608
+ color: var(--ld-white);
508
609
  }
509
610
 
510
611
  .lv-filter-btn--primary:hover {
@@ -514,135 +615,171 @@
514
615
  /* ===== Timeline ===== */
515
616
  .lv-timeline {
516
617
  position: relative;
517
- padding-left: 28px;
618
+ padding-left: var(--ld-px-28);
518
619
  }
519
620
 
520
621
  .lv-timeline::before {
521
622
  content: '';
522
623
  position: absolute;
523
- left: 9px;
624
+ left: var(--ld-px-9);
524
625
  top: 0;
525
626
  bottom: 0;
526
- width: 2px;
627
+ width: var(--ld-px-2);
527
628
  background: var(--lv-timeline-line);
528
629
  }
529
630
 
530
631
  .lv-timeline-item {
531
632
  position: relative;
532
- padding: 0 0 20px 0;
633
+ padding: 0 0 var(--ld-px-20) 0;
533
634
  }
534
635
 
535
636
  .lv-timeline-dot {
536
637
  position: absolute;
537
- left: -25px;
538
- top: 6px;
539
- width: 12px;
540
- height: 12px;
638
+ left: calc(-1 * var(--ld-px-25));
639
+ top: var(--ld-px-6);
640
+ width: var(--ld-px-12);
641
+ height: var(--ld-px-12);
541
642
  border-radius: 50%;
542
643
  background: var(--lv-bg);
543
- border: 2px solid var(--lv-timeline-dot);
644
+ border: var(--ld-px-2) solid var(--lv-timeline-dot);
544
645
  z-index: 1;
545
646
  }
546
647
 
547
648
  .lv-timeline-dot--anomaly {
548
- border-color: #ff4d4f;
549
- background: #fff1f0;
649
+ border-color: var(--ld-danger-500);
650
+ background: var(--ld-danger-50);
550
651
  }
551
652
 
552
653
  .lv-timeline-card {
553
654
  background: var(--lv-bg);
554
- border: 1px solid var(--lv-border);
655
+ border: var(--ld-px-1) solid var(--lv-border);
555
656
  border-radius: var(--lv-radius);
556
- padding: 14px 18px;
657
+ padding: var(--ld-px-14) var(--ld-px-18);
557
658
  cursor: pointer;
558
659
  transition: all 0.2s;
559
- box-shadow: 0 1px 3px var(--lv-shadow);
660
+ box-shadow: 0 var(--ld-px-1) var(--ld-px-3) var(--lv-shadow);
560
661
  }
561
662
 
562
663
  .lv-timeline-card:hover {
563
664
  border-color: var(--lv-primary);
564
- box-shadow: 0 2px 8px var(--lv-shadow);
665
+ box-shadow: 0 var(--ld-px-2) var(--ld-px-8) var(--lv-shadow);
565
666
  }
566
667
 
567
668
  .lv-timeline-card--anomaly {
568
- border-left: 3px solid var(--lv-anomaly-border);
669
+ border-left: var(--ld-px-3) solid var(--lv-anomaly-border);
569
670
  background: var(--lv-anomaly-bg);
570
671
  }
571
672
 
572
673
  .lv-timeline-header {
573
674
  display: flex;
574
675
  align-items: center;
575
- gap: 10px;
676
+ gap: var(--ld-px-10);
576
677
  flex-wrap: wrap;
577
- margin-bottom: 4px;
678
+ margin-bottom: var(--ld-px-4);
578
679
  }
579
680
 
580
681
  .lv-timeline-time {
581
- font-size: 12px;
682
+ font-size: var(--ld-px-12);
582
683
  color: var(--lv-text-muted);
583
684
  font-family: var(--lv-font-mono);
584
685
  }
585
686
 
586
687
  .lv-timeline-user {
587
688
  font-weight: 600;
588
- font-size: 14px;
689
+ font-size: var(--ld-px-14);
589
690
  }
590
691
 
591
692
  .lv-action-tag {
592
693
  display: inline-block;
593
- padding: 1px 8px;
594
- font-size: 12px;
595
- border-radius: 4px;
694
+ padding: var(--ld-px-1) var(--ld-px-8);
695
+ font-size: var(--ld-px-12);
696
+ border-radius: var(--ld-px-4);
596
697
  font-weight: 500;
597
- color: #fff;
698
+ color: var(--ld-white);
699
+ }
700
+
701
+ .lv-action-tag--create,
702
+ .lv-action-tag--approve {
703
+ background: var(--ld-success-500);
704
+ }
705
+
706
+ .lv-action-tag--update,
707
+ .lv-action-tag--login {
708
+ background: var(--ld-primary-500);
709
+ }
710
+
711
+ .lv-action-tag--delete,
712
+ .lv-action-tag--reject {
713
+ background: var(--ld-danger-500);
714
+ }
715
+
716
+ .lv-action-tag--query,
717
+ .lv-action-tag--other {
718
+ background: var(--ld-text-secondary);
719
+ }
720
+
721
+ .lv-action-tag--logout {
722
+ background: var(--ld-primary-700);
723
+ }
724
+
725
+ .lv-action-tag--export {
726
+ background: var(--ld-warning-500);
727
+ }
728
+
729
+ .lv-action-tag--import {
730
+ background: var(--ld-success-500);
598
731
  }
599
732
 
600
733
  .lv-timeline-module {
601
- font-size: 12px;
734
+ font-size: var(--ld-px-12);
602
735
  color: var(--lv-text-secondary);
603
736
  background: var(--lv-bg-secondary);
604
- padding: 1px 8px;
605
- border-radius: 4px;
737
+ padding: var(--ld-px-1) var(--ld-px-8);
738
+ border-radius: var(--ld-px-4);
606
739
  }
607
740
 
608
741
  .lv-timeline-ip {
609
- font-size: 12px;
742
+ font-size: var(--ld-px-12);
610
743
  color: var(--lv-text-muted);
611
744
  font-family: var(--lv-font-mono);
612
745
  }
613
746
 
614
747
  .lv-anomaly-badge {
615
- font-size: 11px;
616
- padding: 1px 6px;
617
- border-radius: 3px;
618
- background: #fff2e8;
619
- color: #d46b08;
620
- border: 1px solid #ffd591;
748
+ font-size: var(--ld-px-11);
749
+ padding: var(--ld-px-1) var(--ld-px-6);
750
+ border-radius: var(--ld-px-3);
751
+ background: var(--ld-warning-50);
752
+ color: var(--ld-warning-700);
753
+ border: var(--ld-px-1) solid var(--ld-warning-300);
621
754
  }
622
755
 
623
756
  .lv-timeline-detail {
624
- font-size: 13px;
757
+ font-size: var(--ld-px-13);
625
758
  color: var(--lv-text-secondary);
626
- margin-top: 6px;
759
+ margin-top: var(--ld-px-6);
627
760
  }
628
761
 
629
762
  .lv-timeline-expand-hint {
630
- font-size: 12px;
763
+ border: 0;
764
+ padding: 0;
765
+ background: transparent;
766
+ font: inherit;
767
+ font-size: var(--ld-px-12);
631
768
  color: var(--lv-primary);
632
- margin-top: 6px;
769
+ margin-top: var(--ld-px-6);
633
770
  cursor: pointer;
634
771
  }
635
772
 
636
773
  /* ===== Detail Panel ===== */
637
774
  .lv-detail {
638
- margin-top: 12px;
639
- padding-top: 12px;
640
- border-top: 1px dashed var(--lv-border);
775
+ margin-top: var(--ld-px-12);
776
+ padding-top: var(--ld-px-12);
777
+ border-top: var(--ld-px-1) dashed var(--lv-border);
641
778
  animation: lv-slide-down 0.2s ease;
642
779
  }
643
780
 
644
781
  @keyframes lv-slide-down {
645
- from { opacity: 0; transform: translateY(-8px); }
782
+ from { opacity: 0; transform: translateY(calc(-1 * var(--ld-px-8))); }
646
783
  to { opacity: 1; transform: translateY(0); }
647
784
  }
648
785
 
@@ -650,25 +787,25 @@
650
787
  .lv-diff-table {
651
788
  width: 100%;
652
789
  border-collapse: collapse;
653
- font-size: 13px;
654
- border: 1px solid var(--lv-border);
655
- border-radius: 6px;
790
+ font-size: var(--ld-px-13);
791
+ border: var(--ld-px-1) solid var(--lv-border);
792
+ border-radius: var(--ld-px-6);
656
793
  overflow: hidden;
657
794
  }
658
795
 
659
796
  .lv-diff-table th {
660
797
  background: var(--lv-bg-secondary);
661
- padding: 8px 12px;
798
+ padding: var(--ld-px-8) var(--ld-px-12);
662
799
  text-align: left;
663
800
  font-weight: 600;
664
- font-size: 12px;
801
+ font-size: var(--ld-px-12);
665
802
  color: var(--lv-text-secondary);
666
- border-bottom: 1px solid var(--lv-border);
803
+ border-bottom: var(--ld-px-1) solid var(--lv-border);
667
804
  }
668
805
 
669
806
  .lv-diff-table td {
670
- padding: 8px 12px;
671
- border-bottom: 1px solid var(--lv-border);
807
+ padding: var(--ld-px-8) var(--ld-px-12);
808
+ border-bottom: var(--ld-px-1) solid var(--lv-border);
672
809
  vertical-align: top;
673
810
  }
674
811
 
@@ -681,23 +818,23 @@
681
818
  color: var(--lv-diff-del-text);
682
819
  text-decoration: line-through;
683
820
  font-family: var(--lv-font-mono);
684
- font-size: 12px;
821
+ font-size: var(--ld-px-12);
685
822
  }
686
823
 
687
824
  .lv-diff-new {
688
825
  background: var(--lv-diff-add-bg);
689
826
  color: var(--lv-diff-add-text);
690
827
  font-family: var(--lv-font-mono);
691
- font-size: 12px;
828
+ font-size: var(--ld-px-12);
692
829
  }
693
830
 
694
831
  /* ===== Metadata ===== */
695
832
  .lv-metadata {
696
- margin-top: 10px;
833
+ margin-top: var(--ld-px-10);
697
834
  background: var(--lv-bg-secondary);
698
- border-radius: 6px;
699
- padding: 10px 14px;
700
- font-size: 12px;
835
+ border-radius: var(--ld-px-6);
836
+ padding: var(--ld-px-10) var(--ld-px-14);
837
+ font-size: var(--ld-px-12);
701
838
  font-family: var(--lv-font-mono);
702
839
  white-space: pre-wrap;
703
840
  word-break: break-all;
@@ -709,21 +846,21 @@
709
846
  display: flex;
710
847
  align-items: center;
711
848
  justify-content: center;
712
- gap: 6px;
713
- padding: 16px 0;
849
+ gap: var(--ld-px-6);
850
+ padding: var(--ld-px-16) 0;
714
851
  }
715
852
 
716
853
  .lv-pagination-btn {
717
854
  display: inline-flex;
718
855
  align-items: center;
719
856
  justify-content: center;
720
- width: 32px;
721
- height: 32px;
722
- border: 1px solid var(--lv-border);
723
- border-radius: 6px;
857
+ width: var(--ld-px-32);
858
+ height: var(--ld-px-32);
859
+ border: var(--ld-px-1) solid var(--lv-border);
860
+ border-radius: var(--ld-px-6);
724
861
  background: var(--lv-bg);
725
862
  color: var(--lv-text);
726
- font-size: 13px;
863
+ font-size: var(--ld-px-13);
727
864
  cursor: pointer;
728
865
  transition: all 0.15s;
729
866
  }
@@ -736,7 +873,7 @@
736
873
  .lv-pagination-btn--active {
737
874
  background: var(--lv-primary);
738
875
  border-color: var(--lv-primary);
739
- color: #fff;
876
+ color: var(--ld-white);
740
877
  }
741
878
 
742
879
  .lv-pagination-btn:disabled {
@@ -745,27 +882,71 @@
745
882
  }
746
883
 
747
884
  .lv-pagination-info {
748
- font-size: 13px;
885
+ font-size: var(--ld-px-13);
749
886
  color: var(--lv-text-secondary);
750
- margin: 0 8px;
887
+ margin: 0 var(--ld-px-8);
751
888
  }
752
889
 
753
890
  /* ===== Empty state ===== */
754
891
  .lv-empty {
755
892
  text-align: center;
756
- padding: 48px 16px;
893
+ padding: var(--ld-px-48) var(--ld-px-16);
757
894
  color: var(--lv-text-muted);
758
- font-size: 14px;
895
+ font-size: var(--ld-px-14);
759
896
  }
760
897
 
761
898
  .lv-empty-icon {
762
- font-size: 48px;
763
- margin-bottom: 12px;
899
+ font-size: var(--ld-px-48);
900
+ margin-bottom: var(--ld-px-12);
764
901
  opacity: 0.3;
765
902
  }
766
903
  `;
767
904
  //#endregion
768
905
  //#region src/core/LogViewer.ts
906
+ const DEFAULT_PAGE_SIZE = 20;
907
+ const MAX_PAGE_SIZE = 1e3;
908
+ function normalizePageSize(value, fallback = DEFAULT_PAGE_SIZE) {
909
+ const pageSize = Number(value);
910
+ if (!Number.isFinite(pageSize) || pageSize <= 0) return fallback;
911
+ return Math.min(MAX_PAGE_SIZE, Math.max(1, Math.floor(pageSize)));
912
+ }
913
+ function normalizePage(value) {
914
+ const page = Number(value);
915
+ if (!Number.isFinite(page) || page < 1) return 1;
916
+ return Math.floor(page);
917
+ }
918
+ function cloneEntries(entries) {
919
+ if (entries === void 0) return [];
920
+ if (!Array.isArray(entries)) throw new TypeError("[LogViewer] data must be an array");
921
+ return entries.map((entry, index) => {
922
+ if (!entry || typeof entry !== "object") throw new TypeError(`[LogViewer] data[${index}] must be an object`);
923
+ const cloned = deepClone(entry);
924
+ const id = String(cloned.id ?? "").trim();
925
+ if (!id) throw new TypeError(`[LogViewer] data[${index}].id must be a non-empty string`);
926
+ return {
927
+ ...cloned,
928
+ id,
929
+ user: String(cloned.user ?? ""),
930
+ module: String(cloned.module ?? ""),
931
+ changes: Array.isArray(cloned.changes) ? cloned.changes : void 0
932
+ };
933
+ });
934
+ }
935
+ function isActionType(value) {
936
+ return Object.keys(ACTION_LABELS).includes(value);
937
+ }
938
+ function sourceEntries(payload) {
939
+ if (Array.isArray(payload)) return payload;
940
+ if (!payload || typeof payload !== "object") throw new TypeError("[LogViewer] dataSource response must be an array");
941
+ const record = payload;
942
+ for (const key of [
943
+ "data",
944
+ "logs",
945
+ "items",
946
+ "entries"
947
+ ]) if (Array.isArray(record[key])) return record[key];
948
+ throw new TypeError("[LogViewer] dataSource response must contain an array");
949
+ }
769
950
  var LogViewer = class {
770
951
  constructor(config) {
771
952
  this.emitter = new EventEmitter();
@@ -777,89 +958,107 @@
777
958
  this.filter = {};
778
959
  this.pagination = {
779
960
  page: 1,
780
- pageSize: 20
961
+ pageSize: DEFAULT_PAGE_SIZE
781
962
  };
782
963
  this.theme = "light";
783
964
  this.expandedIds = /* @__PURE__ */ new Set();
965
+ this.filterEl = null;
784
966
  this.timelineEl = null;
785
967
  this.paginationEl = null;
786
968
  this.destroyed = false;
787
- this.config = config;
969
+ this.requestId = 0;
970
+ this.abortController = null;
971
+ if (typeof document === "undefined") throw new Error("[LogViewer] A browser document is required");
788
972
  const container = typeof config.container === "string" ? document.querySelector(config.container) : config.container;
789
- if (!container) throw new Error(`[LogViewer] Container not found: ${config.container}`);
973
+ if (!container) throw new Error(`[LogViewer] Container not found: ${String(config.container)}`);
974
+ this.config = { ...config };
975
+ this.theme = config.theme === "dark" ? "dark" : "light";
976
+ this.pagination = {
977
+ page: normalizePage(config.pagination?.page),
978
+ pageSize: normalizePageSize(config.pageSize ?? config.pagination?.pageSize)
979
+ };
980
+ this.filter = deepClone(config.filter || {});
981
+ this.data = cloneEntries(config.data);
790
982
  injectStyles();
791
983
  this.root = document.createElement("div");
792
984
  this.root.className = "lv-root";
793
- container.appendChild(this.root);
794
- this.theme = config.theme || "light";
795
985
  this.root.setAttribute("data-theme", this.theme);
796
- if (config.pageSize) this.pagination.pageSize = config.pageSize;
797
- if (config.pagination) Object.assign(this.pagination, config.pagination);
798
- if (config.filter) this.filter = deepClone(config.filter);
799
- if (config.data) this.data = [...config.data];
800
- this.applyFilterAndRender();
986
+ container.appendChild(this.root);
801
987
  this.bindEvents();
988
+ this.applyFilterAndRender();
989
+ if (config.dataSource) this.loadData().catch(() => void 0);
802
990
  }
803
991
  getData() {
804
- return [...this.data];
992
+ return deepClone(this.data);
805
993
  }
806
994
  setData(entries) {
807
- this.data = [...entries];
995
+ if (this.destroyed) return;
996
+ this.data = cloneEntries(entries);
808
997
  this.pagination.page = 1;
998
+ this.expandedIds.clear();
809
999
  this.applyFilterAndRender();
810
- this.emitter.emit("data:change", this.data);
1000
+ this.emitter.emit("data:change", this.getData());
811
1001
  }
812
1002
  appendData(entries) {
813
- this.data.push(...entries);
1003
+ if (this.destroyed) return;
1004
+ this.data.push(...cloneEntries(entries));
814
1005
  this.applyFilterAndRender();
815
- this.emitter.emit("data:change", this.data);
1006
+ this.emitter.emit("data:change", this.getData());
816
1007
  }
817
1008
  getFilter() {
818
1009
  return deepClone(this.filter);
819
1010
  }
820
1011
  setFilter(filter) {
821
- this.filter = deepClone(filter);
1012
+ if (this.destroyed) return;
1013
+ this.filter = deepClone(filter || {});
822
1014
  this.pagination.page = 1;
823
1015
  this.applyFilterAndRender();
824
- this.emitter.emit("filter:change", this.filter);
825
- this.config.onFilterChange?.(this.filter);
1016
+ const current = this.getFilter();
1017
+ this.emitter.emit("filter:change", current);
1018
+ this.config.onFilterChange?.(current);
826
1019
  }
827
1020
  resetFilter() {
828
- this.filter = {};
829
- this.pagination.page = 1;
830
- this.applyFilterAndRender();
831
- this.emitter.emit("filter:change", this.filter);
1021
+ if (this.destroyed) return;
1022
+ this.setFilter({});
832
1023
  }
833
1024
  getTheme() {
834
1025
  return this.theme;
835
1026
  }
836
1027
  setTheme(theme) {
1028
+ if (this.destroyed) return;
1029
+ if (theme !== "light" && theme !== "dark") throw new TypeError("[LogViewer] theme must be light or dark");
1030
+ if (this.theme === theme) return;
837
1031
  this.theme = theme;
838
1032
  this.root.setAttribute("data-theme", theme);
839
1033
  this.emitter.emit("theme:change", theme);
840
1034
  this.config.onThemeChange?.(theme);
841
1035
  }
842
1036
  expandEntry(id) {
1037
+ if (this.destroyed || this.expandedIds.has(id)) return;
843
1038
  this.expandedIds.add(id);
844
- const entry = this.data.find((e) => e.id === id);
845
- if (entry) this.emitter.emit("entry:expand", entry);
1039
+ const entry = this.data.find((item) => item.id === id);
1040
+ if (entry) this.emitter.emit("entry:expand", deepClone(entry));
846
1041
  this.renderTimeline();
847
1042
  }
848
1043
  collapseEntry(id) {
1044
+ if (this.destroyed || !this.expandedIds.has(id)) return;
849
1045
  this.expandedIds.delete(id);
850
- const entry = this.data.find((e) => e.id === id);
851
- if (entry) this.emitter.emit("entry:collapse", entry);
1046
+ const entry = this.data.find((item) => item.id === id);
1047
+ if (entry) this.emitter.emit("entry:collapse", deepClone(entry));
852
1048
  this.renderTimeline();
853
1049
  }
854
1050
  collapseAll() {
1051
+ if (this.destroyed || this.expandedIds.size === 0) return;
855
1052
  this.expandedIds.clear();
856
1053
  this.renderTimeline();
857
1054
  }
858
1055
  goToPage(page) {
859
- const totalPages = Math.ceil(this.filteredData.length / this.pagination.pageSize);
860
- if (page < 1 || page > totalPages) return;
861
- this.pagination.page = page;
862
- this.emitter.emit("page:change", page);
1056
+ if (this.destroyed) return;
1057
+ const totalPages = Math.max(1, Math.ceil(this.filteredData.length / this.pagination.pageSize));
1058
+ const nextPage = Math.min(totalPages, Math.max(1, normalizePage(page)));
1059
+ if (nextPage === this.pagination.page) return;
1060
+ this.pagination.page = nextPage;
1061
+ this.emitter.emit("page:change", nextPage);
863
1062
  this.renderTimeline();
864
1063
  }
865
1064
  getPagination() {
@@ -872,17 +1071,55 @@
872
1071
  return this.emitter.on(event, callback);
873
1072
  }
874
1073
  refresh() {
875
- this.applyFilterAndRender();
1074
+ if (!this.destroyed) this.applyFilterAndRender();
1075
+ }
1076
+ async loadData() {
1077
+ if (this.destroyed || !this.config.dataSource) return;
1078
+ const currentRequest = ++this.requestId;
1079
+ this.abortController?.abort();
1080
+ this.abortController = typeof AbortController !== "undefined" ? new AbortController() : null;
1081
+ try {
1082
+ let payload;
1083
+ if (typeof this.config.dataSource === "function") payload = await this.config.dataSource();
1084
+ else {
1085
+ if (typeof fetch !== "function") throw new Error("[LogViewer] fetch is unavailable");
1086
+ const response = await fetch(this.config.dataSource, { signal: this.abortController?.signal });
1087
+ if (!response.ok) throw new Error(`[LogViewer] dataSource request failed: ${response.status}`);
1088
+ payload = await response.json();
1089
+ }
1090
+ if (this.destroyed || currentRequest !== this.requestId) return;
1091
+ this.setData(sourceEntries(payload));
1092
+ } catch (error) {
1093
+ if (this.destroyed || typeof DOMException !== "undefined" && error instanceof DOMException && error.name === "AbortError") return;
1094
+ this.config.onError?.(error);
1095
+ this.emitter.emit("error", error);
1096
+ throw error;
1097
+ }
876
1098
  }
877
1099
  destroy() {
878
1100
  if (this.destroyed) return;
879
1101
  this.destroyed = true;
1102
+ this.requestId++;
1103
+ this.abortController?.abort();
1104
+ this.abortController = null;
880
1105
  this.emitter.removeAllListeners();
1106
+ this.expandedIds.clear();
1107
+ this.root.replaceChildren();
881
1108
  this.root.remove();
882
1109
  }
883
1110
  applyFilterAndRender() {
884
1111
  this.filteredData = this.filterEngine.filter(this.data, this.filter);
885
- this.filteredData.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
1112
+ this.filteredData = this.filteredData.map((entry, index) => ({
1113
+ entry,
1114
+ index
1115
+ })).sort((a, b) => {
1116
+ const left = new Date(a.entry.timestamp).getTime();
1117
+ const right = new Date(b.entry.timestamp).getTime();
1118
+ const leftTime = Number.isFinite(left) ? left : Number.MIN_SAFE_INTEGER;
1119
+ return (Number.isFinite(right) ? right : Number.MIN_SAFE_INTEGER) - leftTime || a.index - b.index;
1120
+ }).map(({ entry }) => entry);
1121
+ const totalPages = Math.max(1, Math.ceil(this.filteredData.length / this.pagination.pageSize));
1122
+ this.pagination.page = Math.min(this.pagination.page, totalPages);
886
1123
  this.pagination.total = this.filteredData.length;
887
1124
  this.renderTimeline();
888
1125
  }
@@ -891,43 +1128,121 @@
891
1128
  const start = (page - 1) * pageSize;
892
1129
  return this.filteredData.slice(start, start + pageSize);
893
1130
  }
1131
+ renderFilterBar() {
1132
+ if (this.config.showFilter === false) return null;
1133
+ const filter = document.createElement("div");
1134
+ filter.className = "lv-filter";
1135
+ filter.setAttribute("role", "search");
1136
+ const addInput = (field, label) => {
1137
+ const item = document.createElement("label");
1138
+ item.className = "lv-filter-item";
1139
+ const text = document.createElement("span");
1140
+ text.className = "lv-filter-label";
1141
+ text.textContent = label;
1142
+ const input = document.createElement("input");
1143
+ input.className = "lv-filter-input";
1144
+ input.type = "search";
1145
+ input.value = String(this.filter[field] || "");
1146
+ input.setAttribute("data-filter-field", field);
1147
+ input.setAttribute("aria-label", label);
1148
+ item.append(text, input);
1149
+ filter.appendChild(item);
1150
+ };
1151
+ addInput("keyword", this.config.locale === "en-US" ? "Keyword" : "关键词");
1152
+ addInput("user", this.config.locale === "en-US" ? "User" : "用户");
1153
+ addInput("module", this.config.locale === "en-US" ? "Module" : "模块");
1154
+ addInput("ip", "IP");
1155
+ const actionItem = document.createElement("label");
1156
+ actionItem.className = "lv-filter-item";
1157
+ const actionLabel = document.createElement("span");
1158
+ actionLabel.className = "lv-filter-label";
1159
+ actionLabel.textContent = this.config.locale === "en-US" ? "Action" : "操作";
1160
+ const action = document.createElement("select");
1161
+ action.className = "lv-filter-select";
1162
+ action.setAttribute("data-filter-field", "action");
1163
+ action.setAttribute("aria-label", actionLabel.textContent);
1164
+ const allOption = document.createElement("option");
1165
+ allOption.value = "";
1166
+ allOption.textContent = this.config.locale === "en-US" ? "All" : "全部";
1167
+ action.appendChild(allOption);
1168
+ for (const [value, label] of Object.entries(ACTION_LABELS)) {
1169
+ const option = document.createElement("option");
1170
+ option.value = value;
1171
+ option.textContent = label;
1172
+ action.appendChild(option);
1173
+ }
1174
+ action.value = Array.isArray(this.filter.action) ? "" : this.filter.action || "";
1175
+ actionItem.append(actionLabel, action);
1176
+ filter.appendChild(actionItem);
1177
+ const reset = document.createElement("button");
1178
+ reset.type = "button";
1179
+ reset.className = "lv-filter-btn";
1180
+ reset.setAttribute("data-filter-reset", "true");
1181
+ reset.textContent = this.config.locale === "en-US" ? "Reset" : "重置";
1182
+ filter.appendChild(reset);
1183
+ return filter;
1184
+ }
1185
+ updateFilterField(field, value) {
1186
+ const next = this.getFilter();
1187
+ if (!value.trim()) delete next[field];
1188
+ else if (field === "action" && isActionType(value)) next.action = value;
1189
+ else if (field !== "action") next[field] = value;
1190
+ this.setFilter(next);
1191
+ }
894
1192
  renderTimeline() {
895
1193
  if (this.destroyed) return;
896
1194
  const pageData = this.getPageData();
897
- const newTimeline = this.timelineRenderer.render(pageData, this.expandedIds);
898
- for (const entry of pageData) if (this.expandedIds.has(entry.id)) {
899
- const itemEl = newTimeline.querySelector(`[data-id="${entry.id}"] .lv-timeline-card`);
900
- if (itemEl) itemEl.appendChild(this.diffRenderer.renderDetail(entry));
1195
+ const timeline = this.timelineRenderer.render(pageData, this.expandedIds, {
1196
+ showDiff: this.config.showDiff,
1197
+ showAnomalyBadge: this.config.showAnomalyBadge,
1198
+ locale: this.config.locale
1199
+ });
1200
+ if (this.config.showDiff !== false) for (const entry of pageData) {
1201
+ if (!this.expandedIds.has(entry.id)) continue;
1202
+ ([...timeline.children].find((child) => child.getAttribute("data-id") === entry.id)?.querySelector(".lv-timeline-card"))?.appendChild(this.diffRenderer.renderDetail(entry));
901
1203
  }
902
- if (this.timelineEl) this.root.removeChild(this.timelineEl);
903
- this.timelineEl = newTimeline;
904
- this.root.appendChild(this.timelineEl);
905
- const newPagination = this.timelineRenderer.renderPagination(this.pagination.page, this.pagination.pageSize, this.filteredData.length);
906
- if (this.paginationEl) this.root.removeChild(this.paginationEl);
907
- this.paginationEl = newPagination;
908
- this.root.appendChild(this.paginationEl);
1204
+ this.filterEl?.remove();
1205
+ this.timelineEl?.remove();
1206
+ this.paginationEl?.remove();
1207
+ this.filterEl = this.renderFilterBar();
1208
+ this.timelineEl = timeline;
1209
+ this.paginationEl = this.timelineRenderer.renderPagination(this.pagination.page, this.pagination.pageSize, this.filteredData.length);
1210
+ if (this.filterEl) this.root.appendChild(this.filterEl);
1211
+ this.root.append(this.timelineEl, this.paginationEl);
909
1212
  }
910
1213
  bindEvents() {
911
- this.root.addEventListener("click", (e) => {
912
- const target = e.target;
913
- const card = target.closest(".lv-timeline-card");
914
- if (card) {
915
- const id = card.closest(".lv-timeline-item")?.getAttribute("data-id");
916
- if (!id) return;
917
- const entry = this.filteredData.find((en) => en.id === id);
918
- if (!entry) return;
919
- if (target.classList.contains("lv-timeline-expand-hint")) {
920
- if (this.expandedIds.has(id)) this.collapseEntry(id);
921
- else this.expandEntry(id);
922
- return;
923
- }
924
- this.emitter.emit("entry:click", entry);
925
- this.config.onEntryClick?.(entry);
1214
+ this.root.addEventListener("change", (event) => {
1215
+ const target = event.target;
1216
+ if (!(target instanceof HTMLInputElement) && !(target instanceof HTMLSelectElement)) return;
1217
+ const field = target.getAttribute("data-filter-field");
1218
+ if (field) this.updateFilterField(field, target.value);
1219
+ });
1220
+ this.root.addEventListener("click", (event) => {
1221
+ const target = event.target;
1222
+ if (!(target instanceof Element)) return;
1223
+ if (target.closest("[data-filter-reset]")) {
1224
+ this.resetFilter();
1225
+ return;
1226
+ }
1227
+ const hint = target.closest(".lv-timeline-expand-hint");
1228
+ const id = (hint?.closest(".lv-timeline-item") || target.closest(".lv-timeline-item"))?.getAttribute("data-id");
1229
+ if (hint && id) {
1230
+ if (this.expandedIds.has(id)) this.collapseEntry(id);
1231
+ else this.expandEntry(id);
1232
+ return;
1233
+ }
1234
+ const pageButton = target.closest(".lv-pagination-btn");
1235
+ if (pageButton && !pageButton.disabled) {
1236
+ const page = Number(pageButton.getAttribute("data-page"));
1237
+ if (Number.isFinite(page)) this.goToPage(page);
1238
+ return;
926
1239
  }
927
- const pageBtn = target.closest(".lv-pagination-btn");
928
- if (pageBtn && !pageBtn.disabled) {
929
- const page = parseInt(pageBtn.getAttribute("data-page") || "1", 10);
930
- this.goToPage(page);
1240
+ if (!target.closest(".lv-timeline-card") || !id) return;
1241
+ const entry = this.filteredData.find((candidate) => candidate.id === id);
1242
+ if (entry) {
1243
+ const snapshot = deepClone(entry);
1244
+ this.emitter.emit("entry:click", snapshot);
1245
+ this.config.onEntryClick?.(snapshot);
931
1246
  }
932
1247
  });
933
1248
  }