package.json 本地包导入tgz

参考:npm如何引入本地自建的包和需要维护的包_npm 引用本地包-CSDN博客、[如何在项目中引用本地的npm包_npm引入本地包-CSDN博客](https://blog.csdn.net/qq_57956183/article/details/132346194#:~:text=package.json中配置引用本地依赖包。 以下是具体操作步骤: 1. 打开您的 package.json,文件,找到 dependencies 部分。 2. 不要指定包名的版本号,而是指定依赖项所在的本地路径。)、

主要是为了能够使用下载的会员图标库,

package.json:

作为dependencies引入,需要注意的是文件位置,可以通过cd的方式,确认路径是否有问题

还有需要注意的是,打包的tgz中必须包含package.json,否则会报错,这个是卡最多的地方

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"name": "ai-report-assistant-frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc -b && vite build",
"preview": "vite preview"
},
"dependencies": {
"@fortawesome/fontawesome-free": "file:./fontawesome-pro-6.6.0-web.tgz",
},
}

然后需要在main.ts中进行引用

1
import '@fortawesome/fontawesome-free/css/all.min.css';

git冲突

参考:还在恐惧 Git 冲突? 一篇文章拯救你 - 知乎 (zhihu.com)

图标库

Chart Bullet Classic Regular Icon | Font Awesom

clip-path

下面的代码将Arrow tail改为四边形中间内凹一个正三角形的样式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<div class="flex justify-center items-center relative">
<!-- Arrow tail-->
<div class="absolute top-0 -left-3 w-3 h-16 bg-gray-300 opacity-75"
style="clip-path: polygon(0 0, 100% 50%, 0 100%);"></div>

<!-- Main body -->
<div class="w-52 h-16 bg-gray-300 opacity-75 cursor-pointer p-2">
<p>数据源</p>
</div>

<!-- Arrow head -->
<div class="absolute top-0 -right-5 w-5 h-16 bg-gray-300 opacity-75"
style="clip-path: polygon(0 0%, 100% 50%, 0 100%);"></div>
</div>

实现一个四边形中间凹入一个正三角形的效果,核心在于使用 CSS 中的 clip-path 属性来裁剪出我们需要的形状。我们将利用 polygon 函数定义裁剪路径的每个顶点坐标。下面是实现的具体步骤:

  1. 了解 clip-path: polygon()

clip-path: polygon() 允许我们通过指定顶点坐标来裁剪元素的形状。坐标使用百分比来表示,起点为左上角 (0%, 0%),终点为右下角 (100%, 100%)

基础四边形

首先,绘制一个基本的四边形,使用 clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%)。这将裁剪出一个完整的矩形。

1
clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%);

这个路径依次描述四个顶点:

  • (0 0):左上角
  • (100% 0):右上角
  • (100% 100%):右下角
  • (0 100%):左下角
  1. 内凹一个正三角形

为了让中间部分凹入一个正三角形,我们需要在矩形的一侧插入一个新的点,使得矩形的一条边向内凹。

假设你希望左边中间凹入,我们可以通过在左侧边线中间插入一个新的点来实现。将其设置为 (25% 50%),它表示相对于左边线,水平向右 25% 的位置,高度为 50%(即中间位置)。这样可以在四边形的中间位置凹入一个三角形。

  1. 实现凹入的多边形

要在左侧边缘中间凹入一个三角形,我们只需在基本四边形的坐标中插入 (25% 50%) 作为中间点,最终的裁剪路径如下:

1
clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%, 25% 50%);
  • (0 0):左上角
  • (100% 0):右上角
  • (100% 100%):右下角
  • (0 100%):左下角
  • **(25% 50%)**:左边线的中间位置,凹入三角形的顶点
  1. 完整代码实现

在实际 HTML 中,给定一个 div 元素,我们将使用这个 clip-path 来裁剪其外形:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<div class="flex justify-center items-center relative">
<!-- Arrow tail -->
<div class="absolute top-0 -left-3 w-8 h-16 bg-gray-300 opacity-75"
style="clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%, 25% 50%);"></div>

<!-- Main body -->
<div class="w-52 h-16 bg-gray-300 opacity-75 cursor-pointer p-2">
<p>数据源</p>
</div>

<!-- Arrow head -->
<div class="absolute top-0 -right-5 w-5 h-16 bg-gray-300 opacity-75"
style="clip-path: polygon(0 0%, 100% 50%, 0 100%);"></div>
</div>

总结:

  • clip-path: polygon() 允许我们裁剪元素的形状。
  • 通过在四边形的顶点之间插入新的点,我们可以创建一个带有中间凹入正三角形的形状。
  • 具体的 clip-path 路径是根据顶点的顺序定义的,顶点之间连线会形成凹入的效果。

这就是如何通过 clip-path 创建一个中间凹入正三角形的四边形的具体实现原理。

低代码实现

拖拽以及伸缩

滚动跟手逻辑

代码中实现了通过@scroll="onScroll"监听滚动事件,并且在滚动时处理拖拽和调整大小的交互。

1
2
3
4
5
6
const onScroll = (scroll: { scrollLeft: number, scrollTop: number }) => {
if (activeIndex.value !== null && isInteracting.value) {
isScrolling.value = true;
// 滚动处理逻辑
}
};

获取 el-scroll 滚动距离

onScroll 方法中,传入了滚动的 scrollLeftscrollTop,可以用来实时获取滚动距离,特别是在拖动或调整大小时使用。

1
2
3
<el-scrollbar height="95%" wrap-style="width:100%;" class="flex justify-center" @scroll="onScroll">
<!-- 滚动区域内容 -->
</el-scrollbar>

onMouseDown(按下鼠标处理逻辑)

onMouseDown 用于初始化拖拽或调整大小操作,保存当前的鼠标坐标以及操作的类型(拖拽或调整大小)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const onMouseDown = (event: MouseEvent, index: number, handleType: 'drag' | 'resize') => {
isInteracting.value = true;
interactionType.value = handleType;
startX.value = event.clientX;
startY.value = event.clientY;
activeIndex.value = index;

if (handleType === 'drag') {
initialTop.value = statementItems.value[index].top;
initialLeft.value = statementItems.value[index].left;
} else if (handleType === 'resize') {
initialWidth.value = statementItems.value[index].width;
initialHeight.value = statementItems.value[index].height;
}

// 添加鼠标移动和释放的监听事件
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
};

onMouseMove(鼠标移动处理逻辑)

onMouseMove 负责拖动或调整大小操作,根据鼠标移动的距离更新元素的位置或尺寸。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const onMouseMove = (event: MouseEvent) => {
if (!isInteracting.value || activeIndex.value === null) return;

let deltaX = event.clientX - startX.value;
let deltaY = event.clientY - startY.value;

if (interactionType.value === 'drag') {
statementItems.value[activeIndex.value].top = initialTop.value + deltaY;
statementItems.value[activeIndex.value].left = initialLeft.value + deltaX;
} else if (interactionType.value === 'resize') {
statementItems.value[activeIndex.value].width = Math.max(100, initialWidth.value + deltaX);
statementItems.value[activeIndex.value].height = Math.max(100, initialHeight.value + deltaY);
}
};

onMouseUp(释放鼠标处理逻辑)

onMouseUp 用于结束拖拽或调整大小操作,解除事件监听。

1
2
3
4
5
6
7
8
9
10
11
const onMouseUp = () => {
isInteracting.value = false;
interactionType.value = null;

// 恢复文本选择
document.body.style.userSelect = '';

// 解除鼠标事件监听
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
};

自适应布局

检查遮挡逻辑

在拖拽过程中,checkCollision 用于检查当前拖动的元素是否与其他元素发生遮挡。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const checkCollision = () => {
if (activeIndex.value === null) return;

const draggedItem = statementItems.value[activeIndex.value];
statementItems.value.forEach((item, index) => {
if (index !== activeIndex.value) {
const isColliding = checkOverlap(draggedItem, item);
if (isColliding) {
moveDownItems(index, draggedItem.height + 15);
} else {
restoreAllMovedItems();
}
}
});
};

检查两个元素是否遮挡

使用 checkOverlap 函数判断两个元素是否在视觉上发生遮挡,判断依据为元素的四个边界位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
const checkOverlap = (item1: StatementItem, item2: StatementItem) => {
const item1Bottom = item1.top + item1.height;
const item1Right = item1.left + item1.width;
const item2Bottom = item2.top + item2.height;
const item2Right = item2.left + item2.width;

return (
item1.top < item2Bottom &&
item1Bottom > item2.top &&
item1.left < item2Right &&
item1Right > item2.left
);
};

归位逻辑

如果两个元素不再遮挡,使用 restoreAllMovedItems 恢复被下移的元素位置。

1
2
3
4
5
6
const restoreAllMovedItems = () => {
movedItems.forEach(index => {
statementItems.value[index].top = initialPositionsY[index]; // 恢复到初始位置
movedItems.delete(index); // 清除下移记录
});
};

elementplus主题调节

参考:https://element-plus.org/zh-CN/guide/theming

通过开发者选项,找到对应组件的颜色指代代码--el-slider-main-bg-color,然后将其放置到一个统一class类中,当然也可以放在root中,但是会导致加载速度变慢以及多样式覆盖的问题

1
2
3
4
5
6
7
8
9
10
<template>
<el-slider v-model="sliderValue" :max="maxScroll" @input="inputSlider" :show-tooltip="false"
class="el-slider-style"></el-slider>
</template>

<style lang="scss" scoped>
.el-slider-style {
--el-slider-main-bg-color: #333;
}
</style>

position:fixed相对父级元素定位

参考:https://www.zhihu.com/question/24822927

如果父级元素设置了transform属性,position:relative/absolute/fixed会基于此定位,详细请参考:transformedelement creates a containing block for all its positioned descendants

:after、:before

箭头形状组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
<template>
<div class="PipelineHeader">
<!-- 头部 -->
<div v-if="props.info.title !== '新阶段'" class="flex justify-center items-center relative cursor-pointer">
<!-- Arrow tail -->
<div :class="backgroundClass" class="absolute top-0 -left-3 w-3 h-16 opacity-80"></div>

<!-- Main body -->
<div :class="backgroundClass"
class="w-72 h-16 relative flex justify-between items-center opacity-80 overflow-hidden p-3">
<div class="flex flex-col justify-center items-start" v-if="props.info.status !== 'inProgress'">
<p class="font-bold">{{ props.info.title }}</p>
<p v-if="props.info.title==='数据源'">{{ props.info.num }}个数据源</p>
<p v-else>{{ props.info.num }}个任务</p>
</div>

<div class="flex flex-col justify-center items-start" v-else>
<p class="font-bold text-white">{{ props.info.title }}</p>
<p class="text-white">{{ props.info.completedTasks }}/{{ props.info.num }}个任务完成 | {{ props.timer }}s</p>
</div>

<!-- 未进行时三个图标 -->
<div class="flex justify-center items-center gap-4" v-if="props.info.status === 'notStarted'">
<i class="fa-light fa-pen"></i>
<i class="fa-light fa-copy"></i>
<i class="fa-light fa-trash"></i>
</div>

<!-- 完成后打勾图标 -->
<div class="absolute -top-3 right-0" v-if="props.info.status === 'completed'">
<i class="fa-solid fa-circle-check text-7xl text-emerald-300"></i>
</div>

<!-- loading图标 -->
<div class="" v-if="props.info.status === 'inProgress'">
<i class="fa-duotone fa-solid fa-loader rotating fa-xl text-white"></i>
</div>
</div>

<!-- Arrow head -->
<div :class="backgroundClass" class="absolute top-0 -right-5 w-5 h-16 opacity-80"
style="clip-path: polygon(0 0%, 100% 50%, 0 100%);">
</div>
<!-- 添加符号 -->
<div v-if="props.info.title !== '数据源'"
class="w-4 h-4 absolute -left-7 bottom-6 flex justify-center items-center rounded-xl bg-white shadow-[0_2px_6px_0_rgba(37,43,58,0.4)]">
<i class="fa-regular fa-plus fa-2xs"></i>
</div>
</div>
<!-- 头部 -->
<div v-else class="flex justify-center items-center relative cursor-pointer">
<!-- Arrow tail -->
<div class="absolute top-0 -left-3 w-3 h-16 bg-gray-200/60 opacity-80"></div>

<!-- Main body -->
<div class="w-72 h-16 flex justify-between items-center bg-gray-200/60 opacity-80 p-3">
<div class="flex flex-col justify-center items-start">
<p class="font-bold text-black/60">{{ props.info.title }}</p>
<p class="text-black/60">{{ props.info.num }}个数据源</p>
</div>
</div>

<!-- Arrow head -->
<div class="absolute top-0 -right-5 w-5 h-16 bg-gray-200/60 opacity-80"
style="clip-path: polygon(0 0%, 100% 50%, 0 100%);"></div>
<!-- 添加符号 -->
<div
class="w-4 h-4 absolute -left-7 bottom-6 flex justify-center items-center rounded-xl bg-white shadow-[0_2px_6px_0_rgba(37,43,58,0.4)]">
<i class="fa-regular fa-plus fa-2xs"></i>
</div>
</div>
</div>
</template>

<script setup lang="ts">
import { computed } from "vue"
const props = defineProps(['info','timer'])
const emit = defineEmits()


const backgroundClass = computed(() => {
if (props.info.status === 'inProgress') {
return 'bg-gray-600';
} else if (props.info.status === 'completed') {
return 'bg-emerald-100';
} else {
return 'bg-gray-200';
}
});
</script>

<style lang="scss" scoped>
.rotating {
animation: spin 2s linear infinite;
}

@keyframes spin {
0% {
transform: rotate(0deg);
}

100% {
transform: rotate(360deg);
}
}
</style>

WEB自带TTS实现语音文字互转

Statement.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
<template>
<div class="Statement" v-if="props.ifShow">
<div class="w-[1200px] h-14 shadow-xl fixed left-16 bottom-6 flex items-center bg-gray-50 rounded-3xl p-5" v-if="ifShowAI">
<input v-model="message" @keyup.enter="handleEnter" type="text" placeholder="输入消息"
class="bg-transparent outline-none flex-1 placeholder:text-text-200 placeholder:font-bold text-black ml-2" />
<el-icon size="18" class="ml-2" @click="toggleRecognition">
<Microphone />
</el-icon>
</div>
</div>
</template>



<script setup lang="ts">
// 启动语音识别
const startRecognition = () => {
const SpeechRecognition = (window as any).SpeechRecognition || (window as any).webkitSpeechRecognition;
if (!SpeechRecognition) {
alert('当前浏览器不支持语音识别功能');
return;
}

recognition = new SpeechRecognition();
recognition.lang = 'zh-CN'; // 设置语言

recognition.onresult = (event: any) => {
const transcript = event.results[0][0].transcript;
message.value = transcript; // 将识别到的文字保存到 message
};

recognition.onerror = (event: any) => {
console.error('Speech recognition error: ', event.error);
};

recognition.onend = () => {
console.log('Speech recognition ended');
isRecognizing.value = false; // 识别结束后更新状态
};

recognition.start();
isRecognizing.value = true; // 设置为识别状态
console.log('Speech recognition started');
};

// 停止语音识别
const stopRecognition = () => {
if (recognition) {
recognition.stop();
isRecognizing.value = false;
console.log('Speech recognition manually stopped');
}
};

// 切换语音识别状态
const toggleRecognition = () => {
if (isRecognizing.value) {
stopRecognition(); // 如果正在识别,则停止
} else {
startRecognition(); // 如果未在识别,则开始识别
}
};
</script>

echart封装

引用方法

statementltems.ts

下面是假数据部分,需要加新的图标时增加statementItems,type、chart、data、chartOption控制对应的图表参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import { ref } from 'vue';

import { StatementItem } from '../interfaces/StatementItem';

import airLineOptions from '../utils/airLineOptions';
import waterBarOption from '../utils/waterBarOption';
import forestPieOption from '../utils/forestPieOption';
import airHorizontalBarOption from '../utils/airHorizontalBarOption';

import funnelOptions from '../utils/funnelOptions';
import boardOptions from '../utils/boardOptions';
import radarOptions from '../utils/radarOptions';
import boxplotOptions from '../utils/boxplotOptions';
import scatterOption from '../utils/scatterOption';

import { airLineData } from '../constant/airLineData';
import { forestPieData } from '../constant/forestPieData';
import { waterBarData } from '../constant/waterBarData' ;
import { horizontalBarData } from '../constant/horizontalBarData';

import { funnelData } from '../constant/funnelData';
import { boardData } from '../constant/boardData';
import { radarData } from '../constant/radarData';
import { boxplotData } from '../constant/boxplotData';
import { scatterData } from '../constant/scatterData';

export const statementItems = ref<StatementItem[]>([
{ top: 0, left: 50, height: 200, width: 600, label: '空气质量优良天数', type: 'numbers', numbers: ["2", "8", "0","天"] },
{ top: 0, left: 680, height: 200, width: 600, label: '本年度二氧化碳总排放量', type: 'numbers', numbers: ["1", "1", "5", "亿吨",] },
{ top: 220, left: 50, height: 290, width: 1190, label: '年度空气质量统计', type: 'chart', chart: 'line', data: airLineData, chartOption: airLineOptions },
{ top: 550, left: 50, height: 290, width: 350, label: '年度碳排放来源分析', type: 'chart', chart: 'bar', data: waterBarData, chartOption: waterBarOption },
{ top: 550, left: 470, height: 290, width: 350, label: '年度森林覆盖率', type: 'chart', chart: 'pie', data: forestPieData, chartOption: forestPieOption },
{ top: 550, left: 890, height: 290, width: 350, label: '空气质量对比', type: 'chart', chart: 'horizontalBar', data: horizontalBarData, chartOption: airHorizontalBarOption }
]);

export const statementItems2 = ref<StatementItem[]>([
{ top: 0, left: 20, height: 200, width: 350, label: '本年度种草改良总量', type: 'numbers', numbers: ["4", "7", "9", "万公顷"] },
{ top: 0, left: 380, height: 290, width: 580, label: '年度绿化面积', type: 'numbers', numbers: ["8", "0", "0", "万公顷"] },
{ top: 0, left: 970, height: 200, width: 350, label: '本年度治理沙化面积', type: 'numbers', numbers: ["1", "9", "0", "万公顷"] },

{ top: 300, left: 380, height: 500, width: 538, label: '绿化面积对比', type: 'chart', chart: 'radar', data: radarData, chartOption: radarOptions },
{ top: 210, left: 20, height: 290, width: 308, label: '年度绿化来源分析', type: 'chart', chart: 'funnel', data: funnelData, chartOption: funnelOptions },
{ top: 510, left: 20, height: 290, width: 308, label: '年度碳排放来源分析', type: 'chart', chart: 'boxplot', data: boxplotData, chartOption: boxplotOptions },
{ top: 210, left: 970, height: 290, width: 308, label: '年度绿化统计', type: 'chart', chart: 'board', data: boardData, chartOption: boardOptions },

{ top: 510, left: 970, height: 290, width: 308, label: '碳排放量对比', type: 'chart', chart: 'scatter', data: scatterData, chartOption: scatterOption },
]);

Statement.vue

下面是使用假数据循环渲染部分,用到新的图表类型时,需要往getChartComponent中添加对应的图表名称

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
<template>
<div class="Statement" v-if="props.ifShow">
<el-scrollbar height="95%" wrap-style="width:100%;" class="flex justify-center" @scroll="onScroll" v-else>
<div class="w-full flex flex-col justify-center items-center self-center relative overflow-visible">
<!-- 动态渲染可拖动的元素 -->
<div v-for="(item, index) in statementItems" :key="index" :data-id="index"
:style="{ top: `${item.top}px`, left: `${item.left}px`, width: item.type === 'chart' ? 'auto' : `${item.width}px`, height: `${item.height}px`, position: 'absolute' }"
class="shadow-[0_8px_24px_rgba(0,0,0,0.04)] border rounded-lg my-5 p-5 overflow-visible bg-white"
@mouseenter="showDesign(index)" @mouseleave="hiddenDesign">
<!-- 悬浮按钮 -->
<div v-if="hoveredItem === index" @mouseenter="showDesign(index)" @mouseleave="hiddenDesign"
@mousedown="onMouseDown($event, index, 'drag')"
class="absolute w-5 h-8 top-1 -left-6 bg-gray-100 flex justify-center items-center gap-1 rounded-md cursor-move">
<i class="fa-regular fa-ellipsis-vertical" style="color: #4b5563;"></i>
<i class="fa-regular fa-ellipsis-vertical" style="color: #4b5563;"></i>
</div>


<!-- 控制大小按钮 -->
<div v-if="hoveredItem === index"
class="absolute w-5 h-8 -bottom-3 -right-2 flex cursor-nwse-resize"
@mousedown="onMouseDown($event, index, 'resize')">
<i class="fa-solid fa-corner fa-rotate-90" style="color: #4b5563;"></i>
</div>

<!-- 根据不同的类型渲染不同内容 -->
<input class="text-sm font-bold input-reset" type="text" v-model="item.label" />

<!-- 数字数据类型 -->
<div v-if="item.type === 'numbers'" class="h-full flex justify-center items-center gap-2">
<p v-for="n in item.numbers" :key="n"
class="px-2 py-5 text-4xl font-bold bg-gray-100 rounded-lg">{{ n
}}</p>
</div>

<!-- 图表类型 -->
<div v-else-if="item.type === 'chart'" class="w-full h-full">
<component :is="getChartComponent(item.chart)" :width="item.width" :height="item.height - 50"
:data="item.data" :chartOption="item.chartOption" />
</div>

<!-- 移动位置提示 -->
<div v-if="isInteracting && activeIndex === index"
class="absolute top-0 left-0 w-full h-full bg-gray-200 rounded-lg opacity-50 pointer-events-none">
<!-- 显示提示的矩形背景,拖拽时会显示 -->
</div>
</div>
</div>
</el-scrollbar>
<div class="w-[1200px] h-14 shadow-xl fixed left-16 bottom-6 flex items-center bg-gray-50 rounded-3xl p-5" v-if="ifShowAI">

<input v-model="message" @keyup.enter="handleEnter" type="text" placeholder="输入消息"
class="bg-transparent outline-none flex-1 placeholder:text-text-200 placeholder:font-bold text-black ml-2" />

<el-icon size="18" class="ml-2" @click="toggleRecognition">
<Microphone />
</el-icon>
</div>
</div>
</template>

<script setup lang="ts">
const getChartComponent = (chartType: string) => {
const chartComponents: { [key: string]: any } = {
line: LineContainer,
bar: BarContainer,
pie: PieContainer,
horizontalBar: HorizontalBarContainer,
funnel: FunnelContainer,
board: BoardContainer,
radar: RadarContainer,
boxplot: BoxplotContainer,
scatter: ScatterContainer
};
return chartComponents[chartType] || null;
};
</script>

StatementItem.ts

下面是类型接口,需要加图表时,在chart中加入对应的类型

1
2
3
4
5
6
7
8
9
10
11
12
export interface StatementItem {
top: number;
left: number;
height: number;
width: number;
label: string;
type: 'numbers' | 'chart'; // 定义类型为数字或图表
numbers?: string[]; // 如果是数字类型,包含数字数组
chart?: 'line' | 'bar' | 'pie' | 'horizontalBar' | 'funnel' | 'board' | 'radar' | 'boxplot' | 'scatter'; // 图表类型
data?: any; // 图表的数据
chartOption?: any; // 图表的配置选项
}

柱状图

BarContainer.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<template>
<div class="barContainer w-full">
<div ref="barContainer" :style="{ width: typeof width === 'string' ? width : `${width}px`, height: `${height}px` }"></div>
</div>

</template>

<script setup lang="ts">
import { onMounted, nextTick, ref } from "vue"
import * as echarts from 'echarts';
import { ECBasicOption } from 'echarts/types/dist/shared';

// 接收 airLineOptions 函数和数据作为 prop
const props = defineProps<{
width: number|string;
height: number;
data: {
xAxisData: string[];
seriesData: number[];
};
chartOption: (xAxisData: string[], seriesData: number[]) => ECBasicOption
}>();

const barContainer = ref<HTMLElement | null>(null);
let bar: echarts.ECharts | null = null;

onMounted(async () => {
await nextTick();
initWaterBarChart();
});

// 初始化图表方法
const initWaterBarChart = () => {
if (barContainer.value) {
bar = echarts.init(barContainer.value);
renderWaterBar();
}
};

// 渲染图表
const renderWaterBar = () => {
// 使用从父组件传入的 airLineOptions 函数生成图表选项
const options = props.chartOption(props.data.xAxisData, props.data.seriesData);

// 使用 ECharts 实例的 setOption 方法渲染图表
bar?.setOption(options);
};
</script>

<style lang="scss" scoped></style>

waterBarOption.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import { ECBasicOption } from 'echarts/types/dist/shared';

const waterBarOption = (xAxisData: string[], seriesData: number[]): ECBasicOption => {
return {
xAxis: {
type: 'category',
data: xAxisData,
axisLabel: {
rotate: 0, // 旋转横轴标签,适应来源类别显示
},
},
yAxis: {
type: 'value',
position: 'left',
name: '碳排放量 (吨)', // Y轴标签修改为碳排放量
nameTextStyle: {
padding: [0, 0, 10, 0], // 调整标签位置
},
},
grid: {
left: '5%',
right: '5%',
bottom: '10%',
containLabel: true,
},
series: [
{
name: '碳排放量',
type: 'bar',
data: seriesData,
color: ['#5DB1FF'], // 颜色改为适合碳排放的颜色
itemStyle: {
normal: {
barBorderRadius: [8, 8, 0, 0], // 保持柱状图的圆角效果
},
},
},
],
};
};

export default waterBarOption;

waterBarData.ts

1
2
3
4
export const waterBarData = {
xAxisData: ['能源', '工业', '交通', '居民', '农业', '其他'],
seriesData: [5300, 4500, 3200, 2100, 1800, 900], // 从大到小排序后的碳排放量数据
};

仪表盘

BoardContainer.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<template>
<div class="BoardContainer w-full">
<div ref="boardContainer"
:style="{ width: typeof width === 'string' ? width : `${width}px`, height: `${height}px` }"></div>
</div>
</template>

<script setup lang="ts">
import { onMounted, nextTick, ref } from "vue"
import * as echarts from 'echarts';
import { ECBasicOption } from 'echarts/types/dist/shared';

// 接收 boardOptions 函数和数据作为 prop
const props = defineProps<{
width: number | string;
height: number;
data: {
currentValue: number;
};
chartOption: (currentValue: number) => ECBasicOption
}>();

const boardContainer = ref<HTMLElement | null>(null);
let board: echarts.ECharts | null = null;

onMounted(async () => {
await nextTick();
initBoardChart();
});

// 初始化图表方法
const initBoardChart = () => {
if (boardContainer.value) {
board = echarts.init(boardContainer.value);
renderBoardChart();
}
};

// 渲染图表
const renderBoardChart = () => {
// 使用从父组件传入的 boardOptions 函数生成图表选项
const options = props.chartOption(props.data.currentValue);

// 使用 ECharts 实例的 setOption 方法渲染图表
board?.setOption(options);
};
</script>

<style lang="scss" scoped>
.BoardContainer {
// 可以根据需要添加或调整样式
}
</style>

radarOptions.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import { ECBasicOption } from 'echarts/types/dist/shared';

const radarOptions = (indicatorData: { name: string; max: number }[], seriesData: number[]): ECBasicOption => {
return {
tooltip: {
trigger: 'item',
},
radar: {
// 指标配置
indicator: indicatorData,
shape: 'polygon', // 雷达图形状
splitNumber: 5, // 网格分成的层数
axisName: {
color: '#333', // 轴线名称颜色
fontSize: 12,
},
splitLine: {
lineStyle: {
color: '#ddd', // 网格线颜色
},
},
splitArea: {
show: false, // 隐藏雷达图的背景填充
},
},
series: [
{
name: '绿化率',
type: 'radar',
data: [
{
value: seriesData,
name: '年度绿化率',
},
],
areaStyle: {
color: 'rgba(93, 177, 255, 0.5)', // 填充颜色,透明度调整适合展示绿化率
},
lineStyle: {
color: '#5DB1FF', // 线条颜色
},
},
],
};
};

export default radarOptions;

radarData.ts

1
2
3
4
5
6
7
8
9
10
export const radarData = {
indicatorData: [
{ name: '公园绿地', max: 100 },
{ name: '道路绿化', max: 100 },
{ name: '社区绿化', max: 100 },
{ name: '城市绿化', max: 100 },
{ name: '水域绿化', max: 100 },
],
seriesData: [80, 70, 85, 90, 75], // 各个绿化维度的实际值
};

盒须图

BoxplotContainer.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<template>
<div class="boxplotContainer w-full">
<div ref="boxplotContainer"
:style="{ width: typeof width === 'string' ? width : `${width}px`, height: `${height}px` }"></div>
</div>
</template>

<script setup lang="ts">
import { onMounted, nextTick, ref } from "vue";
import * as echarts from 'echarts';
import { ECBasicOption } from 'echarts/types/dist/shared';

// 接收 boxplotOptions 函数和数据作为 prop
const props = defineProps<{
width: number | string;
height: number;
data: {
xAxisData: string[];
seriesData: number[][];
};
chartOption: (xAxisData: string[], seriesData: number[][]) => ECBasicOption
}>();

const boxplotContainer = ref<HTMLElement | null>(null);
let boxplot: echarts.ECharts | null = null;

onMounted(async () => {
await nextTick();
initBoxplotChart();
});

// 初始化图表方法
const initBoxplotChart = () => {
if (boxplotContainer.value) {
boxplot = echarts.init(boxplotContainer.value);
renderBoxplot();
}
};

// 渲染图表
const renderBoxplot = () => {
// 使用从父组件传入的 boxplotOptions 函数生成图表选项
const options = props.chartOption(props.data.xAxisData, props.data.seriesData);

// 使用 ECharts 实例的 setOption 方法渲染图表
boxplot?.setOption(options);
};
</script>

<style lang="scss" scoped></style>

boxplotOptions.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import { ECBasicOption } from 'echarts/types/dist/shared';

// 生成 Boxplot 图表选项的函数
const boxplotOptions = (xAxisData: string[], seriesData: number[][]): ECBasicOption => {
return {
tooltip: {
trigger: 'item',
formatter: function (param: any) {
return [
`Category: ${param.name}`,
`Upper: ${param.data[5]}`,
`Q3: ${param.data[4]}`,
`Median: ${param.data[3]}`,
`Q1: ${param.data[2]}`,
`Lower: ${param.data[1]}`
].join('<br/>');
}
},
grid: {
top: '5%',
left: '0%',
right: '0%',
bottom: '5%',
containLabel: true
},
xAxis: {
type: 'category',
data: xAxisData,
boundaryGap: true,
nameGap: 30,
splitArea: {
show: false
},
axisLabel: {
rotate: 0, // 根据类别显示调整
},
},
yAxis: {
type: 'value',
name: '碳排放量 (吨)',
splitArea: {
show: true
}
},
series: [
{
name: '碳排放分布',
type: 'boxplot',
data: seriesData,
itemStyle: {
color: '#5DB1FF' // 颜色与碳排放主题一致
},
}
]
};
};

export default boxplotOptions;

boxplotData.ts

1
2
3
4
5
6
7
8
9
10
11
12
// Boxplot 数据
export const boxplotData = {
xAxisData: ['能源', '工业', '交通', '居民', '农业', '其他'],
seriesData: [
[700, 1000, 2500, 3000, 4000, 6000], // Energy
[600, 800, 2000, 2500, 3500, 4500], // Industry
[400, 700, 1500, 2000, 2700, 3200], // Transportation
[200, 400, 900, 1200, 1500, 2100], // Residential
[100, 300, 600, 1000, 1200, 1800], // Agriculture
[50, 150, 300, 500, 700, 900], // Others
]
};

漏斗图

funnelContainer.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
<template>
<div class="funnelContainer w-full">
<div ref="funnelContainer"
:style="{ width: typeof width === 'string' ? width : `${width}px`, height: `${height}px` }"></div>
</div>
</template>

<script setup lang="ts">
import { onMounted, nextTick, ref } from 'vue';
import * as echarts from 'echarts';
import { ECBasicOption } from 'echarts/types/dist/shared';

// 接收 funnelOptions 函数和数据作为 prop
const props = defineProps<{
width: number | string;
height: number;
data: {
name: string;
value: number;
}[];
chartOption: (funnelData: { name: string; value: number }[]) => ECBasicOption;
}>();

const funnelContainer = ref<HTMLElement | null>(null);
let funnelChart: echarts.ECharts | null = null;

onMounted(async () => {
await nextTick();
initFunnelChart();
});

// 初始化图表方法
const initFunnelChart = () => {
if (funnelContainer.value) {
funnelChart = echarts.init(funnelContainer.value);
renderFunnelChart();
}
};

// 渲染图表
const renderFunnelChart = () => {
// 使用从父组件传入的 funnelOptions 函数生成图表选项
const options = props.chartOption(props.data);

// 使用 ECharts 实例的 setOption 方法渲染图表
funnelChart?.setOption(options);
};
</script>

<style lang="scss" scoped>
.funnelContainer {
// 可根据需要添加样式
}
</style>

funnelOptions.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import { ECBasicOption } from 'echarts/types/dist/shared';

const funnelOptions = (funnelData: { name: string; value: number }[]): ECBasicOption => {
return {
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b} : {c}%',
},
legend: {
data: funnelData.map(item => item.name),
bottom: '10%',
},
series: [
{
name: '绿化来源',
type: 'funnel',
left: '10%',
top: 10,
bottom: 50,
width: '80%',
min: 0,
max: 40,
minSize: '0%',
maxSize: '100%',
sort: 'descending',
gap: 2,
label: {
show: true,
position: 'inside',
formatter: '{b}: {c}%',
},
labelLine: {
length: 10,
lineStyle: {
width: 1,
type: 'solid',
},
},
itemStyle: {
borderColor: '#fff',
borderWidth: 2,
},
emphasis: {
label: {
fontSize: 20,
},
},
data: funnelData,
},
],
};
};

export default funnelOptions;

funnelData.ts

1
2
3
4
5
6
export const funnelData = [
{ name: '森林', value: 40 },
{ name: '草地', value: 30 },
{ name: '公园', value: 20 },
{ name: '其他', value: 10 },
];

横向柱状图

horizontalBarContainer.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<template>
<div class="horizontalBarContainer w-full">
<div ref="horizontalBarContainer" :style="{ width: typeof width === 'string' ? width : `${width}px`, height: `${height}px` }"></div>
</div>

</template>

<script setup lang="ts">
import { onMounted, nextTick, ref } from "vue"
import * as echarts from 'echarts';
import { ECBasicOption } from 'echarts/types/dist/shared';

// 接收 airLineOptions 函数和数据作为 prop
const props = defineProps<{
width: number|string;
height: number;
data: {
yAxisData: string[];
seriesData: number[];
};
chartOption: (xAxisData: string[], seriesData: number[]) => ECBasicOption
}>();

const horizontalBarContainer = ref<HTMLElement | null>(null);
let horizontalBar: echarts.ECharts | null = null;

onMounted(async () => {
await nextTick();
initAirHorizontalBarChart();
});

// 初始化图表方法
const initAirHorizontalBarChart = () => {
if (horizontalBarContainer.value) {
horizontalBar = echarts.init(horizontalBarContainer.value);
renderAirHorizontalBar();
}
};

// 渲染图表
const renderAirHorizontalBar = () => {
let options = props.chartOption(props.data.yAxisData, props.data.seriesData);

// 使用 setOption 方法设置图表配置
horizontalBar?.setOption(options);
};
</script>

<style lang="scss" scoped></style>

airHorizontalBarOption.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import { ECBasicOption } from 'echarts/types/dist/shared';

const airHorizontalBarOption = (yAxisData: string[], seriesData: number[]):ECBasicOption => {
return {
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' }, // 提示框显示为阴影效果
},
xAxis: {
type: 'value',
name: 'AQI',
nameTextStyle: {
padding: [0, 0, 0, 0], // 调整 x 轴名称位置
},
position: 'top', // x 轴放在图表顶部
},
yAxis: {
type: 'category',
data: yAxisData, // 使用地区名称作为 y 轴
axisLabel: {
rotate: 0, // 不旋转标签
},
},
grid: {
left: '10%',
right: '10%',
bottom: '10%',
containLabel: true,
},
series: [
{
name: 'AQI',
type: 'bar',
data: seriesData,
color: ['#5DB1FF'],
itemStyle: {
barBorderRadius: [0, 8, 8, 0], // 圆角应用到左侧的柱状条
},
emphasis: {
itemStyle: {
color: '#3398DB', // 高亮时的颜色
},
},
},
],
};
};

export default airHorizontalBarOption;

horizontalBarData.ts

1
2
3
4
export const horizontalBarData = {
yAxisData: ['东部地区', '西部地区', '中部地区', '北部地区'], // 不同地区
seriesData: [75, 60, 85, 90], // 各地区的AQI值
};

折线图

lineContainer.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<template>
<div class="lineContainer w-full">
<div ref="lineContainer" :style="{ width: typeof width === 'string' ? width : `${width}px`, height: `${height}px` }"></div>
</div>

</template>

<script setup lang="ts">
import { onMounted, nextTick, ref } from "vue"
import * as echarts from 'echarts';
import { ECBasicOption } from 'echarts/types/dist/shared';

// 接收 airLineOptions 函数和数据作为 prop
const props = defineProps<{
width: number|string;
height: number;
data: {
xAxisData: string[];
seriesData: number[];
};
chartOption: (xAxisData: string[], seriesData: number[]) => ECBasicOption
}>();

const lineContainer = ref<HTMLElement | null>(null);
let line: echarts.ECharts | null = null;

onMounted(async () => {
await nextTick();
initAirLineChart();
});

// 初始化图表方法
const initAirLineChart = () => {
if (lineContainer.value) {
line = echarts.init(lineContainer.value);
renderAirLine();
}
};

// 渲染图表
const renderAirLine = () => {
// 使用从父组件传入的 airLineOptions 函数生成图表选项
const options = props.chartOption(props.data.xAxisData, props.data.seriesData);

// 使用 ECharts 实例的 setOption 方法渲染图表
line?.setOption(options);
};
</script>

<style lang="scss" scoped></style>

airLineOptions.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import { ECBasicOption } from 'echarts/types/dist/shared';

const airLineOptions = (xAxisData: string[], seriesData: number[]): ECBasicOption => {
return {
xAxis: {
type: 'category',
data: xAxisData,
},
yAxis: {
type: 'value',
position: 'left',
name: 'AQI', // 添加 y 轴标签
nameTextStyle: {
padding: [0, 0, 10, -30], // 调整 AQI 标签位置
},
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true,
},
series: [
{
name: 'AQI',
type: 'line',
symbol: 'circle',
symbolSize: 6,
data: seriesData,
color: ['#5DB1FF'],
markLine: {
symbol: ['none', 'none'],
label: { show: false },
data: [{ xAxis: 8 }, { xAxis: 11 }]
},
areaStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{ offset: 0, color: 'rgba(93, 177, 255, 0.3)' },
{ offset: 1, color: 'rgba(93, 177, 255, 0)' },
],
},
},
smooth: 0.5,
},
],
};
};

export default airLineOptions;

airLineData.ts

1
2
3
4
5
6
// airLineData.ts

export const airLineData = {
xAxisData: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
seriesData: [45, 50, 55, 60, 65, 70, 75, 80, 70, 65, 55, 50]
};

饼图

PieContainer.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<template>
<div class="pieContainer w-full">
<div ref="pieContainer" :style="{ width: typeof width === 'string' ? width : `${width}px`, height: `${height}px` }"></div>
</div>
</template>

<script setup lang="ts">
import { ref, onMounted, nextTick } from 'vue';
import * as echarts from 'echarts';
import { ECBasicOption } from 'echarts/types/dist/shared';

const props = defineProps<{
width: number|string;
height: number;
data: {
seriesData: { value: number; name: string }[]
};
chartOption: (seriesData: { value: number; name: string }[]) => ECBasicOption
}>();

const pieContainer = ref<HTMLElement | null>(null);
let pie: echarts.ECharts | null = null;

onMounted(async () => {
await nextTick();
initForestPieChart();
});

const initForestPieChart = () => {
if (pieContainer.value) {
pie = echarts.init(pieContainer.value);
renderForestPie();
}
};

const renderForestPie = () => {

let options = props.chartOption(props.data.seriesData);

// 使用 setOption 方法设置图表配置
pie?.setOption(options);
}
</script>

<style lang="scss" scoped></style>

forestPieOption.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import { ECBasicOption } from 'echarts/types/dist/shared';


const forestPieOption = (seriesData: { value: number; name: string }[]):ECBasicOption => {
return {
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b}: {c}% ({d}%)', // 提示框显示名称、数值和百分比
},
legend: {
data: seriesData.map(item => item.name), // 动态生成图例数据
orient: 'vertical',
left: '70%',
y: 'center',
itemGap: 30,
itemHeight: 15,
},
series: [
{
name: '森林覆盖率',
type: 'pie',
radius: ['40%', '70%'],
center: ['30%', '50%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 10,
borderColor: '#fff',
borderWidth: 2,
},
label: {
show: false,
position: 'center',
},
emphasis: {
label: {
show: true,
fontSize: 20,
fontWeight: 'bold',
},
},
labelLine: {
show: false,
},
data: seriesData,
},
],
};
};

export default forestPieOption;

forestPieData.ts

1
2
3
4
5
6
7
8
export const forestPieData = {
seriesData: [
{ value: 45, name: '东部地区' },
{ value: 30, name: '西部地区' },
{ value: 15, name: '中部地区' },
{ value: 10, name: '北部地区' }
],
};

雷达图

radarContainer.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<template>
<div class="radarContainer w-full">
<div ref="radarContainer"
:style="{ width: typeof width === 'string' ? width : `${width}px`, height: `${height}px` }"></div>
</div>
</template>

<script setup lang="ts">
import { onMounted, nextTick, ref } from "vue";
import * as echarts from 'echarts';
import { ECBasicOption } from 'echarts/types/dist/shared';

// 接收 radarOptions 函数和数据作为 prop
const props = defineProps<{
width: number | string;
height: number;
data: {
indicatorData: { name: string, max: number }[];
seriesData: number[];
};
chartOption: (indicatorData: { name: string; max: number }[], seriesData: number[]) => ECBasicOption;
}>();

const radarContainer = ref<HTMLElement | null>(null);
let radar: echarts.ECharts | null = null;

onMounted(async () => {
await nextTick();
initRadarChart();
});

// 初始化雷达图方法
const initRadarChart = () => {
if (radarContainer.value) {
radar = echarts.init(radarContainer.value);
renderRadarChart();
}
};

// 渲染雷达图
const renderRadarChart = () => {
// 使用从父组件传入的 radarOptions 函数生成图表选项
const options = props.chartOption(props.data.indicatorData, props.data.seriesData);

// 使用 ECharts 实例的 setOption 方法渲染图表
radar?.setOption(options);
};
</script>

<style lang="scss" scoped></style>

radarOptions.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import { ECBasicOption } from 'echarts/types/dist/shared';

const radarOptions = (indicatorData: { name: string; max: number }[], seriesData: number[]): ECBasicOption => {
return {
tooltip: {
trigger: 'item',
},
radar: {
// 指标配置
indicator: indicatorData,
shape: 'polygon', // 雷达图形状
splitNumber: 5, // 网格分成的层数
axisName: {
color: '#333', // 轴线名称颜色
fontSize: 12,
},
splitLine: {
lineStyle: {
color: '#ddd', // 网格线颜色
},
},
splitArea: {
show: false, // 隐藏雷达图的背景填充
},
},
series: [
{
name: '绿化率',
type: 'radar',
data: [
{
value: seriesData,
name: '年度绿化率',
},
],
areaStyle: {
color: 'rgba(93, 177, 255, 0.5)', // 填充颜色,透明度调整适合展示绿化率
},
lineStyle: {
color: '#5DB1FF', // 线条颜色
},
},
],
};
};

export default radarOptions;

radarData.ts

1
2
3
4
5
6
7
8
9
10
export const radarData = {
indicatorData: [
{ name: '公园绿地', max: 100 },
{ name: '道路绿化', max: 100 },
{ name: '社区绿化', max: 100 },
{ name: '城市绿化', max: 100 },
{ name: '水域绿化', max: 100 },
],
seriesData: [80, 70, 85, 90, 75], // 各个绿化维度的实际值
};

散点图

scatterContainer.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<template>
<div class="scatterContainer w-full">
<div ref="scatterContainer"
:style="{ width: typeof width === 'string' ? width : `${width}px`, height: `${height}px` }"></div>
</div>
</template>

<script setup lang="ts">
import { onMounted, nextTick, ref } from "vue";
import * as echarts from 'echarts';
import { ECBasicOption } from 'echarts/types/dist/shared';

// 接收 scatterOption 函数和数据作为 prop
const props = defineProps<{
width: number | string;
height: number;
data: {
xAxisData: string[];
seriesData: number[][];
};
chartOption: (xAxisData: string[], seriesData: number[][]) => ECBasicOption
}>();

const scatterContainer = ref<HTMLElement | null>(null);
let scatter: echarts.ECharts | null = null;

onMounted(async () => {
await nextTick();
initScatterChart();
});

// 初始化图表方法
const initScatterChart = () => {
if (scatterContainer.value) {
scatter = echarts.init(scatterContainer.value);
renderScatterChart();
}
};

// 渲染图表
const renderScatterChart = () => {
// 使用从父组件传入的 scatterOption 函数生成图表选项
const options = props.chartOption(props.data.xAxisData, props.data.seriesData);

// 使用 ECharts 实例的 setOption 方法渲染图表
scatter?.setOption(options);
};
</script>

<style lang="scss" scoped></style>

scatterOption.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import { ECBasicOption } from 'echarts/types/dist/shared';

const scatterOption = (xAxisData: string[], seriesData: number[][]): ECBasicOption => {
return {
xAxis: {
type: 'category',
data: xAxisData,
name: '类别', // X轴名称,可以根据具体数据修改
axisLabel: {
rotate: 45, // 适当旋转以适应类别显示
},
},
yAxis: {
type: 'value',
name: '碳排放量 (吨)', // Y轴名称,适合用于展示碳排放数据
nameTextStyle: {
padding: [0, 0, 10, 0],
},
},
grid: {
left: '5%',
right: '5%',
bottom: '10%',
containLabel: true,
},
series: [
{
name: '碳排放量对比',
type: 'scatter',
data: seriesData,
symbolSize: function (data:any) {
return Math.sqrt(data[1]) / 5; // 根据数据动态调整点的大小
},
color: '#5DB1FF', // 颜色设为适合碳排放主题的蓝色
itemStyle: {
emphasis: {
borderColor: '#333',
borderWidth: 1,
},
},
},
],
tooltip: {
trigger: 'item',
formatter: function (params:any) {
return `${params.name}<br/>碳排放量: ${params.value[1]} 吨`;
},
},
};
};

export default scatterOption;

scatterData.ts

1
2
3
4
5
6
7
8
9
10
11
export const scatterData = {
xAxisData: ['能源', '工业', '交通', '居民', '农业', '其他'],
seriesData: [
['能源', 5300],
['工业', 4500],
['交通', 3200],
['居民', 2100],
['农业', 1800],
['其他', 900]
], // 每个类别对应的碳排放量数据
};

electron + vue3打包项目

步骤

  1. 下载

由于直接npm会导致卡死,所以采用cnpm

1
2
npm install -g cnpm --registry=https://registry.npmmirror.com
cnpm install --save-dev electron electron-builder
  1. 创建 Electron 主进程文件

src/electron/main.js

这里踩了一个大坑就是之前执着用ts,导致后面在“main”的指定中,因为只能指定js类型文件,所以会导致以后每一次main文件修改,都会需要进行编译后,才能运行出修改的代码,但是实际上也不能这么写,后面会在main文件指定底下详细说明

还有一个问题就是require__dirname这些都是commandjs的用法,也就是常规node,如果在es模块下使用时需要添加nodeIntegration:truecontextIsolation:false

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 控制应用生命周期和创建原生浏览器窗口的模组
const { app, BrowserWindow, Menu } = require('electron')
const path = require('path')
// process.env['ELECTRON_DISABLE_SECURITY_WARNINGS'] = true // 关闭控制台的警告

function createWindow() {
// 创建浏览器窗口
const mainWindow = new BrowserWindow({
width: 1200,
height: 800,
webPreferences: {
// 书写渲染进程中的配置
nodeIntegration: true, //开启true这一步很重要,目的是为了vue文件中可以引入node和electron相关的API
contextIsolation: false, // 可以使用require方法
enableRemoteModule: true, // 可以使用remote方法
},
})

// 监听html
mainWindow.loadFile(path.join(app.getAppPath(), 'dist/index.html'))
}
// 这段程序将会在 Electron 结束初始化
// 和创建浏览器窗口的时候调用
// 部分 API 在 ready 事件触发后才能使用。
app.whenReady().then(() => {
createWindow()

app.on('activate', function () {
// 通常在 macOS 上,当点击 dock 中的应用程序图标时,如果没有其他
// 打开的窗口,那么程序会重新创建一个窗口。
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})

// 除了 macOS 外,当所有窗口都被关闭的时候退出程序。 因此,通常对程序和它们在
// 任务栏上的图标来说,应当保持活跃状态,直到用户使用 Cmd + Q 退出。
app.on('window-all-closed', function () {
if (process.platform !== 'darwin') app.quit()
})
  1. 配置 Vite 进行 Electron 打包

vite.config.ts

rollupOptions:

external:

globals:

electron:

build中的outDir指定的是什么文件输出路径

emptyOutDir:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';

export default defineConfig({
plugins: [vue()],

build: {
emptyOutDir: false, // 避免清空 dist 目录
rollupOptions: {
external: ['electron'],
output: {
globals: {
electron: 'electron'
}
}
}
}
});
  1. 配置ts编译方式

tsconfig.node.json

注意tsconfig.node.json主要是需要commandjs的

include内的位置也需要注意,因为是直接处于outDir下面的

所以之前include里面为"src/electron",outDir./dist/electron时,编译出来的位置在./dist/electron/src/electron

解决方式有两种,一种是更改rootDir,但是会报错,因为vite.config.ts不位于这个文件夹下面

第二种方法就是下面的方法将electron文件夹,移出src,并将ouDir设置为"./dist"

同时下面还有支持导出js的配置,只需要把include中的内容改为.js后缀就行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2023"],
"module": "CommonJS",
"skipLibCheck": true,

"allowJs": true,
"checkJs": false,
"noEmitHelpers": true,
"removeComments": true,
"isolatedModules": true,
"moduleDetection": "force",

/* Emit JavaScript files */
"noEmit": false, // 改为 false 以允许编译输出
"outDir": "./dist", // 输出目录为 dist/electron
"esModuleInterop": true, // 允许与 CommonJS 互操作

/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": [
"vite.config.ts", // 保持对 Vite 配置的支持
"electron/**/*" // 包括主进程文件 (例如 electron/main.ts)
],
"exclude": ["node_modules"]
}
  1. 更新 package.json 的脚本

在 package.json 中添加 Electron 的启动脚本以及指定app入口路径:

vue-tsc -b用作build时的ts检查,vue-tsc -b会检查并编译,vue-tsc –noEmit只检查不编译,当然直接去掉这个命令可以直接编译出来,但是不推荐这么做,去损失了ts检查代码的功能

1
2
3
4
5
6
7
8
"main": "dist/electron/main.js",
"scripts": {
"dev": "vite",
"build": "vue-tsc -b && vite build",
"serve": "vite preview",
"electron:dev": "electron .",
"electron:build": "vue-tsc -b && vite build && electron-builder"
},
  1. 更新 Electron Builder 配置

在 package.json 中添加 Electron Builder 的打包配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
"build": {
"appId": "com.example.app",
"mac": {
"target": "dmg",
"category": "public.app-category.productivity"
},
"files": [
"dist/**/*",
"electron/**/*"
],
"extraResources": [
{
"from": "src/assets/",
"to": "resources",
"filter": ["**/*"]
}
],
"directories": {
"buildResources": "build"
}
}
  1. 输入运行命令以及构建命令

打包前注意router必须是hash模式

参考:https://blog.csdn.net/qq_40994260/article/details/107440478/

1
2
3
4
5
import { createRouter, createWebHashHistory } from 'vue-router';
const router = createRouter({
history: createWebHashHistory(),
routes
});

打包后mac相关文件都位于Resources下的app.asar,需要借助Electron官方的工具才能打开,不过会存在很多.lproj后缀的文件夹,里面均为空,其实里面是针对各个语言的配置包,但是这个项目没有配置,所以为空

1
2
sudp npm run electron:dev
sudp npm run electron:build

热更新实现

报错

  1. tsconfig.app.json以及tsconfig.node.json的区分问题

​ 参考:https://blog.csdn.net/2301_79568124/article/details/137783628

  1. main.ts代码在编译后莫名在最后一行多了个export {};导致报错

​ 在 tsconfig.node.json 中调整 compilerOptions,确保正确处理 CommonJS 模块

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"compilerOptions": {
"module": "CommonJS",
"target": "ESNext",
"outDir": "./dist",
"esModuleInterop": true,
"allowJs": true,
"checkJs": false,
"noEmitHelpers": true,
"removeComments": true
},
"include": ["vite.config.ts", "electron/**/*"]
}
  1. 启动electron:build命令,但是没有编译出main.js文件,原因是tsconfig.node.json中include没有增加对应electron文件夹
  2. 编译出main.js文件夹位置错位的问题,主要是outDir以及rootDir
  3. 报错⨯ Application entry file "index.js" in the "/Users/tec/Desktop/ai-report-assistant-frontend/dist/mac-arm64/ai-report-assistant-frontend.app/Contents/Resources/app.asar" does not exist.

​ 这个原因在于main.js文件缺失,解决过程如下,首先是直接报错给gpt,发现”main”没加,然后发现根本就没编译electron文件夹下面的内容

  1. 报错require not defined in ES Module scope以及**__dirname is not defined**

    原因都在于Electron 主进程代码使用了 ES module 格式,但 require 只在 CommonJS 中可用,需要删除package.json中的"type": "module",,并在main.ts中进行相关配置,详情见步骤2,同时在tsconfig.node.json中也需要修改编译的相关配置,详情见bug2,和步骤4

整体思路

  1. 识别问题类型模型
    1. 目标:分类用户的输入为问数、归因或预测。
    2. 方法:使用自然语言处理技术(如BERT或其他预训练模型),并结合有标注的数据集进行监督学习。训练时,输入为用户提问,输出为问题类型标签。
    3. 评估:使用准确率、召回率和F1分数来评估模型性能,确保模型能有效区分不同类型的问题。
  2. 提取关键数据模型
    1. 目标:从用户输入中提取出关键的数值、单位和时间等信息。
    2. 方法:应用命名实体识别(NER)技术,训练模型识别特定实体(如年份、数量等)。可以使用标注数据来进行微调。
    3. 评估:通过查全率和查准率来评估模型在提取关键数据方面的表现,确保其准确性。
  3. 分析原因模型
    1. 目标:分析用户询问的背后原因,并生成相关因素及其贡献。
    2. 方法:利用关联规则学习或因果推断模型,结合历史数据,识别影响温室气体排放的主要因素和子因素。训练时,可以使用结构化数据和文本数据进行联合学习。
    3. 评估:通过对比分析结果与真实数据的吻合度,验证模型的有效性和解释性。
  4. 预测未来趋势模型
    1. 目标:预测未来的温室气体排放量及其变化趋势。
    2. 方法:应用时间序列分析(如ARIMA、LSTM等),使用历史数据进行训练。可以引入外部因素(如政策变化、经济指标等)以提升预测准确性。
    3. 评估:通过均方根误差(RMSE)和平均绝对误差(MAE)来评估模型的预测性能,确保其在实际应用中的可靠性。