diff --git a/main/package.json b/main/package.json
index 6fca603..73cb015 100644
--- a/main/package.json
+++ b/main/package.json
@@ -17,8 +17,10 @@
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.1",
+ "@vueuse/core": "^12.4.0",
"axios": "^1.6.5",
"dayjs": "^1.11.11",
+ "echarts": "^5.6.0",
"element-plus": "^2.7.3",
"file-saver": "^2.0.5",
"js-base64": "^3.7.7",
diff --git a/main/src/components/custom-echart-bar/index.vue b/main/src/components/custom-echart-bar/index.vue
new file mode 100644
index 0000000..8945969
--- /dev/null
+++ b/main/src/components/custom-echart-bar/index.vue
@@ -0,0 +1,86 @@
+
+
+
+
diff --git a/main/src/components/index.js b/main/src/components/index.js
index 590713a..019eefa 100644
--- a/main/src/components/index.js
+++ b/main/src/components/index.js
@@ -1,5 +1,5 @@
import SvgIcon from './svg-icon';
-
import CustomRichEditor from './custom-rich-editor';
+import CustomEchartBar from './custom-echart-bar';
-export { SvgIcon, CustomRichEditor };
+export { SvgIcon, CustomEchartBar, CustomRichEditor };
diff --git a/main/src/hooks/useBreakpoint.js b/main/src/hooks/useBreakpoint.js
new file mode 100644
index 0000000..cf0e081
--- /dev/null
+++ b/main/src/hooks/useBreakpoint.js
@@ -0,0 +1,84 @@
+import { ref, computed, unref } from 'vue';
+import { useEventListener } from './useEventListener';
+
+let globalScreenRef;
+let globalWidthRef;
+let globalRealWidthRef;
+
+const screenMap = new Map();
+screenMap.set('XS', 480);
+screenMap.set('SM', 576);
+screenMap.set('MD', 768);
+screenMap.set('LG', 992);
+screenMap.set('XL', 1200);
+screenMap.set('XXL', 1600);
+
+export function useBreakpoint() {
+ return {
+ screenRef: computed(() => unref(globalScreenRef)),
+ widthRef: globalWidthRef,
+ realWidthRef: globalRealWidthRef,
+ };
+}
+
+// Just call it once
+export function createBreakpointListen(fn) {
+ const screenRef = ref('XL');
+ const realWidthRef = ref(window.innerWidth);
+
+ function getWindowWidth() {
+ const width = document.body.clientWidth;
+ const xs = screenMap.get('XS');
+ const sm = screenMap.get('SM');
+ const md = screenMap.get('MD');
+ const lg = screenMap.get('LG');
+ const xl = screenMap.get('XL');
+ const xxl = screenMap.set('XXL', 1600);
+ if (width < xs) {
+ screenRef.value = xs;
+ } else if (width < sm) {
+ screenRef.value = sm;
+ } else if (width < md) {
+ screenRef.value = md;
+ } else if (width < lg) {
+ screenRef.value = lg;
+ } else if (width < xl) {
+ screenRef.value = xl;
+ } else {
+ screenRef.value = xxl;
+ }
+ realWidthRef.value = width;
+ }
+
+ useEventListener({
+ el: window,
+ name: 'resize',
+
+ listener: () => {
+ getWindowWidth();
+ resizeFn();
+ },
+ // wait: 100,
+ });
+
+ getWindowWidth();
+ globalScreenRef = computed(() => unref(screenRef));
+ globalWidthRef = computed(() => screenMap.get(unref(screenRef)));
+ globalRealWidthRef = computed(() => unref(realWidthRef));
+
+ function resizeFn() {
+ fn?.({
+ screen: globalScreenRef,
+ width: globalWidthRef,
+ realWidth: globalRealWidthRef,
+ screenMap,
+ });
+ }
+
+ resizeFn();
+ return {
+ screenRef: globalScreenRef,
+ widthRef: globalWidthRef,
+ realWidthRef: globalRealWidthRef,
+ };
+}
diff --git a/main/src/hooks/useEcharts.js b/main/src/hooks/useEcharts.js
new file mode 100644
index 0000000..8d14d5d
--- /dev/null
+++ b/main/src/hooks/useEcharts.js
@@ -0,0 +1,107 @@
+import { unref, nextTick, watch, computed, ref } from 'vue';
+import { useDebounceFn, tryOnUnmounted } from '@vueuse/core';
+import { useTimeoutFn } from './useTimeout';
+import { useEventListener } from './useEventListener';
+import { useBreakpoint } from './useBreakpoint';
+import echarts from '../utils/echarts';
+
+export const useEcharts = (elRef, theme = 'default') => {
+ const getDarkMode = computed(() => {
+ return theme === 'default' ? 'dark' : theme;
+ });
+ let chartInstance = null;
+ let resizeFn = resize;
+ const cacheOptions = ref({});
+ let removeResizeFn = () => {};
+
+ resizeFn = useDebounceFn(resize, 200);
+
+ const getOptions = computed(() => {
+ if (getDarkMode.value !== 'dark') {
+ return cacheOptions.value;
+ }
+ return {
+ backgroundColor: 'transparent',
+ ...cacheOptions.value,
+ };
+ });
+
+ function initCharts(t = theme) {
+ const el = unref(elRef);
+ if (!el || !unref(el)) {
+ return;
+ }
+
+ chartInstance = echarts.init(el, t);
+ const { removeEvent } = useEventListener({
+ el: window,
+ name: 'resize',
+ listener: resizeFn,
+ });
+ removeResizeFn = removeEvent;
+ const { widthRef } = useBreakpoint();
+ if (unref(widthRef) <= 768 || el.offsetHeight === 0) {
+ useTimeoutFn(() => {
+ resizeFn();
+ }, 30);
+ }
+ }
+
+ function setOptions(options = {}, clear = true) {
+ cacheOptions.value = options;
+ if (unref(elRef)?.offsetHeight === 0) {
+ useTimeoutFn(() => {
+ setOptions(unref(getOptions));
+ }, 30);
+ return;
+ }
+ nextTick(() => {
+ useTimeoutFn(() => {
+ if (!chartInstance) {
+ initCharts(getDarkMode.value ?? 'default');
+
+ if (!chartInstance) return;
+ }
+ clear && chartInstance?.clear();
+
+ chartInstance?.setOption(unref(getOptions));
+ }, 30);
+ });
+ }
+
+ function resize() {
+ chartInstance?.resize();
+ }
+
+ watch(
+ () => getDarkMode.value,
+ (theme) => {
+ if (chartInstance) {
+ chartInstance.dispose();
+ initCharts(theme);
+ setOptions(cacheOptions.value);
+ }
+ }
+ );
+
+ tryOnUnmounted(() => {
+ if (!chartInstance) return;
+ removeResizeFn();
+ chartInstance.dispose();
+ chartInstance = null;
+ });
+
+ function getInstance() {
+ if (!chartInstance) {
+ initCharts(getDarkMode.value ?? 'default');
+ }
+ return chartInstance;
+ }
+
+ return {
+ setOptions,
+ resize,
+ echarts,
+ getInstance,
+ };
+};
diff --git a/main/src/hooks/useEventListener.js b/main/src/hooks/useEventListener.js
new file mode 100644
index 0000000..5ebdaf7
--- /dev/null
+++ b/main/src/hooks/useEventListener.js
@@ -0,0 +1,38 @@
+import { ref, watch, unref } from 'vue';
+import { useThrottleFn, useDebounceFn } from '@vueuse/core';
+
+export function useEventListener({ el = window, name, listener, options, autoRemove = true, isDebounce = true, wait = 80 }) {
+ let remove;
+ const isAddRef = ref(false);
+
+ if (el) {
+ const element = ref(el);
+
+ const handler = isDebounce ? useDebounceFn(listener, wait) : useThrottleFn(listener, wait);
+ const realHandler = wait ? handler : listener;
+ const removeEventListener = (e) => {
+ isAddRef.value = true;
+ e.removeEventListener(name, realHandler, options);
+ };
+ const addEventListener = (e) => e.addEventListener(name, realHandler, options);
+
+ const removeWatch = watch(
+ element,
+ (v, _ov, cleanUp) => {
+ if (v) {
+ !unref(isAddRef) && addEventListener(v);
+ cleanUp(() => {
+ autoRemove && removeEventListener(v);
+ });
+ }
+ },
+ { immediate: true }
+ );
+
+ remove = () => {
+ removeEventListener(element.value);
+ removeWatch();
+ };
+ }
+ return { removeEvent: remove };
+}
diff --git a/main/src/hooks/useTimeout.js b/main/src/hooks/useTimeout.js
new file mode 100644
index 0000000..ccef4b6
--- /dev/null
+++ b/main/src/hooks/useTimeout.js
@@ -0,0 +1,44 @@
+import { ref, watch } from 'vue';
+import { tryOnUnmounted } from '@vueuse/core';
+
+export function useTimeoutFn(handle, wait, native = false) {
+ if (typeof handle !== 'function') {
+ throw new Error('handle is not Function!');
+ }
+
+ const { readyRef, stop, start } = useTimeoutRef(wait);
+ if (native) {
+ handle();
+ } else {
+ watch(
+ readyRef,
+ (maturity) => {
+ maturity && handle();
+ },
+ { immediate: false }
+ );
+ }
+ return { readyRef, stop, start };
+}
+
+export function useTimeoutRef(wait) {
+ const readyRef = ref(false);
+
+ let timer;
+ function stop() {
+ readyRef.value = false;
+ timer && window.clearTimeout(timer);
+ }
+ function start() {
+ stop();
+ timer = setTimeout(() => {
+ readyRef.value = true;
+ }, wait);
+ }
+
+ start();
+
+ tryOnUnmounted(stop);
+
+ return { readyRef, stop, start };
+}
diff --git a/main/src/plugins/globalComponents.js b/main/src/plugins/globalComponents.js
deleted file mode 100644
index 846a429..0000000
--- a/main/src/plugins/globalComponents.js
+++ /dev/null
@@ -1,8 +0,0 @@
-// import * as components from '../../../global/components';
-
-// // 全局注册组件
-// export const registerGlobalComponents = (app) => {
-// Object.keys(components).forEach((key) => {
-// app.component(key, components[key]);
-// });
-// };
diff --git a/main/src/utils/echarts.js b/main/src/utils/echarts.js
new file mode 100644
index 0000000..9b40020
--- /dev/null
+++ b/main/src/utils/echarts.js
@@ -0,0 +1,51 @@
+import * as echarts from 'echarts/core';
+
+import { BarChart, LineChart, PieChart, MapChart, PictorialBarChart, RadarChart } from 'echarts/charts';
+
+import {
+ TitleComponent,
+ TooltipComponent,
+ GridComponent,
+ PolarComponent,
+ AriaComponent,
+ ParallelComponent,
+ LegendComponent,
+ RadarComponent,
+ ToolboxComponent,
+ DataZoomComponent,
+ VisualMapComponent,
+ TimelineComponent,
+ CalendarComponent,
+ GraphicComponent,
+} from 'echarts/components';
+
+// TODO 如果想换成SVG渲染,就导出SVGRenderer,
+// 并且放到 echarts.use 里,注释掉 CanvasRenderer
+import { /*SVGRenderer*/ CanvasRenderer } from 'echarts/renderers';
+
+echarts.use([
+ LegendComponent,
+ TitleComponent,
+ TooltipComponent,
+ GridComponent,
+ PolarComponent,
+ AriaComponent,
+ ParallelComponent,
+ BarChart,
+ LineChart,
+ PieChart,
+ MapChart,
+ RadarChart,
+ // TODO 因为要兼容Online图表自适应打印,所以改成 CanvasRenderer,可能会模糊
+ CanvasRenderer,
+ PictorialBarChart,
+ RadarComponent,
+ ToolboxComponent,
+ DataZoomComponent,
+ VisualMapComponent,
+ TimelineComponent,
+ CalendarComponent,
+ GraphicComponent,
+]);
+
+export default echarts;
diff --git a/main/src/views/index.vue b/main/src/views/index.vue
index 290dcbc..338069d 100644
--- a/main/src/views/index.vue
+++ b/main/src/views/index.vue
@@ -1,6 +1,8 @@
平台首页
+
+
+
+