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('溯源产品详情');