@flexem/fc-gui 3.0.0-alpha.144 → 3.0.0-alpha.146

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.
@@ -47,18 +47,6 @@ export class ScrollAlarmElement extends ConditionalDynamicDisplayElement {
47
47
  this.initDisplayContainer();
48
48
  // 获取字体设置
49
49
  this.headerFont = ((_e = this.model.generalSetting) === null || _e === void 0 ? void 0 : _e.headerFont) || {};
50
- // 构建符合 CSS font 属性规范的字符串
51
- let fontString = '';
52
- if (this.headerFont.isItalic) {
53
- fontString += 'italic ';
54
- }
55
- if (this.headerFont.isBold) {
56
- fontString += 'bold ';
57
- }
58
- const fontSize = this.headerFont.fontSize || '16px';
59
- const lineHeight = parseInt(fontSize, 10) + 5;
60
- fontString += `${fontSize}/${lineHeight}px ${this.headerFont.fontFamily || 'Microsoft YaHei'}`;
61
- this.fontString = fontString;
62
50
  // 使用静态显示逻辑:只显示字段标签
63
51
  this.renderStaticDisplay();
64
52
  this.setStatusAsNormal();
@@ -91,19 +79,6 @@ export class ScrollAlarmElement extends ConditionalDynamicDisplayElement {
91
79
  this.initDisplayContainer();
92
80
  // 获取字体设置 - 参考 text-element.ts 的实现方式
93
81
  this.headerFont = ((_f = this.model.generalSetting) === null || _f === void 0 ? void 0 : _f.headerFont) || {};
94
- // 构建符合 CSS font 属性规范的字符串
95
- // 格式:font-style font-weight font-size/line-height font-family
96
- let fontString = '';
97
- if (this.headerFont.isItalic) {
98
- fontString += 'italic ';
99
- }
100
- if (this.headerFont.isBold) {
101
- fontString += 'bold ';
102
- }
103
- const fontSize = this.headerFont.fontSize || '16px';
104
- const lineHeight = parseInt(fontSize, 10) + 5;
105
- fontString += `${fontSize}/${lineHeight}px ${this.headerFont.fontFamily || 'Microsoft YaHei'}`;
106
- this.fontString = fontString;
107
82
  this.getAlarmData();
108
83
  }
109
84
  }
@@ -113,7 +88,9 @@ export class ScrollAlarmElement extends ConditionalDynamicDisplayElement {
113
88
  // 初始化显示容器以显示空内容
114
89
  this.initDisplayContainer();
115
90
  if (this.allAlarmsContainer) {
116
- this.allAlarmsContainer.innerHTML = '';
91
+ while (this.allAlarmsContainer.firstChild) {
92
+ this.allAlarmsContainer.removeChild(this.allAlarmsContainer.firstChild);
93
+ }
117
94
  this.totalWidth = 0;
118
95
  this.pageWidths = [];
119
96
  }
@@ -191,8 +168,8 @@ export class ScrollAlarmElement extends ConditionalDynamicDisplayElement {
191
168
  const oldPageWidth = this.pageWidths.pop() || 0;
192
169
  this.totalWidth -= oldPageWidth;
193
170
  const elementsToRemove = Array.from(this.allAlarmsContainer.children).slice(startIndex, startIndex + itemsPerPage);
194
- elementsToRemove.forEach(element => {
195
- this.allAlarmsContainer.removeChild(element);
171
+ elementsToRemove.forEach(el => {
172
+ this.allAlarmsContainer.removeChild(el);
196
173
  });
197
174
  // 插入新的当前页数据
198
175
  this.displayedItems.splice(startIndex, 0, ...newPage);
@@ -232,7 +209,9 @@ export class ScrollAlarmElement extends ConditionalDynamicDisplayElement {
232
209
  // 清空显示
233
210
  this.displayedItems = [];
234
211
  if (this.allAlarmsContainer) {
235
- this.allAlarmsContainer.innerHTML = '';
212
+ while (this.allAlarmsContainer.firstChild) {
213
+ this.allAlarmsContainer.removeChild(this.allAlarmsContainer.firstChild);
214
+ }
236
215
  this.totalWidth = 0;
237
216
  this.pageWidths = [];
238
217
  }
@@ -259,121 +238,153 @@ export class ScrollAlarmElement extends ConditionalDynamicDisplayElement {
259
238
  var _a;
260
239
  const elementHeight = this.model.size.height;
261
240
  const elementWidth = this.model.size.width;
262
- // 创建foreignObject作为容器
263
- this.element = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject');
264
- this.element.setAttribute('width', elementWidth.toString());
265
- this.element.setAttribute('height', elementHeight.toString());
266
- this.$element.append(this.element);
267
- // 创建背景容器,应用headerFillColor背景色
268
- this.container = document.createElement('div');
269
- this.container.style.cssText = `
270
- height: ${elementHeight}px;
271
- width: 100%;
272
- background-color: ${((_a = this.model.generalSetting) === null || _a === void 0 ? void 0 : _a.headerFillColor) || '#ffffff'};
273
- display: flex;
274
- align-items: center;
275
- overflow: hidden;
276
- position: relative;
277
- border: 1px solid #8ea0aa;
278
- border-radius: 2px;
279
- `;
280
- this.element.appendChild(this.container);
281
- // 创建一个大容器来包含所有告警项,初始位置在容器右侧(从右边开始滚入)
282
- this.allAlarmsContainer = document.createElement('div');
283
- this.allAlarmsContainer.style.cssText = `
284
- position: absolute;
285
- top: 50%;
286
- transform: translateY(-50%);
287
- display: flex;
288
- align-items: center;
289
- gap: 80px;
290
- `;
291
- this.container.appendChild(this.allAlarmsContainer);
292
- // 设置初始位置为容器宽度,让内容从右侧滚入
241
+ const bgColor = ((_a = this.model.generalSetting) === null || _a === void 0 ? void 0 : _a.headerFillColor) || '#ffffff';
242
+ const clipId = 'scroll-alarm-clip-' + Math.random().toString(36).slice(2);
243
+ // 背景矩形
244
+ this.bgRect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
245
+ this.bgRect.setAttribute('x', '0');
246
+ this.bgRect.setAttribute('y', '0');
247
+ this.bgRect.setAttribute('width', elementWidth.toString());
248
+ this.bgRect.setAttribute('height', elementHeight.toString());
249
+ this.bgRect.setAttribute('fill', bgColor);
250
+ this.bgRect.setAttribute('stroke', '#8ea0aa');
251
+ this.bgRect.setAttribute('stroke-width', '1');
252
+ this.$element[0].appendChild(this.bgRect);
253
+ // clipPath 限制文字滚动区域
254
+ const defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
255
+ const clipPath = document.createElementNS('http://www.w3.org/2000/svg', 'clipPath');
256
+ clipPath.setAttribute('id', clipId);
257
+ const clipRect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
258
+ clipRect.setAttribute('x', '2');
259
+ clipRect.setAttribute('y', '0');
260
+ clipRect.setAttribute('width', (elementWidth - 4).toString());
261
+ clipRect.setAttribute('height', elementHeight.toString());
262
+ clipPath.appendChild(clipRect);
263
+ defs.appendChild(clipPath);
264
+ this.$element[0].appendChild(defs);
265
+ // 滚动内容的 g 容器,套上 clipPath
266
+ this.scrollGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
267
+ this.scrollGroup.setAttribute('clip-path', `url(#${clipId})`);
268
+ this.$element[0].appendChild(this.scrollGroup);
269
+ // 内部 g 用于整体平移(滚动动画改这个的 transform)
270
+ this.allAlarmsContainer = document.createElementNS('http://www.w3.org/2000/svg', 'g');
271
+ this.scrollGroup.appendChild(this.allAlarmsContainer);
272
+ // 初始位置在右侧屏幕外
293
273
  this.currentLeft = elementWidth;
294
- this.allAlarmsContainer.style.left = `${elementWidth}px`;
295
- // 添加鼠标悬停暂停和恢复滚动功能
296
- this.container.addEventListener('mouseenter', () => this.pauseScroll());
297
- this.container.addEventListener('mouseleave', () => this.resumeScroll());
274
+ this.allAlarmsContainer.setAttribute('transform', `translate(${elementWidth}, 0)`);
275
+ }
276
+ _buildFontAttrs() {
277
+ const f = this.headerFont || {};
278
+ const fontSize = parseInt((f.fontSize || '16px'), 10);
279
+ return {
280
+ fontSize,
281
+ fontFamily: f.fontFamily || 'Microsoft YaHei',
282
+ color: f.color || '#e33c39',
283
+ isBold: !!f.isBold,
284
+ isItalic: !!f.isItalic,
285
+ isUnderline: !!f.isUnderline
286
+ };
287
+ }
288
+ _createSvgTextNode(textContent) {
289
+ const elementHeight = this.model.size.height;
290
+ const fa = this._buildFontAttrs();
291
+ const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
292
+ text.setAttribute('y', (elementHeight / 2).toString());
293
+ text.setAttribute('dominant-baseline', 'central');
294
+ text.setAttribute('fill', fa.color);
295
+ text.setAttribute('font-size', fa.fontSize.toString());
296
+ text.setAttribute('font-family', fa.fontFamily);
297
+ if (fa.isBold) {
298
+ text.setAttribute('font-weight', 'bold');
299
+ }
300
+ if (fa.isItalic) {
301
+ text.setAttribute('font-style', 'italic');
302
+ }
303
+ if (fa.isUnderline) {
304
+ text.setAttribute('text-decoration', 'underline');
305
+ }
306
+ text.textContent = textContent;
307
+ return text;
298
308
  }
299
309
  renderNewPage(pageData) {
310
+ const GAP = 80;
300
311
  let pageWidth = 0;
301
- // 为每个告警数据创建文本容器
312
+ // 新页的起始 x = 已有总宽度 + gap(如果已有内容)
313
+ let xOffset = this.totalWidth > 0 ? this.totalWidth + GAP : 0;
302
314
  pageData.forEach((alarm) => {
303
315
  var _a, _b, _c, _d, _e, _f, _g, _h;
304
- // 创建告警文本容器
305
- const textContainer = document.createElement('div');
306
- textContainer.style.cssText = `
307
- font: ${this.fontString};
308
- color: ${this.headerFont.color || '#e33c39'};
309
- white-space: nowrap;
310
- overflow: visible;
311
- `;
312
- // 应用下划线样式(参考 text-element.ts 的实现)
313
- if (this.headerFont.isUnderline) {
314
- textContainer.style.textDecoration = 'underline';
315
- }
316
- // 构建告警信息内容,根据配置决定显示哪些字段
317
316
  const contentParts = [];
318
- // 显示触发时间
319
317
  if (((_a = this.model.generalSetting) === null || _a === void 0 ? void 0 : _a.showTriggerTime) && alarm.triggeredTime) {
320
- const formattedTime = moment(alarm.triggeredTime).format('YYYY/MM/DD HH:mm:ss');
321
- contentParts.push(formattedTime);
318
+ contentParts.push(moment(alarm.triggeredTime).format('YYYY/MM/DD HH:mm:ss'));
322
319
  }
323
- // 获取当前语言环境
324
320
  const language = ((_d = (_c = (_b = window.abp) === null || _b === void 0 ? void 0 : _b.localization) === null || _c === void 0 ? void 0 : _c.currentLanguage) === null || _d === void 0 ? void 0 : _d.name) || 'zh-Hans';
325
321
  const isChinese = language === 'zh-Hans' || language === 'zh';
326
- // 显示告警等级
327
322
  if (((_e = this.model.generalSetting) === null || _e === void 0 ? void 0 : _e.showAlarmLevel) && alarm.alarmLevel !== undefined) {
328
323
  const levelMap = isChinese ? ['警告', '次要', '主要', '严重'] : ['Warning', 'Minor', 'Major', 'Critical'];
329
- const alarmLevel = levelMap[alarm.alarmLevel] || (isChinese ? '警告' : 'Warning');
330
- contentParts.push(alarmLevel);
324
+ contentParts.push(levelMap[alarm.alarmLevel] || (isChinese ? '警告' : 'Warning'));
331
325
  }
332
- // 显示告警名称
333
326
  if (((_f = this.model.generalSetting) === null || _f === void 0 ? void 0 : _f.showAlarmName) && alarm.name) {
334
327
  contentParts.push(alarm.name);
335
328
  }
336
- // 显示告警内容
337
329
  if (((_g = this.model.generalSetting) === null || _g === void 0 ? void 0 : _g.showAlarmContent) && alarm.message) {
338
330
  contentParts.push(alarm.message);
339
331
  }
340
- // 显示告警状态
341
332
  if (((_h = this.model.generalSetting) === null || _h === void 0 ? void 0 : _h.showAlarmState) && alarm.state !== undefined) {
342
- const stateMap = isChinese ? ['触发/未确认', '触发/已确认', '恢复/未确认', '恢复/已确认'] : ['Triggered/Unconfirmed', 'Triggered/Confirmed', 'Restored/Unconfirmed', 'Restored/Confirmed'];
343
- const alarmState = stateMap[alarm.state] || (isChinese ? '触发/未确认' : 'Triggered/Unconfirmed');
344
- contentParts.push(alarmState);
333
+ const stateMap = isChinese
334
+ ? ['触发/未确认', '触发/已确认', '恢复/未确认', '恢复/已确认']
335
+ : ['Triggered/Unconfirmed', 'Triggered/Confirmed', 'Restored/Unconfirmed', 'Restored/Confirmed'];
336
+ contentParts.push(stateMap[alarm.state] || (isChinese ? '触发/未确认' : 'Triggered/Unconfirmed'));
345
337
  }
346
- // 将所有显示字段用空格连接,确保一行显示
347
- const textContent = contentParts.join(' ');
348
- textContainer.textContent = textContent;
349
- this.allAlarmsContainer.appendChild(textContainer);
350
- // 计算文本宽度
351
- const textWidth = textContainer.offsetWidth;
352
- pageWidth += textWidth + 80; // 80是gap大小
338
+ const textNode = this._createSvgTextNode(contentParts.join(' '));
339
+ textNode.setAttribute('x', xOffset.toString());
340
+ this.allAlarmsContainer.appendChild(textNode);
341
+ const textWidth = textNode.getComputedTextLength
342
+ ? textNode.getComputedTextLength()
343
+ : (textNode.getBBox ? textNode.getBBox().width : 100);
344
+ xOffset += textWidth + GAP;
345
+ pageWidth += (pageWidth > 0 ? GAP : 0) + textWidth;
353
346
  });
354
- // 移除最后一个gap
355
- if (pageWidth > 0) {
356
- pageWidth -= 80;
357
- }
358
- // 保存页宽度
359
347
  this.pageWidths.push(pageWidth);
360
- this.totalWidth += pageWidth;
348
+ this.totalWidth += (this.totalWidth > 0 && pageWidth > 0 ? GAP : 0) + pageWidth;
349
+ }
350
+ _rebaseTextNodes() {
351
+ const GAP = 80;
352
+ let x = 0;
353
+ Array.from(this.allAlarmsContainer.children).forEach(node => {
354
+ node.setAttribute('x', x.toString());
355
+ const w = node.getComputedTextLength
356
+ ? node.getComputedTextLength()
357
+ : (node.getBBox ? node.getBBox().width : 100);
358
+ x += w + GAP;
359
+ });
360
+ const newTotal = x > 0 ? x - GAP : 0;
361
+ this.totalWidth = newTotal;
362
+ this.pageWidths = [newTotal];
361
363
  }
362
364
  removeOldestPage() {
363
365
  // 移除最旧的一页数据
364
366
  this.displayedItems.splice(0, this.maxResultCount);
365
367
  const oldPageWidth = this.pageWidths.shift() || 0;
366
- // 更新总宽度
367
- this.totalWidth -= oldPageWidth;
368
+ this.totalWidth -= oldPageWidth + (this.pageWidths.length > 0 ? 80 : 0);
368
369
  // 从DOM中移除最旧的元素
369
370
  const elementsToRemove = Array.from(this.allAlarmsContainer.children).slice(0, this.maxResultCount);
370
- elementsToRemove.forEach(element => {
371
- this.allAlarmsContainer.removeChild(element);
371
+ elementsToRemove.forEach(el => {
372
+ this.allAlarmsContainer.removeChild(el);
373
+ });
374
+ // SVG text 的 x 坐标从 0 重排(DOM删除后剩余节点要重新编排 x)
375
+ const GAP = 80;
376
+ let x = 0;
377
+ Array.from(this.allAlarmsContainer.children).forEach(node => {
378
+ node.setAttribute('x', x.toString());
379
+ const w = node.getComputedTextLength
380
+ ? node.getComputedTextLength()
381
+ : (node.getBBox ? node.getBBox().width : 100);
382
+ x += w + GAP;
372
383
  });
373
- // 调整当前left位置(保持视觉位置不变)- 这里需要等DOM更新完成后再调整
384
+ // currentLeft 补偿被移除页的宽度(含 gap),保持视觉位置不变
374
385
  requestAnimationFrame(() => {
375
- this.currentLeft += oldPageWidth;
376
- this.allAlarmsContainer.style.left = `${this.currentLeft}px`;
386
+ this.currentLeft += oldPageWidth + 80;
387
+ this.allAlarmsContainer.setAttribute('transform', `translate(${this.currentLeft}, 0)`);
377
388
  });
378
389
  }
379
390
  scrollContent() {
@@ -396,21 +407,19 @@ export class ScrollAlarmElement extends ConditionalDynamicDisplayElement {
396
407
  // 说明所有内容都已经滚出视图(不开启自动循环时多滚动50px确保最后一个字完全滚出)
397
408
  if (this.currentLeft + this.totalWidth < -50) {
398
409
  if (this.autoCycle) {
399
- // 自动循环模式:无缝衔接回到容器右侧
400
410
  if (this.hasMoreData) {
401
- // 有更多数据:继续加载(但实际上已经在上面预加载了)
402
- // 重置到容器宽度,让内容从右侧重新滚入
403
- this.currentLeft = this.container.clientWidth;
411
+ // 有更多数据:重置到容器宽度,让内容从右侧重新滚入
412
+ this.currentLeft = this.model.size.width;
404
413
  }
405
414
  else {
406
- // 没有更多数据了:重置
407
415
  if (this.currentPage > 2) {
408
416
  // 数据总数超过一页:重置到第一页并重新查询
409
417
  this.resetToFirstPage();
410
418
  }
411
419
  else {
412
- // 数据总数只有一页:直接重置到容器右侧,循环滚动当前数据
413
- this.currentLeft = this.container.clientWidth;
420
+ // 单页循环:重排 x 从 0 开始,currentLeft 重置到右侧
421
+ this._rebaseTextNodes();
422
+ this.currentLeft = this.model.size.width;
414
423
  }
415
424
  }
416
425
  }
@@ -422,7 +431,7 @@ export class ScrollAlarmElement extends ConditionalDynamicDisplayElement {
422
431
  }
423
432
  }
424
433
  // 更新显示位置
425
- this.allAlarmsContainer.style.left = `${this.currentLeft}px`;
434
+ this.allAlarmsContainer.setAttribute('transform', `translate(${this.currentLeft}, 0)`);
426
435
  }
427
436
  initScrolling() {
428
437
  clearInterval(this.scrollIntervalId);
@@ -433,8 +442,8 @@ export class ScrollAlarmElement extends ConditionalDynamicDisplayElement {
433
442
  // 确保初始位置在容器右侧(从右边滚入)
434
443
  if (this.currentPage === 1 && this.displayedItems.length <= this.maxResultCount) {
435
444
  // 第一页初始加载时,确保从右侧开始
436
- this.currentLeft = this.container.clientWidth;
437
- this.allAlarmsContainer.style.left = `${this.currentLeft}px`;
445
+ this.currentLeft = this.model.size.width;
446
+ this.allAlarmsContainer.setAttribute('transform', `translate(${this.currentLeft}, 0)`);
438
447
  }
439
448
  const scrollInterval = 100; // 默认100ms,滚动更流畅
440
449
  // 延迟启动滚动确保内容渲染完成
@@ -447,12 +456,6 @@ export class ScrollAlarmElement extends ConditionalDynamicDisplayElement {
447
456
  }, scrollInterval);
448
457
  }, 500);
449
458
  }
450
- pauseScroll() {
451
- this.isScrolling = false;
452
- }
453
- resumeScroll() {
454
- this.isScrolling = true;
455
- }
456
459
  resetToFirstPage() {
457
460
  // 重置到第一页并重新查询数据
458
461
  this.currentPage = 1;
@@ -460,13 +463,15 @@ export class ScrollAlarmElement extends ConditionalDynamicDisplayElement {
460
463
  this.hasTriedFirstPage = false;
461
464
  this.displayedItems = [];
462
465
  if (this.allAlarmsContainer) {
463
- this.allAlarmsContainer.innerHTML = '';
466
+ while (this.allAlarmsContainer.firstChild) {
467
+ this.allAlarmsContainer.removeChild(this.allAlarmsContainer.firstChild);
468
+ }
464
469
  this.totalWidth = 0;
465
470
  this.pageWidths = [];
466
471
  }
467
472
  // 重置到容器右侧,让内容从右边滚入
468
- this.currentLeft = this.container.clientWidth;
469
- this.allAlarmsContainer.style.left = `${this.currentLeft}px`;
473
+ this.currentLeft = this.model.size.width;
474
+ this.allAlarmsContainer.setAttribute('transform', `translate(${this.currentLeft}, 0)`);
470
475
  this.getAlarmData();
471
476
  }
472
477
  updateQueryTimeRange() {
@@ -510,16 +515,18 @@ export class ScrollAlarmElement extends ConditionalDynamicDisplayElement {
510
515
  // 确保宽度和高度为正值,避免 SVG 错误
511
516
  const width = Math.max(this.model.size.width || 100, 0);
512
517
  const height = Math.max(this.model.size.height || 50, 0);
518
+ // 先移除旧的 StateFrame,避免重复追加
519
+ this.$element.find('rect#StateFrame').remove();
513
520
  this.rootElement.append('rect').attr('id', 'StateFrame').attr('fill', 'transparent')
514
521
  .attr('width', width)
515
522
  .attr('height', height);
516
- const document = this.$element[0].ownerDocument;
523
+ const doc = this.$element[0].ownerDocument;
517
524
  const currentRect = this.$element.find('rect#StateFrame').first();
518
525
  if (!currentRect.length) {
519
526
  return;
520
527
  }
521
528
  this.$element.find('image#StateImage').remove();
522
- const imgObj = document.createElementNS('http://www.w3.org/2000/svg', 'image');
529
+ const imgObj = doc.createElementNS('http://www.w3.org/2000/svg', 'image');
523
530
  if (imgObj) {
524
531
  let x = Number(currentRect[0].getAttribute('width')) - 20;
525
532
  const currentRectX = currentRect[0].getAttribute('x');
@@ -534,20 +541,13 @@ export class ScrollAlarmElement extends ConditionalDynamicDisplayElement {
534
541
  imgObj.setAttributeNS(null, 'y', '0');
535
542
  imgObj.setAttributeNS(null, 'height', '20px');
536
543
  imgObj.setAttributeNS(null, 'width', '20px');
537
- const titleElement = document.createElementNS('http://www.w3.org/2000/svg', 'title');
544
+ const titleElement = doc.createElementNS('http://www.w3.org/2000/svg', 'title');
538
545
  imgObj.appendChild(titleElement);
539
546
  this.$element.append(imgObj);
540
547
  }
541
548
  }
542
549
  clearStatus() {
543
- const currentRect = this.$element.find('rect#StateFrame').first();
544
- if (!currentRect.length) {
545
- return;
546
- }
547
- const stroke = currentRect[0].getAttribute('stroke');
548
- if (stroke) {
549
- currentRect[0].removeAttribute('stroke');
550
- }
550
+ this.$element.find('rect#StateFrame').remove();
551
551
  this.$element.find('image#StateImage').remove();
552
552
  }
553
553
  /**
@@ -586,36 +586,20 @@ export class ScrollAlarmElement extends ConditionalDynamicDisplayElement {
586
586
  const isChinese = language === 'zh-Hans' || language === 'zh';
587
587
  textParts.push(isChinese ? '触发/未确认' : 'Triggered/Unconfirmed');
588
588
  }
589
- // 创建文本容器
590
- const textContainer = document.createElement('div');
591
- textContainer.style.cssText = `
592
- font: ${this.fontString};
593
- color: ${this.headerFont.color || '#e33c39'};
594
- white-space: nowrap;
595
- overflow: visible;
596
- `;
597
- // 应用下划线样式
598
- if (this.headerFont.isUnderline) {
599
- textContainer.style.textDecoration = 'underline';
600
- }
601
- // 设置文本内容
602
- textContainer.textContent = textParts.join(' ');
603
- // 使用配置的宽度,而不是 clientWidth(因为DOM可能还没渲染完成)
604
- const containerWidth = this.model.size.width;
605
- // 先设置容器位置到右侧(屏幕外),这样添加元素时不会闪烁
606
- this.currentLeft = containerWidth;
607
- this.allAlarmsContainer.style.left = `${this.currentLeft}px`;
608
- // 添加到DOM以便计算宽度
609
- this.allAlarmsContainer.appendChild(textContainer);
610
- // 使用 setTimeout 确保 DOM 渲染完成后再计算宽度
611
- setTimeout(() => {
612
- // 计算文本宽度
613
- const textWidth = textContainer.offsetWidth;
614
- this.totalWidth = textWidth;
615
- this.pageWidths.push(textWidth);
616
- // 启动滚动(autoCycle 只控制是否循环,不控制是否滚动)
617
- this.initStaticScrolling();
618
- }, 50);
589
+ // 创建 SVG text 节点(与真实运行模式一致,不依赖 foreignObject)
590
+ const textNode = this._createSvgTextNode(textParts.join(' '));
591
+ textNode.setAttribute('x', '0');
592
+ this.currentLeft = this.model.size.width;
593
+ this.allAlarmsContainer.setAttribute('transform', `translate(${this.currentLeft}, 0)`);
594
+ this.allAlarmsContainer.appendChild(textNode);
595
+ // SVG text 宽度可直接通过 getComputedTextLength 同步获取,无需 setTimeout
596
+ const textWidth = textNode.getComputedTextLength
597
+ ? textNode.getComputedTextLength()
598
+ : (textNode.getBBox ? textNode.getBBox().width : 200);
599
+ this.totalWidth = textWidth;
600
+ this.pageWidths.push(textWidth);
601
+ // 启动滚动(autoCycle 只控制是否循环,不控制是否滚动)
602
+ this.initStaticScrolling();
619
603
  }
620
604
  /**
621
605
  * 模拟运行时的静态滚动逻辑
@@ -642,7 +626,7 @@ export class ScrollAlarmElement extends ConditionalDynamicDisplayElement {
642
626
  // 更新位置 - 从右往左滚动
643
627
  this.currentLeft -= scrollStep;
644
628
  // 更新显示位置
645
- this.allAlarmsContainer.style.left = `${this.currentLeft}px`;
629
+ this.allAlarmsContainer.setAttribute('transform', `translate(${this.currentLeft}, 0)`);
646
630
  // 当内容完全滚出容器左侧时(内容右边缘完全移出容器左边缘)
647
631
  // currentLeft + totalWidth 表示内容右边缘的位置
648
632
  // 当这个值 <= -50 时,表示内容完全不可见(不开启自动循环时多滚动50px确保最后一个字完全滚出)
@@ -651,7 +635,7 @@ export class ScrollAlarmElement extends ConditionalDynamicDisplayElement {
651
635
  if (this.autoCycle) {
652
636
  // 自动循环模式:重置到容器右侧(使用配置宽度)
653
637
  this.currentLeft = this.model.size.width;
654
- this.allAlarmsContainer.style.left = `${this.currentLeft}px`;
638
+ this.allAlarmsContainer.setAttribute('transform', `translate(${this.currentLeft}, 0)`);
655
639
  }
656
640
  else {
657
641
  // 非循环模式:停止滚动
@@ -688,7 +672,6 @@ export class ScrollAlarmElement extends ConditionalDynamicDisplayElement {
688
672
  const input = new GetAlarmsArgs(this.alarmNames, this.startTime, this.endTime, queryCount, 0, // skipCount
689
673
  stateFilter, sorting);
690
674
  this.alarmsStore.getHistoryAlarms(input).subscribe(result => {
691
- var _a, _b, _c, _d, _e, _f, _g, _h;
692
675
  if (!result.error && result.items) {
693
676
  const newItems = result.items;
694
677
  // 如果没有数据,清空显示
@@ -701,24 +684,27 @@ export class ScrollAlarmElement extends ConditionalDynamicDisplayElement {
701
684
  this.hasMoreData = true;
702
685
  // 清空DOM
703
686
  if (this.allAlarmsContainer) {
704
- this.allAlarmsContainer.innerHTML = '';
705
- const containerWidth = this.model.size.width;
706
- this.currentLeft = containerWidth;
707
- this.allAlarmsContainer.style.left = `${containerWidth}px`;
687
+ while (this.allAlarmsContainer.firstChild) {
688
+ this.allAlarmsContainer.removeChild(this.allAlarmsContainer.firstChild);
689
+ }
690
+ this.currentLeft = this.model.size.width;
691
+ this.allAlarmsContainer.setAttribute('transform', `translate(${this.currentLeft}, 0)`);
708
692
  }
709
693
  return;
710
694
  }
711
695
  // 如果是首次加载或当前没有数据,直接渲染并启动滚动
712
696
  if (this.displayedItems.length === 0) {
713
697
  this.displayedItems = newItems;
714
- this.allAlarmsContainer.innerHTML = '';
698
+ while (this.allAlarmsContainer.firstChild) {
699
+ this.allAlarmsContainer.removeChild(this.allAlarmsContainer.firstChild);
700
+ }
715
701
  this.totalWidth = 0;
716
702
  this.pageWidths = [];
717
703
  // 设置 hasMoreData 状态,防止滚动时误触发 getAlarmData
718
704
  this.hasMoreData = result.totalCount > newItems.length;
719
705
  // 设置初始位置到容器右侧
720
- this.currentLeft = this.container.clientWidth;
721
- this.allAlarmsContainer.style.left = `${this.currentLeft}px`;
706
+ this.currentLeft = this.model.size.width;
707
+ this.allAlarmsContainer.setAttribute('transform', `translate(${this.currentLeft}, 0)`);
722
708
  this.renderNewPage(newItems);
723
709
  // 启动滚动
724
710
  if (!this.isScrolling) {
@@ -728,71 +714,19 @@ export class ScrollAlarmElement extends ConditionalDynamicDisplayElement {
728
714
  }
729
715
  // 保存当前的滚动位置
730
716
  const currentScrollPosition = this.currentLeft;
731
- // 清空所有DOM元素
732
- this.allAlarmsContainer.innerHTML = '';
733
- // 获取语言设置
734
- const language = ((_c = (_b = (_a = window.abp) === null || _a === void 0 ? void 0 : _a.localization) === null || _b === void 0 ? void 0 : _b.currentLanguage) === null || _c === void 0 ? void 0 : _c.name) || 'zh-Hans';
735
- const isChinese = language === 'zh-Hans' || language === 'zh';
736
- // 重新创建所有新数据的DOM元素
737
- for (let i = 0; i < newItems.length; i++) {
738
- const alarm = newItems[i];
739
- const textContainer = document.createElement('div');
740
- textContainer.style.cssText = `
741
- font: ${this.fontString};
742
- color: ${this.headerFont.color || '#e33c39'};
743
- white-space: nowrap;
744
- overflow: visible;
745
- `;
746
- if (this.headerFont.isUnderline) {
747
- textContainer.style.textDecoration = 'underline';
748
- }
749
- const contentParts = [];
750
- // 显示触发时间
751
- if (((_d = this.model.generalSetting) === null || _d === void 0 ? void 0 : _d.showTriggerTime) && alarm.triggeredTime) {
752
- const formattedTime = moment(alarm.triggeredTime).format('YYYY/MM/DD HH:mm:ss');
753
- contentParts.push(formattedTime);
754
- }
755
- // 显示告警等级
756
- if (((_e = this.model.generalSetting) === null || _e === void 0 ? void 0 : _e.showAlarmLevel) && alarm.alarmLevel !== undefined) {
757
- const levelMap = isChinese ? ['警告', '次要', '主要', '严重'] : ['Warning', 'Minor', 'Major', 'Critical'];
758
- const alarmLevel = levelMap[alarm.alarmLevel] || (isChinese ? '警告' : 'Warning');
759
- contentParts.push(alarmLevel);
760
- }
761
- // 显示告警名称
762
- if (((_f = this.model.generalSetting) === null || _f === void 0 ? void 0 : _f.showAlarmName) && alarm.name) {
763
- contentParts.push(alarm.name);
764
- }
765
- // 显示告警内容
766
- if (((_g = this.model.generalSetting) === null || _g === void 0 ? void 0 : _g.showAlarmContent) && alarm.message) {
767
- contentParts.push(alarm.message);
768
- }
769
- // 显示告警状态
770
- if (((_h = this.model.generalSetting) === null || _h === void 0 ? void 0 : _h.showAlarmState) && alarm.state !== undefined) {
771
- const stateMap = isChinese ? ['触发/未确认', '触发/已确认', '恢复/未确认', '恢复/已确认'] : ['Triggered/Unconfirmed', 'Triggered/Confirmed', 'Restored/Unconfirmed', 'Restored/Confirmed'];
772
- const alarmState = stateMap[alarm.state] || (isChinese ? '触发/未确认' : 'Triggered/Unconfirmed');
773
- contentParts.push(alarmState);
774
- }
775
- textContainer.textContent = contentParts.join(' ');
776
- textContainer.setAttribute('data-index', i.toString());
777
- this.allAlarmsContainer.appendChild(textContainer);
778
- }
779
- // 重新计算totalWidth
780
- const allChildren = Array.from(this.allAlarmsContainer.children);
781
- let newTotalWidth = 0;
782
- allChildren.forEach(child => {
783
- newTotalWidth += child.offsetWidth + 80;
784
- });
785
- if (newTotalWidth > 0) {
786
- newTotalWidth -= 80; // 移除最后一个gap
717
+ // 清空所有SVG text节点,重新用 renderNewPage 渲染
718
+ while (this.allAlarmsContainer.firstChild) {
719
+ this.allAlarmsContainer.removeChild(this.allAlarmsContainer.firstChild);
787
720
  }
721
+ this.totalWidth = 0;
722
+ this.pageWidths = [];
723
+ this.renderNewPage(newItems);
788
724
  // 更新状态
789
725
  this.displayedItems = newItems;
790
- this.totalWidth = newTotalWidth;
791
- this.pageWidths = [newTotalWidth]; // 重置为单页
792
726
  this.hasMoreData = result.totalCount > newItems.length;
793
727
  // 保持滚动位置不变,继续滚动
794
728
  this.currentLeft = currentScrollPosition;
795
- this.allAlarmsContainer.style.left = `${this.currentLeft}px`;
729
+ this.allAlarmsContainer.setAttribute('transform', `translate(${this.currentLeft}, 0)`);
796
730
  }
797
731
  });
798
732
  }
@@ -1 +1 @@
1
- [{"__symbolic":"module","version":4,"metadata":{"ScrollAlarmElement":{"__symbolic":"class","extends":{"__symbolic":"reference","module":"../base/conditional-dynamic-display-element","name":"ConditionalDynamicDisplayElement","line":17,"character":40},"members":{"__ctor__":[{"__symbolic":"constructor","parameters":[{"__symbolic":"error","message":"Could not resolve type","line":52,"character":25,"context":{"typeName":"HTMLElement"}},{"__symbolic":"reference","module":"@angular/core","name":"Injector","line":53,"character":18},{"__symbolic":"reference","module":"../../service","name":"PermissionChecker","line":54,"character":27},{"__symbolic":"reference","module":"../../communication","name":"VariableCommunicator","line":55,"character":30},{"__symbolic":"reference","module":"../../config","name":"VariableStore","line":56,"character":23},{"__symbolic":"reference","module":"../../config","name":"AlarmsStore","line":57,"character":38},{"__symbolic":"reference","name":"string"}]}],"dispose":[{"__symbolic":"method"}],"getAlarmData":[{"__symbolic":"method"}],"initDisplayContainer":[{"__symbolic":"method"}],"renderNewPage":[{"__symbolic":"method"}],"removeOldestPage":[{"__symbolic":"method"}],"scrollContent":[{"__symbolic":"method"}],"initScrolling":[{"__symbolic":"method"}],"pauseScroll":[{"__symbolic":"method"}],"resumeScroll":[{"__symbolic":"method"}],"resetToFirstPage":[{"__symbolic":"method"}],"updateQueryTimeRange":[{"__symbolic":"method"}],"setStatusAsNormal":[{"__symbolic":"method"}],"setStatusAsLoading":[{"__symbolic":"method"}],"renderStatus":[{"__symbolic":"method"}],"clearStatus":[{"__symbolic":"method"}],"renderStaticDisplay":[{"__symbolic":"method"}],"initStaticScrolling":[{"__symbolic":"method"}],"scrollStaticContent":[{"__symbolic":"method"}],"replaceCurrentScrollingContent":[{"__symbolic":"method"}]}}}}]
1
+ [{"__symbolic":"module","version":4,"metadata":{"ScrollAlarmElement":{"__symbolic":"class","extends":{"__symbolic":"reference","module":"../base/conditional-dynamic-display-element","name":"ConditionalDynamicDisplayElement","line":17,"character":40},"members":{"__ctor__":[{"__symbolic":"constructor","parameters":[{"__symbolic":"error","message":"Could not resolve type","line":52,"character":25,"context":{"typeName":"HTMLElement"}},{"__symbolic":"reference","module":"@angular/core","name":"Injector","line":53,"character":18},{"__symbolic":"reference","module":"../../service","name":"PermissionChecker","line":54,"character":27},{"__symbolic":"reference","module":"../../communication","name":"VariableCommunicator","line":55,"character":30},{"__symbolic":"reference","module":"../../config","name":"VariableStore","line":56,"character":23},{"__symbolic":"reference","module":"../../config","name":"AlarmsStore","line":57,"character":38},{"__symbolic":"reference","name":"string"}]}],"dispose":[{"__symbolic":"method"}],"getAlarmData":[{"__symbolic":"method"}],"initDisplayContainer":[{"__symbolic":"method"}],"_buildFontAttrs":[{"__symbolic":"method"}],"_createSvgTextNode":[{"__symbolic":"method"}],"renderNewPage":[{"__symbolic":"method"}],"_rebaseTextNodes":[{"__symbolic":"method"}],"removeOldestPage":[{"__symbolic":"method"}],"scrollContent":[{"__symbolic":"method"}],"initScrolling":[{"__symbolic":"method"}],"resetToFirstPage":[{"__symbolic":"method"}],"updateQueryTimeRange":[{"__symbolic":"method"}],"setStatusAsNormal":[{"__symbolic":"method"}],"setStatusAsLoading":[{"__symbolic":"method"}],"renderStatus":[{"__symbolic":"method"}],"clearStatus":[{"__symbolic":"method"}],"renderStaticDisplay":[{"__symbolic":"method"}],"initStaticScrolling":[{"__symbolic":"method"}],"scrollStaticContent":[{"__symbolic":"method"}],"replaceCurrentScrollingContent":[{"__symbolic":"method"}]}}}}]