feat: 接入echarts

This commit is contained in:
2026-08-16 00:12:40 +08:00
parent f60946a24d
commit 191069cf31
12 changed files with 733 additions and 29 deletions
+1
View File
@@ -16,6 +16,7 @@
}, },
"dependencies": { "dependencies": {
"@tailwindcss/postcss": "^4.3.3", "@tailwindcss/postcss": "^4.3.3",
"echarts": "^6.1.0",
"element-plus": "^2.14.4", "element-plus": "^2.14.4",
"pinia": "^4.0.2", "pinia": "^4.0.2",
"postcss": "^8.5.26", "postcss": "^8.5.26",
+23
View File
@@ -11,6 +11,9 @@ importers:
'@tailwindcss/postcss': '@tailwindcss/postcss':
specifier: ^4.3.3 specifier: ^4.3.3
version: 4.3.3 version: 4.3.3
echarts:
specifier: ^6.1.0
version: 6.1.0
element-plus: element-plus:
specifier: ^2.14.4 specifier: ^2.14.4
version: 2.14.4(vue@3.5.41(typescript@6.0.3)) version: 2.14.4(vue@3.5.41(typescript@6.0.3))
@@ -1076,6 +1079,9 @@ packages:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'} engines: {node: '>=8'}
echarts@6.1.0:
resolution: {integrity: sha512-q0yaFPggC9FUdsWH4blavRWFmxdrIodbkoKNAjJudAI6CA9gNPxHtV2RcZNEepZVlk4yvBYkOkbk6HIVpIyHZA==}
electron-to-chromium@1.5.406: electron-to-chromium@1.5.406:
resolution: {integrity: sha512-hWH5ORBi3d0IipnMh7BN5GDTaAmrSSSWmznwt2zltdiRNEWoEQyTwF0FFSBxzHO7hLSRT6loQu3IQGV0wg/Tvg==} resolution: {integrity: sha512-hWH5ORBi3d0IipnMh7BN5GDTaAmrSSSWmznwt2zltdiRNEWoEQyTwF0FFSBxzHO7hLSRT6loQu3IQGV0wg/Tvg==}
@@ -1823,6 +1829,9 @@ packages:
peerDependencies: peerDependencies:
typescript: '>=4.8.4' typescript: '>=4.8.4'
tslib@2.3.0:
resolution: {integrity: sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==}
type-check@0.4.0: type-check@0.4.0:
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
engines: {node: '>= 0.8.0'} engines: {node: '>= 0.8.0'}
@@ -2101,6 +2110,9 @@ packages:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'} engines: {node: '>=10'}
zrender@6.1.0:
resolution: {integrity: sha512-oEGMDB6pOP2S6OwRR4PdVv610zrjnA3Bh+JnSG12fYJlBKjtNAoEb5fSUoCOOINlH96I2fU38/A2UpRKs67xYQ==}
snapshots: snapshots:
'@alloc/quick-lru@5.2.0': {} '@alloc/quick-lru@5.2.0': {}
@@ -3051,6 +3063,11 @@ snapshots:
detect-libc@2.1.2: {} detect-libc@2.1.2: {}
echarts@6.1.0:
dependencies:
tslib: 2.3.0
zrender: 6.1.0
electron-to-chromium@1.5.406: {} electron-to-chromium@1.5.406: {}
element-plus@2.14.4(vue@3.5.41(typescript@6.0.3)): element-plus@2.14.4(vue@3.5.41(typescript@6.0.3)):
@@ -3753,6 +3770,8 @@ snapshots:
dependencies: dependencies:
typescript: 6.0.3 typescript: 6.0.3
tslib@2.3.0: {}
type-check@0.4.0: type-check@0.4.0:
dependencies: dependencies:
prelude-ls: 1.2.1 prelude-ls: 1.2.1
@@ -4044,3 +4063,7 @@ snapshots:
yaml@2.9.0: {} yaml@2.9.0: {}
yocto-queue@0.1.0: {} yocto-queue@0.1.0: {}
zrender@6.1.0:
dependencies:
tslib: 2.3.0
@@ -18,7 +18,9 @@ function onStart(event: DragEvent) {
@dragstart="onStart" @dragstart="onStart"
> >
<div>{{ props.material.name }}</div> <div>{{ props.material.name }}</div>
<div class="border border-white/20 h-full"><Icon :icon="props.material.icon" /></div> <div class="flex justify-center items-center h-full border border-white/20">
<Icon :icon="props.material.icon" width="64" />
</div>
</div> </div>
</template> </template>
@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { getValue } from '@/utils' import { getValue } from '@/utils'
import { ElInput, ElInputNumber, ElColorPicker } from 'element-plus' import { ElInput, ElInputNumber, ElColorPicker, ElCheckbox, ElSelect } from 'element-plus'
import { useUndoRedo } from '@/composables/useUndoRedo' import { useUndoRedo } from '@/composables/useUndoRedo'
defineProps(['setters', 'formData']) defineProps(['setters', 'formData'])
@@ -11,6 +11,8 @@ const componentMap: Record<string, any> = {
input: ElInput, input: ElInput,
number: (props: Record<string, any>) => h(ElInputNumber, { precision: 0, ...props }), number: (props: Record<string, any>) => h(ElInputNumber, { precision: 0, ...props }),
color: ElColorPicker, color: ElColorPicker,
checkbox: ElCheckbox,
select: ElSelect,
} }
</script> </script>
@@ -21,6 +23,7 @@ const componentMap: Record<string, any> = {
<component <component
:is="componentMap[setter.type]" :is="componentMap[setter.type]"
:model-value="getValue(formData, setter.key)" :model-value="getValue(formData, setter.key)"
v-bind="setter.props"
@update:model-value="(value: any) => applyChange(formData, setter.key, value)" @update:model-value="(value: any) => applyChange(formData, setter.key, value)"
@focus="startBatch()" @focus="startBatch()"
@blur="commitBatch()" @blur="commitBatch()"
@@ -11,7 +11,12 @@ defineOptions({
const editorStore = useEditorStore() const editorStore = useEditorStore()
const { selectedNode } = storeToRefs(editorStore) const { selectedNode } = storeToRefs(editorStore)
const setters = getMaterialSetters(selectedNode.value?.type || '') ?? [] const setters = computed(() => {
return getMaterialSetters(selectedNode.value?.type || '') ?? []
})
// 属性分组默认全部展开
const activeNames = ref(['layout', 'component'])
// 布局属性 // 布局属性
const layoutSetters = [ const layoutSetters = [
@@ -40,11 +45,11 @@ const layoutSetters = [
<template> <template>
<div class="px-2"> <div class="px-2">
<el-collapse> <el-collapse v-model="activeNames">
<el-collapse-item title="布局属性"> <el-collapse-item title="布局属性" name="layout">
<FormCreate :setters="layoutSetters" :form-data="selectedNode" /> <FormCreate :setters="layoutSetters" :form-data="selectedNode" />
</el-collapse-item> </el-collapse-item>
<el-collapse-item title="组件属性"> <el-collapse-item title="组件属性" name="component">
<FormCreate :setters="setters" :form-data="selectedNode" /> <FormCreate :setters="setters" :form-data="selectedNode" />
</el-collapse-item> </el-collapse-item>
</el-collapse> </el-collapse>
+179
View File
@@ -0,0 +1,179 @@
import type { MaterialDefinition } from '@/schema/materials'
export const areaMaterial: MaterialDefinition = {
name: '面积图',
group: 'charts',
icon: 'solar:graph-new-bold',
setters: [
{
type: 'input',
label: '标题',
key: 'props.option.title.text',
},
{
type: 'color',
label: '标题色',
key: 'props.option.title.textStyle.color',
},
{
type: 'checkbox',
label: '图例显示',
key: 'props.option.legend.show',
},
{
type: 'select',
label: '对齐',
key: 'props.option.title.left',
props: {
options: [
{ label: '左对齐', value: 'left' },
{ label: '居中', value: 'center' },
{ label: '右对齐', value: 'right' },
],
},
},
{
type: 'color',
label: '线颜色',
key: 'props.option.series.0.lineStyle.color',
},
{
type: 'color',
label: '面积色',
key: 'props.option.series.0.areaStyle.color',
},
{
type: 'input',
label: 'X字段',
key: 'props.option.series.0.encode.x',
},
{
type: 'input',
label: 'Y字段',
key: 'props.option.series.0.encode.y',
},
{
type: 'number',
label: '上边距',
key: 'props.option.grid.top',
span: 12,
},
{
type: 'number',
label: '右边距',
key: 'props.option.grid.right',
span: 12,
},
{
type: 'number',
label: '下边距',
key: 'props.option.grid.bottom',
span: 12,
},
{
type: 'number',
label: '左边距',
key: 'props.option.grid.left',
span: 12,
},
],
schema: {
type: 'area-chart',
name: '面积图',
layout: {
x: 0,
y: 0,
width: 400,
height: 260,
},
props: {
option: {
legend: {
top: 38,
left: 'center',
itemWidth: 12,
itemHeight: 8,
show: true,
textStyle: {
color: '#cbd5e1',
},
},
title: {
text: '成交额趋势',
top: 8,
left: 'center',
textStyle: {
color: '#ffffff',
fontSize: 16,
},
},
tooltip: {
trigger: 'axis',
},
dataset: {
source: [
{ date: '周一', revenue: 120 },
{ date: '周二', revenue: 200 },
{ date: '周三', revenue: 150 },
{ date: '周四', revenue: 260 },
{ date: '周五', revenue: 330 },
{ date: '周六', revenue: 420 },
{ date: '周日', revenue: 510 },
],
},
grid: {
top: 86,
right: 24,
bottom: 32,
left: 48,
containLabel: true,
},
xAxis: {
type: 'category',
boundaryGap: false,
axisLine: {
lineStyle: {
color: '#64748b',
},
},
axisLabel: {
color: '#cbd5e1',
},
},
yAxis: {
type: 'value',
axisLabel: {
color: '#cbd5e1',
},
splitLine: {
lineStyle: {
color: 'rgba(148, 163, 184, 0.18)',
},
},
},
series: [
{
name: '成交额',
type: 'line',
smooth: true,
symbolSize: 8,
encode: {
x: 'date',
y: 'revenue',
},
lineStyle: {
width: 3,
color: '#22d3ee',
},
itemStyle: {
color: '#22d3ee',
},
areaStyle: {
color: 'rgba(34, 211, 238, 0.25)',
},
},
],
},
},
},
}
+163
View File
@@ -0,0 +1,163 @@
import type { MaterialDefinition } from '@/schema/materials'
export const barMaterial: MaterialDefinition = {
name: '柱状图',
group: 'charts',
icon: 'solar:chart-2-bold',
setters: [
{
type: 'input',
label: '标题',
key: 'props.option.title.text',
},
{
type: 'color',
label: '标题色',
key: 'props.option.title.textStyle.color',
},
{
type: 'checkbox',
label: '图例显示',
key: 'props.option.legend.show',
},
{
type: 'select',
label: '对齐',
key: 'props.option.title.left',
props: {
options: [
{ label: '左对齐', value: 'left' },
{ label: '居中', value: 'center' },
{ label: '右对齐', value: 'right' },
],
},
},
{
type: 'color',
label: '柱颜色',
key: 'props.option.series.0.itemStyle.color',
},
{
type: 'input',
label: 'X字段',
key: 'props.option.series.0.encode.x',
},
{
type: 'input',
label: 'Y字段',
key: 'props.option.series.0.encode.y',
},
{
type: 'number',
label: '上边距',
key: 'props.option.grid.top',
span: 12,
},
{
type: 'number',
label: '右边距',
key: 'props.option.grid.right',
span: 12,
},
{
type: 'number',
label: '下边距',
key: 'props.option.grid.bottom',
span: 12,
},
{
type: 'number',
label: '左边距',
key: 'props.option.grid.left',
span: 12,
},
],
schema: {
type: 'bar-chart',
name: '柱状图',
layout: {
x: 0,
y: 0,
width: 400,
height: 260,
},
props: {
option: {
legend: {
top: 38,
left: 'center',
itemWidth: 12,
itemHeight: 8,
show: true,
textStyle: {
color: '#cbd5e1',
},
},
title: {
text: '销售额统计',
top: 8,
left: 'center',
textStyle: {
color: '#ffffff',
fontSize: 16,
},
},
tooltip: {},
dataset: {
source: [
{ month: '一月', sales: 120 },
{ month: '二月', sales: 200 },
{ month: '三月', sales: 150 },
{ month: '四月', sales: 80 },
{ month: '五月', sales: 170 },
{ month: '六月', sales: 240 },
],
},
grid: {
top: 86,
right: 24,
bottom: 32,
left: 48,
containLabel: true,
},
xAxis: {
type: 'category',
axisLine: {
lineStyle: {
color: '#64748b',
},
},
axisLabel: {
color: '#cbd5e1',
},
},
yAxis: {
type: 'value',
axisLabel: {
color: '#cbd5e1',
},
splitLine: {
lineStyle: {
color: 'rgba(148, 163, 184, 0.18)',
},
},
},
series: [
{
name: '销售额',
type: 'bar',
barWidth: '45%',
encode: {
x: 'month',
y: 'sales',
},
itemStyle: {
color: '#22d3ee',
borderRadius: [4, 4, 0, 0],
},
},
],
},
},
},
}
+50
View File
@@ -0,0 +1,50 @@
<!-- echarts组件 -->
<script setup lang="ts">
import type { MaterialSchema } from '@/schema/materials'
import { init, type EChartsType } from 'echarts'
defineOptions({
name: 'ChartMaterial',
})
const props = defineProps<{ schema: MaterialSchema }>()
const chartRef = useTemplateRef('chartRef')
let chart: EChartsType
watch(
() => props.schema.props.option,
(newOption) => {
if (chartRef.value) {
chart.setOption(newOption)
}
},
{ deep: true },
)
onMounted(() => {
if (chartRef.value) {
chart = init(chartRef.value)
chart.setOption(props.schema.props.option)
// 监听窗口大小变化,自动调整图表大小
const resizeObserver = new ResizeObserver(() => {
chart.resize()
})
resizeObserver.observe(chartRef.value)
onBeforeUnmount(() => {
resizeObserver.disconnect()
if (chart) {
chart.dispose()
}
})
}
})
</script>
<template>
<div class="w-full h-full" ref="chartRef"></div>
</template>
<style scoped></style>
+13 -22
View File
@@ -1,26 +1,17 @@
import type { MaterialDefinition } from '@/schema/materials' import type { MaterialDefinition } from '@/schema/materials'
import ChartMaterial from './component.vue'
import { barMaterial } from './bar'
import { areaMaterial } from './area'
import { lineMaterial } from './line'
import { pieMaterial } from './pie'
export const chartsMaterial: MaterialDefinition = { export function install(register: (material: MaterialDefinition, component: Component) => void) {
name: '柱状图',
icon: '',
group: 'charts',
setters: [],
schema: {
type: 'chart',
name: '柱状图',
layout: {
x: 0,
y: 0,
width: 400,
height: 300,
},
props: {
data: [],
},
},
}
export function install(register: (material: MaterialDefinition) => void) {
// 注册柱状图物料 // 注册柱状图物料
register(chartsMaterial) register(barMaterial, ChartMaterial)
// 注册面积图物料
register(areaMaterial, ChartMaterial)
// 注册折线图物料
register(lineMaterial, ChartMaterial)
// 注册饼图物料
register(pieMaterial, ChartMaterial)
} }
+171
View File
@@ -0,0 +1,171 @@
import type { MaterialDefinition } from '@/schema/materials'
export const lineMaterial: MaterialDefinition = {
name: '折线图',
group: 'charts',
icon: 'solar:graph-up-bold',
setters: [
{
type: 'input',
label: '标题',
key: 'props.option.title.text',
},
{
type: 'color',
label: '标题色',
key: 'props.option.title.textStyle.color',
},
{
type: 'checkbox',
label: '图例显示',
key: 'props.option.legend.show',
},
{
type: 'select',
label: '对齐',
key: 'props.option.title.left',
props: {
options: [
{ label: '左对齐', value: 'left' },
{ label: '居中', value: 'center' },
{ label: '右对齐', value: 'right' },
],
},
},
{
type: 'color',
label: '线颜色',
key: 'props.option.series.0.lineStyle.color',
},
{
type: 'input',
label: 'X字段',
key: 'props.option.series.0.encode.x',
},
{
type: 'input',
label: 'Y字段',
key: 'props.option.series.0.encode.y',
},
{
type: 'number',
label: '上边距',
key: 'props.option.grid.top',
span: 12,
},
{
type: 'number',
label: '右边距',
key: 'props.option.grid.right',
span: 12,
},
{
type: 'number',
label: '下边距',
key: 'props.option.grid.bottom',
span: 12,
},
{
type: 'number',
label: '左边距',
key: 'props.option.grid.left',
span: 12,
},
],
schema: {
type: 'line-chart',
name: '折线图',
layout: {
x: 0,
y: 0,
width: 400,
height: 260,
},
props: {
option: {
legend: {
top: 38,
left: 'center',
itemWidth: 12,
itemHeight: 8,
show: true,
textStyle: {
color: '#cbd5e1',
},
},
title: {
text: '访问量趋势',
top: 8,
left: 'center',
textStyle: {
color: '#ffffff',
fontSize: 16,
},
},
tooltip: {
trigger: 'axis',
},
dataset: {
source: [
{ date: '周一', visits: 820 },
{ date: '周二', visits: 932 },
{ date: '周三', visits: 901 },
{ date: '周四', visits: 934 },
{ date: '周五', visits: 1290 },
{ date: '周六', visits: 1330 },
{ date: '周日', visits: 1320 },
],
},
grid: {
top: 86,
right: 24,
bottom: 32,
left: 48,
containLabel: true,
},
xAxis: {
type: 'category',
boundaryGap: false,
axisLine: {
lineStyle: {
color: '#64748b',
},
},
axisLabel: {
color: '#cbd5e1',
},
},
yAxis: {
type: 'value',
axisLabel: {
color: '#cbd5e1',
},
splitLine: {
lineStyle: {
color: 'rgba(148, 163, 184, 0.18)',
},
},
},
series: [
{
name: '访问量',
type: 'line',
smooth: true,
symbolSize: 8,
encode: {
x: 'date',
y: 'visits',
},
lineStyle: {
width: 3,
color: '#38bdf8',
},
itemStyle: {
color: '#38bdf8',
},
},
],
},
},
},
}
+116
View File
@@ -0,0 +1,116 @@
import type { MaterialDefinition } from '@/schema/materials'
export const pieMaterial: MaterialDefinition = {
name: '饼图',
group: 'charts',
icon: 'solar:pie-chart-2-bold',
setters: [
{
type: 'input',
label: '标题',
key: 'props.option.title.text',
},
{
type: 'color',
label: '标题色',
key: 'props.option.title.textStyle.color',
},
{
type: 'checkbox',
label: '图例显示',
key: 'props.option.legend.show',
},
{
type: 'select',
label: '对齐',
key: 'props.option.title.left',
props: {
options: [
{ label: '左对齐', value: 'left' },
{ label: '居中', value: 'center' },
{ label: '右对齐', value: 'right' },
],
},
},
{
type: 'color',
label: '主色',
key: 'props.option.color.0',
},
{
type: 'input',
label: '名称字段',
key: 'props.option.series.0.encode.itemName',
},
{
type: 'input',
label: '数值字段',
key: 'props.option.series.0.encode.value',
},
],
schema: {
type: 'pie-chart',
name: '饼图',
layout: {
x: 0,
y: 0,
width: 400,
height: 260,
},
props: {
option: {
color: ['#22d3ee', '#a78bfa', '#f59e0b', '#34d399', '#fb7185'],
legend: {
top: 38,
left: 'center',
itemWidth: 12,
itemHeight: 8,
show: true,
textStyle: {
color: '#cbd5e1',
},
},
title: {
text: '访问来源占比',
top: 8,
left: 'center',
textStyle: {
color: '#ffffff',
fontSize: 16,
},
},
tooltip: {
trigger: 'item',
},
dataset: {
source: [
{ channel: '搜索引擎', value: 1048 },
{ channel: '直接访问', value: 735 },
{ channel: '联盟广告', value: 484 },
{ channel: '视频广告', value: 300 },
],
},
series: [
{
name: '访问来源',
type: 'pie',
center: ['50%', '62%'],
avoidLabelOverlap: true,
label: {
color: '#e2e8f0',
},
labelLine: {
lineStyle: {
color: '#94a3b8',
},
},
encode: {
itemName: 'channel',
value: 'value',
},
},
],
},
},
},
}
+1 -1
View File
@@ -3,7 +3,7 @@ import TextMaterial from './component.vue'
const textMaterial: MaterialDefinition = { const textMaterial: MaterialDefinition = {
name: '文本', name: '文本',
icon: '', icon: 'solar:text-bold',
group: 'info', group: 'info',
setters: [ setters: [
{ {