+
@@ -10,417 +10,279 @@
import * as echarts from 'echarts'
import { useDataStore } from '@/store/dataList'
import { onMounted, watch, ref, computed, nextTick, onUnmounted } from 'vue'
+const dataStore = useDataStore()
+const hasValidData = ref(false)
-// 接收从父组件传入的图表数据
-const props = defineProps({
- chartData: {
- type: Object,
- default: null
- }
-})
-
-const chartContainer = ref() // 容器引用
-const chartInstance = ref(null) // 当前图表实例
-const hasValidData = ref(false) // 是否有有效数据
-const chartOptions = ref(null) // 保存当前配置
+// 添加必要的变量
+const KlineCanvs = ref(null)
+const chartInstance = ref(null)
+const chartOptions = ref(null)
-// 检查数据是否有效
-const checkDataValid = (data) => {
- return data &&
- data.Kline &&
- Array.isArray(data.Kline) &&
- data.Kline.length > 0 &&
- data.Kline.every(item =>
- Array.isArray(item) &&
- item.length >= 5 &&
- !isNaN(Number(item[1])) &&
- !isNaN(Number(item[2])) &&
- !isNaN(Number(item[3])) &&
- !isNaN(Number(item[4]))
- );
+// 添加计算MA数据的函数
+function calculateMA(dayCount, data) {
+ const result = [];
+ for (let i = 0; i < data.length; i++) {
+ if (i < dayCount - 1) {
+ result.push('-');
+ continue;
+ }
+ let sum = 0;
+ for (let j = 0; j < dayCount; j++) {
+ sum += parseFloat(data[i - j][1]);
+ }
+ result.push((sum / dayCount).toFixed(2));
+ }
+ return result;
}
-// 监听传入的数据变化
+// 监听数据变化
watch(
- () => props.chartData,
+ dataStore.KLineData,
(newValue) => {
- console.log('KLine组件收到数据:', newValue)
- // 使用数据验证函数检查
- const isValid = checkDataValid(newValue);
- hasValidData.value = isValid;
-
- if (isValid) {
- nextTick(() => {
- try {
- // 仅当容器存在且没有初始化过图表时创建
- if (chartContainer.value && !chartInstance.value) {
- // 使用try-catch捕获图表初始化和数据处理过程中的错误
- chartInstance.value = echarts.init(chartContainer.value)
- // 使用深拷贝防止数据被修改
- const instanceData = JSON.parse(JSON.stringify(newValue))
- KlineCanvsEcharts(instanceData, chartInstance.value)
- }
- } catch (error) {
- console.error('初始化K线图表时出错:', error)
- hasValidData.value = false;
- }
- })
- } else {
- console.error('无效的K线数据格式:', newValue)
+ console.log('KLine组件: 监听到数据变化', newValue);
+ if (!newValue) {
+ console.warn('KLine组件: 数据为null或undefined');
+ hasValidData.value = false;
+ return;
+ }
+
+ try {
+ const currentData = JSON.parse(JSON.stringify(toRaw(newValue))); // 深拷贝防止切换回来数据更改
+ if (currentData) {
+ nextTick(() => {
+ KlineCanvsEcharts(currentData);
+ });
+ } else {
+ console.warn('KLine组件: 深拷贝后数据为空');
+ hasValidData.value = false;
+ }
+ } catch (error) {
+ console.error('KLine组件: 数据处理错误', error);
+ hasValidData.value = false;
}
},
- { immediate: true }
-)
+ { immediate: true, deep: true }
+);
-function KlineCanvsEcharts(datatok, instance) {
- console.log('传入的K线数据:', datatok);
+// 组件挂载时初始化
+onMounted(() => {
+ console.log('KLine组件挂载完成');
+
+ // 添加窗口resize事件监听
+ const resizeHandler = () => {
+ if (chartInstance.value) {
+ chartInstance.value.resize();
+ }
+ };
+
+ window.addEventListener('resize', resizeHandler);
+
+ // 组件卸载时清理
+ onUnmounted(() => {
+ window.removeEventListener('resize', resizeHandler);
+ if (chartInstance.value) {
+ chartInstance.value.dispose();
+ chartInstance.value = null;
+ }
+ });
+});
- // 更严格的数据验证
+// K线图表生成函数
+function KlineCanvsEcharts(datatok) {
+ console.log('KLine组件: 开始处理数据', datatok);
+
+ // 数据验证
if (!datatok) {
- console.error('K线数据为空');
+ console.warn('KLine组件: 数据无效,为null或undefined');
+ hasValidData.value = false;
return;
}
-
- if (!datatok.Kline) {
- console.error('K线数据中没有Kline属性:', datatok);
- return;
- }
-
- if (!Array.isArray(datatok.Kline)) {
- console.error('Kline数据不是数组:', typeof datatok.Kline);
- return;
- }
-
- if (datatok.Kline.length === 0) {
- console.error('Kline数组为空');
+
+ // 兼容两种可能的数据格式
+ let klineData = null;
+ let chartName = '';
+
+ if (datatok.Kline && Array.isArray(datatok.Kline)) {
+ klineData = datatok.Kline;
+ chartName = datatok.name || '股票K线图';
+ } else if (datatok.KLine20 && Array.isArray(datatok.KLine20)) {
+ klineData = datatok.KLine20;
+ chartName = datatok.name || '股票K线图';
+ } else {
+ console.warn('KLine组件: 找不到有效的K线数据');
+ hasValidData.value = false;
return;
}
-
- // 验证数组中的每个元素是否符合要求
- const isValidKlineItem = datatok.Kline.every(item =>
- Array.isArray(item) && item.length >= 5 &&
- !isNaN(Number(item[1])) && !isNaN(Number(item[2])) &&
- !isNaN(Number(item[3])) && !isNaN(Number(item[4]))
- );
-
- if (!isValidKlineItem) {
- console.error('Kline数据格式不正确, 期望格式: [[date, open, close, low, high], ...]');
- console.error('实际数据样例:', datatok.Kline[0]);
+
+ if (klineData.length === 0) {
+ console.warn('KLine组件: K线数据为空数组');
+ hasValidData.value = false;
return;
}
-
- // 切割数据方法
- const data = datatok.Kline;
- const spliteDate = (a) => {
- const categoryData = [];
- let value = [];
- try {
- for (let i = 0; i < a.length; i++) {
- // 确保每个数据项都是有效的
- if (Array.isArray(a[i]) && a[i].length >= 5) {
- categoryData.push(a[i][0]);
- // 确保转换为数字类型
- value.push([
- Number(a[i][1]),
- Number(a[i][2]),
- Number(a[i][3]),
- Number(a[i][4])
- ]);
- }
+
+ // 数据处理
+ const categoryData = [];
+ const values = [];
+
+ try {
+ for (let i = 0; i < klineData.length; i++) {
+ const item = klineData[i];
+ if (!Array.isArray(item) || item.length < 5) {
+ console.warn(`KLine组件: 第${i + 1}个数据点格式无效`, item);
+ continue; // 跳过无效数据点而不是中断整个处理
}
- } catch (error) {
- console.error('处理K线数据时出错:', error);
- return { categoryData: [], value: [] };
+ categoryData.push(item[0]); // 日期
+ values.push([
+ parseFloat(item[1]), // 开盘
+ parseFloat(item[2]), // 收盘
+ parseFloat(item[3]), // 最低
+ parseFloat(item[4]) // 最高
+ ]);
}
- return { categoryData, value };
- };
-
- const dealData = spliteDate(data);
-
- console.log('处理后的K线数据:', dealData);
- if (dealData.value.length === 0 || dealData.categoryData.length === 0) {
- console.error('空数据,无法渲染图表');
- return;
- }
-
- // 给配置项
- const KlineOption = {
- title: {
- text: datatok.name || '股票K线图',
- top: 20,
- left: 20
- },
- tooltip: {
- trigger: 'axis',
- show: true,
- formatter: function (params) {
- if (!params || params.length === 0) return '';
- const param = params[0];
- return `
-
${param.name}
-
开盘价: ${param.data[1]}
-
收盘价: ${param.data[2]}
-
最低价: ${param.data[3]}
-
最高价: ${param.data[4]}
- ${params[1] && params[1].value !== '-' ? '
MA5: ' + params[1].value + '
' : ''}
- `;
- },
- axisPointer: {
- animation: false,
- type: 'cross',
- lineStyle: {
- color: '#376df4',
- width: 2,
- opacity: 1
- }
- },
- confine: true,
- backgroundColor: 'rgba(255, 255, 255, 0.95)',
- borderColor: '#ccc',
- borderWidth: 1,
- padding: 10,
- textStyle: {
- color: '#333'
- }
- },
- // 横坐标内容
- xAxis: {
- type: 'category',
- data: dealData.categoryData,
- axisLine: { lineStyle: { color: '#8392A5' } }
- },
- //控制坐标轴
- grid: {
- left: '12%',
- right: '10%',
- bottom: '10%',
- top: '18%'
- },
- yAxis: {
- scale: !0, //true
- // 自定义纵坐标现实的数据
- axisLabel: {
- formatter: function (value) {
- return value // 返回原始值
- }
+
+ if (categoryData.length === 0 || values.length === 0) {
+ console.warn('KLine组件: 处理后无有效数据点');
+ hasValidData.value = false;
+ return;
+ }
+
+ // 计算MA5
+ const ma5Data = calculateMA(5, klineData);
+
+ // 确保容器存在
+ if (!KlineCanvs.value) {
+ console.warn('KLine组件: 找不到容器元素');
+ hasValidData.value = false;
+ return;
+ }
+
+ // 创建或更新图表实例
+ if (chartInstance.value) {
+ chartInstance.value.dispose();
+ }
+ chartInstance.value = echarts.init(KlineCanvs.value);
+
+ // 图表配置
+ const KlineOption = {
+ title: {
+ text: chartName,
+ left: 0
},
- axisLine: { lineStyle: { color: '#8392A5' } },
- splitLine: {
- show: !1
- }
- },
- // 下拉条
- dataZoom: [
- {
- textStyle: {
- color: '#8392A5'
+ tooltip: {
+ trigger: 'axis',
+ formatter: function (params) {
+ if (!params || params.length === 0) return '';
+ const param = params[0];
+ let result = param.name + '
' +
+ '开盘价' + param.data[0] + '
' +
+ '收盘价' + param.data[1] + '
' +
+ '最低价' + param.data[2] + '
' +
+ '最高价' + param.data[3];
+
+ // 判断MA5是否存在
+ if (params[1] && params[1].value !== '-') {
+ result += '
' + params[1].seriesName + ':' + params[1].value;
+ }
+
+ return result;
},
- handleIcon:
- 'M10.7,11.9v-1.3H9.3v1.3c-4.9,0.3-8.8,4.4-8.8,9.4c0,5,3.9,9.1,8.8,9.4v1.3h1.3v-1.3c4.9-0.3,8.8-4.4,8.8-9.4C19.5,16.3,15.6,12.2,10.7,11.9z M13.3,24.4H6.7V23h6.6V24.4z M13.3,19.6H6.7v-1.4h6.6V19.6z',
- handleSize: '80%',
- dataBackground: {
- areaStyle: {
- color: '#8392A5'
- },
+ axisPointer: {
+ animation: false,
+ type: 'cross',
lineStyle: {
- opacity: 0.8,
- color: '#8392A5'
+ color: '#376df4',
+ width: 2,
+ opacity: 1
}
- },
- handleStyle: {
- color: '#fff',
- shadowBlur: 3,
- shadowColor: 'rgba(0, 0, 0, 0.6)',
- shadowOffsetX: 2,
- shadowOffsetY: 2
}
},
- {
- show: !1,
- type: 'slider'
+ grid: {
+ left: '12%',
+ right: '10%',
+ bottom: '15%',
+ top: '18%'
},
- {
- type: 'inside'
- }
- ],
- animation: !1, //false
- // 线条的数据
- series: [
- {
- type: 'candlestick',
- name: '\u65e5K',
- // 数据
- data: dealData.value,
- itemStyle: {
- normal: {
- color0: '#FD1050',
- color: '#0CF49B',
- borderColor0: '#FD1050',
- borderColor: '#0CF49B'
+ xAxis: {
+ type: 'category',
+ data: categoryData,
+ axisLine: { lineStyle: { color: '#8392A5' } }
+ },
+ yAxis: {
+ scale: true,
+ axisLabel: {
+ formatter: function (value) {
+ return value; // 返回原始值
}
- }
+ },
+ axisLine: { lineStyle: { color: '#8392A5' } },
+ splitLine: { show: false }
},
- {
- name: 'MA5',
- type: 'line',
- // 此处需要接口调用同类型数据
- // 计算出MA5的数据
- data: (function (a) {
- for (var MA5 = [], d = 0, g = dealData.value.length; d < g; d++) {
- if (d < a) {
- MA5.push('-')
- } else {
- for (var f = 0, e = 0; e < a; e++) {
- f += dealData.value[d - e][1]
- }
- MA5.push((f / a).toFixed(2))
- }
+ dataZoom: [
+ {
+ type: 'inside',
+ start: 50,
+ end: 100
+ },
+ {
+ show: true,
+ type: 'slider',
+ top: '90%',
+ start: 50,
+ end: 100
+ }
+ ],
+ series: [
+ {
+ name: chartName,
+ type: 'candlestick',
+ data: values,
+ itemStyle: {
+ color: '#ef232a',
+ color0: '#14b143',
+ borderColor: '#ef232a',
+ borderColor0: '#14b143'
}
- return MA5
- })(5),
- smooth: !0
- }
- ]
- }
-
- // 应用配置到图表实例
- instance.setOption(KlineOption, true);
-
- // 保存配置到ref,供暴露的方法使用
- chartOptions.value = KlineOption;
-
- // 绑定点击事件
- instance.on('click', function(params) {
- console.log('图表点击事件:', params);
- if (params.componentType === 'series') {
- const date = params.name;
- const data = params.data;
- if (Array.isArray(data) && data.length >= 5) {
- console.log(`点击了 ${date} 的K线数据: 开盘${data[1]}, 收盘${data[2]}, 最低${data[3]}, 最高${data[4]}`);
- // 在实际使用中可以替换为更友好的交互方式,如显示详细信息面板
- }
- }
- });
-
- // 添加更多事件监听
- instance.on('datazoom', function(params) {
- console.log('图表缩放事件:', params);
- // 可以在这里处理缩放事件,如记录当前缩放状态等
- });
-
- instance.on('legendselectchanged', function(params) {
- console.log('图例选择变化:', params);
- });
-
- // 绑定窗口resize事件
- const resizeHandler = () => {
- if (instance) {
- instance.resize();
- }
- };
-
- window.addEventListener('resize', resizeHandler);
-
- // 返回清理函数供组件卸载时使用
- return () => {
- window.removeEventListener('resize', resizeHandler);
- instance.off('click');
- instance.off('datazoom');
- instance.off('legendselectchanged');
- };
-}
-
-// 组件卸载时清理资源
-onUnmounted(() => {
- if (chartInstance.value) {
- // 移除所有事件监听
- chartInstance.value.off('click');
- chartInstance.value.off('datazoom');
- chartInstance.value.off('legendselectchanged');
-
- // 销毁图表实例
- chartInstance.value.dispose();
- chartInstance.value = null;
- }
-})
-
-onMounted(() => {
- console.log('KLine组件挂载完成')
-})
-
-// 对外暴露方法,允许父组件访问图表实例和方法
-defineExpose({
- getChartInstance: () => chartInstance.value,
- getOptions: () => chartOptions.value,
- resize: () => {
- if (chartInstance.value) {
- chartInstance.value.resize()
- }
- },
- // 缩放到指定日期范围
- zoomToDateRange: (startDate, endDate) => {
- if (!chartInstance.value || !chartOptions.value) return;
-
- const categoryData = chartOptions.value.xAxis[0].data;
- if (!Array.isArray(categoryData)) return;
-
- const startIndex = categoryData.findIndex(date => date >= startDate);
- const endIndex = categoryData.findIndex(date => date >= endDate);
-
- if (startIndex === -1 || endIndex === -1) return;
+ },
+ {
+ name: 'MA5',
+ type: 'line',
+ data: ma5Data,
+ smooth: true,
+ lineStyle: {
+ opacity: 0.5
+ }
+ }
+ ]
+ };
- // 计算百分比
- const total = categoryData.length;
- const start = (startIndex / total) * 100;
- const end = (endIndex / total) * 100;
+ // 应用配置
+ chartInstance.value.setOption(KlineOption);
+ chartOptions.value = KlineOption;
- // 应用缩放
- chartInstance.value.dispatchAction({
- type: 'dataZoom',
- start: start,
- end: end
+ // 绑定事件
+ chartInstance.value.off('click');
+ chartInstance.value.on('click', function(params) {
+ console.log('图表点击事件:', params);
+ if (params.componentType === 'series') {
+ const date = params.name;
+ const data = params.data;
+ console.log(`点击了 ${date} 的K线数据:`, data);
+ }
});
- },
- // 高亮特定日期的K线
- highlightDate: (date) => {
- if (!chartInstance.value || !chartOptions.value) return;
- const categoryData = chartOptions.value.xAxis[0].data;
- if (!Array.isArray(categoryData)) return;
+ // 设置数据有效标志
+ hasValidData.value = true;
+ console.log('KLine组件: 图表渲染完成');
- const index = categoryData.findIndex(d => d === date);
- if (index !== -1) {
- chartInstance.value.dispatchAction({
- type: 'highlight',
- seriesIndex: 0,
- dataIndex: index
- });
-
- // 同时显示tooltip
- chartInstance.value.dispatchAction({
- type: 'showTip',
- seriesIndex: 0,
- dataIndex: index
- });
- }
- },
- // 清除高亮
- clearHighlight: () => {
- if (chartInstance.value) {
- chartInstance.value.dispatchAction({
- type: 'downplay'
- });
-
- chartInstance.value.dispatchAction({
- type: 'hideTip'
- });
- }
+ } catch (error) {
+ console.error('KLine组件: 图表渲染错误', error);
+ hasValidData.value = false;
}
-})
+}