|
|
@@ -1,259 +1,289 @@
|
|
|
-<!-- 横形柱状图 -->
|
|
|
<script lang="ts" setup>
|
|
|
import * as echarts from 'echarts'
|
|
|
import { useThemeStore } from '@/stores/modules/theme'
|
|
|
-import { onMounted, ref, reactive, watch, computed } from 'vue'
|
|
|
+import { onMounted, ref, reactive, watch, computed, onBeforeUnmount, nextTick } from 'vue'
|
|
|
import { formatNumber } from '@/utils/tools'
|
|
|
+
|
|
|
+// --- Types ---
|
|
|
+interface SellerItem {
|
|
|
+ name: string
|
|
|
+ value: number
|
|
|
+ color?: string
|
|
|
+ city_name?: string
|
|
|
+ [key: string]: any
|
|
|
+}
|
|
|
+
|
|
|
+interface IntervalConfig {
|
|
|
+ Max?: number
|
|
|
+ interval?: number
|
|
|
+}
|
|
|
+
|
|
|
+interface Props {
|
|
|
+ SellerData?: SellerItem[]
|
|
|
+ Interval?: IntervalConfig
|
|
|
+ saveImageName?: string
|
|
|
+}
|
|
|
+
|
|
|
+// --- Props & Emits ---
|
|
|
+const props = withDefaults(defineProps<Props>(), {
|
|
|
+ SellerData: () => [],
|
|
|
+ Interval: () => ({}),
|
|
|
+ saveImageName: 'chart_image'
|
|
|
+})
|
|
|
+
|
|
|
+const emits = defineEmits<{
|
|
|
+ ClickParams: [data: { name: string; cityName?: string; value: number }]
|
|
|
+}>()
|
|
|
+
|
|
|
const themeStore = useThemeStore()
|
|
|
-const props = defineProps({
|
|
|
- SellerData: Array,
|
|
|
- Interval: Object,
|
|
|
- saveImageName: String
|
|
|
+const sellerRef = ref<HTMLElement | null>(null)
|
|
|
+let chartInstance: echarts.ECharts | null = null
|
|
|
+
|
|
|
+// --- Computed Data ---
|
|
|
+// 确保数据是响应式的,但不需要在 computed 里做复杂映射,直接在 option 中处理或使用简单映射
|
|
|
+const sortedData = computed(() => {
|
|
|
+ if (!props.SellerData || props.SellerData.length === 0) return []
|
|
|
+ return [...props.SellerData].sort((a, b) => a.value - b.value)
|
|
|
})
|
|
|
-const seller_data = ref(props.SellerData)
|
|
|
-const seller_interval = ref(props.Interval)
|
|
|
-const seller_ref = ref()
|
|
|
-watch(
|
|
|
- () => props.SellerData,
|
|
|
- (current) => {
|
|
|
- seller_data.value = current
|
|
|
- nextTick(() => {
|
|
|
- initChart()
|
|
|
- })
|
|
|
- },
|
|
|
- {
|
|
|
- deep: true
|
|
|
- }
|
|
|
-)
|
|
|
-watch(
|
|
|
- () => props.Interval,
|
|
|
- (current) => {
|
|
|
- seller_interval.value = current
|
|
|
- initOption.xAxis.max = Max.value
|
|
|
- initOption.xAxis.interval = interval.value
|
|
|
- nextTick(() => {
|
|
|
- initChart()
|
|
|
- })
|
|
|
- },
|
|
|
- {
|
|
|
- deep: true
|
|
|
+
|
|
|
+const xAxisMax = computed(() => props.Interval?.Max)
|
|
|
+const xAxisInterval = computed(() => props.Interval?.interval)
|
|
|
+
|
|
|
+// --- Chart Initialization ---
|
|
|
+const initChart = () => {
|
|
|
+ if (!sellerRef.value) return
|
|
|
+
|
|
|
+ // 如果实例已存在,先销毁避免重复初始化导致的内存泄漏或渲染异常
|
|
|
+ if (chartInstance) {
|
|
|
+ chartInstance.dispose()
|
|
|
}
|
|
|
-)
|
|
|
-// 最大值
|
|
|
-const Max = computed(() => {
|
|
|
- return seller_interval.value?.Max
|
|
|
-})
|
|
|
-// 刻度
|
|
|
-const interval = computed(() => {
|
|
|
- return seller_interval.value?.interval
|
|
|
-})
|
|
|
-// y轴值
|
|
|
-const sellerName = computed(() => {
|
|
|
- return seller_data.value?.map((item: any) => {
|
|
|
- return item.name
|
|
|
- })
|
|
|
-})
|
|
|
-// 数额
|
|
|
-const sellerValue = computed(() => {
|
|
|
- return seller_data.value?.map((item: any) => {
|
|
|
- return item.value
|
|
|
- })
|
|
|
-})
|
|
|
-// 获取数据中的color
|
|
|
-const ColorValue = computed(() => {
|
|
|
- return seller_data.value?.map((item: any) => {
|
|
|
- return item.color
|
|
|
- })
|
|
|
-})
|
|
|
-const initOption = reactive({
|
|
|
- // 间距
|
|
|
- grid: {
|
|
|
- top: '12%',
|
|
|
- left: '3%',
|
|
|
- right: '6%',
|
|
|
- bottom: '3%',
|
|
|
- containLabel: true // 距离包含坐标轴上的文字
|
|
|
- },
|
|
|
- // hover时的文字显示
|
|
|
- tooltip: {
|
|
|
- show: true,
|
|
|
- backgroundColor: '#2b2f36',
|
|
|
- borderColor: '#2b2f36',
|
|
|
- formatter: function (params: any) {
|
|
|
- var str =
|
|
|
- params.name +
|
|
|
- '<div style= ' +
|
|
|
- 'color:#FFF>' +
|
|
|
- params.marker +
|
|
|
- formatNumber(params.value) +
|
|
|
- '</div>'
|
|
|
- return str
|
|
|
+
|
|
|
+ chartInstance = echarts.init(sellerRef.value)
|
|
|
+ updateOption()
|
|
|
+
|
|
|
+ // 绑定点击事件
|
|
|
+ chartInstance.off('click') // 防止重复绑定
|
|
|
+ chartInstance.on('click', handleChartClick)
|
|
|
+
|
|
|
+ // 监听窗口大小变化
|
|
|
+ window.addEventListener('resize', handleResize)
|
|
|
+}
|
|
|
+
|
|
|
+const updateOption = () => {
|
|
|
+ if (!chartInstance) return
|
|
|
+
|
|
|
+ const isDark = themeStore.theme === 'dark'
|
|
|
+ const gridColor = isDark ? '#3F434A' : '#eaebed'
|
|
|
+ const textColor = '#B5B9BF'
|
|
|
+ const toolboxBorderColor = isDark ? '#f0f1f3' : '#ed6d00'
|
|
|
+ const toolboxBgColor = isDark ? '#2B2F36' : '#fff'
|
|
|
+
|
|
|
+ const option = {
|
|
|
+ grid: {
|
|
|
+ top: '12%',
|
|
|
+ left: '3%',
|
|
|
+ right: '6%',
|
|
|
+ bottom: '3%',
|
|
|
+ containLabel: true
|
|
|
},
|
|
|
- textStyle: {
|
|
|
- color: '#FFF',
|
|
|
- fontWeight: 700,
|
|
|
- fontFamily: 'Lato-Light',
|
|
|
- fontSize: '14px'
|
|
|
- }
|
|
|
- },
|
|
|
- // X轴
|
|
|
- xAxis: {
|
|
|
- splitLine: {
|
|
|
- lineStyle: {
|
|
|
- type: 'dashed',
|
|
|
- color: '#eaebed'
|
|
|
+ tooltip: {
|
|
|
+ show: true,
|
|
|
+ backgroundColor: '#2b2f36',
|
|
|
+ borderColor: '#2b2f36',
|
|
|
+ formatter: (params: any) => {
|
|
|
+ return `${params.name}<div style="color:#FFF">${params.marker}${formatNumber(params.value)}</div>`
|
|
|
+ },
|
|
|
+ textStyle: {
|
|
|
+ color: '#FFF',
|
|
|
+ fontWeight: 700,
|
|
|
+ fontFamily: 'Lato-Light',
|
|
|
+ fontSize: '14px'
|
|
|
}
|
|
|
},
|
|
|
- type: 'value',
|
|
|
- axisLine: {
|
|
|
- show: false
|
|
|
+ xAxis: {
|
|
|
+ type: 'value',
|
|
|
+ splitLine: {
|
|
|
+ lineStyle: {
|
|
|
+ type: 'dashed',
|
|
|
+ color: gridColor
|
|
|
+ }
|
|
|
+ },
|
|
|
+ axisLine: { show: false },
|
|
|
+ axisLabel: {
|
|
|
+ fontFamily: 'Lato-Light',
|
|
|
+ color: textColor,
|
|
|
+ formatter: (value: number) => formatNumber(value, 0)
|
|
|
+ },
|
|
|
+ min: 0,
|
|
|
+ max: xAxisMax.value, // 直接使用 computed,echarts setOption 会处理响应式更新
|
|
|
+ interval: xAxisInterval.value
|
|
|
},
|
|
|
- axisLabel: {
|
|
|
- fontFamily: 'Lato-Light',
|
|
|
- color: '#B5B9BF',
|
|
|
- formatter: function (value: any) {
|
|
|
- return formatNumber(value, 0)
|
|
|
+ yAxis: {
|
|
|
+ type: 'category',
|
|
|
+ data: sortedData.value.map((item) => item.name),
|
|
|
+ axisLine: { show: false },
|
|
|
+ axisTick: { show: false },
|
|
|
+ axisLabel: {
|
|
|
+ fontFamily: 'Lato-Light',
|
|
|
+ color: textColor
|
|
|
}
|
|
|
+ // 移除 yAxis 上的 tooltip 配置,通常 tooltip 在根级别或 xAxis 控制即可,避免冲突
|
|
|
},
|
|
|
- min: 0, // 最小值
|
|
|
- max: Max.value, // 最大值
|
|
|
- interval: interval.value // 刻度
|
|
|
- },
|
|
|
- // y轴
|
|
|
- yAxis: {
|
|
|
- axisLine: {
|
|
|
- show: false
|
|
|
- },
|
|
|
- axisTick: {
|
|
|
- show: false
|
|
|
- },
|
|
|
- axisLabel: {
|
|
|
- fontFamily: 'Lato-Light',
|
|
|
- color: '#B5B9BF'
|
|
|
- },
|
|
|
- type: 'category',
|
|
|
- data: sellerName,
|
|
|
- // 工具提示
|
|
|
- tooltip: {
|
|
|
- trigger: 'axis', // 触发类型,轴触发,axis为鼠标移到一条柱状图显示
|
|
|
- axisPointer: {
|
|
|
- type: 'line', // 默认为line,line为直线,cross为十字准星,shadow为阴影
|
|
|
- z: 0,
|
|
|
- lineStyle: {
|
|
|
- color: '#FFF'
|
|
|
+ series: [
|
|
|
+ {
|
|
|
+ type: 'bar',
|
|
|
+ data: sortedData.value.map((item) => item.value),
|
|
|
+ barWidth: '20',
|
|
|
+ barCategoryGap: '0%',
|
|
|
+ itemStyle: {
|
|
|
+ color: (params: { dataIndex: number }) => {
|
|
|
+ const colors = sortedData.value.map((item) => item.color).filter(Boolean)
|
|
|
+ if (colors.length === 0) return undefined // 让 ECharts 使用默认色
|
|
|
+ return colors[params.dataIndex % colors.length]
|
|
|
+ }
|
|
|
+ },
|
|
|
+ label: {
|
|
|
+ show: true,
|
|
|
+ color: '#646A73',
|
|
|
+ position: 'right',
|
|
|
+ fontFamily: 'Lato-Light',
|
|
|
+ formatter: (data: { value: number }) => formatNumber(data.value)
|
|
|
}
|
|
|
}
|
|
|
- }
|
|
|
- },
|
|
|
- series: [
|
|
|
- {
|
|
|
- type: 'bar',
|
|
|
- data: sellerValue,
|
|
|
- barWidth: '20',
|
|
|
- itemStyle: {
|
|
|
- color: function (params: { dataIndex: number }) {
|
|
|
- return ColorValue.value[params.dataIndex % ColorValue.value.length]
|
|
|
+ ],
|
|
|
+ toolbox: {
|
|
|
+ top: 4,
|
|
|
+ right: 8,
|
|
|
+ showTitle: false,
|
|
|
+ iconStyle: {
|
|
|
+ borderColor: toolboxBorderColor
|
|
|
+ },
|
|
|
+ emphasis: {
|
|
|
+ iconStyle: {
|
|
|
+ borderColor: '#ff7500'
|
|
|
}
|
|
|
},
|
|
|
- barCategoryGap: '0%', // 消除不同系列间的间隔
|
|
|
- // 设置柱形文字的样式
|
|
|
- label: {
|
|
|
- show: true,
|
|
|
- color: '#646A73',
|
|
|
- position: 'right',
|
|
|
- fontFamily: 'Lato-Light',
|
|
|
- // 数据每三位加一个逗号
|
|
|
- formatter: function (data: { value: { toString: () => string } }) {
|
|
|
- return formatNumber(Number(data.value.toString()))
|
|
|
+ feature: {
|
|
|
+ saveAsImage: {
|
|
|
+ icon: 'path://M588.8 821.44H179.52a38.4 38.4 0 0 1-38.4-38.4v-105.088l202.432-115.584 112.96 64.576c7.872 4.48 17.6 4.48 25.472 0l400.768-228.8-0.32-0.512h0.64v-156.8a89.6 89.6 0 0 0-89.6-89.6H179.456a89.6 89.6 0 0 0-89.6 89.6v542.208a89.6 89.6 0 0 0 89.6 89.6H588.8v-51.2zM141.184 240.896a38.4 38.4 0 0 1 38.4-38.4h613.824a38.4 38.4 0 0 1 38.4 38.4v127.36L469.248 575.104 356.288 510.72a25.6 25.6 0 0 0-19.2-2.496l-6.144 2.56-189.76 108.288V240.896z m168.448 226.432c44.416 0 80.96-33.792 85.376-76.992l0.384-8.768c0-44.416-33.728-80.96-76.992-85.376l-8.768-0.448c-47.36 0-85.824 38.4-85.824 85.76l0.448 8.832c4.096 40.32 36.16 72.448 76.544 76.544l8.832 0.448z m0-51.2a34.624 34.624 0 1 1 0-69.312 34.624 34.624 0 0 1 0 69.312z m445.888 449.024a25.6 25.6 0 0 0 36.224-0.064l138.368-138.368-36.224-36.224-94.592 94.656V545.536h-51.2v239.616l-94.72-94.656-36.224 36.224 138.368 138.432z',
|
|
|
+ show: true,
|
|
|
+ name: props.saveImageName,
|
|
|
+ type: 'png',
|
|
|
+ backgroundColor: toolboxBgColor
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
- ],
|
|
|
- toolbox: {
|
|
|
- top: 4,
|
|
|
- right: 8,
|
|
|
- iconStyle: {
|
|
|
- borderColor: '#ed6d00'
|
|
|
- },
|
|
|
- emphasis: {
|
|
|
- iconStyle: {
|
|
|
- borderColor: '#ff7500'
|
|
|
- } // hover上去时的颜色
|
|
|
- },
|
|
|
- feature: {
|
|
|
- saveAsImage: {
|
|
|
- icon: 'path://M588.8 821.44H179.52a38.4 38.4 0 0 1-38.4-38.4v-105.088l202.432-115.584 112.96 64.576c7.872 4.48 17.6 4.48 25.472 0l400.768-228.8-0.32-0.512h0.64v-156.8a89.6 89.6 0 0 0-89.6-89.6H179.456a89.6 89.6 0 0 0-89.6 89.6v542.208a89.6 89.6 0 0 0 89.6 89.6H588.8v-51.2zM141.184 240.896a38.4 38.4 0 0 1 38.4-38.4h613.824a38.4 38.4 0 0 1 38.4 38.4v127.36L469.248 575.104 356.288 510.72a25.6 25.6 0 0 0-19.2-2.496l-6.144 2.56-189.76 108.288V240.896z m168.448 226.432c44.416 0 80.96-33.792 85.376-76.992l0.384-8.768c0-44.416-33.728-80.96-76.992-85.376l-8.768-0.448c-47.36 0-85.824 38.4-85.824 85.76l0.448 8.832c4.096 40.32 36.16 72.448 76.544 76.544l8.832 0.448z m0-51.2a34.624 34.624 0 1 1 0-69.312 34.624 34.624 0 0 1 0 69.312z m445.888 449.024a25.6 25.6 0 0 0 36.224-0.064l138.368-138.368-36.224-36.224-94.592 94.656V545.536h-51.2v239.616l-94.72-94.656-36.224 36.224 138.368 138.432z',
|
|
|
- show: true,
|
|
|
- name: props.saveImageName,
|
|
|
- type: 'png',
|
|
|
- backgroundColor: '#fff'
|
|
|
- }
|
|
|
- },
|
|
|
- showTitle: false
|
|
|
}
|
|
|
-})
|
|
|
|
|
|
+ chartInstance.setOption(option, { notMerge: false }) // notMerge: false 允许增量更新
|
|
|
+}
|
|
|
+
|
|
|
+const handleResize = () => {
|
|
|
+ chartInstance?.resize()
|
|
|
+}
|
|
|
+
|
|
|
+const handleChartClick = (params: any) => {
|
|
|
+ const clickedItem = sortedData.value.find((item) => item.name === params.name)
|
|
|
+ if (clickedItem) {
|
|
|
+ emits('ClickParams', {
|
|
|
+ name: clickedItem.name,
|
|
|
+ cityName: clickedItem.city_name,
|
|
|
+ value: clickedItem.value
|
|
|
+ })
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// --- Watchers ---
|
|
|
+// 监听数据变化
|
|
|
+watch(
|
|
|
+ () => props.SellerData,
|
|
|
+ () => {
|
|
|
+ nextTick(() => {
|
|
|
+ updateOption()
|
|
|
+ })
|
|
|
+ },
|
|
|
+ { deep: true }
|
|
|
+)
|
|
|
+
|
|
|
+// 监听区间配置变化
|
|
|
+watch(
|
|
|
+ () => props.Interval,
|
|
|
+ () => {
|
|
|
+ nextTick(() => {
|
|
|
+ updateOption()
|
|
|
+ })
|
|
|
+ },
|
|
|
+ { deep: true }
|
|
|
+)
|
|
|
+
|
|
|
+// 监听主题变化
|
|
|
+watch(
|
|
|
+ () => themeStore.theme,
|
|
|
+ () => {
|
|
|
+ nextTick(() => {
|
|
|
+ updateOption()
|
|
|
+ })
|
|
|
+ },
|
|
|
+ { immediate: true }
|
|
|
+)
|
|
|
+
|
|
|
+// --- Lifecycle ---
|
|
|
onMounted(() => {
|
|
|
initChart()
|
|
|
- clickParams()
|
|
|
- watch(
|
|
|
- () => themeStore.theme,
|
|
|
- (newVal) => {
|
|
|
- if (newVal === 'dark') {
|
|
|
- initOption.xAxis.splitLine.lineStyle.color = '#3F434A'
|
|
|
- initOption.toolbox.iconStyle.borderColor = '#f0f1f3'
|
|
|
- initOption.toolbox.feature.saveAsImage.backgroundColor = '#2B2F36'
|
|
|
- initChart()
|
|
|
- } else {
|
|
|
- initOption.xAxis.splitLine.lineStyle.color = '#eaebed'
|
|
|
- initOption.toolbox.iconStyle.borderColor = '#2B2F36'
|
|
|
- initOption.toolbox.feature.saveAsImage.backgroundColor = '#fff'
|
|
|
- initChart()
|
|
|
- }
|
|
|
- },
|
|
|
- {
|
|
|
- immediate: true,
|
|
|
- deep: true
|
|
|
- }
|
|
|
- )
|
|
|
})
|
|
|
-const emits = defineEmits(['ClickParams'])
|
|
|
-const paramsdata = ref()
|
|
|
-const paramscityname = ref()
|
|
|
-const clickParams = () => {
|
|
|
- const seller_chart = echarts.init(seller_ref.value)
|
|
|
- // 监听点击事件
|
|
|
- seller_chart.on('click', function (params) {
|
|
|
- paramsdata.value = params.name
|
|
|
- seller_data.value?.forEach((item: any) => {
|
|
|
- if (item.name == paramsdata.value) {
|
|
|
- paramscityname.value = item.city_name
|
|
|
- }
|
|
|
+
|
|
|
+onBeforeUnmount(() => {
|
|
|
+ if (chartInstance) {
|
|
|
+ chartInstance.dispose()
|
|
|
+ chartInstance = null
|
|
|
+ }
|
|
|
+ window.removeEventListener('resize', handleResize)
|
|
|
+})
|
|
|
+
|
|
|
+// --- Expose ---
|
|
|
+// 如果父组件需要访问内部数据,可以通过 emit 返回,或者暴露特定方法
|
|
|
+// 原代码暴露了 ref,这里为了保持兼容,创建一个响应式对象暴露
|
|
|
+const exposedData = ref({
|
|
|
+ paramsdata: '',
|
|
|
+ paramscityname: ''
|
|
|
+})
|
|
|
+
|
|
|
+// 拦截 emit 来更新暴露的数据 (可选,取决于父组件如何使用)
|
|
|
+const originalEmit = emits
|
|
|
+// 注意:在 setup 中直接重写 emits 比较麻烦,通常建议父组件直接监听事件获取数据。
|
|
|
+// 如果必须保持原有 expose 行为,可以在 click handler 里更新这个 ref
|
|
|
+const handleChartClickWithExpose = (params: any) => {
|
|
|
+ const clickedItem = sortedData.value.find((item) => item.name === params.name)
|
|
|
+ if (clickedItem) {
|
|
|
+ exposedData.value.paramsdata = clickedItem.name
|
|
|
+ exposedData.value.paramscityname = clickedItem.city_name || ''
|
|
|
+ emits('ClickParams', {
|
|
|
+ name: clickedItem.name,
|
|
|
+ cityName: clickedItem.city_name,
|
|
|
+ value: clickedItem.value
|
|
|
})
|
|
|
- emits('ClickParams')
|
|
|
- })
|
|
|
-}
|
|
|
-const initChart = () => {
|
|
|
- seller_data.value?.sort((a: any, b: any) => {
|
|
|
- return a.value - b.value // 从大到小排序
|
|
|
- })
|
|
|
- const seller_chart = echarts.init(seller_ref.value)
|
|
|
- //图表响应式
|
|
|
- seller_chart.setOption(initOption)
|
|
|
- //图表响应式
|
|
|
- window.addEventListener('resize', () => {
|
|
|
- seller_chart.resize()
|
|
|
- })
|
|
|
+ }
|
|
|
}
|
|
|
+// 重新绑定带 expose 逻辑的点击事件
|
|
|
+watch(
|
|
|
+ () => chartInstance,
|
|
|
+ (newInst) => {
|
|
|
+ if (newInst) {
|
|
|
+ newInst.off('click')
|
|
|
+ newInst.on('click', handleChartClickWithExpose)
|
|
|
+ }
|
|
|
+ }
|
|
|
+)
|
|
|
+
|
|
|
defineExpose({
|
|
|
- paramsdata,
|
|
|
- paramscityname
|
|
|
+ paramsdata: computed(() => exposedData.value.paramsdata),
|
|
|
+ paramscityname: computed(() => exposedData.value.paramscityname)
|
|
|
})
|
|
|
</script>
|
|
|
|
|
|
<template>
|
|
|
<div class="com-container">
|
|
|
- <div ref="seller_ref" id="seller_chart"></div>
|
|
|
+ <div ref="sellerRef" class="seller-chart"></div>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
@@ -264,8 +294,10 @@ defineExpose({
|
|
|
overflow: hidden;
|
|
|
position: relative;
|
|
|
}
|
|
|
-#seller_chart {
|
|
|
+
|
|
|
+.seller-chart {
|
|
|
width: 100%;
|
|
|
height: 310px;
|
|
|
+ // 移除 #id 选择器,使用 class 更符合 Vue 规范
|
|
|
}
|
|
|
</style>
|