From 065a05d9f4f89d9481d1b13cca476af09b5ffe73 Mon Sep 17 00:00:00 2001 From: bunny <1319900154@qq.com> Date: Sat, 24 May 2025 19:31:33 +0800 Subject: [PATCH] =?UTF-8?q?:sparkles:=20=E5=8F=B3=E4=BE=A7=E4=B8=8A?= =?UTF-8?q?=E9=83=A8=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build/utils.ts | 54 ++++++-- mock/data-analyse.ts | 15 +++ src/api/dataAnalyse.ts | 5 + src/store/modules/dataAnalyse.ts | 8 ++ src/utils/chart.ts | 16 +++ .../data-analyse/charts/content-footer.ts | 5 +- src/views/data-analyse/charts/right-body.ts | 97 +++++++++++++- src/views/data-analyse/charts/right-footer.ts | 24 +++- src/views/data-analyse/charts/right-top.ts | 120 ++++++++++++++++++ .../components/data-analyse-right.vue | 36 +++++- 10 files changed, 359 insertions(+), 21 deletions(-) create mode 100644 src/views/data-analyse/charts/right-top.ts diff --git a/build/utils.ts b/build/utils.ts index baabe97..ae05eb4 100644 --- a/build/utils.ts +++ b/build/utils.ts @@ -94,20 +94,54 @@ export const logOutputSize = (): string => { return `${size.toFixed(2)} ${units[index]}`; } - // 计算文件夹字节大小 - function getFolderSize(folderPath: string) { + /** + * 计算文件夹大小(排除图片文件) + * @param folderPath 文件夹路径 + * @param currentDepth 当前递归深度(内部使用) + * @returns 文件夹大小(字节) + */ + function getFolderSize(folderPath: string, currentDepth: number = 0): number { + // 公共常量定义 + const EXCLUDED_FILE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg']; + const MAX_DEPTH = 10; // 最大递归深度限制 + + // 安全检查 + if (!fs.existsSync(folderPath)) { + throw new Error(`文件夹不存在: ${folderPath}`); + } + + if (currentDepth > MAX_DEPTH) { + console.warn(`达到最大递归深度 ${MAX_DEPTH},停止遍历: ${folderPath}`); + return 0; + } + let size = 0; - fs.readdirSync(folderPath).forEach((fileName: string) => { - const filePath = path.join(folderPath, fileName); - const stats = fs.statSync(filePath); + try { + const files = fs.readdirSync(folderPath); - if (stats.isFile()) { - size += stats.size; - } else if (stats.isDirectory()) { - size += getFolderSize(filePath); + for (const fileName of files) { + const filePath = path.join(folderPath, fileName); + + try { + const stats = fs.statSync(filePath); + + if (stats.isFile()) { + // 检查文件扩展名是否在排除列表中 + const ext = path.extname(fileName).toLowerCase(); + if (!EXCLUDED_FILE_EXTENSIONS.includes(ext)) { + size += stats.size; + } + } else if (stats.isDirectory()) { + size += getFolderSize(filePath, currentDepth + 1); + } + } catch (error) { + console.error(`无法访问文件: ${filePath}`, error); + } } - }); + } catch (error) { + console.error(`无法读取目录: ${folderPath}`, error); + } return size; } diff --git a/mock/data-analyse.ts b/mock/data-analyse.ts index 0ab554e..4fca254 100644 --- a/mock/data-analyse.ts +++ b/mock/data-analyse.ts @@ -71,6 +71,7 @@ export default defineFakeRoute([ message: '操作成功', }), }, + // 销售设备数量区域占比 { url: `${BASE_URL}/region-sales-ratio`, method: 'GET', @@ -101,4 +102,18 @@ export default defineFakeRoute([ message: '操作成功', }), }, + { + url: `${BASE_URL}/data-ratio`, + method: 'GET', + response: () => ({ + code: 200, + data: Array(6) + .fill(0) + .map((_, index) => ({ + name: `数据-${index + 1}`, + value: randomNumber(), + })), + message: '操作成功', + }), + }, ]); diff --git a/src/api/dataAnalyse.ts b/src/api/dataAnalyse.ts index 4ac6a6c..b315486 100644 --- a/src/api/dataAnalyse.ts +++ b/src/api/dataAnalyse.ts @@ -24,3 +24,8 @@ export const getDataShow = () => { export const getRegionSalesRatio = () => { return request.get('data-analyse/region-sales-ratio'); }; + +/* 数据占有率 */ +export const getDataRatio = () => { + return request.get('data-analyse/data-ratio'); +}; diff --git a/src/store/modules/dataAnalyse.ts b/src/store/modules/dataAnalyse.ts index 414fd97..e80060c 100644 --- a/src/store/modules/dataAnalyse.ts +++ b/src/store/modules/dataAnalyse.ts @@ -3,6 +3,7 @@ import { defineStore } from 'pinia'; import { getBrandsDistribution, getCompanySalesDistribution, + getDataRatio, getDataShow, getDeviceSalesStats, getRegionSalesRatio, @@ -25,6 +26,8 @@ export const useDataAnalyseStore = defineStore('dataAnalyseStore', { dataShow: [], // 销售设备数量区域占比 regionSalesRatio: [], + // 数据占有率 + dataRatio: [], }), actions: { /* 销售设备总量 */ @@ -51,6 +54,11 @@ export const useDataAnalyseStore = defineStore('dataAnalyseStore', { async fetchRegionSalesRatio() { this.regionSalesRatio = await getRegionSalesRatio(); }, + + /* 数据占有率 */ + async fetchDataRatio() { + this.dataRatio = await getDataRatio(); + }, }, }); diff --git a/src/utils/chart.ts b/src/utils/chart.ts index 4e097f1..bc69184 100644 --- a/src/utils/chart.ts +++ b/src/utils/chart.ts @@ -1,4 +1,5 @@ import { useDebounceFn, useEventListener } from '@vueuse/core'; +import type { EChartsType } from 'echarts'; import echarts from '@/plugins/echarts'; @@ -38,3 +39,18 @@ export const graphicLinearGradient = ( ] ); }; + +export const resetSelect = (myChart: EChartsType) => { + myChart.dispatchAction({ + type: 'downplay', + seriesIndex: 0, + }); +}; + +export const selectSector = (myChart: EChartsType, dataIndex: number) => { + myChart.dispatchAction({ + type: 'highlight', + seriesIndex: 0, + dataIndex, + }); +}; diff --git a/src/views/data-analyse/charts/content-footer.ts b/src/views/data-analyse/charts/content-footer.ts index 45bd0d5..a8e9ccd 100644 --- a/src/views/data-analyse/charts/content-footer.ts +++ b/src/views/data-analyse/charts/content-footer.ts @@ -1,3 +1,4 @@ +import type { EChartsOption } from 'echarts'; import type { Ref } from 'vue'; import echarts from '@/plugins/echarts'; @@ -5,7 +6,7 @@ import { debounceChart } from '@/utils/chart'; let myChart = null; -const option = { +const option: EChartsOption = { grid: { left: '4%', right: '4%', @@ -21,9 +22,9 @@ const option = { }, tooltip: { trigger: 'axis' }, legend: { + show: false, data: ['增加值'], icon: 'rich', - show: true, itemWidth: 18, itemHeight: 2, textStyle: { color: '#AFBDD1', fontSize: '12px' }, diff --git a/src/views/data-analyse/charts/right-body.ts b/src/views/data-analyse/charts/right-body.ts index af8ac14..d1d04c2 100644 --- a/src/views/data-analyse/charts/right-body.ts +++ b/src/views/data-analyse/charts/right-body.ts @@ -1,3 +1,4 @@ +import type { EChartsOption } from 'echarts'; import type { Ref } from 'vue'; import echarts from '@/plugins/echarts'; @@ -5,7 +6,101 @@ import { debounceChart } from '@/utils/chart'; let myChart = null; -const option = {}; +const option: EChartsOption = { + grid: { + left: '4%', + right: '4%', + bottom: '19%', + top: '20%', + containLabel: true, + }, + title: { + text: '单位:(万元)', + textStyle: { color: '#83A2C0FF', fontSize: 12 }, + top: '4%', + left: '2%', + }, + tooltip: { trigger: 'axis' }, + legend: { + show: false, + data: ['增加值'], + icon: 'rich', + itemWidth: 18, + itemHeight: 2, + textStyle: { color: '#AFBDD1', fontSize: '12px' }, + top: 8, + right: 10, + itemGap: 34, + }, + + xAxis: { + data: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + type: 'category', + boundaryGap: false, + axisLine: { + symbol: 'none', + lineStyle: { + color: '#50637A', + }, + }, + axisTick: { show: false }, + axisLabel: { + interval: 0, + color: '#6071A9', + fontSize: 12, + padding: [10, 0, 0, 0], + }, + }, + yAxis: { + type: 'value', + axisLabel: { + color: '#6071A9', + fontSize: 12, + padding: [0, 10, 0, 0], + }, + splitLine: { + lineStyle: { + color: '#50637A', + type: 'dashed', + }, + }, + }, + series: [ + { + name: '增加值', + data: [], + type: 'line', + // smooth: true, + color: '#00F7FF', + lineStyle: { + width: 2, + }, + areaStyle: { + color: new echarts.graphic.LinearGradient( + 0, + 0, + 0, + 1, + [ + { + offset: 0, + color: 'rgba(0, 247, 255, .6)', + }, + { + offset: 0.8, + color: 'rgba(0, 247, 255, .2)', + }, + ], + false + ), + shadowColor: 'rgba(0, 0, 0, 0.1)', + shadowBlur: 10, + }, + symbol: 'circle', + symbolSize: 6, + }, + ], +}; /** 渲染图表 */ export const renderBodyChart = (element: Ref) => { diff --git a/src/views/data-analyse/charts/right-footer.ts b/src/views/data-analyse/charts/right-footer.ts index 89a3ead..14e7312 100644 --- a/src/views/data-analyse/charts/right-footer.ts +++ b/src/views/data-analyse/charts/right-footer.ts @@ -13,7 +13,29 @@ const data = [ { name: '朱八', value: 10086 }, ]; -const option = {}; +const option = { + title: { + text: '34467', + subtext: '历史预警数', + left: 'center', + top: 'center', + textStyle: { color: '#fff', fontSize: 34 }, + subtextStyle: { color: '#fff', fontSize: 16 }, + }, + grid: { containLabel: false }, + polar: { radius: [60, '100%'] }, + angleAxis: { show: false, startAngle: 90 }, + radiusAxis: { show: false, type: 'category' }, + tooltip: {}, + series: { + type: 'bar', + data: [], + barWidth: 20, + barGap: 0, + coordinateSystem: 'polar', + label: { show: true, position: 'middle' }, + }, +}; /** 渲染图表 */ export const renderFooterChart = (element: Ref) => { diff --git a/src/views/data-analyse/charts/right-top.ts b/src/views/data-analyse/charts/right-top.ts new file mode 100644 index 0000000..b3f60e9 --- /dev/null +++ b/src/views/data-analyse/charts/right-top.ts @@ -0,0 +1,120 @@ +import { useIntervalFn } from '@vueuse/core'; +import type { EChartsOption } from 'echarts'; +import { type Ref, ref } from 'vue'; + +import echarts from '@/plugins/echarts'; +import { debounceChart, resetSelect, selectSector } from '@/utils/chart'; + +const totalIndex = ref(0); +const currentIndex = ref(0); +let myChart = null; + +const option: EChartsOption = { + tooltip: { position: 'right', trigger: 'item' }, + legend: { + orient: 'horizontal', // 垂直排列 + right: 0, // 距离右侧10px + left: 240, + top: 'center', // 垂直居中 + align: 'left', // 文本左对齐 + icon: 'circle', // 圆形图标 + itemGap: 50, // 图例项间隔 + itemWidth: 10, // 图例标记宽度 + itemHeight: 10, // 图例标记高度 + textStyle: { color: '#fff', fontSize: 14 }, + formatter(name) { + const series = myChart.getOption().series; + // 获取对应系列的数据 + const seriesData = series[0].data; + const dataIndex = series[0].data.findIndex((item) => item.name === name); + const value = seriesData[dataIndex]?.value; + + return `${name}: ${value}%`; + }, + }, + grid: { + left: '0%', // 图表左侧不留白 + right: '0%', // 为图例留出25%空间 + top: '0%', + bottom: '0%', + containLabel: false, + }, + series: [ + { + name: '数据占有率', + type: 'pie', + radius: ['54%', '70%'], + center: ['24%', '50%'], + avoidLabelOverlap: false, + padAngle: 4, + itemStyle: { borderRadius: 4 }, + label: { show: false, position: 'center' }, + emphasis: { + label: { + show: true, + color: '#fff', + fontSize: 29, + fontWeight: 'lighter', + formatter: ({ value }) => { + return `${value}%\n总占比`; + }, + }, + }, + labelLine: { show: false }, + data: [], + }, + ], + visualMap: { + show: false, + showLabel: false, + type: 'piecewise', + pieces: [ + { min: 0, max: 20, label: '低', color: '#FF6363' }, + { min: 20, max: 30, label: '低中', color: '#FFCC00' }, + { min: 30, max: 50, label: '中', color: '#00FFFF' }, + { min: 50, max: 70, label: '中高', color: '#00CCFF' }, + { min: 70, max: 100, label: '高', color: '#0096FF' }, + ], + seriesIndex: 0, + }, +}; + +/** 渲染图表 */ +export const renderTopChart = (element: Ref) => { + myChart = echarts.init(element.value, null, { + renderer: 'canvas', + devicePixelRatio: window.devicePixelRatio, + }); + + debounceChart(myChart); + + myChart.setOption(option); + + // 轮播数据 + const { pause, resume } = useIntervalFn(() => { + currentIndex.value = currentIndex.value >= totalIndex.value ? 0 : currentIndex.value; + + resetSelect(myChart); + selectSector(myChart, currentIndex.value); + + currentIndex.value++; + }, 1000); + + // 鼠标移入移出暂停动画 + element.value.addEventListener('mouseenter', () => { + resetSelect(myChart); + pause(); + }); + element.value.addEventListener('mouseleave', () => { + resume(); + }); +}; + +/** 更新图表数据 */ +export const updateTopChart = (data: any) => { + totalIndex.value = data.length; + + const series = myChart.getOption().series; + series[0].data = data; + myChart.setOption({ series }); +}; diff --git a/src/views/data-analyse/components/data-analyse-right.vue b/src/views/data-analyse/components/data-analyse-right.vue index e654516..274bd9f 100644 --- a/src/views/data-analyse/components/data-analyse-right.vue +++ b/src/views/data-analyse/components/data-analyse-right.vue @@ -1,23 +1,40 @@ @@ -25,7 +42,7 @@ onMounted(() => {
-
+
@@ -50,6 +67,11 @@ onMounted(() => { margin: 10px 0 0 0; width: 100%; height: 305px; + + &-chart { + width: 100%; + height: 100%; + } } &__body {