浏览代码

添加图表可视化

kagg886 1 月之前
父节点
当前提交
a4301480aa
共有 2 个文件被更改,包括 329 次插入37 次删除
  1. 13 3
      src/components/markdown/plugins/impl/VueCharts.vue
  2. 316 34
      src/components/markdown/plugins/impl/VueStructData.vue

+ 13 - 3
src/components/markdown/plugins/impl/VueCharts.vue

@@ -1,5 +1,5 @@
 <script setup lang="ts">
-import { onMounted, onUnmounted, ref } from 'vue'
+import { onMounted, onUnmounted, ref, watch } from 'vue'
 import * as echarts from 'echarts'
 
 type Props = {
@@ -16,7 +16,8 @@ const resizeHandler = () => {
 }
 
 const showOrigin = ref(false)
-onMounted(() => {
+
+const setOption = () => {
 	let data: echarts.EChartsOption
 	try {
 		data = JSON.parse(decodeURIComponent(prop.data))
@@ -25,13 +26,22 @@ onMounted(() => {
 		return
 	}
 	instance = echarts.init(dom.value)
-
 	try {
 		instance.setOption(data)
 	} catch (e) {
 		showOrigin.value = true
 	}
+}
 
+watch(
+	() => prop.data,
+	() => {
+		setOption()
+	}
+)
+
+onMounted(() => {
+	setOption()
 	window.addEventListener('resize', resizeHandler)
 })
 

+ 316 - 34
src/components/markdown/plugins/impl/VueStructData.vue

@@ -1,11 +1,11 @@
 <script setup lang="ts">
-import { ref, onMounted, computed } from 'vue'
+import { ref, onMounted, watch, computed, nextTick } from 'vue'
 import { ElMessage } from 'element-plus'
-import { Download, Grid, TrendCharts, BarChart } from '@element-plus/icons-vue'
+import { Download, Grid, TrendCharts, DataLine } from '@element-plus/icons-vue'
 import assist from '/@/api/assist'
 import { useLoading } from '/@/utils/loading-util'
-import VueCharts from './VueCharts.vue'
 import download from 'downloadjs'
+import VueCharts from './VueCharts.vue'
 
 const props = defineProps<{
 	uuid: string
@@ -33,8 +33,8 @@ const exportToCSV = () => {
 	try {
 		// 构建CSV内容
 		const headers = data.value.fields.map((it) => `"${it}"`).join(',')
-		const rows = data.value.data.map(rowJson => {
-			return data.value!.fields.map(field => `"${rowJson[field]}"`).join(',')
+		const rows = data.value.data.map((rowJson) => {
+			return data.value!.fields.map((field) => `"${rowJson[field]}"`).join(',')
 		})
 
 		const csvContent = [headers, ...rows].join('\n')
@@ -51,10 +51,218 @@ const exportToCSV = () => {
 // 组件挂载时加载数据
 onMounted(doLoadingStruct)
 
+// 显示模式:表格、柱状图、折线图
 const display = ref<'table' | 'bar' | 'line'>('table')
 
-const barChart = ref()
-const lineChart = ref()
+// 图表组件引用
+const chart = ref()
+
+// 图表配置选项
+const chartOptions = ref('')
+
+// 生成柱状图配置 - 统计每列数据的分布情况
+const generateBarChartOptions = () => {
+	if (!data.value || !data.value.fields || !data.value.data) return ''
+
+	// 统计每个字段的数据分布
+	const fieldStats: { [field: string]: { [value: string]: number } } = {}
+
+	// 初始化统计对象
+	data.value.fields.forEach(field => {
+		fieldStats[field] = {}
+	})
+
+	// 统计每个字段中每个值的出现次数
+	data.value.data.forEach(row => {
+		data.value!.fields.forEach(field => {
+			const value = row[field] || '空值'
+			fieldStats[field][value] = (fieldStats[field][value] || 0) + 1
+		})
+	})
+
+	// 准备图表数据
+	const xAxisData = data.value.fields // 横坐标是表头(字段名)
+	const seriesData: { [value: string]: number[] } = {}
+
+	// 收集所有可能的值
+	const allValues = new Set<string>()
+	Object.values(fieldStats).forEach(fieldStat => {
+		Object.keys(fieldStat).forEach(value => allValues.add(value))
+	})
+
+	// 为每个值创建一个系列
+	Array.from(allValues).forEach(value => {
+		seriesData[value] = data.value!.fields.map(field => fieldStats[field][value] || 0)
+	})
+
+	// 生成系列配置
+	const series = Object.entries(seriesData).map(([value, counts]) => ({
+		name: value,
+		type: 'bar',
+		data: counts,
+		stack: 'total' // 使用堆叠柱状图更好地展示分布
+	}))
+
+	const option = {
+		title: {
+			text: '数据分布统计',
+			left: 'center'
+		},
+		tooltip: {
+			trigger: 'axis',
+			axisPointer: {
+				type: 'shadow'
+			},
+			backgroundColor: 'rgba(50, 50, 50, 0.9)',
+			borderColor: '#ccc',
+			borderWidth: 1,
+			textStyle: {
+				color: '#fff',
+				fontSize: 12
+			},
+			padding: [8, 12],
+			extraCssText: 'max-width: 300px; word-wrap: break-word;',
+			formatter: (params: any) => {
+				if (!params || params.length === 0) return ''
+				let result = `<div style="font-weight: bold; margin-bottom: 4px;">${params[0].axisValue}</div>`
+				params.forEach((param: any) => {
+					if (param.value > 0) { // 只显示有数据的项
+						result += `<div style="margin: 2px 0;">${param.marker}<span style="margin-left: 4px;">${param.seriesName}: ${param.value}</span></div>`
+					}
+				})
+				return result
+			}
+		},
+		legend: {
+			data: Object.keys(seriesData),
+			top: 30,
+			type: 'scroll' // 如果图例太多,使用滚动
+		},
+		grid: {
+			left: '3%',
+			right: '4%',
+			bottom: '3%',
+			containLabel: true
+		},
+		xAxis: {
+			type: 'category',
+			data: xAxisData,
+			axisLabel: {
+				rotate: 45 // 如果字段名太长,旋转显示
+			}
+		},
+		yAxis: {
+			type: 'value',
+			name: '出现次数'
+		},
+		series: series
+	}
+
+	return encodeURIComponent(JSON.stringify(option))
+}
+
+// 生成折线图配置 - 展示数据趋势(按行索引)
+const generateLineChartOptions = () => {
+	if (!data.value || !data.value.fields || !data.value.data) return ''
+
+	// 尝试找到数值型字段
+	const numericFields = data.value.fields.filter(field => {
+		return data.value!.data.some(row => {
+			const value = row[field]
+			return !isNaN(parseFloat(value)) && isFinite(parseFloat(value))
+		})
+	})
+
+	// 如果没有数值型字段,则显示每个字段的数据长度趋势
+	const fieldsToShow = numericFields.length > 0 ? numericFields : data.value.fields
+
+	// X轴数据:行索引或者第一列数据
+	const xAxisData = data.value.data.map((_, index) => `第${index + 1}行`)
+
+	// 生成系列数据
+	const series = fieldsToShow.map(field => {
+		const seriesData = data.value!.data.map(row => {
+			const value = row[field]
+			// 如果是数值型字段,直接使用数值
+			if (numericFields.includes(field)) {
+				return parseFloat(value) || 0
+			}
+			// 否则使用字符串长度
+			return (value || '').toString().length
+		})
+
+		return {
+			name: field,
+			type: 'line',
+			smooth: true,
+			data: seriesData,
+			connectNulls: false
+		}
+	})
+
+	const option = {
+		title: {
+			text: numericFields.length > 0 ? '数值趋势图' : '字段长度趋势图',
+			left: 'center'
+		},
+		tooltip: {
+			trigger: 'axis',
+			formatter: (params: any) => {
+				let result = `${params[0].axisValue}<br/>`
+				params.forEach((param: any) => {
+					const suffix = numericFields.includes(param.seriesName) ? '' : ' (字符长度)'
+					result += `${param.marker}${param.seriesName}: ${param.value}${suffix}<br/>`
+				})
+				return result
+			}
+		},
+		legend: {
+			data: fieldsToShow,
+			top: 30,
+			type: 'scroll'
+		},
+		grid: {
+			left: '3%',
+			right: '4%',
+			bottom: '3%',
+			containLabel: true
+		},
+		xAxis: {
+			type: 'category',
+			boundaryGap: false,
+			data: xAxisData,
+			axisLabel: {
+				rotate: 45
+			}
+		},
+		yAxis: {
+			type: 'value',
+			name: numericFields.length > 0 ? '数值' : '字符长度'
+		},
+		series: series
+	}
+
+	return encodeURIComponent(JSON.stringify(option))
+}
+
+// 监听显示模式变化,生成对应的图表配置
+watch(display, async (newVal) => {
+	if (newVal === 'table') return
+
+	// 根据显示模式生成对应的图表配置
+	switch (newVal) {
+		case 'bar':
+			chartOptions.value = generateBarChartOptions()
+			break
+		case 'line':
+			chartOptions.value = generateLineChartOptions()
+			break
+	}
+
+	await nextTick()
+	// 通知图表组件重新计算大小
+	chart.value?.resize()
+})
 </script>
 
 <template>
@@ -62,31 +270,77 @@ const lineChart = ref()
 		<el-card v-loading="loadingStruct" shadow="none" :body-style="{ padding: 0, margin: 0 }">
 			<template #header>
 				<div class="card-header">
-					<span>结构化数据表格</span>
-					<el-popover placement="top" :width="120" trigger="hover" content="导出为CSV文件">
-						<template #reference>
+					<span>结构化数据{{ display === 'table' ? '表格' : display === 'bar' ? '柱状图' : '折线图' }}</span>
+					<div class="header-controls">
+						<!-- 显示模式切换按钮组 -->
+						<el-button-group class="display-toggle">
+							<el-button
+								:type="display === 'table' ? 'primary' : ''"
+								:icon="Grid"
+								size="small"
+								@click="display = 'table'"
+								:disabled="!data || !data.fields || !data.data || data.data.length === 0"
+							>
+								表格
+							</el-button>
 							<el-button
-								:icon="Download"
+								:type="display === 'bar' ? 'primary' : ''"
+								:icon="TrendCharts"
 								size="small"
-								text
-								circle
-								@click="exportToCSV"
+								@click="display = 'bar'"
 								:disabled="!data || !data.fields || !data.data || data.data.length === 0"
-								class="export-btn"
-							/>
-						</template>
-					</el-popover>
+							>
+								柱状图
+							</el-button>
+							<el-button
+								:type="display === 'line' ? 'primary' : ''"
+								:icon="DataLine"
+								size="small"
+								@click="display = 'line'"
+								:disabled="!data || !data.fields || !data.data || data.data.length === 0"
+							>
+								折线图
+							</el-button>
+						</el-button-group>
+
+						<!-- 导出按钮 -->
+						<el-popover placement="top" :width="120" trigger="hover" content="导出为CSV文件">
+							<template #reference>
+								<el-button
+									:icon="Download"
+									size="small"
+									text
+									circle
+									@click="exportToCSV"
+									:disabled="!data || !data.fields || !data.data || data.data.length === 0"
+									class="export-btn"
+								/>
+							</template>
+						</el-popover>
+					</div>
 				</div>
 			</template>
-			<!-- 表格内容 -->
-			<div v-if="data && data.fields && data.data" class="table-wrapper">
-				<el-table :data="data.data" stripe border size="small" max-height="400">
-					<el-table-column v-for="field in data.fields" :key="field" :prop="field" :label="field" align="center" min-width="120">
-						<template #default="{ row }">
-							<span>{{ row[field] || '-' }}</span>
-						</template>
-					</el-table-column>
-				</el-table>
+			<!-- 内容区域 -->
+			<div v-if="data && data.fields && data.data">
+				<!-- 表格视图 -->
+				<div v-if="display === 'table'" class="table-wrapper">
+					<el-table :data="data.data" stripe border size="small" max-height="400">
+						<el-table-column v-for="field in data.fields" :key="field" :prop="field" :label="field" align="center" min-width="120">
+							<template #default="{ row }">
+								<span>{{ row[field] || '-' }}</span>
+							</template>
+						</el-table-column>
+					</el-table>
+				</div>
+
+				<!-- 图表视图 -->
+				<div v-else class="chart-wrapper">
+					<VueCharts
+						ref="chart"
+						:data="chartOptions"
+						v-if="chartOptions"
+					/>
+				</div>
 			</div>
 
 			<!-- 空数据状态 -->
@@ -108,16 +362,34 @@ const lineChart = ref()
 		font-weight: 500;
 		color: var(--el-text-color-primary);
 
-		.export-btn {
-			color: var(--el-color-primary);
+		.header-controls {
+			display: flex;
+			align-items: center;
+			gap: 12px;
 
-			&:hover {
-				background-color: var(--el-color-primary-light-9);
+			.display-toggle {
+				.el-button {
+					padding: 6px 12px;
+					font-size: 12px;
+
+					&:disabled {
+						color: var(--el-text-color-disabled);
+						cursor: not-allowed;
+					}
+				}
 			}
 
-			&:disabled {
-				color: var(--el-text-color-disabled);
-				cursor: not-allowed;
+			.export-btn {
+				color: var(--el-color-primary);
+
+				&:hover {
+					background-color: var(--el-color-primary-light-9);
+				}
+
+				&:disabled {
+					color: var(--el-text-color-disabled);
+					cursor: not-allowed;
+				}
 			}
 		}
 	}
@@ -128,6 +400,16 @@ const lineChart = ref()
 		}
 	}
 
+	.chart-wrapper {
+		padding: 20px;
+		min-height: 400px;
+
+		:deep(div) {
+			width: 100% !important;
+			height: 400px !important;
+		}
+	}
+
 	.empty-state {
 		padding: 40px 0;
 		text-align: center;