import * as React from 'react'; import * as echarts from 'echarts'; import './style.scss'; /** * 图表组件属性接口 * 定义了图表组件接收的所有配置参数 */ interface ChartWidgetProps { /** 图表类型:line(折线图)、bar(柱状图)、pie(饼图)、scatter(散点图)、radar(雷达图)、gauge(仪表盘)、funnel(漏斗图)、sankey(桑基图) */ chartType: string; /** 图表主标题 */ title: string; /** 图表副标题 */ subtitle: string; /** 图表宽度(像素) */ width: number; /** 图表高度(像素) */ height: number; /** 图表主题:default(默认)、dark(深色)、light(明亮)、business(商务) */ theme: string; /** 是否显示图例 */ showLegend: boolean; /** 是否显示提示框 */ showTooltip: boolean; /** 是否显示数据缩放控件 */ showDataZoom: boolean; /** 是否显示网格线 */ showGrid: boolean; /** 背景颜色 */ backgroundColor: string; /** 文字颜色 */ textColor: string; /** 模拟数据对象,包含各种图表类型的数据 */ mockData: any; /** 数据配置对象 */ dataConfig: any; /** AMIS 数据对象,包含用户信息和系统信息 */ data?: any; } /** * 酷炫图表组件 * 基于 ECharts 实现的多类型图表组件,支持折线图、柱状图、饼图、散点图、雷达图、仪表盘、漏斗图、桑基图 * 提供丰富的配置选项和交互功能 */ export default class ChartWidget extends React.PureComponent { /** 图表容器的 DOM 引用 */ private chartRef: React.RefObject; /** ECharts 实例对象 */ private chartInstance: echarts.ECharts | null = null; constructor(props: ChartWidgetProps) { super(props); this.chartRef = React.createRef(); } /** * 组件默认属性配置 * 当父组件未传入对应属性时使用这些默认值 */ static defaultProps = { chartType: 'line', title: '图表标题', subtitle: '', width: 800, height: 400, theme: 'default', showLegend: true, showTooltip: true, showDataZoom: false, showGrid: true, backgroundColor: 'transparent', textColor: '#333', mockData: {}, dataConfig: {}, }; /** * 组件挂载完成后初始化图表 */ componentDidMount() { this.initChart(); } /** * 组件更新时检查是否需要重新渲染图表 * @param prevProps 上一次的属性 */ componentDidUpdate(prevProps: ChartWidgetProps) { if (this.shouldUpdateChart(prevProps)) { this.updateChart(); } } /** * 组件卸载前清理图表实例,防止内存泄漏 */ componentWillUnmount() { if (this.chartInstance) { this.chartInstance.dispose(); } } /** * 判断是否需要更新图表 * 通过比较当前属性和上一次属性,决定是否重新渲染图表 * @param prevProps 上一次的属性 * @returns 是否需要更新图表 */ shouldUpdateChart(prevProps: ChartWidgetProps) { const { chartType, mockData, dataConfig, width, height, theme, title, subtitle, backgroundColor, textColor, showLegend, showTooltip, showDataZoom, showGrid, } = this.props; return ( chartType !== prevProps.chartType || mockData !== prevProps.mockData || dataConfig !== prevProps.dataConfig || title !== prevProps.title || subtitle !== prevProps.subtitle || width !== prevProps.width || height !== prevProps.height || theme !== prevProps.theme || backgroundColor !== prevProps.backgroundColor || textColor !== prevProps.textColor || showLegend !== prevProps.showLegend || showTooltip !== prevProps.showTooltip || showDataZoom !== prevProps.showDataZoom || showGrid !== prevProps.showGrid ); } /** * 初始化图表实例 * 创建 ECharts 实例并设置初始配置 */ initChart() { if (!this.chartRef.current) return; this.chartInstance = echarts.init(this.chartRef.current, this.props.theme); this.updateChart(); } /** * 更新图表配置 * 根据当前属性生成新的图表配置并应用 */ updateChart() { if (!this.chartInstance) return; try { const option = this.getChartOption(); // 验证 option 是否有效 if (option && option.series && Array.isArray(option.series)) { this.chartInstance.setOption(option, true); } else { console.warn('Chart option is invalid:', option); } } catch (error) { console.error('Error updating chart:', error); } } /** * 获取图表配置选项 * 根据组件属性生成 ECharts 配置对象 * @returns ECharts 配置对象 */ getChartOption() { const { chartType, title, subtitle, showLegend, showTooltip, showDataZoom, showGrid, backgroundColor, textColor, mockData, dataConfig, } = this.props; // 基础配置选项,所有图表类型共享 const baseOption = { backgroundColor: backgroundColor || 'transparent', textStyle: { color: textColor || '#333', }, title: { text: title || '图表标题', subtext: subtitle || '', left: 'left', textStyle: { color: textColor || '#333', fontSize: 18, fontWeight: 'bold', }, subtextStyle: { color: textColor || '#666', fontSize: 12, }, }, tooltip: showTooltip ? { trigger: 'axis', backgroundColor: 'rgba(0, 0, 0, 0.8)', borderColor: '#333', textStyle: { color: '#fff', }, axisPointer: { type: 'cross', crossStyle: { color: '#999', }, }, } : undefined, legend: showLegend ? { data: mockData?.series?.map((s: any) => s.name) || [], top: 'bottom', textStyle: { color: textColor || '#333', }, } : undefined, grid: showGrid ? { left: '3%', right: '4%', bottom: '10%', top: '18%', containLabel: true, } : undefined, dataZoom: showDataZoom ? [ { type: 'inside', start: 0, end: 100, }, { start: 0, end: 100, height: 30, bottom: 10, }, ] : undefined, }; // 根据图表类型返回对应的配置 switch (chartType) { case 'line': return this.getLineChartOption(baseOption, mockData, dataConfig); case 'bar': return this.getBarChartOption(baseOption, mockData, dataConfig); case 'pie': return this.getPieChartOption(baseOption, mockData, dataConfig); case 'scatter': return this.getScatterChartOption(baseOption, mockData, dataConfig); case 'radar': return this.getRadarChartOption(baseOption, mockData, dataConfig); case 'gauge': return this.getGaugeChartOption(baseOption, mockData, dataConfig); case 'funnel': return this.getFunnelChartOption(baseOption, mockData, dataConfig); case 'sankey': return this.getSankeyChartOption(baseOption, mockData, dataConfig); default: return this.getLineChartOption(baseOption, mockData, dataConfig); } } /** * 获取折线图配置 * @param baseOption 基础配置选项 * @param mockData 模拟数据 * @param dataConfig 数据配置 * @returns 折线图配置对象 */ getLineChartOption(baseOption: any, mockData: any, dataConfig: any) { const series = mockData?.series || [ { name: '数据', data: [120, 200, 150, 80, 70, 110, 130], }, ]; // 确保每个 series 都有正确的 type 属性 const formattedSeries = series.map((item: any) => ({ name: item.name || '数据', type: 'line', data: item.data || [], smooth: true, symbol: 'circle', symbolSize: 6, lineStyle: { width: 3, }, areaStyle: { opacity: 0.3, }, })); return { ...baseOption, xAxis: { type: 'category', data: mockData?.xAxis || [ 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun', ], axisLine: { lineStyle: { color: this.props.textColor || '#333', }, }, }, yAxis: { type: 'value', axisLine: { lineStyle: { color: this.props.textColor || '#333', }, }, }, series: formattedSeries, }; } /** * 获取柱状图配置 * @param baseOption 基础配置选项 * @param mockData 模拟数据 * @param dataConfig 数据配置 * @returns 柱状图配置对象 */ getBarChartOption(baseOption: any, mockData: any, dataConfig: any) { const series = mockData?.series || [ { name: '数据', data: [120, 200, 150, 80, 70, 110, 130], }, ]; // 确保每个 series 都有正确的 type 属性 const formattedSeries = series.map((item: any) => ({ name: item.name || '数据', type: 'bar', data: item.data || [], itemStyle: { borderRadius: [4, 4, 0, 0], }, })); return { ...baseOption, xAxis: { type: 'category', data: mockData?.xAxis || [ 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun', ], axisLine: { lineStyle: { color: this.props.textColor || '#333', }, }, }, yAxis: { type: 'value', axisLine: { lineStyle: { color: this.props.textColor || '#333', }, }, }, series: formattedSeries, }; } /** * 获取饼图配置 * @param baseOption 基础配置选项 * @param mockData 模拟数据 * @param dataConfig 数据配置 * @returns 饼图配置对象 */ getPieChartOption(baseOption: any, mockData: any, dataConfig: any) { // 饼图数据获取逻辑修复 let pieData = []; // 尝试多种数据源 if (mockData?.pieData && Array.isArray(mockData.pieData)) { // 优先使用专门的饼图数据 pieData = mockData.pieData; } else if (mockData?.series?.[0]?.data && Array.isArray(mockData.series[0].data)) { // 使用系列数据 pieData = mockData.series[0].data; } else if (mockData?.series && Array.isArray(mockData.series)) { // 将系列数据转换为饼图格式 pieData = mockData.series.map((item: any) => ({ name: item.name || '数据', value: Array.isArray(item.data) ? item.data.reduce((sum: number, val: number) => sum + val, 0) : item.value || 0 })); } else { // 使用默认数据 pieData = [ { value: 1048, name: '搜索引擎' }, { value: 735, name: '直接访问' }, { value: 580, name: '邮件营销' }, { value: 484, name: '联盟广告' }, { value: 300, name: '视频广告' }, ]; } return { ...baseOption, series: [ { name: '数据', type: 'pie', radius: ['40%', '70%'], center: ['50%', '50%'], data: pieData, label: { show: true, position: 'outside', fontSize: 12, color: this.props.textColor || '#333', formatter: '{c} ({d}%)', overflow: 'truncate' }, labelLine: { show: true, length: 15, length2: 10, smooth: true, lineStyle: { color: this.props.textColor || '#333', width: 1 } }, emphasis: { itemStyle: { shadowBlur: 10, shadowOffsetX: 0, shadowColor: 'rgba(0, 0, 0, 0.5)', }, label: { show: true, fontSize: 14, fontWeight: 'bold' } }, }, ], }; } /** * 获取散点图配置 * @param baseOption 基础配置选项 * @param mockData 模拟数据 * @param dataConfig 数据配置 * @returns 散点图配置对象 */ getScatterChartOption(baseOption: any, mockData: any, dataConfig: any) { return { ...baseOption, xAxis: { type: 'value', scale: true, axisLine: { lineStyle: { color: this.props.textColor || '#333', }, }, }, yAxis: { type: 'value', scale: true, axisLine: { lineStyle: { color: this.props.textColor || '#333', }, }, }, series: [ { name: '数据', type: 'scatter', data: mockData?.series?.[0]?.data || [ [161.2, 51.6], [167.5, 59.0], [159.5, 49.2], [157.0, 63.0], [155.8, 53.6], [170.0, 59.0], [159.1, 47.6], [166.0, 69.8], [176.2, 66.8], [160.2, 75.2], ], symbolSize: 8, }, ], }; } /** * 获取雷达图配置 * @param baseOption 基础配置选项 * @param mockData 模拟数据 * @param dataConfig 数据配置 * @returns 雷达图配置对象 */ getRadarChartOption(baseOption: any, mockData: any, dataConfig: any) { return { ...baseOption, radar: { indicator: mockData?.indicator || [ { name: '销售', max: 6500 }, { name: '管理', max: 16000 }, { name: '信息技术', max: 30000 }, { name: '客服', max: 38000 }, { name: '研发', max: 52000 }, { name: '市场', max: 25000 }, ], }, series: [ { name: '数据', type: 'radar', data: mockData?.series || [ { value: [4200, 3000, 20000, 35000, 50000, 18000], name: '预算分配', }, ], }, ], }; } /** * 获取仪表盘配置 * @param baseOption 基础配置选项 * @param mockData 模拟数据 * @param dataConfig 数据配置 * @returns 仪表盘配置对象 */ getGaugeChartOption(baseOption: any, mockData: any, dataConfig: any) { return { ...baseOption, series: [ { name: '数据', type: 'gauge', data: mockData?.series?.[0]?.data || [{ value: 50, name: '完成率' }], detail: { formatter: '{value}%', }, }, ], }; } /** * 获取漏斗图配置 * @param baseOption 基础配置选项 * @param mockData 模拟数据 * @param dataConfig 数据配置 * @returns 漏斗图配置对象 */ getFunnelChartOption(baseOption: any, mockData: any, dataConfig: any) { return { ...baseOption, series: [ { name: '数据', type: 'funnel', left: '10%', top: 60, bottom: 60, width: '80%', min: 0, max: 100, minSize: '0%', maxSize: '100%', sort: 'descending', gap: 2, label: { show: true, position: 'inside', }, labelLine: { length: 10, lineStyle: { width: 1, type: 'solid', }, }, itemStyle: { borderColor: '#fff', borderWidth: 1, }, emphasis: { label: { fontSize: 20, }, }, data: mockData?.series?.[0]?.data || [ { value: 60, name: '访问' }, { value: 40, name: '咨询' }, { value: 20, name: '订单' }, { value: 80, name: '点击' }, { value: 100, name: '展现' }, ], }, ], }; } /** * 获取桑基图配置 * @param baseOption 基础配置选项 * @param mockData 模拟数据 * @param dataConfig 数据配置 * @returns 桑基图配置对象 */ getSankeyChartOption(baseOption: any, mockData: any, dataConfig: any) { return { ...baseOption, series: [ { name: '数据', type: 'sankey', data: mockData?.nodes || [ { name: '节点1' }, { name: '节点2' }, { name: '节点3' }, { name: '节点4' }, ], links: mockData?.links || [ { source: '节点1', target: '节点2', value: 10 }, { source: '节点2', target: '节点3', value: 15 }, { source: '节点3', target: '节点4', value: 20 }, ], emphasis: { focus: 'adjacency', }, lineStyle: { color: 'gradient', curveness: 0.5, }, }, ], }; } /** * 渲染组件 * 显示用户信息(如果可用)和图表容器 * @returns JSX 元素 */ render() { const { width, height } = this.props; const curAmisData = this.props.data || {}; const userInfo = curAmisData.__AmisCurrentUser; const systemInfo = curAmisData.__AmisSystemInfo || {}; return (
{/* 显示当前用户信息 */} {userInfo && userInfo.name && (
用户:{userInfo.name} {systemInfo.tenantName && ( 租户:{systemInfo.tenantName} )}
)} {/* 图表容器 */}
); } }