useECharts.ts 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  1. import type { EChartsOption } from 'echarts';
  2. import { onActivated, Ref } from 'vue';
  3. import { useTimeoutFn } from '@jeesite/core/hooks/core/useTimeout';
  4. import { tryOnUnmounted } from '@vueuse/core';
  5. import { unref, nextTick, watch, computed, ref } from 'vue';
  6. import { useDebounceFn } from '@vueuse/core';
  7. import { useEventListener } from '@jeesite/core/hooks/event/useEventListener';
  8. import { useBreakpoint } from '@jeesite/core/hooks/event/useBreakpoint';
  9. import echarts from '@jeesite/core/utils/lib/echarts';
  10. import { useRootSetting } from '@jeesite/core/hooks/setting/useRootSetting';
  11. type EventListenerTarget = {
  12. addEventListener: (
  13. type: string,
  14. listener: EventListenerOrEventListenerObject,
  15. options?: boolean | AddEventListenerOptions,
  16. ) => void;
  17. };
  18. function toArray<T>(value?: T | T[]) {
  19. if (!value) {
  20. return [];
  21. }
  22. return Array.isArray(value) ? value : [value];
  23. }
  24. function hasChartSize(el?: HTMLDivElement | null) {
  25. return !!el?.clientWidth && !!el?.clientHeight;
  26. }
  27. function needActiveWheelListener(options: EChartsOption) {
  28. const chartOptions = options as Record<string, any>;
  29. if (toArray(chartOptions.dataZoom).length > 0) {
  30. return true;
  31. }
  32. const roamTargets = [...toArray(chartOptions.geo), ...toArray(chartOptions.bmap), ...toArray(chartOptions.series)];
  33. return roamTargets.some((item) => !!item?.roam);
  34. }
  35. function getWheelListenerTargets() {
  36. const targets = new Set<EventListenerTarget>();
  37. const targetTypes = [
  38. EventTarget,
  39. typeof Window === 'undefined' ? undefined : Window,
  40. typeof Document === 'undefined' ? undefined : Document,
  41. typeof Element === 'undefined' ? undefined : Element,
  42. ];
  43. targetTypes.forEach((targetType) => {
  44. if (typeof targetType !== 'undefined' && targetType.prototype?.addEventListener) {
  45. targets.add(targetType.prototype as EventListenerTarget);
  46. }
  47. });
  48. if (typeof window !== 'undefined' && typeof window.addEventListener === 'function') {
  49. targets.add(window as unknown as EventListenerTarget);
  50. }
  51. if (typeof document !== 'undefined' && typeof document.addEventListener === 'function') {
  52. targets.add(document as unknown as EventListenerTarget);
  53. }
  54. return [...targets];
  55. }
  56. function normalizeWheelListenerOptions(options?: boolean | AddEventListenerOptions) {
  57. if (typeof options === 'boolean') {
  58. return { capture: options, passive: true };
  59. }
  60. return { ...options, passive: true };
  61. }
  62. function withPassiveWheelListeners<T>(callback: () => T) {
  63. if (typeof EventTarget === 'undefined') {
  64. return callback();
  65. }
  66. const targets = getWheelListenerTargets();
  67. const originalAddEventListeners = new Map<EventListenerTarget, EventListenerTarget['addEventListener']>();
  68. targets.forEach((target) => {
  69. const originalAddEventListener = target.addEventListener;
  70. originalAddEventListeners.set(target, originalAddEventListener);
  71. target.addEventListener = function (
  72. type: string,
  73. listener: EventListenerOrEventListenerObject,
  74. options?: boolean | AddEventListenerOptions,
  75. ) {
  76. if (type !== 'wheel' && type !== 'mousewheel') {
  77. return originalAddEventListener.call(this, type, listener, options);
  78. }
  79. return originalAddEventListener.call(this, type, listener, normalizeWheelListenerOptions(options));
  80. };
  81. });
  82. try {
  83. return callback();
  84. } finally {
  85. originalAddEventListeners.forEach((originalAddEventListener, target) => {
  86. target.addEventListener = originalAddEventListener;
  87. });
  88. }
  89. }
  90. export function useECharts(elRef: Ref<HTMLDivElement>, theme: 'light' | 'dark' | 'default' = 'default') {
  91. const { getDarkMode: getSysDarkMode } = useRootSetting();
  92. const getDarkMode = computed(() => {
  93. return theme === 'default' ? getSysDarkMode.value : theme;
  94. });
  95. let chartInstance: echarts.ECharts | null = null;
  96. let resizeFn: Fn = resize;
  97. const cacheOptions = ref({}) as Ref<EChartsOption>;
  98. let removeResizeFn: Fn = () => {};
  99. resizeFn = useDebounceFn(resize, 200);
  100. const getOptions = computed(() => {
  101. if (getDarkMode.value !== 'dark') {
  102. return cacheOptions.value as EChartsOption;
  103. }
  104. return {
  105. backgroundColor: 'transparent',
  106. ...cacheOptions.value,
  107. } as EChartsOption;
  108. });
  109. function initCharts(t = theme) {
  110. const el = unref(elRef);
  111. if (!el || !unref(el)) {
  112. return;
  113. }
  114. if (!hasChartSize(el)) {
  115. useTimeoutFn(() => {
  116. initCharts(t);
  117. }, 30);
  118. return;
  119. }
  120. const options = unref(getOptions);
  121. chartInstance = needActiveWheelListener(options)
  122. ? echarts.init(el, t)
  123. : withPassiveWheelListeners(() => echarts.init(el, t));
  124. const { removeEvent } = useEventListener({
  125. el: window,
  126. name: 'resize',
  127. listener: resizeFn,
  128. });
  129. removeResizeFn = removeEvent;
  130. const { widthRef, screenEnum } = useBreakpoint();
  131. if (unref(widthRef) <= screenEnum.MD || el.offsetHeight === 0) {
  132. useTimeoutFn(() => {
  133. resizeFn();
  134. }, 30);
  135. }
  136. }
  137. function setOptions(options: EChartsOption, clear = true) {
  138. cacheOptions.value = options;
  139. if (!hasChartSize(unref(elRef))) {
  140. useTimeoutFn(() => {
  141. setOptions(unref(getOptions));
  142. }, 30);
  143. return;
  144. }
  145. nextTick(() => {
  146. useTimeoutFn(() => {
  147. if (!chartInstance) {
  148. initCharts(getDarkMode.value as 'default');
  149. if (!chartInstance) return;
  150. }
  151. clear && chartInstance?.clear();
  152. chartInstance?.setOption(unref(getOptions));
  153. }, 30);
  154. });
  155. }
  156. function resize() {
  157. chartInstance?.resize();
  158. }
  159. onActivated(() => {
  160. resize();
  161. });
  162. watch(
  163. () => getDarkMode.value,
  164. (theme) => {
  165. if (chartInstance) {
  166. chartInstance.dispose();
  167. initCharts(theme as 'default');
  168. setOptions(cacheOptions.value);
  169. }
  170. },
  171. );
  172. tryOnUnmounted(() => {
  173. if (!chartInstance) return;
  174. removeResizeFn();
  175. chartInstance.dispose();
  176. chartInstance = null;
  177. });
  178. function getInstance(): echarts.ECharts | null {
  179. if (!chartInstance) {
  180. initCharts(getDarkMode.value as 'default');
  181. }
  182. return chartInstance;
  183. }
  184. return {
  185. setOptions,
  186. resize,
  187. echarts,
  188. getInstance,
  189. };
  190. }