@flexem/fc-gui 3.0.0-alpha.18 → 3.0.0-alpha.181

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 (194) hide show
  1. package/CHANGELOG.md +419 -1
  2. package/assets/img/black_first_page.png +0 -0
  3. package/assets/img/black_last_page.png +0 -0
  4. package/assets/img/black_next_page.png +0 -0
  5. package/assets/img/black_previous_page.png +0 -0
  6. package/assets/img/exclamation-outline-circle.png +0 -0
  7. package/bundles/@flexem/fc-gui.umd.js +37413 -32291
  8. package/bundles/@flexem/fc-gui.umd.js.map +1 -1
  9. package/bundles/@flexem/fc-gui.umd.min.js +5 -5
  10. package/bundles/@flexem/fc-gui.umd.min.js.map +1 -1
  11. package/communication/variable/variable-communicator.d.ts +5 -0
  12. package/communication/variable/variable-state-enum.d.ts +1 -0
  13. package/communication/variable/variable-state-enum.js +1 -0
  14. package/communication/variable/variable-state-enum.metadata.json +1 -1
  15. package/communication/variable/variable-value.d.ts +4 -1
  16. package/communication/variable/variable-value.js +4 -1
  17. package/communication/variable/variable-value.metadata.json +1 -1
  18. package/config/alarm/alarm.store.d.ts +6 -0
  19. package/config/alarm/alarm.store.js +0 -0
  20. package/config/alarm/alarm.store.metadata.json +1 -0
  21. package/config/alarm/get-alarms-args.d.ts +12 -0
  22. package/config/alarm/get-alarms-args.js +13 -0
  23. package/config/alarm/get-alarms-args.metadata.json +1 -0
  24. package/config/alarm/index.d.ts +2 -0
  25. package/config/alarm/index.js +1 -0
  26. package/config/alarm/index.metadata.json +1 -0
  27. package/config/config-store.d.ts +2 -0
  28. package/config/gui-feature-config.d.ts +3 -0
  29. package/config/gui-feature-config.js +3 -0
  30. package/config/gui-feature-config.metadata.json +1 -0
  31. package/config/index.d.ts +2 -0
  32. package/config/index.js +2 -0
  33. package/config/index.metadata.json +1 -1
  34. package/config/variable/variable-store.d.ts +1 -0
  35. package/elements/air-quality/air-quality-element.js +3 -4
  36. package/elements/alarm/alarm-element.d.ts +89 -0
  37. package/elements/alarm/alarm-element.js +739 -0
  38. package/elements/alarm/alarm-element.metadata.json +1 -0
  39. package/elements/bar-graph-element.d.ts +10 -2
  40. package/elements/bar-graph-element.js +135 -5
  41. package/elements/bar-graph-element.metadata.json +1 -1
  42. package/elements/base/readable-element.d.ts +6 -1
  43. package/elements/base/readable-element.js +69 -2
  44. package/elements/base/readable-element.metadata.json +1 -1
  45. package/elements/base/state-control-element.d.ts +13 -1
  46. package/elements/base/state-control-element.js +42 -8
  47. package/elements/base/state-control-element.metadata.json +1 -1
  48. package/elements/character-display/character-display-element.d.ts +9 -0
  49. package/elements/character-display/character-display-element.js +18 -2
  50. package/elements/character-display/character-display-element.metadata.json +1 -1
  51. package/elements/datetime-display/datetime-display-element.d.ts +1 -0
  52. package/elements/datetime-display/datetime-display-element.js +10 -2
  53. package/elements/datetime-display/datetime-display-element.metadata.json +1 -1
  54. package/elements/datetime-display/time-zone-select-json.d.ts +8 -0
  55. package/elements/datetime-display/time-zone-select-json.js +558 -0
  56. package/elements/historical-curve/historical-curve.element.d.ts +45 -3
  57. package/elements/historical-curve/historical-curve.element.js +722 -45
  58. package/elements/historical-curve/historical-curve.element.metadata.json +1 -1
  59. package/elements/main-element.d.ts +1 -0
  60. package/elements/main-element.js +71 -9
  61. package/elements/main-element.metadata.json +1 -1
  62. package/elements/meter-element.d.ts +7 -1
  63. package/elements/meter-element.js +76 -7
  64. package/elements/meter-element.metadata.json +1 -1
  65. package/elements/numerical-display/numerical-display-element.d.ts +31 -3
  66. package/elements/numerical-display/numerical-display-element.js +160 -12
  67. package/elements/numerical-display/numerical-display-element.metadata.json +1 -1
  68. package/elements/per-view-variable-communicator.d.ts +4 -0
  69. package/elements/per-view-variable-communicator.js +21 -1
  70. package/elements/per-view-variable-communicator.metadata.json +1 -1
  71. package/elements/ring-graph/ring-graph-element.d.ts +13 -1
  72. package/elements/ring-graph/ring-graph-element.js +164 -3
  73. package/elements/ring-graph/ring-graph-element.metadata.json +1 -1
  74. package/elements/scroll-alarm/scroll-alarm-element.d.ts +79 -0
  75. package/elements/scroll-alarm/scroll-alarm-element.js +845 -0
  76. package/elements/scroll-alarm/scroll-alarm-element.metadata.json +1 -0
  77. package/elements/shared/graph/graph-state-element.d.ts +1 -0
  78. package/elements/shared/graph/graph-state-element.js +30 -1
  79. package/elements/shared/graph/graph-state-element.metadata.json +1 -1
  80. package/elements/shared/text/text-element.d.ts +14 -0
  81. package/elements/shared/text/text-element.js +42 -4
  82. package/elements/shared/text/text-element.metadata.json +1 -1
  83. package/elements/shared/text/text-state-element.d.ts +39 -2
  84. package/elements/shared/text/text-state-element.js +242 -67
  85. package/elements/shared/text/text-state-element.metadata.json +1 -1
  86. package/elements/static-elements/hyperlink-element.d.ts +28 -2
  87. package/elements/static-elements/hyperlink-element.js +153 -4
  88. package/elements/static-elements/hyperlink-element.metadata.json +1 -1
  89. package/elements/static-elements/text-element.d.ts +28 -2
  90. package/elements/static-elements/text-element.js +162 -12
  91. package/elements/static-elements/text-element.metadata.json +1 -1
  92. package/elements/switch-indicator-light/bit-indicator-light-operator.d.ts +1 -0
  93. package/elements/switch-indicator-light/bit-indicator-light-operator.js +10 -3
  94. package/elements/switch-indicator-light/bit-indicator-light-operator.metadata.json +1 -1
  95. package/elements/switch-indicator-light/switch-indicator-light-element.d.ts +20 -2
  96. package/elements/switch-indicator-light/switch-indicator-light-element.js +85 -12
  97. package/elements/switch-indicator-light/switch-indicator-light-element.metadata.json +1 -1
  98. package/elements/switch-indicator-light/word-indicator-light-operator.d.ts +1 -0
  99. package/elements/switch-indicator-light/word-indicator-light-operator.js +10 -3
  100. package/elements/switch-indicator-light/word-indicator-light-operator.metadata.json +1 -1
  101. package/elements/video/video-element.d.ts +10 -0
  102. package/elements/video/video-element.js +139 -21
  103. package/elements/video/video-element.metadata.json +1 -1
  104. package/elements/view-operation/view-operation.element.d.ts +28 -2
  105. package/elements/view-operation/view-operation.element.js +165 -5
  106. package/elements/view-operation/view-operation.element.metadata.json +1 -1
  107. package/elements/weather/weater-element.js +0 -1
  108. package/gui/gui-context.d.ts +25 -2
  109. package/gui/gui-host.d.ts +1 -1
  110. package/gui/gui-view.d.ts +19 -2
  111. package/gui/gui-view.js +147 -12
  112. package/gui/gui-view.metadata.json +1 -1
  113. package/gui/gui.component.d.ts +3 -0
  114. package/gui/gui.component.js +15 -2
  115. package/gui/gui.component.metadata.json +1 -1
  116. package/localization/localization.service.d.ts +11 -0
  117. package/localization/localization.service.js +16 -5
  118. package/localization/localization.service.metadata.json +1 -1
  119. package/localization/localization.service.zh_CN.js +12 -1
  120. package/localization/localization.service.zh_CN.metadata.json +1 -1
  121. package/modal/write-character/write-character-modal.component.d.ts +3 -2
  122. package/modal/write-character/write-character-modal.component.html +4 -4
  123. package/modal/write-character/write-character-modal.component.js +7 -5
  124. package/modal/write-character/write-character-modal.component.less +15 -0
  125. package/modal/write-character/write-character-modal.component.metadata.json +1 -1
  126. package/modal/write-value/write-value-modal-args.d.ts +5 -1
  127. package/modal/write-value/write-value-modal-args.js +3 -1
  128. package/modal/write-value/write-value-modal-args.metadata.json +1 -1
  129. package/modal/write-value/write-value-modal.component.d.ts +15 -8
  130. package/modal/write-value/write-value-modal.component.html +10 -5
  131. package/modal/write-value/write-value-modal.component.js +99 -21
  132. package/modal/write-value/write-value-modal.component.less +16 -1
  133. package/modal/write-value/write-value-modal.component.metadata.json +1 -1
  134. package/model/alarm/alarm.model.d.ts +14 -0
  135. package/model/alarm/alarm.model.js +0 -0
  136. package/model/alarm/alarm.model.metadata.json +1 -0
  137. package/model/bar-graph/bar-graph.d.ts +4 -0
  138. package/model/base/font-setting-model.d.ts +22 -1
  139. package/model/base/font-setting-model.metadata.json +1 -1
  140. package/model/base/readable-model.d.ts +4 -0
  141. package/model/character-display/character-display.d.ts +1 -0
  142. package/model/datetime-display/datetime-display.d.ts +1 -0
  143. package/model/historical-curve/historical-curve-axis-settings.d.ts +11 -0
  144. package/model/historical-curve/historical-curve-axis-settings.js +5 -0
  145. package/model/historical-curve/historical-curve-axis-settings.metadata.json +1 -1
  146. package/model/historical-curve/historical-curve-chanel.model.d.ts +8 -0
  147. package/model/historical-curve/historical-curve.display-settings.d.ts +1 -0
  148. package/model/meter/meter.d.ts +4 -0
  149. package/model/numerical-display/numerical-display.d.ts +1 -0
  150. package/model/numerical-display/numerical-operation.d.ts +10 -2
  151. package/model/ring-graph/ring-graph.model.d.ts +8 -0
  152. package/model/scroll-alarm/scroll-alarm.model.d.ts +21 -0
  153. package/model/scroll-alarm/scroll-alarm.model.js +0 -0
  154. package/model/scroll-alarm/scroll-alarm.model.metadata.json +1 -0
  155. package/model/settings/popup/view-popup-location-type.d.ts +2 -1
  156. package/model/settings/popup/view-popup-location-type.js +1 -0
  157. package/model/settings/popup/view-popup-location-type.metadata.json +1 -1
  158. package/model/shared/state/state.d.ts +1 -0
  159. package/model/shared/state/state.js +1 -0
  160. package/model/shared/state/state.metadata.json +1 -1
  161. package/model/shared/text/font.d.ts +1 -1
  162. package/model/shared/text/text.d.ts +3 -0
  163. package/model/switch-indicator-light/switch-indicator-light.d.ts +3 -0
  164. package/model/view-operation/view-operation-element.model.d.ts +7 -1
  165. package/package.json +1 -1
  166. package/public_api.js +1 -0
  167. package/remote/communication/variable/remote-variable-communicator.d.ts +30 -0
  168. package/remote/communication/variable/remote-variable-communicator.js +193 -3
  169. package/remote/communication/variable/remote-variable-communicator.metadata.json +1 -1
  170. package/remote/communication/variable/remote-variable-protocol.d.ts +8 -0
  171. package/service/index.d.ts +4 -0
  172. package/service/index.js +1 -0
  173. package/service/index.metadata.json +1 -1
  174. package/service/language.service.d.ts +37 -0
  175. package/service/language.service.js +0 -0
  176. package/service/language.service.metadata.json +1 -0
  177. package/service/released-variable/index.d.ts +1 -0
  178. package/service/released-variable/index.js +0 -0
  179. package/service/released-variable/index.metadata.json +1 -0
  180. package/service/released-variable/released-variable.service.d.ts +4 -0
  181. package/service/released-variable/released-variable.service.js +0 -0
  182. package/service/released-variable/released-variable.service.metadata.json +1 -0
  183. package/service/system-text-library.service.d.ts +77 -0
  184. package/service/system-text-library.service.js +29 -0
  185. package/service/system-text-library.service.metadata.json +1 -0
  186. package/service/text-library.service.d.ts +49 -0
  187. package/service/text-library.service.js +0 -0
  188. package/service/text-library.service.metadata.json +1 -0
  189. package/shared/gui-consts.d.ts +2 -0
  190. package/shared/gui-consts.js +3 -1
  191. package/shared/gui-consts.metadata.json +1 -1
  192. package/utils/data-type/fbox-data-type.service.js +40 -0
  193. package/utils/numerical-operation.service.js +28 -4
  194. package/view/popup-view.service.d.ts +1 -1
@@ -5,15 +5,21 @@ import * as nv from 'nvd3';
5
5
  import { GetHistoryDataArgs } from '../../config';
6
6
  import { HistoricalCurveTimeRange } from '../../config/history-data/historical-curve.time-range';
7
7
  import { LOCALIZATION } from '../../localization';
8
+ import { SYSTEM_TEXT_LIBRARY_TYPES, TIME_PERIOD_KEYS } from '../../service';
8
9
  import { ConditionalDisplayElement } from '../base/conditional-display-element';
9
10
  import { HistoricalCurveElementStatus } from './historical-curve-element-status';
10
11
  import { LOGGER_SERVICE_TOKEN } from '../../logger';
11
12
  import { GlobalSettings, DisplayMode } from '../../settings';
12
13
  import { CurveType } from '../../model/historical-curve/curve-type';
14
+ import { AxisRangeType } from '../../model/historical-curve/historical-curve-axis-settings';
13
15
  export class HistoricalCurveElement extends ConditionalDisplayElement {
14
- constructor(element, injector, permissionChecker, variableCommunicator, variableStore, historyDataStore, signalRAppId) {
16
+ constructor(element, injector, permissionChecker, variableCommunicator, variableStore, historyDataStore, signalRAppId, systemTextLibraryService, languageService, guiContext, releasedVariableService) {
15
17
  super(element, permissionChecker, variableCommunicator, variableStore, signalRAppId);
16
18
  this.historyDataStore = historyDataStore;
19
+ this.systemTextLibraryService = systemTextLibraryService;
20
+ this.languageService = languageService;
21
+ this.guiContext = guiContext;
22
+ this.releasedVariableService = releasedVariableService;
17
23
  this.displayOption = {
18
24
  dataLimit: 500,
19
25
  dataZoomHeight: 32,
@@ -21,16 +27,28 @@ export class HistoricalCurveElement extends ConditionalDisplayElement {
21
27
  marginRight: 20,
22
28
  mobileMinWidth: 450,
23
29
  operationAreaHeight: 32,
24
- operationAreaMarginTop: 10,
30
+ operationAreaMarginTop: 25,
25
31
  operationSelectFontSize: '16px',
26
32
  operationButtonWidth: 24,
27
33
  operationButtonHeight: 24,
28
34
  operationButtonMargin: 4
29
35
  };
36
+ this.customStartTime = null;
37
+ this.customEndTime = null;
30
38
  this.elementStatus = HistoricalCurveElementStatus.Loading;
39
+ this.data = [];
40
+ this.needResize = true;
41
+ this.dropdownListEl = null;
42
+ this.dropdownTriggerEl = null;
43
+ this._outsideClickHandler = null;
44
+ this._fullscreenChangeHandler = null;
45
+ this._resizeHideHandler = null;
46
+ this.setNeedResize = () => {
47
+ this.needResize = false;
48
+ setTimeout(() => this.needResize = true, 500);
49
+ };
31
50
  this.logger = injector.get(LOGGER_SERVICE_TOKEN);
32
51
  this.localization = injector.get(LOCALIZATION);
33
- this.timePeriods = this.getValidTimePeriods();
34
52
  this.updateTimeRange(this.model.displaySetting.displayTimePeriod);
35
53
  this.refreshIntervalId = setInterval(() => {
36
54
  this.loadFirstPage();
@@ -38,7 +56,7 @@ export class HistoricalCurveElement extends ConditionalDisplayElement {
38
56
  }, this.model.displaySetting.refreshInterval * 1000);
39
57
  this.isMobileMode = DisplayMode.Mobile === injector.get(GlobalSettings).displayMode;
40
58
  if (this.isMobileMode) {
41
- this.displayOption.operationAreaMarginTop = 20;
59
+ this.displayOption.operationAreaMarginTop = 35;
42
60
  if (this.model.displaySetting.size.width >= this.displayOption.mobileMinWidth) {
43
61
  this.displayOption.operationAreaHeight = 68;
44
62
  this.displayOption.operationSelectFontSize = '24px';
@@ -47,29 +65,196 @@ export class HistoricalCurveElement extends ConditionalDisplayElement {
47
65
  this.displayOption.operationButtonMargin = 6;
48
66
  }
49
67
  }
68
+ // 初始化时间段数据(此时文本库可能未加载,先填充空文本占位)
69
+ this.timePeriods = this.getValidTimePeriods();
50
70
  this.loadFirstPage();
71
+ this.initKeyboardListener();
72
+ // 订阅语种变化事件 和 文本库缓存更新事件
73
+ this.subscribeLanguageChange();
74
+ }
75
+ getIconScale() {
76
+ const initSize = this.model.displaySetting.initSize;
77
+ const size = this.model.displaySetting.size;
78
+ if (!initSize || !initSize.width || !initSize.height) {
79
+ return 1;
80
+ }
81
+ const scaleX = size.width / initSize.width;
82
+ const scaleY = size.height / initSize.height;
83
+ return Math.min(scaleX, scaleY);
51
84
  }
52
85
  dispose() {
53
86
  clearInterval(this.refreshIntervalId);
54
87
  if (this.chartElement) {
55
88
  this.chartElement.tooltip.hidden(true);
56
89
  }
90
+ if (this.resizeEventListener) {
91
+ this.resizeEventListener.clear();
92
+ }
93
+ if (this.isAndroid) {
94
+ window.removeEventListener('native.keyboardshow', this.setNeedResize);
95
+ window.removeEventListener('native.keyboardhide', this.setNeedResize);
96
+ }
97
+ // 清理下拉相关事件监听及 DOM
98
+ if (this._outsideClickHandler) {
99
+ document.removeEventListener('click', this._outsideClickHandler);
100
+ this._outsideClickHandler = null;
101
+ }
102
+ if (this._fullscreenChangeHandler) {
103
+ document.removeEventListener('fullscreenchange', this._fullscreenChangeHandler);
104
+ document.removeEventListener('webkitfullscreenchange', this._fullscreenChangeHandler);
105
+ this._fullscreenChangeHandler = null;
106
+ }
107
+ if (this._resizeHideHandler) {
108
+ window.removeEventListener('resize', this._resizeHideHandler);
109
+ this._resizeHideHandler = null;
110
+ }
111
+ if (this.dropdownListEl && this.dropdownListEl.parentNode) {
112
+ this.dropdownListEl.parentNode.removeChild(this.dropdownListEl);
113
+ this.dropdownListEl = null;
114
+ }
115
+ // 取消语种变化订阅
116
+ if (this.languageChangeSubscription) {
117
+ this.languageChangeSubscription.unsubscribe();
118
+ this.languageChangeSubscription = undefined;
119
+ }
57
120
  this.logger.debug(`[GUI]Dispose Histoical Curve Refresh Interval:${d3.time.format('%x %X')(new Date())}`);
58
121
  }
122
+ initKeyboardListener() {
123
+ this.isAndroid = !!navigator.userAgent.match(/(Android)/i);
124
+ if (this.isAndroid) {
125
+ window.addEventListener('native.keyboardshow', this.setNeedResize);
126
+ window.addEventListener('native.keyboardhide', this.setNeedResize);
127
+ }
128
+ }
129
+ /**
130
+ * 订阅语种变化事件
131
+ */
132
+ subscribeLanguageChange() {
133
+ var _a;
134
+ if (this.guiContext && this.guiContext.languageChanged$) {
135
+ this.languageChangeSubscription = this.guiContext.languageChanged$.subscribe(() => {
136
+ // 只更新时间段选择器的文案,不重新查询数据
137
+ this.updateLanguageTexts();
138
+ });
139
+ }
140
+ // 订阅文本库缓存更新事件,解决首次加载时文本库未就绪导致下拉选项为空的问题
141
+ if ((_a = this.systemTextLibraryService) === null || _a === void 0 ? void 0 : _a.cacheUpdated) {
142
+ const cacheSubscription = this.systemTextLibraryService.cacheUpdated.subscribe(() => {
143
+ this.updateLanguageTexts();
144
+ });
145
+ if (this.languageChangeSubscription) {
146
+ this.languageChangeSubscription.add(cacheSubscription);
147
+ }
148
+ else {
149
+ this.languageChangeSubscription = cacheSubscription;
150
+ }
151
+ }
152
+ }
153
+ /**
154
+ * 更新语种相关的文案(时间段选择器)
155
+ */
156
+ updateLanguageTexts() {
157
+ const updatedTimePeriods = this.getValidTimePeriods();
158
+ this.timePeriods = updatedTimePeriods;
159
+ // 更新下拉列表各选项文字
160
+ if (this.dropdownListEl) {
161
+ const items = this.dropdownListEl.querySelectorAll('.hc-dropdown-item');
162
+ items.forEach((item, i) => {
163
+ if (i < updatedTimePeriods.length) {
164
+ item.textContent = updatedTimePeriods[i].name;
165
+ }
166
+ });
167
+ // 文字更新后重新测量宽度,修正首次加载时文字为空导致宽度过小的问题
168
+ const prevDisplay = this.dropdownListEl.style.display;
169
+ const prevVisibility = this.dropdownListEl.style.visibility;
170
+ this.dropdownListEl.style.display = 'block';
171
+ this.dropdownListEl.style.visibility = 'hidden';
172
+ this.dropdownListEl.style.width = '';
173
+ const newWidth = this.dropdownListEl.offsetWidth;
174
+ this.dropdownListEl.style.display = prevDisplay;
175
+ this.dropdownListEl.style.visibility = prevVisibility;
176
+ this.dropdownListEl.style.width = newWidth + 'px';
177
+ if (this.dropdownTriggerEl) {
178
+ this.dropdownTriggerEl.style.width = newWidth + 'px';
179
+ }
180
+ }
181
+ // 更新触发按钮文字(显示当前选中项的最新语言文案)
182
+ if (this.dropdownTriggerEl) {
183
+ const selected = updatedTimePeriods.find(p => p.key === Number(this.currentTimePeriod));
184
+ if (selected) {
185
+ const textNode = Array.from(this.dropdownTriggerEl.childNodes).find(n => n.nodeType === Node.TEXT_NODE);
186
+ if (textNode) {
187
+ textNode.textContent = selected.name;
188
+ }
189
+ else {
190
+ this.dropdownTriggerEl.insertBefore(document.createTextNode(selected.name), this.dropdownTriggerEl.firstChild);
191
+ }
192
+ }
193
+ }
194
+ }
195
+ /**
196
+ * 获取当前语种的 culture 代码
197
+ */
198
+ getCurrentCulture() {
199
+ var _a, _b, _c, _d, _e, _f;
200
+ // 获取当前语种ID
201
+ const currentLanguageId = (_c = (_b = (_a = this.guiContext) === null || _a === void 0 ? void 0 : _a.getCurrentLanguageId) === null || _b === void 0 ? void 0 : _b.call(_a)) !== null && _c !== void 0 ? _c : null;
202
+ // 确定要使用的语种代码(culture)
203
+ const defaultLanguage = ((_d = this.languageService) === null || _d === void 0 ? void 0 : _d.getDefaultLanguage()) || 'zh-CN';
204
+ if (currentLanguageId === null || currentLanguageId === undefined) {
205
+ // 设备未设置当前语种,使用默认语种
206
+ return defaultLanguage;
207
+ }
208
+ else {
209
+ // 设备已设置当前语种,获取对应的语种代码
210
+ const currentLanguage = (_f = (_e = this.guiContext) === null || _e === void 0 ? void 0 : _e.getLanguageCultureById) === null || _f === void 0 ? void 0 : _f.call(_e, currentLanguageId);
211
+ if (currentLanguage) {
212
+ return currentLanguage;
213
+ }
214
+ else {
215
+ // 无法获取语种代码,使用默认语种
216
+ return defaultLanguage;
217
+ }
218
+ }
219
+ }
59
220
  getValidTimePeriods() {
60
221
  const timePeriods = new Array();
61
- timePeriods.push({ key: 1, name: this.localization.lastOneHour });
62
- timePeriods.push({ key: 2, name: this.localization.lastTwentyFourHours });
63
- timePeriods.push({ key: 3, name: this.localization.lastSevenDays });
64
- timePeriods.push({ key: 4, name: this.localization.lastThirtyDays });
65
- timePeriods.push({ key: 5, name: this.localization.lastOneYear });
222
+ timePeriods.push({ key: 6, name: this.getTimePeriodText(TIME_PERIOD_KEYS.LAST_THIRTY_MINUTES) });
223
+ timePeriods.push({ key: 1, name: this.getTimePeriodText(TIME_PERIOD_KEYS.LAST_ONE_HOUR) });
224
+ timePeriods.push({ key: 7, name: this.getTimePeriodText(TIME_PERIOD_KEYS.LAST_EIGHT_HOURS) });
225
+ timePeriods.push({ key: 2, name: this.getTimePeriodText(TIME_PERIOD_KEYS.LAST_TWENTY_FOUR_HOURS) });
226
+ timePeriods.push({ key: 3, name: this.getTimePeriodText(TIME_PERIOD_KEYS.LAST_SEVEN_DAYS) });
227
+ timePeriods.push({ key: 4, name: this.getTimePeriodText(TIME_PERIOD_KEYS.LAST_THIRTY_DAYS) });
228
+ timePeriods.push({ key: 5, name: this.getTimePeriodText(TIME_PERIOD_KEYS.LAST_ONE_YEAR) });
229
+ timePeriods.push({ key: 8, name: this.getTimePeriodText(TIME_PERIOD_KEYS.CUSTOM) });
66
230
  return timePeriods;
67
231
  }
232
+ /**
233
+ * 获取时间段文案(从系统文本库获取多语种翻译)
234
+ */
235
+ getTimePeriodText(textKey) {
236
+ const currentCulture = this.getCurrentCulture();
237
+ const systemType = SYSTEM_TEXT_LIBRARY_TYPES.COMPONENT_BUILTIN;
238
+ if (this.systemTextLibraryService) {
239
+ const translation = this.systemTextLibraryService.getSystemTextValue(systemType, textKey, currentCulture);
240
+ if (translation) {
241
+ return translation;
242
+ }
243
+ }
244
+ return '';
245
+ }
68
246
  updateTimeRange(timePeriodType) {
69
247
  this.currentTimePeriod = +timePeriodType;
70
248
  this.updateQueryTimeRange();
71
249
  }
72
250
  updateQueryTimeRange() {
251
+ if (this.currentTimePeriod === 8) {
252
+ if (this.customStartTime && this.customEndTime) {
253
+ this.startTime = this.customStartTime.clone();
254
+ this.endTime = this.customEndTime.clone();
255
+ }
256
+ return;
257
+ }
73
258
  this.endTime = moment();
74
259
  switch (this.currentTimePeriod) {
75
260
  case 1:
@@ -84,13 +269,29 @@ export class HistoricalCurveElement extends ConditionalDisplayElement {
84
269
  case 5:
85
270
  this.startTime = moment().subtract(1, 'years');
86
271
  break;
272
+ case 6:
273
+ this.startTime = moment().subtract(30, 'minutes');
274
+ break;
275
+ case 7:
276
+ this.startTime = moment().subtract(8, 'hours');
277
+ break;
87
278
  default:
88
279
  this.startTime = moment().subtract(1, 'days');
89
280
  }
90
281
  }
91
282
  reRenderElement(startTime, endTime, limit, rangeType) {
283
+ // 清理图表实例
284
+ if (this.chartElement) {
285
+ // 隐藏 tooltip
286
+ if (this.chartElement.tooltip) {
287
+ this.chartElement.tooltip.hidden(true);
288
+ }
289
+ // 清理图表引用
290
+ this.chartElement = null;
291
+ }
292
+ // 清除 DOM 元素
92
293
  this.rootElement.selectAll('*').remove();
93
- this.chartElement = null;
294
+ // 重新渲染
94
295
  this.renderElement(startTime, endTime, limit, rangeType);
95
296
  }
96
297
  renderElement(startTime, endTime, limit, rangeType) {
@@ -122,28 +323,103 @@ export class HistoricalCurveElement extends ConditionalDisplayElement {
122
323
  }
123
324
  });
124
325
  }
326
+ setupTooltipAutoHide(chart) {
327
+ const chartContainer = this.rootElement.select('.nv-focus').node();
328
+ if (!chartContainer || !chart)
329
+ return;
330
+ let timeoutId;
331
+ // 鼠标移入图表时显示 tooltip
332
+ chartContainer.addEventListener('mouseover', () => {
333
+ hideTooltipAfterDelay();
334
+ });
335
+ const clearTooltipTimeout = () => {
336
+ if (timeoutId) {
337
+ clearTimeout(timeoutId);
338
+ timeoutId = null;
339
+ }
340
+ };
341
+ const hideTooltipAfterDelay = () => {
342
+ clearTooltipTimeout();
343
+ timeoutId = setTimeout(() => {
344
+ chart.tooltip.hidden(true);
345
+ }, 2000); // 2秒延迟
346
+ };
347
+ }
125
348
  renderChart(result) {
126
349
  const chartWidth = this.model.displaySetting.size.width;
127
- const chartHeight = this.model.displaySetting.size.height - this.displayOption.operationAreaHeight - this.displayOption.operationAreaMarginTop;
350
+ const scale = this.getIconScale();
351
+ // 图表主体与底部操作区之间的预留高度,需要与操作区渲染时使用的同一个 scale 保持一致,
352
+ // 否则图表按未缩放高度占满后,缩放后的操作区会超出元件整体高度
353
+ const chartHeight = this.model.displaySetting.size.height - (this.displayOption.operationAreaHeight + this.displayOption.operationAreaMarginTop) * scale;
128
354
  const data = new Array();
129
355
  each(this.model.dataSetting.channels, (channel, key) => {
130
356
  const values = new Array();
131
357
  each(result, v => values.push({ x: moment(v.time).local().toDate().valueOf(), y: v.values[key] }));
132
358
  data.push({ key: channel.name, area: channel.projectEnabled, values: values });
133
359
  });
134
- nv.addGraph(() => {
135
- if (this.model.displaySetting.curveType === CurveType.BarGroup || this.model.displaySetting.curveType === CurveType.BarStack) {
136
- return this.getMultiBarWithFocusChart(chartWidth, chartHeight, data);
137
- }
138
- else {
139
- return this.getLineChart(chartWidth, chartHeight, data);
360
+ this.data = data;
361
+ let chart;
362
+ if (this.model.displaySetting.curveType === CurveType.BarGroup || this.model.displaySetting.curveType === CurveType.BarStack) {
363
+ chart = this.getMultiBarWithFocusChart(chartWidth, chartHeight, data, scale);
364
+ }
365
+ else {
366
+ chart = this.getLineChart(chartWidth, chartHeight, data, scale);
367
+ }
368
+ // 设置 tooltip 自动隐藏逻辑
369
+ this.setupTooltipAutoHide(chart);
370
+ return chart;
371
+ }
372
+ initPoint() {
373
+ try {
374
+ const legendList = this.$element
375
+ .find('.nv-legend')
376
+ .find('.nv-series');
377
+ let hiddenCount = 0;
378
+ for (let i = 0; i < this.data.length; i++) {
379
+ const channel = this.model.dataSetting.channels[i];
380
+ if (legendList.eq(i).children().eq(0).css('fill-opacity') === '1') {
381
+ const pointList = this.$element
382
+ .find('.nv-scatterWrap')
383
+ .find('.nv-series-' + (i - hiddenCount))
384
+ .find('.nv-point');
385
+ if (pointList && pointList.length) {
386
+ for (let j = 0; j < pointList.length; j++) {
387
+ const point = pointList.eq(j);
388
+ const previousPoint = pointList.eq(j - 1);
389
+ if (j && point.attr('transform').split(',')[1] !== previousPoint.attr('transform').split(',')[1]) {
390
+ if (channel.enablePoint && channel.pointColor) {
391
+ const pointStyle = {
392
+ 'stroke-opacity': 1,
393
+ 'stroke-width': '2px',
394
+ 'stroke': channel.pointColor,
395
+ 'fill-opacity': 1,
396
+ 'fill': channel.pointColor
397
+ };
398
+ point.addClass('nv-mark-point');
399
+ point.css(pointStyle);
400
+ previousPoint.addClass('nv-mark-point');
401
+ previousPoint.css(pointStyle);
402
+ }
403
+ }
404
+ }
405
+ }
406
+ }
407
+ else {
408
+ hiddenCount++;
409
+ }
140
410
  }
141
- });
411
+ }
412
+ catch (e) {
413
+ console.log(e);
414
+ }
142
415
  }
143
- getLineChart(chartWidth, chartHeight, data) {
416
+ getLineChart(chartWidth, chartHeight, data, scale = 1) {
144
417
  const chart = nv.models.lineChart().showLegend(true)
145
418
  .margin({ top: 0, bottom: 0, left: this.displayOption.marginLeft, right: this.displayOption.marginRight })
146
419
  .noData(this.localization.chartNoData);
420
+ if (this.model.displaySetting.axisSetting.yAxisRangeType === AxisRangeType.Custom) {
421
+ chart.yDomain([this.model.displaySetting.axisSetting.yAxisMin, this.model.displaySetting.axisSetting.yAxisMax]);
422
+ }
147
423
  if (!this.isMobileMode) {
148
424
  chart.focusEnable(true);
149
425
  chart.focus.margin({ top: 10, right: 0, bottom: 0, left: 0 });
@@ -151,10 +427,23 @@ export class HistoricalCurveElement extends ConditionalDisplayElement {
151
427
  chart.focus.showXAxis(false);
152
428
  }
153
429
  this.renderCommonProperty(chart, chartWidth, chartHeight, data);
154
- this.renderOperationArea(chartWidth, chartHeight);
430
+ this.renderOperationArea(chartWidth, chartHeight, scale);
431
+ this.initPoint();
432
+ chart.legend.dispatch.on('legendClick', () => {
433
+ setTimeout(() => {
434
+ this.$element.find('.nv-mark-point').css({
435
+ 'stroke-opacity': 0,
436
+ 'stroke-width': 0,
437
+ 'stroke': 'unset',
438
+ 'fill-opacity': 0,
439
+ 'fill': 'unset'
440
+ }).removeClass('nv-mark-point');
441
+ this.initPoint();
442
+ }, 1);
443
+ });
155
444
  return chart;
156
445
  }
157
- getMultiBarWithFocusChart(chartWidth, chartHeight, data) {
446
+ getMultiBarWithFocusChart(chartWidth, chartHeight, data, scale = 1) {
158
447
  const chart = nv.models.multiBarWithFocusChart()
159
448
  .margin({
160
449
  top: 0, bottom: 0, left: this.displayOption.marginLeft, right: this.displayOption.marginRight
@@ -168,13 +457,16 @@ export class HistoricalCurveElement extends ConditionalDisplayElement {
168
457
  chart.multibar.stacked(true);
169
458
  chart.multibar2.stacked(true);
170
459
  }
460
+ if (this.model.displaySetting.axisSetting.yAxisRangeType === AxisRangeType.Custom) {
461
+ chart.yDomain([this.model.displaySetting.axisSetting.yAxisMin, this.model.displaySetting.axisSetting.yAxisMax]);
462
+ }
171
463
  if (!this.isMobileMode) {
172
464
  chart.focusEnable(true);
173
465
  chart.focusShowAxisX(false);
174
466
  }
175
467
  chart.xAxis.tickFormat(d => this.timeFormat(d, '%x %X'));
176
468
  this.renderCommonProperty(chart, chartWidth, chartHeight, data);
177
- this.renderOperationArea(chartWidth, chartHeight);
469
+ this.renderOperationArea(chartWidth, chartHeight, scale);
178
470
  const focusContext = this.rootElement.select('.nv-context');
179
471
  focusContext.select('.nv-barsWrap.nvd3-svg').attr('transform', 'translate(0,30)');
180
472
  focusContext.select('.nv-brushBackground').attr('transform', 'translate(0,30)');
@@ -188,9 +480,27 @@ export class HistoricalCurveElement extends ConditionalDisplayElement {
188
480
  return chart;
189
481
  }
190
482
  renderCommonProperty(chart, chartWidth, chartHeight, data) {
483
+ var _a, _b;
191
484
  chart.tooltip.headerFormatter(d => this.timeFormat(d, '%x %X'));
192
485
  if (this.model.displaySetting.showAxis) {
193
- chart.xAxis.showMaxMin(true).tickFormat(d => this.timeFormat(d, '%X'));
486
+ chart.xAxis.showMaxMin(true).tickFormat(d => {
487
+ this.$element.find('.nv-mark-point').css({
488
+ 'stroke-opacity': 0,
489
+ 'stroke-width': 0,
490
+ 'stroke': 'unset',
491
+ 'fill-opacity': 0,
492
+ 'fill': 'unset'
493
+ }).removeClass('nv-mark-point');
494
+ clearTimeout(this.timer);
495
+ this.timer = undefined;
496
+ this.timer = setTimeout(() => {
497
+ this.initPoint();
498
+ }, 100);
499
+ if (this.currentTimePeriod === 3 || this.currentTimePeriod === 4 || this.currentTimePeriod === 5) {
500
+ return this.timeFormat(d, '%y-%m-%d');
501
+ }
502
+ return this.timeFormat(Number(d), '%X');
503
+ });
194
504
  if (this.model.displaySetting.axisSetting) {
195
505
  if (this.model.displaySetting.axisSetting.showAxisLabel && this.model.displaySetting.axisSetting.axisLabelFont) {
196
506
  chart.xAxis.fontSize(this.model.displaySetting.axisSetting.axisLabelFont.fontSize);
@@ -211,52 +521,419 @@ export class HistoricalCurveElement extends ConditionalDisplayElement {
211
521
  chart.color(this.model.dataSetting.channels.map(c => c.connectorColor));
212
522
  this.rootElement.append('g').datum(data).call(chart);
213
523
  this.rootElement.selectAll('.nv-noData').attr('x', chartWidth / 2).attr('y', chartHeight / 2 + this.displayOption.operationAreaHeight);
214
- nv.utils.windowResize(() => {
524
+ this.resizeEventListener = nv.utils.windowResize(() => {
525
+ if (!this.needResize)
526
+ return;
215
527
  chart.update();
216
528
  this.rootElement.selectAll('.nv-noData').attr('x', chartWidth / 2).attr('y', chartHeight / 2 + this.displayOption.operationAreaHeight);
217
529
  });
530
+ const fontSize = (_b = (_a = this.model.displaySetting.axisSetting) === null || _a === void 0 ? void 0 : _a.axisLabelFont) === null || _b === void 0 ? void 0 : _b.fontSize;
218
531
  this.rootElement.selectAll('.domain').style('stroke-opacity', 1);
219
532
  if (this.model.displaySetting.showAxis && this.model.displaySetting.axisSetting) {
220
533
  const axisColor = this.model.displaySetting.axisSetting.axisColor;
221
534
  this.rootElement.selectAll('.domain').style('stroke', axisColor);
222
- if (this.model.displaySetting.axisSetting.showAxisLabel) {
223
- const fontSize = this.model.displaySetting.axisSetting.axisLabelFont.fontSize;
535
+ if (fontSize && this.model.displaySetting.axisSetting.showAxisLabel) {
224
536
  this.rootElement.selectAll('.nv-axisMaxMin').select('text').style('font-size', fontSize);
225
537
  }
226
538
  }
539
+ let strokeWidth = 0;
540
+ if (this.model.displaySetting.axisSetting.showTick !== false) {
541
+ strokeWidth = 1;
542
+ }
543
+ this.rootElement
544
+ .selectAll('.nv-x')
545
+ .selectAll('.tick')
546
+ .selectAll('line')
547
+ .attr('style', `stroke:${this.model.displaySetting.axisSetting.xAxisTickColor || 'rgb(127, 147, 159)'};stroke-width:${strokeWidth};`);
548
+ this.rootElement
549
+ .selectAll('.nv-y')
550
+ .selectAll('.tick')
551
+ .selectAll('line')
552
+ .attr('style', `stroke:${this.model.displaySetting.axisSetting.yAxisTickColor || 'rgb(127, 147, 159)'};stroke-width:${strokeWidth};`);
553
+ if (fontSize && this.currentTimePeriod === 3 || this.currentTimePeriod === 4 || this.currentTimePeriod === 5) {
554
+ const self = this;
555
+ this.rootElement
556
+ .selectAll('.nv-x')
557
+ .selectAll('.tick')
558
+ .selectAll('text')
559
+ .data(function (d) {
560
+ return [self.timeFormat(Number(d), '%y-%m-%d'), self.timeFormat(Number(d), '%H:%M:%S')];
561
+ })
562
+ .enter()
563
+ .append('text')
564
+ .attr('class', 'full-date')
565
+ .attr('x', 0)
566
+ .attr('y', 0)
567
+ .attr('dy', '2.3em')
568
+ .style('text-anchor', 'middle')
569
+ .style('font-size', fontSize)
570
+ .text((d) => d);
571
+ this.rootElement
572
+ .selectAll('.nv-axisMaxMin-x')
573
+ .selectAll('text')
574
+ .data(function (d) {
575
+ return [self.timeFormat(Number(d), '%y-%m-%d'), self.timeFormat(Number(d), '%H:%M:%S')];
576
+ })
577
+ .enter()
578
+ .append('text')
579
+ .attr('class', 'full-date')
580
+ .attr('x', 0)
581
+ .attr('y', 0)
582
+ .attr('dy', '2.3em')
583
+ .style('text-anchor', 'middle')
584
+ .style('font-size', fontSize)
585
+ .text((d) => d);
586
+ const focusWrap = this.rootElement.selectAll('.nv-focusWrap');
587
+ if (focusWrap.size()) {
588
+ let h = focusWrap.attr('transform');
589
+ if (h && h.length && h.indexOf(',') !== -1) {
590
+ h = h.slice(0, -1).split(',')[1];
591
+ this.rootElement
592
+ .selectAll('.nv-focusWrap')
593
+ .attr('transform', `translate(0,${Number(h) + 15})`);
594
+ }
595
+ }
596
+ const resizeObserver = new window.MutationObserver(() => {
597
+ this.rootElement
598
+ .selectAll('.nv-x')
599
+ .selectAll('.tick')
600
+ .selectAll('.full-date')
601
+ .remove();
602
+ this.rootElement
603
+ .selectAll('.nv-x')
604
+ .selectAll('.tick')
605
+ .selectAll('text')
606
+ .data(function (d) {
607
+ return [self.timeFormat(Number(d), '%y-%m-%d'), self.timeFormat(Number(d), '%H:%M:%S')];
608
+ })
609
+ .enter()
610
+ .append('text')
611
+ .attr('class', 'full-date')
612
+ .attr('x', 0)
613
+ .attr('y', 0)
614
+ .attr('dy', '2.3em')
615
+ .style('text-anchor', 'middle')
616
+ .style('font-size', fontSize)
617
+ .text((d) => d);
618
+ });
619
+ const extent = document.getElementsByClassName('extent');
620
+ if (extent.length) {
621
+ resizeObserver.observe(extent[0], { attributes: true });
622
+ }
623
+ }
624
+ else {
625
+ this.rootElement
626
+ .selectAll('.full-date')
627
+ .remove();
628
+ }
227
629
  }
228
- renderOperationArea(chartWidth, chartHeight) {
229
- const operationArea = this.rootElement.append('g').attr('transform', `translate(0,${chartHeight + this.displayOption.operationAreaMarginTop})`)
230
- .append('foreignObject').attr('width', chartWidth).attr('height', this.displayOption.operationAreaHeight).attr('fill', 'none')
231
- .append('xhtml:div').style('height', (this.displayOption.operationAreaHeight - 4) + 'px').style('overflow', 'hidden').style('margin-top', '4px');
232
- const selectElement = operationArea.append('select').style('margin-left', this.displayOption.marginLeft + 'px')
233
- .style('font-size', this.displayOption.operationSelectFontSize).on('change', () => {
234
- const displayTimePeriod = this.rootElement.select('select').property('value');
235
- this.updateTimeRange(displayTimePeriod);
236
- this.reRenderElement(this.startTime, this.endTime, this.displayOption.dataLimit, HistoricalCurveTimeRange.BeginOpenEndOpen);
630
+ renderOperationArea(chartWidth, chartHeight, scale = 1) {
631
+ const backgroundColor = this.model.displaySetting.axisSetting.filterBackgroudColor || 'inherit';
632
+ const operationArea = this.rootElement.append('g').attr('transform', `translate(0,${chartHeight + this.displayOption.operationAreaMarginTop * scale})`)
633
+ .append('foreignObject').attr('width', chartWidth).attr('height', this.displayOption.operationAreaHeight * scale).attr('fill', 'none')
634
+ .append('xhtml:div').style('height', (this.displayOption.operationAreaHeight * scale - 4) + 'px').style('overflow', 'hidden').style('margin-top', '4px');
635
+ const rect = this.$element.parent().parent().find('rect');
636
+ const fillColor = rect.attr('fill');
637
+ const dropdownBg = this.model.displaySetting.axisSetting.filterBackgroudColor || fillColor || '#fff';
638
+ const dropdownWrapper = operationArea.append('div')
639
+ .style('display', 'inline-block')
640
+ .style('margin-left', (this.displayOption.marginLeft * scale) + 'px')
641
+ .style('vertical-align', 'middle');
642
+ const selectedText = this.timePeriods.find(t => t.key === Number(this.currentTimePeriod));
643
+ const arrowIconUrl = 'url("data:image/svg+xml,'
644
+ + '%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%228%22 height=%225%22 viewBox=%220 0 8 5%22%3E'
645
+ + '%3Cpath d=%22M0 0L4 5L8 0z%22 fill=%22%23666%22/%3E%3C/svg%3E")';
646
+ const dropdownTrigger = dropdownWrapper.append('div')
647
+ .attr('class', 'hc-dropdown-trigger')
648
+ .style('background-color', backgroundColor)
649
+ .style('background-image', arrowIconUrl)
650
+ .style('background-repeat', 'no-repeat')
651
+ .style('background-position', 'right 4px center')
652
+ .style('font-size', (parseFloat(this.displayOption.operationSelectFontSize) * scale) + 'px')
653
+ .style('cursor', 'pointer')
654
+ .style('padding', '0 18px 0 4px')
655
+ .style('border', '1px solid #ccc')
656
+ .style('border-radius', '2px')
657
+ .style('white-space', 'nowrap')
658
+ .style('line-height', (this.displayOption.operationAreaHeight * scale - 8) + 'px')
659
+ .style('min-height', (this.displayOption.operationAreaHeight * scale - 8) + 'px')
660
+ .text(selectedText ? selectedText.name : '');
661
+ const dropdownListEl = document.createElement('div');
662
+ dropdownListEl.style.cssText = 'display:none;position:fixed;background:' + dropdownBg
663
+ + ';border:1px solid #ccc;border-radius:2px;z-index:9999;box-shadow:0 2px 6px rgba(0,0,0,0.15);';
664
+ document.body.appendChild(dropdownListEl);
665
+ this.dropdownListEl = dropdownListEl;
666
+ this.dropdownTriggerEl = dropdownTrigger.node();
667
+ const dropdownList = d3.select(dropdownListEl);
668
+ this.timePeriods.forEach(tp => {
669
+ dropdownList.append('div')
670
+ .attr('class', 'hc-dropdown-item')
671
+ .style('padding', '4px 8px')
672
+ .style('cursor', 'pointer')
673
+ .style('white-space', 'nowrap')
674
+ .style('font-size', (parseFloat(this.displayOption.operationSelectFontSize) * scale) + 'px')
675
+ .style('line-height', (this.displayOption.operationAreaHeight * scale - 8) + 'px')
676
+ .style('min-height', (this.displayOption.operationAreaHeight * scale - 8) + 'px')
677
+ .style('background', tp.key === Number(this.currentTimePeriod) ? '#1890ff' : 'transparent')
678
+ .style('color', tp.key === Number(this.currentTimePeriod) ? '#fff' : '#333')
679
+ .text(tp.name)
680
+ .on('mouseover', function () {
681
+ const el = d3.select(this);
682
+ if (!el.classed('hc-dropdown-selected')) {
683
+ el.style('background', '#e6f7ff').style('color', '#333');
684
+ }
685
+ })
686
+ .on('mouseout', function () {
687
+ const el = d3.select(this);
688
+ if (!el.classed('hc-dropdown-selected')) {
689
+ el.style('background', 'transparent').style('color', '#333');
690
+ }
691
+ })
692
+ .on('click', () => {
693
+ dropdownListEl.style.display = 'none';
694
+ // 从 this.timePeriods 实时查找,确保语种切换后显示最新文案
695
+ const currentPeriod = this.timePeriods.find(p => p.key === tp.key);
696
+ const currentName = currentPeriod ? currentPeriod.name : tp.name;
697
+ dropdownTrigger.text(currentName);
698
+ // 更新选中状态
699
+ dropdownList.selectAll('.hc-dropdown-item')
700
+ .classed('hc-dropdown-selected', false)
701
+ .style('background', 'transparent')
702
+ .style('color', '#333');
703
+ d3.select(d3.event.currentTarget)
704
+ .classed('hc-dropdown-selected', true)
705
+ .style('background', '#1890ff')
706
+ .style('color', '#fff');
707
+ if (tp.key === 8) {
708
+ const isSimulation = !this.releasedVariableService;
709
+ if (!isSimulation) {
710
+ this.showCustomTimeRangeModal();
711
+ }
712
+ return;
713
+ }
714
+ this.updateTimeRange(tp.key);
715
+ this.reRenderElement(this.startTime, this.endTime, this.displayOption.dataLimit, HistoricalCurveTimeRange.BeginOpenEndOpen);
716
+ });
237
717
  });
238
- const options = selectElement.selectAll('option').data(this.timePeriods).enter().append('option');
239
- options.text(d => d.name).attr('value', d => d.key).property('selected', d => d.key === Number(this.currentTimePeriod));
240
- const buttonWidth = this.displayOption.operationButtonWidth + 'px', buttonHeight = this.displayOption.operationButtonHeight + 'px';
718
+ // 测量下拉列表最大宽度,让 trigger 和列表宽度一致
719
+ dropdownListEl.style.display = 'block';
720
+ dropdownListEl.style.visibility = 'hidden';
721
+ const listContentWidth = dropdownListEl.offsetWidth;
722
+ dropdownListEl.style.display = 'none';
723
+ dropdownListEl.style.visibility = '';
724
+ dropdownListEl.style.width = listContentWidth + 'px';
725
+ dropdownTrigger.node().style.width = listContentWidth + 'px';
726
+ dropdownTrigger.node().style.boxSizing = 'border-box';
727
+ const hideDropdown = () => { dropdownListEl.style.display = 'none'; };
728
+ dropdownTrigger.on('click', () => {
729
+ const isVisible = dropdownListEl.style.display !== 'none';
730
+ if (isVisible) {
731
+ hideDropdown();
732
+ }
733
+ else {
734
+ const triggerNode = dropdownTrigger.node();
735
+ const triggerRect = triggerNode.getBoundingClientRect();
736
+ // 设置宽度至少和 trigger 一样宽
737
+ dropdownListEl.style.minWidth = triggerRect.width + 'px';
738
+ dropdownListEl.style.left = triggerRect.left + 'px';
739
+ dropdownListEl.style.display = 'block';
740
+ const listHeight = dropdownListEl.offsetHeight;
741
+ const spaceBelow = window.innerHeight - triggerRect.bottom;
742
+ if (spaceBelow < listHeight) {
743
+ // 向上展开
744
+ dropdownListEl.style.top = '';
745
+ dropdownListEl.style.bottom = (window.innerHeight - triggerRect.top) + 'px';
746
+ }
747
+ else {
748
+ // 向下展开
749
+ dropdownListEl.style.bottom = '';
750
+ dropdownListEl.style.top = triggerRect.bottom + 'px';
751
+ }
752
+ }
753
+ });
754
+ // 点击外部收起
755
+ this._outsideClickHandler = (event) => {
756
+ const triggerNode = dropdownTrigger.node();
757
+ if (!dropdownListEl.contains(event.target) && !triggerNode.contains(event.target)) {
758
+ hideDropdown();
759
+ }
760
+ };
761
+ document.addEventListener('click', this._outsideClickHandler);
762
+ // 全屏切换(含点击全屏按钮和 F11)时收起
763
+ this._fullscreenChangeHandler = () => { hideDropdown(); };
764
+ document.addEventListener('fullscreenchange', this._fullscreenChangeHandler);
765
+ document.addEventListener('webkitfullscreenchange', this._fullscreenChangeHandler);
766
+ // F11 退出全屏时 keydown 被浏览器拦截,用 resize 兜底
767
+ this._resizeHideHandler = () => { hideDropdown(); };
768
+ window.addEventListener('resize', this._resizeHideHandler);
769
+ // 滚动时收起
770
+ document.addEventListener('scroll', hideDropdown, true);
771
+ const buttonWidth = (this.displayOption.operationButtonWidth * scale) + 'px', buttonHeight = (this.displayOption.operationButtonHeight * scale) + 'px';
241
772
  operationArea.append('button').style('width', buttonWidth).style('height', buttonHeight).style('border', 'none')
242
- .style('float', 'right').style('background', 'white').style('background-image', 'url(assets/img/last_page.svg)')
773
+ .style('float', 'right').style('background-image', 'url(assets/img/black_last_page.png)')
774
+ .style('background-color', backgroundColor).style('background-size', 'cover')
243
775
  .on('click', () => { this.loadLastPage(); });
244
776
  operationArea.append('button').style('width', buttonWidth).style('height', buttonHeight).style('border', 'none')
245
- .style('float', 'right').style('background', 'white').style('background-image', 'url(assets/img/next_page.svg)')
246
- .style('margin', `0 ${this.displayOption.operationButtonMargin}px 0 0`)
777
+ .style('float', 'right').style('background-image', 'url(assets/img/black_next_page.png)')
778
+ .style('margin', `0 ${this.displayOption.operationButtonMargin * scale}px 0 0`)
779
+ .style('background-color', backgroundColor).style('background-size', 'cover')
247
780
  .on('click', () => { this.loadNextPage(); });
248
781
  operationArea.append('button').style('width', buttonWidth).style('height', buttonHeight).style('border', 'none')
249
- .style('float', 'right').style('background', 'white').style('background-image', 'url(assets/img/previous_page.svg)')
250
- .style('margin', `0 ${this.displayOption.operationButtonMargin}px 0 0`)
782
+ .style('float', 'right').style('background-image', 'url(assets/img/black_previous_page.png)')
783
+ .style('margin', `0 ${this.displayOption.operationButtonMargin * scale}px 0 0`)
784
+ .style('background-color', backgroundColor).style('background-size', 'cover')
251
785
  .on('click', () => { this.loadPreviousPage(); });
252
786
  operationArea.append('button').style('width', buttonWidth).style('height', buttonHeight).style('border', 'none')
253
- .style('float', 'right').style('background', 'white').style('background-image', 'url(assets/img/first_page.svg)')
254
- .style('margin', `0 ${this.displayOption.operationButtonMargin}px 0 0`)
787
+ .style('float', 'right').style('background-image', 'url(assets/img/black_first_page.png)')
788
+ .style('margin', `0 ${this.displayOption.operationButtonMargin * scale}px 0 0`)
789
+ .style('background-color', backgroundColor).style('background-size', 'cover')
255
790
  .on('click', () => { this.loadFirstPage(); });
256
791
  }
257
792
  timeFormat(datetime, specifier) {
258
793
  return d3.time.format(specifier)(new Date(datetime));
259
794
  }
795
+ fmtDatetimeLocal(d) {
796
+ const p2 = (n) => (n < 10 ? '0' : '') + n;
797
+ return d.getFullYear() + '-' + p2(d.getMonth() + 1) + '-' + p2(d.getDate())
798
+ + 'T' + p2(d.getHours()) + ':' + p2(d.getMinutes()) + ':' + p2(d.getSeconds());
799
+ }
800
+ showCustomTimeRangeModal() {
801
+ const now = new Date();
802
+ const defaultStart = this.customStartTime ? this.customStartTime.toDate()
803
+ : new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0);
804
+ const defaultEnd = this.customEndTime ? this.customEndTime.toDate()
805
+ : new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59);
806
+ if (this.guiContext && this.guiContext.showCustomTimeRangeModal) {
807
+ this.guiContext.showCustomTimeRangeModal({ startTime: defaultStart, endTime: defaultEnd }).then(result => {
808
+ if (result) {
809
+ const momentAny = moment;
810
+ this.customStartTime = momentAny(result.startTime);
811
+ this.customEndTime = momentAny(result.endTime);
812
+ this.updateTimeRange(8);
813
+ this.reRenderElement(this.startTime, this.endTime, this.displayOption.dataLimit, HistoricalCurveTimeRange.BeginOpenEndOpen);
814
+ }
815
+ });
816
+ return;
817
+ }
818
+ this.showCustomTimeRangeModalFallback();
819
+ }
820
+ showCustomTimeRangeModalFallback() {
821
+ const prevTimePeriod = this.currentTimePeriod;
822
+ const now = new Date();
823
+ const defaultStart = this.fmtDatetimeLocal(this.customStartTime ? this.customStartTime.toDate()
824
+ : new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0));
825
+ const defaultEnd = this.fmtDatetimeLocal(this.customEndTime ? this.customEndTime.toDate()
826
+ : new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59));
827
+ // 临时将 <html lang> 切换为当前应用语言,使原生 datetime-local
828
+ // 弹出日历的 UI(星期/月份文字等)跟随应用语种显示,关闭时还原
829
+ const currentLang = this.getCurrentCulture();
830
+ const prevDocLang = document.documentElement.lang;
831
+ document.documentElement.lang = currentLang;
832
+ const overlay = document.createElement('div');
833
+ overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.5);z-index:9999;';
834
+ // 部分浏览器可直接读取元素上的 lang 属性来本地化原生控件
835
+ overlay.lang = currentLang;
836
+ const modal = document.createElement('div');
837
+ let modalCss = 'position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);'
838
+ + 'background:#fff;border-radius:4px;';
839
+ if (this.isMobileMode) {
840
+ modalCss += 'width:90%;';
841
+ }
842
+ else {
843
+ modalCss += 'min-width:460px;';
844
+ }
845
+ modalCss += 'box-shadow:0 4px 12px rgba(0,0,0,0.15);font-family:inherit;font-size:14px;';
846
+ modal.style.cssText = modalCss;
847
+ const header = document.createElement('div');
848
+ header.style.cssText = 'display:flex;align-items:center;justify-content:space-between;padding:12px 16px;border-bottom:1px solid #e8e8e8;';
849
+ const title = document.createElement('span');
850
+ title.style.cssText = 'font-size:16px;font-weight:500;color:#333;';
851
+ title.textContent = this.localization.customTimeRange;
852
+ const closeBtn = document.createElement('span');
853
+ closeBtn.style.cssText = 'cursor:pointer;font-size:18px;color:#999;line-height:1;';
854
+ closeBtn.textContent = '×';
855
+ header.appendChild(title);
856
+ header.appendChild(closeBtn);
857
+ const body = document.createElement('div');
858
+ body.style.cssText = 'padding:20px 16px;display:flex;align-items:center;flex-wrap:wrap;gap:8px;';
859
+ const label = document.createElement('span');
860
+ label.style.cssText = 'color:#333;white-space:nowrap;';
861
+ label.textContent = this.localization.timeRange + ':';
862
+ const inputStyle = 'border:1px solid #d9d9d9;border-radius:4px;padding:4px 8px;font-size:14px;height:32px;outline:none;color:#333;';
863
+ const startInput = document.createElement('input');
864
+ startInput.type = 'datetime-local';
865
+ startInput.setAttribute('step', '1');
866
+ startInput.setAttribute('lang', currentLang);
867
+ startInput.value = defaultStart;
868
+ startInput.style.cssText = inputStyle;
869
+ const separator = document.createElement('span');
870
+ separator.textContent = ' - ';
871
+ separator.style.cssText = 'color:#333;';
872
+ const endInput = document.createElement('input');
873
+ endInput.type = 'datetime-local';
874
+ endInput.setAttribute('step', '1');
875
+ endInput.setAttribute('lang', currentLang);
876
+ endInput.value = defaultEnd;
877
+ endInput.style.cssText = inputStyle;
878
+ body.appendChild(label);
879
+ body.appendChild(startInput);
880
+ body.appendChild(separator);
881
+ body.appendChild(endInput);
882
+ const footer = document.createElement('div');
883
+ footer.style.cssText = 'display:flex;justify-content:flex-end;gap:8px;padding:12px 16px;border-top:1px solid #e8e8e8;';
884
+ const cancelBtn = document.createElement('button');
885
+ cancelBtn.textContent = this.localization.cancel;
886
+ cancelBtn.style.cssText = 'padding:5px 16px;border:1px solid #d9d9d9;border-radius:4px;background:#fff;cursor:pointer;font-size:14px;color:#333;';
887
+ const saveBtn = document.createElement('button');
888
+ saveBtn.textContent = this.localization.submit;
889
+ saveBtn.style.cssText = 'padding:5px 16px;border:none;border-radius:4px;background:#1890ff;color:#fff;cursor:pointer;font-size:14px;';
890
+ footer.appendChild(cancelBtn);
891
+ footer.appendChild(saveBtn);
892
+ modal.appendChild(header);
893
+ modal.appendChild(body);
894
+ modal.appendChild(footer);
895
+ overlay.appendChild(modal);
896
+ document.body.appendChild(overlay);
897
+ const close = (revert) => {
898
+ // 还原弹窗打开前的 <html lang>,避免影响页面其他部分
899
+ document.documentElement.lang = prevDocLang;
900
+ document.body.removeChild(overlay);
901
+ if (revert) {
902
+ const selectEl = this.$element && this.$element.find('select');
903
+ if (selectEl && selectEl.length) {
904
+ selectEl.val(prevTimePeriod.toString());
905
+ }
906
+ }
907
+ };
908
+ closeBtn.addEventListener('click', () => close(true));
909
+ cancelBtn.addEventListener('click', () => close(true));
910
+ saveBtn.addEventListener('click', () => {
911
+ const startVal = startInput.value;
912
+ const endVal = endInput.value;
913
+ if (!startVal) {
914
+ startInput.style.borderColor = 'red';
915
+ return;
916
+ }
917
+ const startDate = new Date(startVal);
918
+ const endDate = endVal
919
+ ? new Date(endVal)
920
+ : new Date(startDate.getFullYear(), startDate.getMonth(), startDate.getDate(), 23, 59, 59);
921
+ if (isNaN(startDate.getTime())) {
922
+ startInput.style.borderColor = 'red';
923
+ return;
924
+ }
925
+ if (endDate.getTime() <= startDate.getTime()) {
926
+ endInput.style.borderColor = 'red';
927
+ return;
928
+ }
929
+ const momentAny = moment;
930
+ this.customStartTime = momentAny(startDate);
931
+ this.customEndTime = momentAny(endDate);
932
+ close(false);
933
+ this.updateTimeRange(8);
934
+ this.reRenderElement(this.startTime, this.endTime, this.displayOption.dataLimit, HistoricalCurveTimeRange.BeginOpenEndOpen);
935
+ });
936
+ }
260
937
  loadFirstPage() {
261
938
  this.updateQueryTimeRange();
262
939
  this.reRenderElement(this.startTime, this.endTime, this.displayOption.dataLimit, HistoricalCurveTimeRange.BeginOpenEndOpen);