产出品管理 - 产出品概览页面饼图展示开发
This commit is contained in:
parent
f9f83986df
commit
b741c624f2
@ -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']
|
||||
|
@ -0,0 +1,101 @@
|
||||
<template>
|
||||
<div ref="chartRef" :style="{ height, width }"></div>
|
||||
</template>
|
||||
<script>
|
||||
import { ref, reactive, watch, watchEffect } from 'vue';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { useEcharts } from '../../hooks/useEcharts';
|
||||
|
||||
export default {
|
||||
name: 'CustomEchartPie',
|
||||
props: {
|
||||
chartData: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
size: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
option: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: '100%',
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: 'calc(100vh - 78px)',
|
||||
},
|
||||
},
|
||||
emits: ['click'],
|
||||
setup(props, { emit }) {
|
||||
const chartRef = ref(null);
|
||||
const { setOptions, getInstance, resize, startAutoPlay } = useEcharts(chartRef);
|
||||
const option = reactive({
|
||||
tooltip: {
|
||||
backgroundColor: '#fff', // 背景颜色(支持RGBA格式)
|
||||
borderColor: 'rgba(0, 0, 0, 0.1)', // 边框颜色
|
||||
borderWidth: 1, // 边框宽度
|
||||
textStyle: {
|
||||
color: '#333', // 文字颜色
|
||||
fontSize: 12,
|
||||
},
|
||||
formatter: '{b} ({c})',
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'pie',
|
||||
radius: '72%',
|
||||
center: ['50%', '55%'],
|
||||
data: [],
|
||||
labelLine: { show: true },
|
||||
label: {
|
||||
show: true,
|
||||
formatter: '{b} \n ({d}%)',
|
||||
color: '#B1B9D3',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
watchEffect(() => {
|
||||
props.chartData && initCharts();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.size,
|
||||
() => {
|
||||
resize();
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
|
||||
function initCharts() {
|
||||
if (props.option) {
|
||||
Object.assign(option, cloneDeep(props.option));
|
||||
}
|
||||
option.series[0].data = props.chartData;
|
||||
setOptions(option);
|
||||
startAutoPlay({
|
||||
interval: 2000,
|
||||
seriesIndex: 0,
|
||||
showTooltip: true,
|
||||
});
|
||||
resize();
|
||||
getInstance()?.off('click', onClick);
|
||||
getInstance()?.on('click', onClick);
|
||||
}
|
||||
|
||||
function onClick(params) {
|
||||
emit('click', params);
|
||||
}
|
||||
|
||||
return { chartRef };
|
||||
},
|
||||
};
|
||||
</script>
|
84
sub-government-affairs-service/src/hooks/useBreakpoint.js
Normal file
84
sub-government-affairs-service/src/hooks/useBreakpoint.js
Normal file
@ -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,
|
||||
};
|
||||
}
|
191
sub-government-affairs-service/src/hooks/useEcharts.js
Normal file
191
sub-government-affairs-service/src/hooks/useEcharts.js
Normal file
@ -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,
|
||||
};
|
||||
};
|
38
sub-government-affairs-service/src/hooks/useEventListener.js
Normal file
38
sub-government-affairs-service/src/hooks/useEventListener.js
Normal file
@ -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 };
|
||||
}
|
44
sub-government-affairs-service/src/hooks/useTimeout.js
Normal file
44
sub-government-affairs-service/src/hooks/useTimeout.js
Normal file
@ -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 };
|
||||
}
|
@ -247,7 +247,6 @@
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
padding: 3px 0;
|
||||
|
||||
}
|
||||
.el-icon-custom {
|
||||
vertical-align: middle;
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
66
sub-government-affairs-service/src/utils/echarts.js
Normal file
66
sub-government-affairs-service/src/utils/echarts.js
Normal file
@ -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;
|
@ -14,3 +14,67 @@ import { getAssetsFile } from '@/utils';
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- <template>
|
||||
<div class="app-container">
|
||||
<div class="container-custom">
|
||||
<h2 class="custom-h2">产出品概览</h2>
|
||||
<div ref="searchBarRef" class="search-box">
|
||||
<div class="search-bar">
|
||||
<div class="search-bar-left">
|
||||
<el-form ref="searchForm" :inline="true" :model="formInline" class="demo-form-inline" :label-width="'auto'">
|
||||
<el-form-item label="" style="margin-left: -15px">
|
||||
<AreaCascader v-model:region-code="formInline.regionCode" v-model:grid-id="formInline.gridId" label="行政区域-网格" :width="500" />
|
||||
</el-form-item>
|
||||
<el-form-item label="">
|
||||
<el-button type="primary" icon="Search" @click="onSubmit">查询</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="statistics-cont">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12" class="statistics-echart-box">
|
||||
<p class="statistics-echart-title">产出品产量数据</p>
|
||||
<customEchartPie ref="pie1" :chart-data="chartData1" :option="option" :width="'100%'" :height="'100%'"></customEchartPie>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, onMounted } from 'vue';
|
||||
import customEchartPie from '@/components/custom-echart-pie';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
// 查询条件
|
||||
const formInline = reactive({
|
||||
regionCode: '',
|
||||
regionName: '',
|
||||
gridId: '',
|
||||
gridName: '',
|
||||
});
|
||||
const searchForm = ref(null);
|
||||
|
||||
const chartData1 = ref([
|
||||
{ value: 1048, name: '蔬菜类' },
|
||||
{ value: 735, name: '水果类' },
|
||||
{ value: 484, name: '药材类' },
|
||||
]);
|
||||
const option = reactive({
|
||||
tooltip: {
|
||||
formatter: '{b} ({c} 吨)',
|
||||
},
|
||||
});
|
||||
const onSubmit = () => {
|
||||
console.log(formInline);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
// onSubmit();
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped></style> -->
|
||||
|
@ -137,7 +137,7 @@
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<el-descriptions title="区块链认证信息" border class="mb-20 custom-descriptions" :column="1">
|
||||
<el-descriptions-item label="质检报告">
|
||||
<el-descriptions-item label="认证信息">
|
||||
<img :src="dialogForm?.traceUrl ?? ''" alt="" />
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
@ -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('溯源产品详情');
|
||||
|
Loading…
x
Reference in New Issue
Block a user