diff --git a/sub-government-affairs-service/components.d.ts b/sub-government-affairs-service/components.d.ts index ae4ec06..6bdedb7 100644 --- a/sub-government-affairs-service/components.d.ts +++ b/sub-government-affairs-service/components.d.ts @@ -10,6 +10,7 @@ declare module 'vue' { AreaCascader: typeof import('./src/components/AreaCascader/index.vue')['default'] AreaSelect: typeof import('./src/components/AreaSelect/index.vue')['default'] CodeDialog: typeof import('./src/components/code-dialog/index.vue')['default'] + CustomEchartPie: typeof import('./src/components/custom-echart-pie/index.vue')['default'] FileUploader: typeof import('./src/components/FileUploader/index.vue')['default'] LandSelect: typeof import('./src/components/LandSelect.vue')['default'] RouterLink: typeof import('vue-router')['RouterLink'] diff --git a/sub-government-affairs-service/src/components/custom-echart-pie/index.vue b/sub-government-affairs-service/src/components/custom-echart-pie/index.vue new file mode 100644 index 0000000..85d198a --- /dev/null +++ b/sub-government-affairs-service/src/components/custom-echart-pie/index.vue @@ -0,0 +1,101 @@ + + diff --git a/sub-government-affairs-service/src/hooks/useBreakpoint.js b/sub-government-affairs-service/src/hooks/useBreakpoint.js new file mode 100644 index 0000000..32311f2 --- /dev/null +++ b/sub-government-affairs-service/src/hooks/useBreakpoint.js @@ -0,0 +1,84 @@ +import { ref, computed, unref } from 'vue'; +import { useEventListener } from './useEventListener'; + +let globalScreenRef = 0; +let globalWidthRef = 0; +let globalRealWidthRef = 0; + +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/sub-government-affairs-service/src/hooks/useEcharts.js b/sub-government-affairs-service/src/hooks/useEcharts.js new file mode 100644 index 0000000..cc00ec0 --- /dev/null +++ b/sub-government-affairs-service/src/hooks/useEcharts.js @@ -0,0 +1,191 @@ +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 autoPlayTimer = ref(null); + const currentIndex = ref(-1); + const dataLength = ref(0); + + // 新增方法 - 启动轮播 + const startAutoPlay = (options = {}) => { + const { + interval = 2000, // 轮播间隔(ms) + seriesIndex = 0, // 默认操作第一个系列 + showTooltip = true, // 是否显示提示框 + } = options; + + stopAutoPlay(); // 先停止已有轮播 + + // 获取数据长度 + const seriesData = unref(getOptions).series?.[seriesIndex]?.data; + dataLength.value = seriesData?.length || 0; + if (dataLength.value === 0) return; + + autoPlayTimer.value = setInterval(() => { + currentIndex.value = (currentIndex.value + 1) % dataLength.value; + + // 执行轮播动作 + chartInstance?.dispatchAction({ + type: 'downplay', + seriesIndex: seriesIndex, + }); + chartInstance?.dispatchAction({ + type: 'highlight', + seriesIndex: seriesIndex, + dataIndex: currentIndex.value, + }); + if (showTooltip) { + chartInstance?.dispatchAction({ + type: 'showTip', + seriesIndex: seriesIndex, + dataIndex: currentIndex.value, + }); + } + }, interval); + }; + + // 新增方法 - 停止轮播 + const stopAutoPlay = () => { + if (autoPlayTimer.value) { + clearInterval(autoPlayTimer.value); + autoPlayTimer.value = null; + } + }; + + const getDarkMode = computed(() => { + return theme === 'default' ? 'dark' : theme; + }); + let chartInstance = null; + let resizeFn = resize; + const cacheOptions = ref({}); + let removeResizeFn = null; + + 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; + } + nextTick(() => { + if (el.offsetWidth === 0 || el.offsetHeight === 0) { + // console.warn('图表容器不可见,延迟初始化'); + useTimeoutFn(() => initCharts(t), 100); + 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) { + const mergedOptions = { + animation: true, + animationDuration: 3000, + animationEasing: 'cubicOut', + ...unref(options), + animationThreshold: 2000, // 数据量超过2000自动关闭动画 + animationDelayUpdate: (idx) => idx * 50, // 数据项延迟 + }; + cacheOptions.value = mergedOptions; + 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(); + } + + /** + * 注册地图数据 + * @param {string} mapName - 地图名称 + * @param {object} geoJSON - GeoJSON 数据 + */ + function registerMap(mapName, geoJSON) { + if (!mapName || !geoJSON) { + console.warn('地图名称或 GeoJSON 数据无效'); + return; + } + echarts.registerMap(mapName, geoJSON); + } + + watch( + () => getDarkMode.value, + (theme) => { + if (chartInstance) { + chartInstance.dispose(); + initCharts(theme); + setOptions(cacheOptions.value); + } + } + ); + + tryOnUnmounted(() => { + stopAutoPlay(); // 清理定时器 + if (!chartInstance) return; + removeResizeFn(); + chartInstance.dispose(); + chartInstance = null; + }); + + function getInstance() { + if (!chartInstance) { + initCharts(getDarkMode.value ?? 'default'); + } + return chartInstance; + } + + return { + setOptions, + resize, + echarts, + getInstance: () => chartInstance, + registerMap, + startAutoPlay, // 暴露轮播方法 + stopAutoPlay, + }; +}; diff --git a/sub-government-affairs-service/src/hooks/useEventListener.js b/sub-government-affairs-service/src/hooks/useEventListener.js new file mode 100644 index 0000000..5ebdaf7 --- /dev/null +++ b/sub-government-affairs-service/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/sub-government-affairs-service/src/hooks/useTimeout.js b/sub-government-affairs-service/src/hooks/useTimeout.js new file mode 100644 index 0000000..ccef4b6 --- /dev/null +++ b/sub-government-affairs-service/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/sub-government-affairs-service/src/styles/custom.scss b/sub-government-affairs-service/src/styles/custom.scss index 84cf943..e5a7042 100644 --- a/sub-government-affairs-service/src/styles/custom.scss +++ b/sub-government-affairs-service/src/styles/custom.scss @@ -104,12 +104,12 @@ .el-table__empty-text { width: 200px; } - .el-button-custom{ + .el-button-custom { font-size: 14px !important; color: #25bf82; padding: 0; } - .el-button-delete{ + .el-button-delete { font-size: 14px !important; color: #ff4d4f; padding: 0; @@ -247,7 +247,6 @@ justify-content: space-between; gap: 10px; padding: 3px 0; - } .el-icon-custom { vertical-align: middle; @@ -356,7 +355,7 @@ font-weight: bold; margin-bottom: 20px; } - .dialog-form-item{ + .dialog-form-item { margin-right: 20px; .el-input, .el-select { @@ -373,3 +372,16 @@ } } +.statistics-cont { + padding: 10px 20px; + .statistics-echart-box { + height: 400px; + padding-bottom: 50px; + box-sizing: content-box; + background-color: #fff; + } + .statistics-echart-title { + font-size: 18px; + font-weight: bold; + } +} diff --git a/sub-government-affairs-service/src/utils/echarts.js b/sub-government-affairs-service/src/utils/echarts.js new file mode 100644 index 0000000..f03b63c --- /dev/null +++ b/sub-government-affairs-service/src/utils/echarts.js @@ -0,0 +1,66 @@ +import * as echarts from 'echarts/core'; + +import { + BarChart, + LineChart, + PieChart, + MapChart, + PictorialBarChart, + RadarChart, + GraphChart, + GaugeChart, + ScatterChart, + EffectScatterChart, +} from 'echarts/charts'; +import 'echarts-gl'; +import 'echarts-liquidfill'; +import 'echarts-wordcloud'; + +import { + TitleComponent, + TooltipComponent, + GridComponent, + PolarComponent, + AriaComponent, + ParallelComponent, + LegendComponent, + RadarComponent, + ToolboxComponent, + DataZoomComponent, + VisualMapComponent, + TimelineComponent, + CalendarComponent, + GraphicComponent, +} from 'echarts/components'; + +import { CanvasRenderer } from 'echarts/renderers'; + +echarts.use([ + LegendComponent, + TitleComponent, + TooltipComponent, + GridComponent, + PolarComponent, + AriaComponent, + ParallelComponent, + BarChart, + LineChart, + PieChart, + MapChart, + RadarChart, + CanvasRenderer, + PictorialBarChart, + RadarComponent, + ToolboxComponent, + DataZoomComponent, + VisualMapComponent, + TimelineComponent, + CalendarComponent, + GraphicComponent, + GraphChart, + GaugeChart, + ScatterChart, + EffectScatterChart, +]); + +export default echarts; diff --git a/sub-government-affairs-service/src/views/output-products/output-statistics/index.vue b/sub-government-affairs-service/src/views/output-products/output-statistics/index.vue index 33de73c..9abf2ee 100644 --- a/sub-government-affairs-service/src/views/output-products/output-statistics/index.vue +++ b/sub-government-affairs-service/src/views/output-products/output-statistics/index.vue @@ -14,3 +14,67 @@ import { getAssetsFile } from '@/utils'; background-repeat: no-repeat; } + + diff --git a/sub-government-affairs-service/src/views/trace/products/packaging/index.vue b/sub-government-affairs-service/src/views/trace/products/packaging/index.vue index 4a47304..1ac343d 100644 --- a/sub-government-affairs-service/src/views/trace/products/packaging/index.vue +++ b/sub-government-affairs-service/src/views/trace/products/packaging/index.vue @@ -137,7 +137,7 @@ - + @@ -274,7 +274,7 @@ const seedTypeChange = () => { // 重新获取表格数据,需添加参数 }; -const tabsRadio = ref(2); +const tabsRadio = ref(1); const dialogFormVisible = ref(false); const dialogRef = ref(null); const dialogTitle = ref('溯源产品详情');