浏览代码

add new tools: chart maker

zxlie 6 月之前
父节点
当前提交
27a8ac7011

+ 32 - 0
.cursorrules

@@ -0,0 +1,32 @@
+# 角色
+你是一个Chrome浏览器扩展开发专家,对Chrome Extension Manifest V3非常熟悉。你需要帮助我开发和维护一个名为FeHelper的Chrome扩展。
+
+# 项目结构规范
+- apps/目录是项目的主目录
+- 每个功能模块都是apps/下的独立目录
+- manifest.json 是扩展的配置文件
+- background/ 目录包含后台服务脚本
+- popup/ 目录包含扩展的弹出窗口页面
+- options/ 目录包含扩展的配置页面
+- static/ 目录包含静态资源
+
+# 编码规范
+- 使用ES6+语法
+- 模块化开发,每个功能保持独立
+- 遵循Chrome Extension V3的最佳实践
+- 代码需要清晰的注释和文档
+- 保持一致的代码风格和缩进
+
+# 功能模块开发规范
+- 每个新功能模块需要在apps/下创建独立目录
+- 模块目录需包含完整的HTML、JS、CSS文件
+- 新增模块需要在manifest.json中正确配置
+- 需要在web_accessible_resources中声明可访问的资源
+- 遵循Chrome Extension的安全策略和最佳实践
+
+# 注意事项
+- 权限申请需要最小化原则
+- 需要考虑跨域访问的限制
+- 注意性能优化和资源占用
+- 保持代码的可维护性和可扩展性
+- 遵循Chrome商店的发布规范

+ 8 - 0
apps/background/tools.js

@@ -240,6 +240,14 @@ let toolMap = {
             text: 'Excel转JSON'
         }]
     },
+    'chart-maker': {
+        name: '图表制作工具',
+        tips: '快速制作各类数据可视化图表,支持柱状图、折线图、饼图等多种图表类型,可导出为图片格式',
+        menuConfig: [{
+            icon: '📊',
+            text: '图表制作工具'
+        }]
+    },
 };
 
 export default toolMap;

+ 1130 - 0
apps/chart-maker/chart-generator.js

@@ -0,0 +1,1130 @@
+// 图表实例,方便后续更新
+window.chartInstance = null;
+
+// 注册Chart.js插件
+if (Chart && Chart.register) {
+    // 如果有ChartDataLabels插件,注册它
+    if (window.ChartDataLabels) {
+        Chart.register(ChartDataLabels);
+    }
+}
+
+// 生成图表的主函数
+function createChart(data, settings) {
+    // 获取Canvas元素
+    const canvas = document.getElementById('chart-canvas');
+    const ctx = canvas.getContext('2d');
+    
+    // 如果已有图表,先销毁
+    if (window.chartInstance) {
+        window.chartInstance.destroy();
+    }
+    
+    // 应用颜色方案
+    applyColorScheme(data, settings.colorScheme, settings.type);
+    
+    // 配置图表选项
+    const options = getChartOptions(settings);
+    
+    // 处理特殊图表类型
+    let type = settings.type;
+    let chartData = {...data};
+
+    // 检查是否为首系列图表类型
+    const isFirstSeriesOnly = settings.type.includes(" (首系列)");
+    if (isFirstSeriesOnly) {
+        // 提取真正的图表类型
+        type = settings.type.replace(" (首系列)", "");
+        
+        // 只保留第一个数据系列
+        if (chartData.datasets.length > 1) {
+            const firstDataset = chartData.datasets[0];
+            chartData.datasets = [{
+                ...firstDataset,
+                label: firstDataset.label || '数据'
+            }];
+        }
+    }
+
+    // 移除可能存在的旧堆叠设置
+    if (options.scales && options.scales.x) {
+        delete options.scales.x.stacked;
+    }
+    if (options.scales && options.scales.y) {
+        delete options.scales.y.stacked;
+    }
+
+    // 移除旧的填充设置和其他特殊属性
+    chartData.datasets.forEach(dataset => {
+        delete dataset.fill;
+        delete dataset.tension;
+        delete dataset.stepped;
+        delete dataset.borderDash;
+    });
+
+    // 基本图表类型处理
+    switch(type) {
+        // 柱状图系列
+        case 'horizontalBar':
+            type = 'bar';
+            options.indexAxis = 'y';
+            break;
+        case 'stackedBar':
+            type = 'bar';
+            if (!options.scales) options.scales = {};
+            if (!options.scales.x) options.scales.x = {};
+            if (!options.scales.y) options.scales.y = {};
+            options.scales.x.stacked = true;
+            options.scales.y.stacked = true;
+            break;
+        case 'groupedBar':
+            type = 'bar';
+            // 分组柱状图是默认行为
+            break;
+        case 'gradientBar':
+            type = 'bar';
+            // 渐变效果在applyColorScheme函数中处理
+            break;
+        case 'barWithError':
+            type = 'bar';
+            // 添加误差线
+            chartData.datasets.forEach(dataset => {
+                dataset.errorBars = {
+                    y: {
+                        plus: dataset.data.map(() => Math.random() * 5 + 2),
+                        minus: dataset.data.map(() => Math.random() * 5 + 2)
+                    }
+                };
+            });
+            break;
+        case 'rangeBar':
+            type = 'bar';
+            // 转换数据为范围格式
+            chartData.datasets.forEach(dataset => {
+                dataset.data = dataset.data.map(value => {
+                    const min = Math.max(0, value - Math.random() * value * 0.4);
+                    return [min, value];
+                });
+            });
+            break;
+        
+        // 线/面积图系列
+        case 'area':
+            type = 'line';
+            chartData.datasets.forEach(dataset => {
+                dataset.fill = true;
+            });
+            break;
+        case 'curvedLine':
+            type = 'line';
+            chartData.datasets.forEach(dataset => {
+                dataset.tension = 0.4; // 更平滑的曲线
+            });
+            break;
+        case 'stepLine':
+            type = 'line';
+            chartData.datasets.forEach(dataset => {
+                dataset.stepped = true;
+            });
+            break;
+        case 'stackedArea':
+            type = 'line';
+            chartData.datasets.forEach(dataset => {
+                dataset.fill = true;
+            });
+            if (!options.scales) options.scales = {};
+            if (!options.scales.y) options.scales.y = {};
+            options.scales.y.stacked = true;
+            break;
+        case 'streamgraph':
+            type = 'line';
+            // 流图效果:堆叠面积图 + 居中对齐
+            chartData.datasets.forEach(dataset => {
+                dataset.fill = true;
+            });
+            if (!options.scales) options.scales = {};
+            if (!options.scales.y) options.scales.y = {};
+            options.scales.y.stacked = true;
+            options.scales.y.offset = true; // 居中对齐堆叠
+            break;
+        case 'timeline':
+            type = 'line';
+            chartData.datasets.forEach(dataset => {
+                dataset.stepped = 'before';
+                dataset.borderDash = [5, 5]; // 虚线效果
+            });
+            break;
+        
+        // 饼图/环形图系列
+        case 'halfPie':
+            type = 'doughnut';
+            options.circumference = Math.PI;
+            options.rotation = -Math.PI / 2;
+            break;
+        case 'nestedPie':
+            type = 'doughnut';
+            // 嵌套效果通过多个饼图叠加实现,简化实现仅调整内外半径
+            if (chartData.datasets.length > 0) {
+                chartData.datasets[0].radius = '70%';
+                chartData.datasets[0].weight = 0.7;
+            }
+            break;
+        
+        // 散点/气泡图系列
+        case 'scatter':
+            chartData.datasets = transformScatterData(chartData.datasets);
+            break;
+        case 'bubble':
+            chartData.datasets = transformBubbleData(chartData.datasets);
+            type = 'bubble';
+            break;
+        case 'scatterSmooth':
+            chartData.datasets = transformScatterData(chartData.datasets);
+            type = 'scatter';
+            // 添加趋势线
+            chartData.datasets.forEach(dataset => {
+                const smoothedDataset = {
+                    ...dataset,
+                    type: 'line',
+                    data: [...dataset.data],
+                    pointRadius: 0,
+                    tension: 0.4,
+                    fill: false
+                };
+                chartData.datasets.push(smoothedDataset);
+            });
+            break;
+        
+        // 专业图表系列
+        case 'funnel':
+            type = 'bar';
+            // 简化的漏斗图实现
+            options.indexAxis = 'y';
+            if (chartData.datasets.length > 0) {
+                // 对数据进行排序
+                const sortedData = [...chartData.datasets[0].data].sort((a, b) => b - a);
+                chartData.datasets[0].data = sortedData;
+                // 确保Y轴反转
+                if (!options.scales) options.scales = {};
+                if (!options.scales.y) options.scales.y = {};
+                options.scales.y.reverse = true;
+            }
+            break;
+        case 'gauge':
+            type = 'doughnut';
+            // 简化的仪表盘实现
+            if (chartData.datasets.length > 0 && chartData.datasets[0].data.length > 0) {
+                const value = chartData.datasets[0].data[0];
+                const max = Math.max(...chartData.datasets[0].data) * 1.2;
+                const remainder = max - value;
+                
+                chartData.datasets[0].data = [value, remainder];
+                chartData.datasets[0].backgroundColor = ['#36A2EB', '#E0E0E0'];
+                chartData.labels = ['Value', ''];
+                
+                options.circumference = Math.PI;
+                options.rotation = -Math.PI;
+                options.cutout = '70%';
+            }
+            break;
+        case 'boxplot':
+            // 简化的箱线图实现(基于柱状图)
+            type = 'bar';
+            // 转换数据为箱线图格式
+            chartData.datasets.forEach(dataset => {
+                dataset.data = dataset.data.map(value => {
+                    const q1 = Math.max(0, value * 0.7);
+                    const median = value * 0.85;
+                    const q3 = value * 1.15;
+                    const min = Math.max(0, q1 - (median - q1));
+                    const max = q3 + (q3 - median);
+                    return [min, q1, median, q3, max];
+                });
+            });
+            break;
+        case 'waterfall':
+            type = 'bar';
+            // 瀑布图实现
+            if (chartData.datasets.length > 0) {
+                const data = chartData.datasets[0].data;
+                let cumulative = 0;
+                
+                // 创建新的数据数组,包含每个点的起点和终点
+                const waterfallData = data.map((value, index) => {
+                    const start = cumulative;
+                    cumulative += value;
+                    return {
+                        start: start,
+                        end: cumulative,
+                        value: value
+                    };
+                });
+                
+                // 转换为柱状图数据
+                chartData.datasets[0].data = waterfallData.map(d => d.end - d.start);
+                
+                // 添加起点数据集
+                chartData.datasets.push({
+                    label: '起点',
+                    data: waterfallData.map(d => d.start),
+                    backgroundColor: 'rgba(0,0,0,0)',
+                    borderColor: 'rgba(0,0,0,0)',
+                    stack: 'waterfall'
+                });
+                
+                // 设置为堆叠柱状图
+                if (!options.scales) options.scales = {};
+                if (!options.scales.x) options.scales.x = {};
+                if (!options.scales.y) options.scales.y = {};
+                options.scales.x.stacked = true;
+                options.scales.y.stacked = true;
+            }
+            break;
+        case 'treemap':
+        case 'sunburst':
+        case 'sankey':
+        case 'chord':
+        case 'network':
+            // 这些高级图表需要专门的库支持,这里简化为提示信息
+            type = 'bar';
+            if (chartData.datasets.length > 0) {
+                // 显示一个提示信息
+                chartData.datasets = [{
+                    label: `${settings.type}需要专门的图表库支持`,
+                    data: [100],
+                    backgroundColor: '#f8d7da'
+                }];
+                chartData.labels = ['请尝试其他图表类型'];
+            }
+            break;
+    }
+
+    // 热力图特殊处理
+    if (type === 'heatmap') {
+        // 热力图不是Chart.js的标准类型,需要使用插件或自定义渲染
+        // 简单实现一个基于颜色渐变的矩阵图
+        type = 'matrix';
+        renderHeatmap(ctx, chartData, options);
+        return;
+    }
+
+    // 饼图、环形图和极地面积图特殊处理 (如果不是由其他类型转换而来的)
+    if (['pie', 'doughnut', 'polarArea'].includes(type) && !['halfPie', 'nestedPie', 'gauge'].includes(settings.type) && !isFirstSeriesOnly) {
+        // 如果有多个数据集,只取第一个
+        if (chartData.datasets.length > 1) {
+            const firstDataset = chartData.datasets[0];
+            chartData.datasets = [{
+                ...firstDataset,
+                label: undefined // 这些图表类型不需要数据集标签
+            }];
+        }
+    }
+
+    // 创建图表实例
+    window.chartInstance = new Chart(ctx, {
+        type: type,
+        data: chartData,
+        options: options
+    });
+    
+    // 设置标记属性,表示图表已渲染,去除背景
+    canvas.setAttribute('data-chart-rendered', 'true');
+    
+    return window.chartInstance;
+}
+
+// 辅助函数:获取当前图表实例
+function getChartInstance() {
+    return window.chartInstance;
+}
+
+// 辅助函数:设置当前图表实例
+function setChartInstance(instance) {
+    window.chartInstance = instance;
+}
+
+// 获取图表配置选项
+function getChartOptions(settings) {
+    const options = {
+        responsive: true,
+        maintainAspectRatio: false,
+        plugins: {
+            title: {
+                display: !!settings.title,
+                text: settings.title,
+                font: {
+                    size: 18,
+                    weight: 'bold'
+                },
+                padding: {
+                    top: 10,
+                    bottom: 20
+                }
+            },
+            legend: {
+                display: settings.legendPosition !== 'none',
+                position: settings.legendPosition === 'none' ? 'top' : settings.legendPosition,
+                labels: {
+                    usePointStyle: true,
+                    padding: 15,
+                    font: {
+                        size: 12
+                    }
+                }
+            },
+            tooltip: {
+                enabled: true,
+                backgroundColor: 'rgba(0, 0, 0, 0.7)',
+                titleFont: {
+                    size: 14
+                },
+                bodyFont: {
+                    size: 13
+                },
+                padding: 10,
+                displayColors: true
+            }
+        },
+        animation: {
+            duration: settings.animateChart ? 1000 : 0,
+            easing: 'easeOutQuart'
+        }
+    };
+    
+    // 检查是否为简单数据模式(只有一个数据集)
+    const isSimpleData = settings.isSimpleData || 
+                         (window.chartData && window.chartData.datasets && window.chartData.datasets.length === 1);
+    
+    // 如果是简单数据模式,隐藏图例
+    if (isSimpleData) {
+        options.plugins.legend.display = false;
+    }
+    
+    // 只有部分图表类型需要轴线配置
+    if (!['pie', 'doughnut', 'polarArea'].includes(settings.type.replace(" (首系列)", ""))) { // 兼容(首系列)后缀
+        options.scales = {
+            x: {
+                title: {
+                    display: !!settings.xAxisLabel,
+                    text: settings.xAxisLabel,
+                    font: {
+                        size: 14,
+                        weight: 'bold'
+                    },
+                    padding: {
+                        top: 10
+                    }
+                },
+                grid: {
+                    display: settings.showGridLines,
+                    color: 'rgba(0, 0, 0, 0.1)'
+                },
+                ticks: {
+                    font: {
+                        size: 12
+                    }
+                }
+            },
+            y: {
+                title: {
+                    display: !!settings.yAxisLabel,
+                    text: settings.yAxisLabel,
+                    font: {
+                        size: 14,
+                        weight: 'bold'
+                    },
+                    padding: {
+                        bottom: 10
+                    }
+                },
+                grid: {
+                    display: settings.showGridLines,
+                    color: 'rgba(0, 0, 0, 0.1)'
+                },
+                ticks: {
+                    font: {
+                        size: 12
+                    },
+                    beginAtZero: true
+                }
+            }
+        };
+        
+        // 水平柱状图X和Y轴配置需要互换
+        if (settings.type === 'horizontalBar') {
+            const temp = options.scales.x;
+            options.scales.x = options.scales.y;
+            options.scales.y = temp;
+        }
+    }
+    
+    // 数据标签配置
+    if (settings.showDataLabels) {
+        options.plugins.datalabels = {
+            display: true,
+            color: function(context) {
+                const actualType = settings.type.replace(" (首系列)", "");
+                const dataset = context.dataset;
+                
+                // 首先检查数据集是否有自定义的datalabels配置
+                if (dataset.datalabels && dataset.datalabels.color) {
+                    const labelColors = dataset.datalabels.color;
+                    
+                    // 如果color是数组,则使用对应索引的颜色
+                    if (Array.isArray(labelColors)) {
+                        return labelColors[context.dataIndex] || '#333333';
+                    }
+                    // 如果color是单个颜色值
+                    return labelColors;
+                }
+                
+                // 如果没有自定义配置,则使用智能检测
+                // 为饼图和环形图使用对比色
+                if (['pie', 'doughnut', 'polarArea'].includes(actualType)) {
+                    // 获取背景色
+                    const index = context.dataIndex;
+                    const backgroundColor = dataset.backgroundColor[index];
+                    
+                    // 计算背景色的亮度
+                    return isColorDark(backgroundColor) ? '#ffffff' : '#000000';
+                } else if (actualType === 'bar' || actualType === 'horizontalBar' || 
+                           actualType === 'stackedBar' || actualType === 'gradientBar') {
+                    // 柱状图系列也需要对比色
+                    let backgroundColor;
+                    
+                    // 背景色可能是数组或单个颜色
+                    if (Array.isArray(dataset.backgroundColor)) {
+                        backgroundColor = dataset.backgroundColor[context.dataIndex];
+                    } else {
+                        backgroundColor = dataset.backgroundColor;
+                    }
+                    
+                    return isColorDark(backgroundColor) ? '#ffffff' : '#333333';
+                } else {
+                    // 其他图表类型使用默认深色
+                    return '#333333';
+                }
+            },
+            align: function(context) {
+                const dataset = context.dataset;
+                // 使用数据集中的align配置(如果有的话)
+                if (dataset.datalabels && dataset.datalabels.align) {
+                    return dataset.datalabels.align;
+                }
+                
+                // 默认配置
+                const chartType = settings.type.replace(" (首系列)", "");
+                if (['line', 'area', 'scatter', 'bubble'].includes(chartType)) {
+                    return 'top';
+                }
+                return 'center';
+            },
+            font: {
+                weight: 'bold'
+            },
+            formatter: function(value, context) {
+                const actualType = settings.type.replace(" (首系列)", "");
+                
+                // 饼图、环形图和极地面积图显示百分比
+                if (['pie', 'doughnut', 'polarArea'].includes(actualType)) {
+                    // 计算百分比
+                    const dataset = context.chart.data.datasets[context.datasetIndex];
+                    const total = dataset.data.reduce((total, value) => total + value, 0);
+                    const percentage = ((value / total) * 100).toFixed(1) + '%';
+                    
+                    // 对于较小的扇区只显示百分比,否则显示值和百分比
+                    const percent = value / total * 100;
+                    if (percent < 5) {
+                        return percentage;
+                    } else {
+                        return `${value} (${percentage})`;
+                    }
+                }
+                
+                // 对散点图特殊处理
+                if (settings.type === 'scatter') {
+                    if (context && context.dataset && context.dataset.data && 
+                        context.dataset.data[context.dataIndex] && 
+                        typeof context.dataset.data[context.dataIndex].y !== 'undefined') {
+                        return context.dataset.data[context.dataIndex].y;
+                    }
+                    return '';
+                }
+                
+                return value;
+            }
+        };
+    }
+    
+    return options;
+}
+
+// 判断颜色是否为深色
+function isColorDark(color) {
+    // 处理rgba格式
+    if (color && color.startsWith('rgba')) {
+        const parts = color.match(/rgba\((\d+),\s*(\d+),\s*(\d+),\s*([\d.]+)\)/);
+        if (parts) {
+            const r = parseInt(parts[1]);
+            const g = parseInt(parts[2]);
+            const b = parseInt(parts[3]);
+            // 计算亮度 (根据人眼对RGB的敏感度加权)
+            const brightness = (r * 0.299 + g * 0.587 + b * 0.114) / 255;
+            return brightness < 0.7; // 亮度小于0.7认为是深色
+        }
+    }
+    
+    // 处理rgb格式
+    if (color && color.startsWith('rgb(')) {
+        const parts = color.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
+        if (parts) {
+            const r = parseInt(parts[1]);
+            const g = parseInt(parts[2]);
+            const b = parseInt(parts[3]);
+            const brightness = (r * 0.299 + g * 0.587 + b * 0.114) / 255;
+            return brightness < 0.7;
+        }
+    }
+    
+    // 处理十六进制格式
+    if (color && color.startsWith('#')) {
+        color = color.replace('#', '');
+        const r = parseInt(color.length === 3 ? color.substring(0, 1).repeat(2) : color.substring(0, 2), 16);
+        const g = parseInt(color.length === 3 ? color.substring(1, 2).repeat(2) : color.substring(2, 4), 16);
+        const b = parseInt(color.length === 3 ? color.substring(2, 3).repeat(2) : color.substring(4, 6), 16);
+        const brightness = (r * 0.299 + g * 0.587 + b * 0.114) / 255;
+        return brightness < 0.7;
+    }
+    
+    // 默认返回true,使用白色文本
+    return true;
+}
+
+// 应用颜色方案
+function applyColorScheme(data, colorScheme, chartType) {
+    // 定义颜色方案 - 全新设计,确保各个方案风格迥异
+    const colorSchemes = {
+        default: [
+            '#4e73df', '#1cc88a', '#36b9cc', '#f6c23e', '#e74a3b',
+            '#6f42c1', '#fd7e14', '#20c9a6', '#36b9cc', '#858796'
+        ],
+        pastel: [
+            '#FFB6C1', '#FFD700', '#98FB98', '#87CEFA', '#FFA07A',
+            '#DDA0DD', '#FFDAB9', '#B0E0E6', '#F0E68C', '#E6E6FA'
+        ],
+        bright: [
+            '#FF1E1E', '#FFFF00', '#00FF00', '#00FFFF', '#0000FF',
+            '#FF00FF', '#FF7F00', '#FF1493', '#00FA9A', '#7B68EE'
+        ],
+        cool: [
+            '#5F4B8B', '#42BFDD', '#00A7E1', '#00344B', '#143642',
+            '#0F8B8D', '#4CB5F5', '#1D3557', '#A8DADC', '#457B9D'
+        ],
+        warm: [
+            '#FF7700', '#FF9E00', '#FFCF00', '#FFF400', '#E20000',
+            '#D91A1A', '#A60000', '#FF5252', '#FF7B7B', '#FFBF69'
+        ],
+        corporate: [
+            '#003F5C', '#2F4B7C', '#665191', '#A05195', '#D45087',
+            '#F95D6A', '#FF7C43', '#FFA600', '#004D40', '#00695C'
+        ],
+        contrast: [
+            '#000000', '#E63946', '#457B9D', '#F1C40F', '#2ECC71',
+            '#9B59B6', '#1ABC9C', '#F39C12', '#D35400', '#7F8C8D'
+        ],
+        rainbow: [
+            '#FF0000', '#FF7F00', '#FFFF00', '#00FF00', '#0000FF',
+            '#4B0082', '#9400D3', '#FF1493', '#00FFFF', '#FF00FF'
+        ],
+        earth: [
+            '#5D4037', '#795548', '#A1887F', '#4E342E', '#3E2723',
+            '#33691E', '#558B2F', '#7CB342', '#8D6E63', '#6D4C41'
+        ],
+        ocean: [
+            '#006064', '#00838F', '#0097A7', '#00ACC1', '#00BCD4',
+            '#26C6DA', '#4DD0E1', '#80DEEA', '#01579B', '#0277BD'
+        ],
+        vintage: [
+            '#8D8741', '#659DBD', '#DAAD86', '#BC986A', '#FBEEC1',
+            '#605B56', '#837A75', '#9E8B8B', '#D8C3A5', '#E8DDCD'
+        ]
+    };
+    
+    // 获取选定的颜色方案
+    const colors = colorSchemes[colorScheme] || colorSchemes.default;
+    
+    const actualChartType = chartType.replace(" (首系列)", ""); // 获取基础类型
+
+    // 为每个数据集应用颜色
+    data.datasets.forEach((dataset, index) => {
+        const color = colors[index % colors.length];
+        
+        // 设置不同图表类型的颜色
+        if (['pie', 'doughnut', 'polarArea', 'halfPie', 'nestedPie', 'gauge'].includes(actualChartType)) {
+            // 这些图表类型需要为每个数据点设置不同颜色
+            // 对于gauge特殊处理,不使用这种方式
+            if (actualChartType === 'gauge' && dataset.backgroundColor) {
+                // 保留gauge的特殊颜色设置
+            } else {
+                dataset.backgroundColor = dataset.data.map((_, i) => colors[i % colors.length]);
+                dataset.borderColor = 'white';
+                dataset.borderWidth = 1;
+                
+                // 为每个扇区添加对应的前景色(用于数据标签)
+                dataset.datalabels = {
+                    color: dataset.backgroundColor.map(bgColor => isColorDark(bgColor) ? '#ffffff' : '#000000')
+                };
+            }
+        } else if (['line', 'area', 'stackedArea', 'curvedLine', 'stepLine', 'timeline', 'streamgraph'].includes(actualChartType)) {
+            // 折线图和面积图样式
+            dataset.borderColor = color;
+            // 根据图表类型调整透明度
+            let alpha = 0.1; // 默认折线图半透明
+            if (['area', 'stackedArea', 'streamgraph'].includes(actualChartType)) {
+                alpha = 0.3; // 面积图相对更不透明
+            }
+            dataset.backgroundColor = hexToRgba(color, alpha);
+            dataset.pointBackgroundColor = color;
+            dataset.pointBorderColor = '#fff';
+            dataset.pointHoverBackgroundColor = '#fff';
+            dataset.pointHoverBorderColor = color;
+            
+            // 特殊线型
+            if (actualChartType === 'curvedLine') {
+                dataset.tension = 0.4;
+            } else if (actualChartType === 'stepLine') {
+                dataset.stepped = true;
+            } else if (actualChartType === 'timeline') {
+                dataset.stepped = 'before';
+                dataset.borderDash = [5, 5];
+            } else {
+                dataset.tension = 0.3; 
+            }
+            
+            // 设置数据标签颜色
+            // 对于线图和面积图,标签通常放在点的上方,使用与线条相同的颜色
+            dataset.datalabels = {
+                color: isColorDark(color) ? color : '#333333',
+                align: 'top'
+            };
+        } else if (actualChartType === 'radar') {
+            // 雷达图样式
+            dataset.borderColor = color;
+            dataset.backgroundColor = hexToRgba(color, 0.2);
+            dataset.pointBackgroundColor = color;
+            dataset.pointBorderColor = '#fff';
+            
+            // 雷达图数据标签颜色 - 使用与边框相同的颜色
+            dataset.datalabels = {
+                color: isColorDark(color) ? color : '#333333'
+            };
+        } else if (['scatter', 'bubble', 'scatterSmooth'].includes(actualChartType)) {
+            // 散点图样式
+            dataset.backgroundColor = color;
+            dataset.borderColor = hexToRgba(color, 0.8);
+            
+            // 散点平滑图特殊处理
+            if (actualChartType === 'scatterSmooth' && dataset.type === 'line') {
+                dataset.borderColor = color;
+                dataset.backgroundColor = 'transparent';
+            }
+            
+            // 散点图数据标签颜色
+            dataset.datalabels = {
+                color: isColorDark(color) ? '#ffffff' : '#333333',
+                align: 'top'
+            };
+        } else if (actualChartType === 'gradientBar') {
+            // 渐变柱状图
+            const ctx = document.createElement('canvas').getContext('2d');
+            const gradient = ctx.createLinearGradient(0, 0, 0, 300);
+            gradient.addColorStop(0, color);
+            gradient.addColorStop(1, hexToRgba(color, 0.3));
+            dataset.backgroundColor = gradient;
+            dataset.borderColor = color;
+            dataset.borderWidth = 1;
+            dataset.hoverBackgroundColor = color;
+            
+            // 渐变柱状图标签颜色 - 使用顶部颜色判断
+            dataset.datalabels = {
+                color: isColorDark(color) ? '#ffffff' : '#333333'
+            };
+        } else if (actualChartType === 'waterfall') {
+            // 瀑布图特殊处理
+            if (dataset.label === '起点') {
+                // 这是为瀑布图添加的起点数据集,保持透明
+            } else {
+                const values = dataset.data;
+                // 根据值的正负设置不同颜色
+                const positiveColor = '#36b9cc';
+                const negativeColor = '#e74a3b';
+                
+                dataset.backgroundColor = values.map(value => 
+                    value >= 0 ? hexToRgba(positiveColor, 0.7) : hexToRgba(negativeColor, 0.7)
+                );
+                dataset.borderColor = values.map(value => 
+                    value >= 0 ? positiveColor : negativeColor
+                );
+                dataset.borderWidth = 1;
+                
+                // 瀑布图数据标签颜色 - 根据每个柱子的背景色决定
+                dataset.datalabels = {
+                    color: values.map(value => 
+                        value >= 0 ? (isColorDark(positiveColor) ? '#ffffff' : '#333333') : 
+                                    (isColorDark(negativeColor) ? '#ffffff' : '#333333')
+                    )
+                };
+            }
+        } else if (actualChartType === 'funnel') {
+            // 漏斗图特殊处理 - 使用渐变颜色
+            const data = dataset.data;
+            if (data.length) {
+                dataset.backgroundColor = data.map((_, i) => {
+                    const ratio = 1 - (i / data.length); // 1 到 0
+                    return hexToRgba(color, 0.5 + ratio * 0.5); // 透明度从1到0.5
+                });
+                dataset.borderColor = color;
+                dataset.borderWidth = 1;
+                
+                // 漏斗图数据标签颜色 - 根据每个部分的背景色决定
+                dataset.datalabels = {
+                    color: dataset.backgroundColor.map(bgColor => isColorDark(bgColor) ? '#ffffff' : '#333333')
+                };
+            }
+        } else {
+            // 默认样式(用于柱状图等)
+            dataset.backgroundColor = hexToRgba(color, 0.7);
+            dataset.borderColor = color;
+            dataset.borderWidth = 1;
+            dataset.hoverBackgroundColor = color;
+            
+            // 默认数据标签颜色 - 根据背景色决定
+            dataset.datalabels = {
+                color: isColorDark(dataset.backgroundColor) ? '#ffffff' : '#333333'
+            };
+        }
+    });
+}
+
+// 将十六进制颜色转换为rgba格式
+function hexToRgba(hex, alpha) {
+    // 移除井号
+    hex = hex.replace('#', '');
+    
+    // 解析RGB值
+    const r = parseInt(hex.length === 3 ? hex.substring(0, 1).repeat(2) : hex.substring(0, 2), 16);
+    const g = parseInt(hex.length === 3 ? hex.substring(1, 2).repeat(2) : hex.substring(2, 4), 16);
+    const b = parseInt(hex.length === 3 ? hex.substring(2, 3).repeat(2) : hex.substring(4, 6), 16);
+    
+    // 返回rgba字符串
+    return `rgba(${r}, ${g}, ${b}, ${alpha})`;
+}
+
+// 渲染热力图(自定义实现)
+function renderHeatmap(ctx, data, options) {
+    // 清除Canvas
+    ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
+    
+    // 设置尺寸和边距
+    const margin = {
+        top: 50,
+        right: 30,
+        bottom: 50,
+        left: 60
+    };
+    
+    const width = ctx.canvas.width - margin.left - margin.right;
+    const height = ctx.canvas.height - margin.top - margin.bottom;
+    
+    // 获取数据
+    const rows = data.labels;
+    const columns = data.datasets.map(dataset => dataset.label);
+    
+    // 创建值矩阵
+    const matrix = [];
+    rows.forEach((_, rowIndex) => {
+        const row = [];
+        data.datasets.forEach(dataset => {
+            row.push(dataset.data[rowIndex]);
+        });
+        matrix.push(row);
+    });
+    
+    // 找出最大值和最小值
+    const allValues = matrix.flat();
+    const min = Math.min(...allValues);
+    const max = Math.max(...allValues);
+    
+    // 绘制标题
+    if (options.plugins && options.plugins.title && options.plugins.title.display) {
+        ctx.textAlign = 'center';
+        ctx.font = '18px Arial';
+        ctx.fillStyle = '#333';
+        ctx.fillText(options.plugins.title.text, ctx.canvas.width / 2, 25);
+    }
+    
+    // 绘制单元格和标签
+    const cellWidth = width / columns.length;
+    const cellHeight = height / rows.length;
+    
+    // 行标签(Y轴)
+    ctx.textAlign = 'right';
+    ctx.textBaseline = 'middle';
+    ctx.font = '12px Arial';
+    ctx.fillStyle = '#666';
+    
+    rows.forEach((label, i) => {
+        const y = margin.top + i * cellHeight + cellHeight / 2;
+        ctx.fillText(label, margin.left - 10, y);
+    });
+    
+    // 列标签(X轴)
+    ctx.textAlign = 'center';
+    ctx.textBaseline = 'top';
+    
+    columns.forEach((label, i) => {
+        const x = margin.left + i * cellWidth + cellWidth / 2;
+        ctx.fillText(label, x, margin.top + height + 10);
+    });
+    
+    // 绘制热力图单元格
+    matrix.forEach((row, i) => {
+        row.forEach((value, j) => {
+            // 归一化值 (0-1)
+            const normalizedValue = (value - min) / (max - min || 1);
+            
+            // 计算颜色(红-黄-绿渐变)
+            const color = getHeatmapColor(normalizedValue);
+            
+            // 绘制单元格
+            const x = margin.left + j * cellWidth;
+            const y = margin.top + i * cellHeight;
+            
+            ctx.fillStyle = color;
+            ctx.fillRect(x, y, cellWidth, cellHeight);
+            
+            // 添加值标签,根据背景色的亮度自动选择标签颜色
+            const brightness = getColorBrightness(color);
+            ctx.fillStyle = brightness < 0.7 ? 'white' : 'black'; // 亮度阈值为0.7
+            ctx.textAlign = 'center';
+            ctx.textBaseline = 'middle';
+            ctx.font = 'bold 12px Arial';
+            ctx.fillText(value, x + cellWidth / 2, y + cellHeight / 2);
+        });
+    });
+    
+    // 绘制坐标轴
+    ctx.strokeStyle = '#ddd';
+    ctx.lineWidth = 1;
+    
+    // X轴
+    ctx.beginPath();
+    ctx.moveTo(margin.left, margin.top + height);
+    ctx.lineTo(margin.left + width, margin.top + height);
+    ctx.stroke();
+    
+    // Y轴
+    ctx.beginPath();
+    ctx.moveTo(margin.left, margin.top);
+    ctx.lineTo(margin.left, margin.top + height);
+    ctx.stroke();
+}
+
+// 获取颜色亮度
+function getColorBrightness(color) {
+    // 处理rgb格式
+    if (color.startsWith('rgb(')) {
+        const parts = color.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
+        if (parts) {
+            const r = parseInt(parts[1]);
+            const g = parseInt(parts[2]);
+            const b = parseInt(parts[3]);
+            // 计算亮度 (根据人眼对RGB的敏感度加权)
+            return (r * 0.299 + g * 0.587 + b * 0.114) / 255;
+        }
+    }
+    
+    // 默认返回0.5
+    return 0.5;
+}
+
+// 获取热力图颜色
+function getHeatmapColor(value) {
+    // 红-黄-绿渐变
+    const r = value < 0.5 ? 255 : Math.round(255 * (1 - 2 * (value - 0.5)));
+    const g = value < 0.5 ? Math.round(255 * (2 * value)) : 255;
+    const b = 0;
+    
+    return `rgb(${r}, ${g}, ${b})`;
+}
+
+// 注册Chart.js插件以支持数据标签
+Chart.register({
+    id: 'datalabels',
+    beforeDraw: (chart) => {
+        const ctx = chart.ctx;
+        const options = chart.options.plugins.datalabels;
+        
+        if (!options || !options.display) {
+            return;
+        }
+        
+        chart.data.datasets.forEach((dataset, datasetIndex) => {
+            const meta = chart.getDatasetMeta(datasetIndex);
+            
+            meta.data.forEach((element, index) => {
+                // 获取值
+                let value = dataset.data[index];
+                if (typeof value === 'object' && value !== null) {
+                    // 散点图等复杂数据结构
+                    value = value.y;
+                }
+                
+                // 获取位置
+                const { x, y } = element.getCenterPoint();
+                
+                // 确定文本颜色
+                let fillColor;
+                if (typeof options.color === 'function') {
+                    fillColor = options.color({
+                        datasetIndex, 
+                        index, 
+                        dataset,
+                        dataIndex: index,
+                        chart: chart
+                    });
+                } else {
+                    fillColor = options.color || '#666';
+                }
+                
+                ctx.fillStyle = fillColor;
+                
+                // 设置字体
+                ctx.font = options.font.weight + ' 12px Arial';
+                ctx.textAlign = 'center';
+                ctx.textBaseline = 'middle';
+                
+                // 格式化值
+                let text = typeof options.formatter === 'function' 
+                    ? options.formatter(value, {
+                        datasetIndex, 
+                        index, 
+                        dataset,
+                        dataIndex: index,
+                        chart: chart
+                      }) 
+                    : value;
+                
+                // 绘制文本
+                ctx.fillText(text, x, y - 15);
+            });
+        });
+    }
+});
+
+// 辅助函数:转换散点图数据
+function transformScatterData(datasets) {
+    return datasets.map(dataset => {
+        if (!dataset.data || !Array.isArray(dataset.data)) {
+            return {
+                ...dataset,
+                data: []
+            };
+        }
+        
+        return {
+            ...dataset,
+            data: dataset.data.map((value, index) => {
+                // 确保value是一个有效的数值
+                const y = parseFloat(value);
+                if (isNaN(y)) {
+                    return { x: index + 1, y: 0 };
+                }
+                return { x: index + 1, y: y };
+            })
+        };
+    });
+}
+
+// 辅助函数:转换气泡图数据
+function transformBubbleData(datasets) {
+    return datasets.map(dataset => {
+        if (!dataset.data || !Array.isArray(dataset.data)) {
+            return {
+                ...dataset,
+                data: []
+            };
+        }
+        
+        return {
+            ...dataset,
+            data: dataset.data.map((value, index) => {
+                // 确保value是一个有效的数值
+                const y = parseFloat(value);
+                if (isNaN(y)) {
+                    return { x: index + 1, y: 0, r: 5 };
+                }
+                // 气泡大小与值成比例
+                const r = Math.max(5, Math.min(20, y / 10));
+                return { x: index + 1, y: y, r: r };
+            })
+        };
+    });
+}
+
+/**
+ * 初始化图表类型预览画廊
+ */
+function initChartTypeGallery() {
+    // 获取所有图表类型预览项
+    const chartTypeItems = document.querySelectorAll('.chart-type-item');
+    
+    // 获取图表类型选择下拉框
+    const chartTypeSelect = document.getElementById('chart-type');
+    
+    // 为每个预览项添加点击事件
+    chartTypeItems.forEach(item => {
+        item.addEventListener('click', function() {
+            // 获取图表类型值
+            const chartType = this.getAttribute('data-chart-type');
+            
+            // 设置下拉框的值
+            chartTypeSelect.value = chartType;
+            
+            // 触发change事件以更新图表
+            const event = new Event('change');
+            chartTypeSelect.dispatchEvent(event);
+            
+            // 更新活动状态
+            chartTypeItems.forEach(item => item.classList.remove('active'));
+            this.classList.add('active');
+            
+            // 如果已经有图表实例,立即生成图表
+            if (window.chartInstance) {
+                // 假设generateChart是全局函数
+                if (typeof window.generateChart === 'function') {
+                    window.generateChart();
+                }
+            }
+        });
+    });
+    
+    // 初始化时设置当前选中的图表类型为活动状态
+    const currentChartType = chartTypeSelect.value;
+    const activeItem = document.querySelector(`.chart-type-item[data-chart-type="${currentChartType}"]`);
+    if (activeItem) {
+        activeItem.classList.add('active');
+    }
+    
+    // 当下拉框选择变化时,同步更新活动预览项
+    chartTypeSelect.addEventListener('change', function() {
+        const selectedType = this.value;
+        chartTypeItems.forEach(item => {
+            if (item.getAttribute('data-chart-type') === selectedType) {
+                item.classList.add('active');
+            } else {
+                item.classList.remove('active');
+            }
+        });
+    });
+} 

+ 11 - 0
apps/chart-maker/chart-icon.svg

@@ -0,0 +1,11 @@
+<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <rect width="512" height="512" rx="80" fill="#4568DC"/>
+  <rect x="96" y="352" width="64" height="96" rx="8" fill="white"/>
+  <rect x="192" y="256" width="64" height="192" rx="8" fill="white"/>
+  <rect x="288" y="160" width="64" height="288" rx="8" fill="white"/>
+  <rect x="384" y="64" width="64" height="384" rx="8" fill="white"/>
+  <path d="M96 144C96 136.268 102.268 130 110 130H434C441.732 130 448 136.268 448 144V144C448 151.732 441.732 158 434 158H110C102.268 158 96 151.732 96 144V144Z" fill="#B06AB3" fill-opacity="0.6"/>
+  <path d="M96 96C96 88.268 102.268 82 110 82H338C345.732 82 352 88.268 352 96V96C352 103.732 345.732 110 338 110H110C102.268 110 96 103.732 96 96V96Z" fill="#B06AB3" fill-opacity="0.6"/>
+  <circle cx="448" cy="96" r="16" fill="white"/>
+  <circle cx="352" cy="144" r="16" fill="white"/>
+</svg>

+ 317 - 0
apps/chart-maker/index.html

@@ -0,0 +1,317 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>图表生成器 - 快速创建专业数据可视化</title>
+    <link rel="stylesheet" href="style.css">
+    <link rel="icon" href="chart-icon.svg" type="image/svg+xml">
+    <!-- 引入Chart.js及其插件 (改为本地引用) -->
+    <script src="lib/chart.min.js"></script>
+    <!-- 引入html2canvas库用于导出图片 (改为本地引用) -->
+    <script src="lib/html2canvas.min.js"></script>
+    <!-- 引入XLSX库用于解析Excel文件 (改为本地引用) -->
+    <script src="lib/xlsx.full.min.js"></script>
+    <!-- 引入Hammer.js用于图表交互 (改为本地引用) -->
+    <script src="lib/hammer.min.js"></script>
+    <!-- 引入Chart.js缩放插件 (改为本地引用) -->
+    <script src="lib/chartjs-plugin-zoom.min.js"></script>
+    <!-- 引入Chart.js数据标签插件 -->
+    <script src="lib/chartjs-plugin-datalabels.min.js"></script>
+</head>
+<body>
+    <div class="wrapper" id="pageContainer">
+        <div class="page-header">
+            <a href="https://www.baidufe.com/fehelper/index/index.html" target="_blank" class="header-link">
+                <img src="../static/img/fe-16.png" alt="fehelper"/>
+                <span class="fehelper-text">FeHelper</span>
+            </a>
+            <span class="page-title-suffix">:图表生成器</span>
+        </div>
+    </div>
+
+    <main class="container">
+        <div class="app-container">
+            <!-- 左侧面板:数据输入区 -->
+            <div class="sidebar">
+                <div class="panel">
+                    <h2>数据输入</h2>
+                    <div class="input-group">
+                        <label>数据输入方式</label>
+                        <div class="input-methods">
+                            <label><input type="radio" name="data-input-method" value="manual" checked> 手动录入</label>
+                            <label><input type="radio" name="data-input-method" value="upload-csv"> 上传Excel/CSV</label>
+                        </div>
+                    </div>
+
+                    <div class="input-group" id="manual-format-container" style="display: none;">
+                        <label for="manual-format">选择数据格式</label>
+                        <select id="manual-format">
+                            <option value="simple">简单数据 (标签,数值)</option>
+                            <option value="series">系列数据 (多组)</option>
+                            <option value="csv">CSV格式</option>
+                        </select>
+                    </div>
+
+                    <div class="input-group" id="simple-data-container">
+                        <label for="data-input">输入数据 (每行一条,格式: 标签,数值)</label>
+                        <textarea id="data-input" rows="8" placeholder="产品A,120&#10;产品B,80&#10;产品C,60&#10;产品D,40&#10;产品E,30"></textarea>
+                    </div>
+
+                    <div class="input-group" id="series-data-container" style="display: none;">
+                        <label for="series-data-input">输入系列数据 (每行一个系列,格式: 系列名,值1,值2...)</label>
+                        <textarea id="series-data-input" rows="8" placeholder="一季度,120,80,60,40,30&#10;二季度,130,70,65,45,35&#10;三季度,140,90,55,48,38"></textarea>
+                        <label for="series-labels">标签 (逗号分隔)</label>
+                        <input type="text" id="series-labels" placeholder="产品A,产品B,产品C,产品D,产品E">
+                    </div>
+
+                    <div class="input-group" id="csv-data-container" style="display: none;">
+                        <label for="csv-data-input">粘贴CSV数据 (首行为标题)</label>
+                        <textarea id="csv-data-input" rows="8" placeholder="类别,一季度,二季度,三季度&#10;产品A,120,130,140&#10;产品B,80,70,90&#10;产品C,60,65,55&#10;产品D,40,45,48"></textarea>
+                        <div class="csv-options">
+                            <label>
+                                <input type="checkbox" id="first-row-header" checked> 
+                                第一行为标题
+                            </label>
+                            <label>
+                                <input type="checkbox" id="first-col-labels" checked> 
+                                第一列为标签
+                            </label>
+                        </div>
+                    </div>
+
+                    <div class="input-group">
+                        <input type="file" id="file-upload" accept=".csv, .xlsx">
+                        <label for="file-upload">选择CSV/Excel文件</label>
+                    </div>
+
+                    <div class="input-group">
+                        <button id="generate-btn" class="btn btn-primary">生成图表</button>
+                        <button id="sample-data-btn" class="btn btn-secondary">加载样例数据</button>
+                    </div>
+                </div>
+                
+                <div class="panel">
+                    <h2>图表设置</h2>
+                    <input type="hidden" id="chart-type" value="bar">
+                    <div class="input-group">
+                        <label for="chart-title">图表标题</label>
+                        <input type="text" id="chart-title" placeholder="图表标题">
+                    </div>
+                    
+                    <div class="input-group">
+                        <label for="x-axis-label">X轴标签</label>
+                        <input type="text" id="x-axis-label" placeholder="X轴标签">
+                    </div>
+                    
+                    <div class="input-group">
+                        <label for="y-axis-label">Y轴标签</label>
+                        <input type="text" id="y-axis-label" placeholder="Y轴标签">
+                    </div>
+
+                    <div class="input-group">
+                        <label for="color-scheme">颜色方案</label>
+                        <select id="color-scheme">
+                            <option value="default">默认方案</option>
+                            <option value="pastel">柔和色系</option>
+                            <option value="bright">明亮色系</option>
+                            <option value="cool">冷色调</option>
+                            <option value="warm">暖色调</option>
+                            <option value="corporate">企业风格</option>
+                            <option value="contrast">高对比度</option>
+                            <option value="rainbow">彩虹色系</option>
+                            <option value="earth">大地色系</option>
+                            <option value="ocean">海洋色系</option>
+                            <option value="vintage">复古色系</option>
+                        </select>
+                    </div>
+
+                    <div class="input-group">
+                        <label for="legend-position">图例位置</label>
+                        <select id="legend-position">
+                            <option value="top">顶部</option>
+                            <option value="right">右侧</option>
+                            <option value="bottom">底部</option>
+                            <option value="left">左侧</option>
+                            <option value="none">不显示</option>
+                        </select>
+                    </div>
+                    
+                    <div class="input-group checkbox-group">
+                        <!-- 删除"显示网格线"和"启用动画"复选框 -->
+                    </div>
+                </div>
+            </div>
+
+            <!-- 右侧面板:图表显示区域 -->
+            <div class="main-content">
+                <div class="chart-container">
+                    <div class="chart-actions">
+                        <div class="checkbox-group" style="text-align: left;">
+                            <label><input type="checkbox" id="show-grid-lines" checked> 显示网格线</label>
+                            <label><input type="checkbox" id="animate-chart" checked> 启用动画</label>
+                        </div>
+                        <button id="export-png-btn" class="btn btn-primary" disabled>导出PNG</button>
+                        <button id="export-jpg-btn" class="btn btn-primary" disabled>导出JPG</button>
+                        <button id="copy-img-btn" class="btn btn-secondary" disabled>复制图像</button>
+                    </div>
+                    <div id="chart-wrapper">
+                        <canvas id="chart-canvas"></canvas>
+                    </div>
+                    
+                    <div class="chart-type-gallery">
+                        <h3>图表类型选择</h3>
+                        <div class="chart-type-scroller">
+                            <div class="chart-type-group">
+                                <h4>基础图表</h4>
+                                <div class="chart-type-items">
+                                    <div class="chart-type-item" data-chart-type="bar">
+                                        <svg width="60" height="60" viewBox="0 0 60 60">
+                                            <rect x="10" y="10" width="8" height="40" fill="#4e73df" />
+                                            <rect x="26" y="20" width="8" height="30" fill="#4e73df" />
+                                            <rect x="42" y="30" width="8" height="20" fill="#4e73df" />
+                                        </svg>
+                                        <span>柱状图</span>
+                                    </div>
+                                    <div class="chart-type-item" data-chart-type="horizontalBar">
+                                        <svg width="60" height="60" viewBox="0 0 60 60">
+                                            <rect x="10" y="10" width="40" height="8" fill="#4e73df" />
+                                            <rect x="10" y="26" width="30" height="8" fill="#4e73df" />
+                                            <rect x="10" y="42" width="20" height="8" fill="#4e73df" />
+                                        </svg>
+                                        <span>水平柱状图</span>
+                                    </div>
+                                    <div class="chart-type-item" data-chart-type="line">
+                                        <svg width="60" height="60" viewBox="0 0 60 60">
+                                            <polyline points="10,45 25,15 40,30 55,10" stroke="#4e73df" stroke-width="2" fill="none" />
+                                            <circle cx="10" cy="45" r="3" fill="#4e73df" />
+                                            <circle cx="25" cy="15" r="3" fill="#4e73df" />
+                                            <circle cx="40" cy="30" r="3" fill="#4e73df" />
+                                            <circle cx="55" cy="10" r="3" fill="#4e73df" />
+                                        </svg>
+                                        <span>折线图</span>
+                                    </div>
+                                    <div class="chart-type-item" data-chart-type="area">
+                                        <svg width="60" height="60" viewBox="0 0 60 60">
+                                            <path d="M10,45 L25,15 L40,30 L55,10 L55,50 L10,50 Z" fill="#4e73df" opacity="0.3" />
+                                            <polyline points="10,45 25,15 40,30 55,10" stroke="#4e73df" stroke-width="2" fill="none" />
+                                            <circle cx="10" cy="45" r="3" fill="#4e73df" />
+                                            <circle cx="25" cy="15" r="3" fill="#4e73df" />
+                                            <circle cx="40" cy="30" r="3" fill="#4e73df" />
+                                            <circle cx="55" cy="10" r="3" fill="#4e73df" />
+                                        </svg>
+                                        <span>面积图</span>
+                                    </div>
+                                </div>
+                            </div>
+                            
+                            <div class="chart-type-group">
+                                <h4>饼图/环形图</h4>
+                                <div class="chart-type-items">
+                                    <div class="chart-type-item" data-chart-type="pie">
+                                        <svg width="60" height="60" viewBox="0 0 60 60">
+                                            <path d="M30,30 L30,5 A25,25 0 0,1 55,30 Z" fill="#4e73df" />
+                                            <path d="M30,30 L55,30 A25,25 0 0,1 30,55 Z" fill="#1cc88a" />
+                                            <path d="M30,30 L30,55 A25,25 0 0,1 5,30 Z" fill="#36b9cc" />
+                                            <path d="M30,30 L5,30 A25,25 0 0,1 30,5 Z" fill="#f6c23e" />
+                                        </svg>
+                                        <span>饼图</span>
+                                    </div>
+                                    <div class="chart-type-item" data-chart-type="doughnut">
+                                        <svg width="60" height="60" viewBox="0 0 60 60">
+                                            <path d="M30,30 L30,5 A25,25 0 0,1 55,30 L45,30 A15,15 0 0,0 30,15 Z" fill="#4e73df" />
+                                            <path d="M30,30 L55,30 A25,25 0 0,1 30,55 L30,45 A15,15 0 0,0 45,30 Z" fill="#1cc88a" />
+                                            <path d="M30,30 L30,55 A25,25 0 0,1 5,30 L15,30 A15,15 0 0,0 30,45 Z" fill="#36b9cc" />
+                                            <path d="M30,30 L5,30 A25,25 0 0,1 30,5 L30,15 A15,15 0 0,0 15,30 Z" fill="#f6c23e" />
+                                        </svg>
+                                        <span>环形图</span>
+                                    </div>
+                                    <div class="chart-type-item" data-chart-type="polarArea">
+                                        <svg width="60" height="60" viewBox="0 0 60 60">
+                                            <path d="M30,30 L30,10 A20,20 0 0,1 50,30 Z" fill="#4e73df" />
+                                            <path d="M30,30 L50,30 A20,20 0 0,1 30,50 Z" fill="#1cc88a" />
+                                            <path d="M30,30 L30,50 A20,20 0 0,1 10,30 Z" fill="#36b9cc" />
+                                            <path d="M30,30 L10,30 A20,20 0 0,1 30,10 Z" fill="#f6c23e" />
+                                        </svg>
+                                        <span>极地面积图</span>
+                                    </div>
+                                </div>
+                            </div>
+                            
+                            <div class="chart-type-group">
+                                <h4>高级图表</h4>
+                                <div class="chart-type-items">
+                                    <div class="chart-type-item" data-chart-type="scatter">
+                                        <svg width="60" height="60" viewBox="0 0 60 60">
+                                            <circle cx="15" cy="20" r="3" fill="#4e73df" />
+                                            <circle cx="25" cy="40" r="3" fill="#4e73df" />
+                                            <circle cx="35" cy="15" r="3" fill="#4e73df" />
+                                            <circle cx="45" cy="30" r="3" fill="#4e73df" />
+                                            <circle cx="20" cy="45" r="3" fill="#4e73df" />
+                                        </svg>
+                                        <span>散点图</span>
+                                    </div>
+                                    <div class="chart-type-item" data-chart-type="radar">
+                                        <svg width="60" height="60" viewBox="0 0 60 60">
+                                            <polygon points="30,10 45,20 40,40 20,40 15,20" stroke="#4e73df" stroke-width="1" fill="#4e73df" fill-opacity="0.2" />
+                                            <line x1="30" y1="30" x2="30" y2="10" stroke="#ccc" />
+                                            <line x1="30" y1="30" x2="45" y2="20" stroke="#ccc" />
+                                            <line x1="30" y1="30" x2="40" y2="40" stroke="#ccc" />
+                                            <line x1="30" y1="30" x2="20" y2="40" stroke="#ccc" />
+                                            <line x1="30" y1="30" x2="15" y2="20" stroke="#ccc" />
+                                        </svg>
+                                        <span>雷达图</span>
+                                    </div>
+                                    <div class="chart-type-item" data-chart-type="bubble">
+                                        <svg width="60" height="60" viewBox="0 0 60 60">
+                                            <circle cx="15" cy="25" r="5" fill="#4e73df" fill-opacity="0.7" />
+                                            <circle cx="30" cy="15" r="7" fill="#4e73df" fill-opacity="0.7" />
+                                            <circle cx="45" cy="35" r="4" fill="#4e73df" fill-opacity="0.7" />
+                                            <circle cx="25" cy="45" r="6" fill="#4e73df" fill-opacity="0.7" />
+                                        </svg>
+                                        <span>气泡图</span>
+                                    </div>
+                                    <div class="chart-type-item" data-chart-type="stackedBar">
+                                        <svg width="60" height="60" viewBox="0 0 60 60">
+                                            <rect x="10" y="10" width="8" height="15" fill="#4e73df" />
+                                            <rect x="10" y="25" width="8" height="15" fill="#1cc88a" />
+                                            <rect x="26" y="20" width="8" height="10" fill="#4e73df" />
+                                            <rect x="26" y="30" width="8" height="10" fill="#1cc88a" />
+                                            <rect x="42" y="30" width="8" height="5" fill="#4e73df" />
+                                            <rect x="42" y="35" width="8" height="5" fill="#1cc88a" />
+                                        </svg>
+                                        <span>堆叠柱状图</span>
+                                    </div>
+                                    <div class="chart-type-item" data-chart-type="stackedArea">
+                                        <svg width="60" height="60" viewBox="0 0 60 60">
+                                            <path d="M10,45 L25,35 L40,30 L55,20 L55,50 L10,50 Z" fill="#4e73df" opacity="0.3" />
+                                            <path d="M10,30 L25,20 L40,20 L55,10 L55,20 L40,30 L25,35 L10,45 Z" fill="#1cc88a" opacity="0.3" />
+                                            <polyline points="10,30 25,20 40,20 55,10" stroke="#1cc88a" stroke-width="2" fill="none" />
+                                            <polyline points="10,45 25,35 40,30 55,20" stroke="#4e73df" stroke-width="2" fill="none" />
+                                        </svg>
+                                        <span>堆叠面积图</span>
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+                
+                <div class="tips-panel">
+                    <h3>使用提示</h3>
+                    <ul>
+                        <li>选择适合您数据的图表类型:数值比较用柱状图,趋势用折线图,占比用饼图</li>
+                        <li>点击图表类型预览图可以直接切换图表</li>
+                        <li>生成图表后可以导出为PNG或JPG格式,方便插入PPT或Word文档</li>
+                        <li>使用"数据标签"功能可以在图表上直接显示数值</li>
+                        <li>尝试不同的颜色方案,为您的报告增添专业感</li>
+                    </ul>
+                </div>
+            </div>
+        </div>
+    </main>
+    
+    <script src="main.js"></script>
+    <script src="chart-generator.js"></script>
+</body>
+</html> 

文件差异内容过多而无法显示
+ 6 - 0
apps/chart-maker/lib/chart.min.js


文件差异内容过多而无法显示
+ 6 - 0
apps/chart-maker/lib/chartjs-plugin-datalabels.min.js


文件差异内容过多而无法显示
+ 6 - 0
apps/chart-maker/lib/chartjs-plugin-zoom.min.js


文件差异内容过多而无法显示
+ 5 - 0
apps/chart-maker/lib/hammer.min.js


文件差异内容过多而无法显示
+ 19 - 0
apps/chart-maker/lib/html2canvas.min.js


文件差异内容过多而无法显示
+ 1 - 0
apps/chart-maker/lib/xlsx.full.min.js


+ 712 - 0
apps/chart-maker/main.js

@@ -0,0 +1,712 @@
+document.addEventListener('DOMContentLoaded', function() {
+    // 获取DOM元素
+    const simpleDataContainer = document.getElementById('simple-data-container');
+    const seriesDataContainer = document.getElementById('series-data-container');
+    const csvDataContainer = document.getElementById('csv-data-container');
+    const generateBtn = document.getElementById('generate-btn');
+    const sampleDataBtn = document.getElementById('sample-data-btn');
+    const exportPngBtn = document.getElementById('export-png-btn');
+    const exportJpgBtn = document.getElementById('export-jpg-btn');
+    const copyImgBtn = document.getElementById('copy-img-btn');
+    const chartTypeSelect = document.getElementById('chart-type');
+    const manualFormatSelect = document.getElementById('manual-format');
+    const fileUploadInput = document.getElementById('file-upload');
+    const manualFormatContainer = document.getElementById('manual-format-container');
+    const manualInputContainers = [simpleDataContainer, seriesDataContainer, csvDataContainer];
+    
+    // 初始化显示状态
+    const initialMethod = document.querySelector('input[name="data-input-method"]:checked').value;
+    toggleManualInputs(initialMethod === 'manual');
+    fileUploadInput.parentElement.style.display = initialMethod === 'upload-csv' ? 'block' : 'none';
+
+    // 初始化图表类型画廊
+    initChartTypeGallery();
+
+    function toggleManualInputs(show) {
+        manualFormatContainer.style.display = show ? 'block' : 'none';
+        const selectedFormat = manualFormatSelect.value;
+        manualInputContainers.forEach(container => {
+            const containerId = container.id.split('-')[0]; // 'simple', 'series', 'csv'
+            container.style.display = (show && containerId === selectedFormat) ? 'block' : 'none';
+        });
+    }
+
+    // 初始化时调用updateChartTypeOptions函数
+    // 无论当前选择的是什么输入方式,都初始化图表类型
+    // 默认使用'series'格式以显示最多的图表类型选项
+    updateChartTypeOptions('series');
+
+    // 监听数据输入方式切换
+    document.querySelectorAll('input[name="data-input-method"]').forEach(radio => {
+        radio.addEventListener('change', function() {
+            const method = this.value;
+            toggleManualInputs(method === 'manual');
+            fileUploadInput.parentElement.style.display = method === 'upload-csv' ? 'block' : 'none';
+            
+            // 在切换到"上传Excel/CSV"时,更新图表类型选项为多系列数据
+            if (method === 'upload-csv') {
+                updateChartTypeOptions('series');
+            } else if (method === 'manual') {
+                // 切换回"手动录入"时,根据当前选择的格式更新图表类型选项
+                updateChartTypeOptions(manualFormatSelect.value);
+                uploadedData = null;
+                fileUploadInput.value = ''; // 清空文件选择
+            }
+        });
+    });
+
+    // 监听手动格式选择变化
+    manualFormatSelect.addEventListener('change', function() {
+        const format = this.value;
+        manualInputContainers.forEach(container => {
+            const containerId = container.id.split('-')[0];
+            container.style.display = (containerId === format) ? 'block' : 'none';
+        });
+        
+        // 更新图表类型选项
+        updateChartTypeOptions(format);
+    });
+
+    // 文件上传处理
+    fileUploadInput.addEventListener('change', function(event) {
+        const file = event.target.files[0];
+        if (!file) {
+            uploadedData = null;
+            return;
+        }
+
+        const reader = new FileReader();
+        reader.onload = function(e) {
+            try {
+                const data = new Uint8Array(e.target.result);
+                const workbook = XLSX.read(data, { type: 'array' });
+                const firstSheetName = workbook.SheetNames[0];
+                const worksheet = workbook.Sheets[firstSheetName];
+                const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1 });
+                uploadedData = parseExcelData(jsonData);
+                showNotification('文件上传成功,可以点击"生成图表"');
+                
+                // 上传Excel文件后,更新图表类型选项为多系列数据类型
+                updateChartTypeOptions('series');
+            } catch (error) {
+                showNotification('文件解析失败: ' + error.message, true);
+                uploadedData = null;
+                fileUploadInput.value = ''; // 清空文件选择
+            }
+        };
+        reader.onerror = function() {
+            showNotification('文件读取失败', true);
+            uploadedData = null;
+            fileUploadInput.value = ''; // 清空文件选择
+        };
+        reader.readAsArrayBuffer(file);
+    });
+
+    // 生成图表按钮点击事件 (修改为独立的函数)
+    function generateChart() {
+        try {
+            console.log('开始生成图表...');
+            let parsedData;
+            const method = document.querySelector('input[name="data-input-method"]:checked').value;
+            console.log('数据输入方式:', method);
+
+            if (method === 'upload-csv' && uploadedData) {
+                parsedData = uploadedData;
+                console.log('使用上传的数据');
+            } else if (method === 'manual') {
+                parsedData = parseInputData(); // 使用现有的手动数据解析函数
+                console.log('使用手动输入的数据');
+            } else if (method === 'upload-csv' && !uploadedData) {
+                throw new Error('请先上传文件');
+            } else {
+                throw new Error('请选择有效的数据输入方式并提供数据');
+            }
+            
+            console.log('解析后的数据:', parsedData);
+            
+            if (!parsedData || 
+                (parsedData.labels && parsedData.labels.length === 0) || 
+                (parsedData.datasets && parsedData.datasets.length === 0)) {
+                throw new Error('无法解析数据或数据为空');
+            }
+            
+            // 保存数据到全局变量,方便其他函数访问
+            window.chartData = parsedData;
+            
+            const chartSettings = getChartSettings();
+            
+            // 将简单数据标记添加到设置中
+            if (parsedData.isSimpleData) {
+                chartSettings.isSimpleData = true;
+            }
+            
+            console.log('图表设置:', chartSettings);
+            
+            // 调用chart-generator.js中的createChart函数
+            if (typeof createChart !== 'function') {
+                throw new Error('createChart函数未定义,请确保chart-generator.js正确加载');
+            }
+            
+            createChart(parsedData, chartSettings);
+            console.log('图表生成成功');
+            
+            exportPngBtn.disabled = false;
+            exportJpgBtn.disabled = false;
+            copyImgBtn.disabled = false;
+        } catch (error) {
+            console.error('生成图表时出错:', error);
+            showNotification(error.message, true);
+        }
+    }
+
+    // 将generateChart函数暴露为全局函数
+    window.generateChart = generateChart;
+
+    generateBtn.addEventListener('click', generateChart);
+
+    // 监听图表设置的变化事件,实时更新图表 (仅在有图表时)
+    ['chart-title', 'x-axis-label', 'y-axis-label', 'color-scheme', 'legend-position'].forEach(id => {
+        document.getElementById(id).addEventListener('input', function() {
+            const instance = getChartInstance();
+            if (instance) { // 检查是否有图表实例
+                generateChart(); // 重新生成图表以应用设置
+            }
+        });
+    });
+    
+    // 初始化图表类型画廊
+    function initChartTypeGallery() {
+        console.log('初始化图表类型预览画廊...');
+        
+        // 获取所有图表类型预览项
+        const chartTypeItems = document.querySelectorAll('.chart-type-item');
+        console.log(`找到${chartTypeItems.length}个图表类型预览项`);
+        
+        // 为每个预览项添加点击事件
+        chartTypeItems.forEach(item => {
+            item.addEventListener('click', function() {
+                const chartType = this.getAttribute('data-chart-type');
+                console.log('选择了图表类型:', chartType);
+                
+                // 更新活动状态
+                chartTypeItems.forEach(item => item.classList.remove('active'));
+                this.classList.add('active');
+                
+                // 无论是否有图表实例都应该重新生成图表
+                // 删除之前的检查条件,始终调用generateChart
+                generateChart();
+            });
+        });
+        
+        // 初始设置默认图表类型为活动状态
+        const defaultChartType = "bar"; // 默认为柱状图
+        const activeItem = document.querySelector(`.chart-type-item[data-chart-type="${defaultChartType}"]`);
+        if (activeItem) {
+            activeItem.classList.add('active');
+        }
+    }
+    
+    // 加载样例数据
+    sampleDataBtn.addEventListener('click', function() {
+        // 确保选中"手动录入"选项
+        const manualRadio = document.querySelector('input[name="data-input-method"][value="manual"]');
+        if (manualRadio && !manualRadio.checked) {
+            manualRadio.checked = true;
+            // 触发change事件以显示相关的输入控件
+            manualRadio.dispatchEvent(new Event('change'));
+        }
+        
+        const currentFormat = manualFormatSelect.value;
+        
+        switch(currentFormat) {
+            case 'simple':
+                document.getElementById('data-input').value = 
+                    '智能手机,2458\n平板电脑,1678\n笔记本电脑,1892\n智能手表,986\n耳机,1342';
+                document.getElementById('chart-title').value = '2023年电子产品销量(万台)';
+                document.getElementById('x-axis-label').value = '产品类别';
+                document.getElementById('y-axis-label').value = '销量(万台)';
+                break;
+            case 'series':
+                document.getElementById('series-data-input').value = 
+                    '第一季度,2458,1678,1892,986,1342\n第二季度,2612,1524,1953,1104,1587\n第三季度,2845,1701,2135,1287,1643\n第四季度,3256,1835,2278,1452,1821';
+                document.getElementById('series-labels').value = 
+                    '智能手机,平板电脑,笔记本电脑,智能手表,耳机';
+                document.getElementById('chart-title').value = '2023年电子产品季度销量(万台)';
+                document.getElementById('x-axis-label').value = '产品类别';
+                document.getElementById('y-axis-label').value = '销量(万台)';
+                break;
+            case 'csv':
+                document.getElementById('csv-data-input').value = 
+                    '品牌,2021年,2022年,2023年\n华为,786.5,845.2,921.6\n小米,651.2,712.8,768.3\n苹果,598.7,642.1,724.5\n三星,542.3,575.8,612.4\nOPPO,487.6,524.3,547.8\nvivo,452.8,501.7,532.9';
+                document.getElementById('chart-title').value = '国内智能手机品牌销量趋势(万台)';
+                document.getElementById('x-axis-label').value = '品牌';
+                document.getElementById('y-axis-label').value = '销量(万台)';
+                break;
+        }
+        
+        // 提示用户下一步操作
+        showNotification('已加载样例数据,点击"生成图表"查看效果');
+    });
+    
+    // 显示通知
+    function showNotification(message, isError = false) {
+        // 移除现有通知
+        const existingNotification = document.querySelector('.notification');
+        if (existingNotification) {
+            existingNotification.remove();
+        }
+        
+        // 创建新通知
+        const notification = document.createElement('div');
+        notification.className = 'notification' + (isError ? ' error' : '');
+        notification.textContent = message;
+        
+        // 添加到文档
+        document.body.appendChild(notification);
+        
+        // 显示通知
+        setTimeout(() => notification.classList.add('show'), 10);
+        
+        // 自动隐藏
+        setTimeout(() => {
+            notification.classList.remove('show');
+            setTimeout(() => notification.remove(), 300);
+        }, 3000);
+    }
+    
+    // 解析输入数据
+    function parseInputData() {
+        const currentFormat = manualFormatSelect.value;
+        
+        switch(currentFormat) {
+            case 'simple':
+                return parseSimpleData();
+            case 'series':
+                return parseSeriesData();
+            case 'csv':
+                return parseCsvData();
+            default:
+                throw new Error('未知的数据格式');
+        }
+    }
+    
+    // 解析简单数据
+    function parseSimpleData() {
+        const input = document.getElementById('data-input').value.trim();
+        if (!input) {
+            throw new Error('请输入数据');
+        }
+        
+        const lines = input.split('\n').filter(line => line.trim());
+        const labels = [];
+        const data = [];
+        
+        lines.forEach(line => {
+            const parts = line.split(',').map(part => part.trim());
+            if (parts.length >= 2) {
+                labels.push(parts[0]);
+                const value = parseFloat(parts[1]);
+                if (isNaN(value)) {
+                    throw new Error(`"${parts[1]}"不是有效的数值`);
+                }
+                data.push(value);
+            }
+        });
+        
+        if (labels.length === 0 || data.length === 0) {
+            throw new Error('无法解析数据,请检查格式是否正确');
+        }
+        
+        return {
+            labels: labels,
+            datasets: [{
+                data: data,
+                label: '数值'
+            }],
+            isSimpleData: true // 添加标记,表示这是简单数据格式
+        };
+    }
+    
+    // 解析系列数据
+    function parseSeriesData() {
+        const input = document.getElementById('series-data-input').value.trim();
+        const labelsInput = document.getElementById('series-labels').value.trim();
+        
+        if (!input) {
+            throw new Error('请输入系列数据');
+        }
+        
+        if (!labelsInput) {
+            throw new Error('请输入标签数据');
+        }
+        
+        const lines = input.split('\n').filter(line => line.trim());
+        const labels = labelsInput.split(',').map(label => label.trim());
+        const datasets = [];
+        
+        lines.forEach(line => {
+            const parts = line.split(',').map(part => part.trim());
+            if (parts.length >= 2) {
+                const seriesName = parts[0];
+                const seriesData = parts.slice(1).map(val => {
+                    const value = parseFloat(val);
+                    if (isNaN(value)) {
+                        throw new Error(`"${val}"不是有效的数值`);
+                    }
+                    return value;
+                });
+                
+                datasets.push({
+                    label: seriesName,
+                    data: seriesData
+                });
+            }
+        });
+        
+        if (labels.length === 0 || datasets.length === 0) {
+            throw new Error('无法解析数据,请检查格式是否正确');
+        }
+        
+        return {
+            labels: labels,
+            datasets: datasets
+        };
+    }
+    
+    // 解析CSV数据
+    function parseCsvData() {
+        const input = document.getElementById('csv-data-input').value.trim();
+        const firstRowHeader = document.getElementById('first-row-header').checked;
+        const firstColLabels = document.getElementById('first-col-labels').checked;
+        
+        if (!input) {
+            throw new Error('请输入CSV数据');
+        }
+        
+        const lines = input.split('\n').filter(line => line.trim());
+        if (lines.length < 2) {
+            throw new Error('CSV数据至少需要两行');
+        }
+        
+        const rows = lines.map(line => line.split(',').map(cell => cell.trim()));
+        
+        let labels = [];
+        let datasets = [];
+        
+        if (firstRowHeader && firstColLabels) {
+            // 第一行是标题,第一列是标签
+            labels = rows.slice(1).map(row => row[0]);
+            
+            const headers = rows[0].slice(1);
+            headers.forEach((header, i) => {
+                const data = rows.slice(1).map(row => {
+                    const value = parseFloat(row[i+1]);
+                    if (isNaN(value)) {
+                        throw new Error(`"${row[i+1]}"不是有效的数值`);
+                    }
+                    return value;
+                });
+                
+                datasets.push({
+                    label: header,
+                    data: data
+                });
+            });
+        } else if (firstRowHeader && !firstColLabels) {
+            // 第一行是标题,但第一列不是标签
+            labels = Array.from({length: rows[0].length}, (_, i) => `数据${i+1}`);
+            
+            const headers = rows[0];
+            headers.forEach((header, i) => {
+                const data = rows.slice(1).map(row => {
+                    const value = parseFloat(row[i]);
+                    if (isNaN(value)) {
+                        throw new Error(`"${row[i]}"不是有效的数值`);
+                    }
+                    return value;
+                });
+                
+                datasets.push({
+                    label: header,
+                    data: data
+                });
+            });
+        } else if (!firstRowHeader && firstColLabels) {
+            // 第一行不是标题,第一列是标签
+            labels = rows.map(row => row[0]);
+            
+            for (let i = 1; i < rows[0].length; i++) {
+                const data = rows.map(row => {
+                    const value = parseFloat(row[i]);
+                    if (isNaN(value)) {
+                        throw new Error(`"${row[i]}"不是有效的数值`);
+                    }
+                    return value;
+                });
+                
+                datasets.push({
+                    label: `系列${i}`,
+                    data: data
+                });
+            }
+        } else {
+            // 第一行不是标题,第一列也不是标签
+            labels = Array.from({length: rows.length}, (_, i) => `标签${i+1}`);
+            
+            for (let i = 0; i < rows[0].length; i++) {
+                const data = rows.map(row => {
+                    const value = parseFloat(row[i]);
+                    if (isNaN(value)) {
+                        throw new Error(`"${row[i]}"不是有效的数值`);
+                    }
+                    return value;
+                });
+                
+                datasets.push({
+                    label: `系列${i+1}`,
+                    data: data
+                });
+            }
+        }
+        
+        if (labels.length === 0 || datasets.length === 0) {
+            throw new Error('无法解析数据,请检查格式是否正确');
+        }
+        
+        return {
+            labels: labels,
+            datasets: datasets
+        };
+    }
+    
+    // 获取图表设置
+    function getChartSettings() {
+        // 从活跃的图表类型项获取图表类型,而不是从下拉框
+        let chartType = 'bar'; // 默认值
+        const activeChartTypeItem = document.querySelector('.chart-type-item.active');
+        if (activeChartTypeItem) {
+            chartType = activeChartTypeItem.getAttribute('data-chart-type');
+        }
+        
+        return {
+            type: chartType,
+            title: document.getElementById('chart-title').value,
+            xAxisLabel: document.getElementById('x-axis-label').value,
+            yAxisLabel: document.getElementById('y-axis-label').value,
+            colorScheme: document.getElementById('color-scheme').value,
+            legendPosition: document.getElementById('legend-position').value,
+            showGridLines: document.getElementById('show-grid-lines').checked,
+            animateChart: document.getElementById('animate-chart').checked
+        };
+    }
+    
+    // 导出PNG图像
+    exportPngBtn.addEventListener('click', function() {
+        exportChart('png');
+    });
+    
+    // 导出JPG图像
+    exportJpgBtn.addEventListener('click', function() {
+        exportChart('jpg');
+    });
+    
+    // 复制图像到剪贴板
+    copyImgBtn.addEventListener('click', function() {
+        copyChartToClipboard();
+    });
+    
+    // 导出图表为图像
+    function exportChart(format) {
+        const chartWrapper = document.getElementById('chart-wrapper');
+        
+        // 创建加载指示器
+        const loadingOverlay = document.createElement('div');
+        loadingOverlay.className = 'loading-overlay';
+        loadingOverlay.innerHTML = '<div class="loading-spinner"></div>';
+        chartWrapper.appendChild(loadingOverlay);
+        
+        setTimeout(() => {
+            // 获取原始canvas的尺寸
+            const originalCanvas = document.getElementById('chart-canvas');
+            const width = originalCanvas.width;
+            const height = originalCanvas.height;
+            
+            // 创建一个临时的高分辨率canvas
+            const tempCanvas = document.createElement('canvas');
+            const tempCtx = tempCanvas.getContext('2d');
+            
+            // 设置更高的分辨率
+            const scale = 8; // 提升到8倍分辨率
+            tempCanvas.width = width * scale;
+            tempCanvas.height = height * scale;
+            
+            // 优化渲染质量
+            tempCtx.imageSmoothingEnabled = true;
+            tempCtx.imageSmoothingQuality = 'high';
+            
+            html2canvas(originalCanvas, {
+                backgroundColor: '#ffffff',
+                scale: scale, // 使用8倍缩放
+                width: width,
+                height: height,
+                useCORS: true,
+                allowTaint: true,
+                logging: false,
+                imageTimeout: 0,
+                onclone: (document) => {
+                    const clonedCanvas = document.getElementById('chart-canvas');
+                    if(clonedCanvas) {
+                        clonedCanvas.style.width = width + 'px';
+                        clonedCanvas.style.height = height + 'px';
+                    }
+                },
+                // 添加高级渲染选项
+                canvas: tempCanvas,
+                renderCallback: (canvas) => {
+                    // 应用锐化效果
+                    const ctx = canvas.getContext('2d');
+                    ctx.filter = 'contrast(1.1) saturate(1.2)';
+                }
+            }).then(canvas => {
+                // 移除加载指示器
+                loadingOverlay.remove();
+                
+                // 导出图像时使用更高的质量设置
+                let imgUrl;
+                if (format === 'jpg') {
+                    // JPEG使用最高质量
+                    imgUrl = canvas.toDataURL('image/jpeg', 1.0);
+                } else {
+                    // PNG使用无损压缩
+                    imgUrl = canvas.toDataURL('image/png');
+                }
+                
+                // 创建下载链接
+                const link = document.createElement('a');
+                const chartTitle = document.getElementById('chart-title').value || '图表';
+                const fileName = `${chartTitle.replace(/[^\w\u4e00-\u9fa5]/g, '_')}_Ultra_HD.${format}`;
+                
+                link.download = fileName;
+                link.href = imgUrl;
+                link.click();
+                
+                showNotification(`已成功导出超高清${format.toUpperCase()}图像`);
+            }).catch(error => {
+                // 移除加载指示器
+                loadingOverlay.remove();
+                
+                showNotification('导出图像失败,请重试', true);
+                console.error('导出图像出错:', error);
+            });
+        }, 100);
+    }
+    
+    // 复制图表到剪贴板
+    function copyChartToClipboard() {
+        const chartWrapper = document.getElementById('chart-wrapper');
+        
+        // 创建加载指示器
+        const loadingOverlay = document.createElement('div');
+        loadingOverlay.className = 'loading-overlay';
+        loadingOverlay.innerHTML = '<div class="loading-spinner"></div>';
+        chartWrapper.appendChild(loadingOverlay);
+        
+        setTimeout(() => {
+            html2canvas(document.getElementById('chart-canvas'), {
+                backgroundColor: '#ffffff',
+                scale: 2
+            }).then(canvas => {
+                // 移除加载指示器
+                loadingOverlay.remove();
+                
+                canvas.toBlob(blob => {
+                    try {
+                        // 尝试使用现代API复制到剪贴板
+                        if (navigator.clipboard && navigator.clipboard.write) {
+                            const clipboardItem = new ClipboardItem({'image/png': blob});
+                            navigator.clipboard.write([clipboardItem])
+                                .then(() => {
+                                    showNotification('图表已复制到剪贴板');
+                                })
+                                .catch(err => {
+                                    console.error('剪贴板API错误:', err);
+                                    legacyCopyToClipboard(canvas);
+                                });
+                        } else {
+                            legacyCopyToClipboard(canvas);
+                        }
+                    } catch (e) {
+                        console.error('复制到剪贴板出错:', e);
+                        legacyCopyToClipboard(canvas);
+                    }
+                });
+            }).catch(error => {
+                // 移除加载指示器
+                loadingOverlay.remove();
+                
+                showNotification('复制图像失败,请重试', true);
+                console.error('复制图像出错:', error);
+            });
+        }, 100);
+    }
+    
+    // 兼容性较好的复制方法(通过创建临时链接)
+    function legacyCopyToClipboard(canvas) {
+        const imgUrl = canvas.toDataURL('image/png');
+        
+        // 创建临时链接
+        const link = document.createElement('a');
+        link.download = '图表.png';
+        link.href = imgUrl;
+        
+        showNotification('已准备下载图表,无法直接复制到剪贴板');
+        link.click();
+    }
+    
+    // 解析Excel数据
+    function parseExcelData(jsonData) {
+        if (!jsonData || jsonData.length < 2 || !jsonData[0] || jsonData[0].length < 2) {
+            throw new Error('Excel数据格式不正确,至少需要表头行和数据行');
+        }
+        
+        // 假设第一行为标题,第一列为标签
+        const labels = jsonData.slice(1).map(row => row && row[0] ? row[0].toString() : '');
+        const datasets = [];
+
+        const headers = jsonData[0].slice(1);
+        headers.forEach((header, i) => {
+            const data = jsonData.slice(1).map(row => {
+                // 确保每个单元格数据都是数值类型
+                if (!row || !row[i + 1]) return 0;
+                const value = parseFloat(row[i + 1]);
+                return isNaN(value) ? 0 : value;
+            });
+            
+            datasets.push({
+                label: header ? header.toString() : `系列${i+1}`,
+                data: data
+            });
+        });
+
+        return {
+            labels: labels,
+            datasets: datasets
+        };
+    }
+
+    // 从chart-generator.js中导入图表生成函数
+    function getChartInstance() {
+        return window.chartInstance;
+    }
+
+    function setChartInstance(instance) {
+        window.chartInstance = instance;
+    }
+
+    // 根据数据格式更新图表类型选项
+    function updateChartTypeOptions(dataFormat) {
+        // 由于移除了图表类型下拉框,这个函数现在仅记录当前数据格式,不再修改任何选项
+        console.log('当前数据格式:', dataFormat);
+        // 未来可以根据数据格式来调整图表类型画廊的可见性或提示,但现在不需要操作
+    }
+}); 

+ 645 - 0
apps/chart-maker/style.css

@@ -0,0 +1,645 @@
+/* 基本样式重置 */
+
+* {
+    margin: 0;
+    padding: 0;
+    box-sizing: border-box;
+}
+
+body {
+    font-family: 'PingFang SC', 'Microsoft YaHei', 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
+    line-height: 1.6;
+    color: #333;
+    background-color: #f5f7fa;
+    background-image: linear-gradient(to right, rgba(245, 247, 250, 0.8) 1px, transparent 1px),
+                      linear-gradient(to bottom, rgba(245, 247, 250, 0.8) 1px, transparent 1px);
+    background-size: 20px 20px;
+    padding-top: 15px;
+    max-width: 1200px;
+    margin: 0 auto;
+}
+
+.container {
+    width: 100%;
+    max-width: 1200px;
+    margin: 0 auto;
+    padding: 0 20px;
+}
+
+/* 主内容区 */
+main {
+    padding: 40px 0;
+}
+
+.app-container {
+    display: flex;
+    gap: 30px;
+    margin-bottom: 30px;
+}
+
+/* 左侧边栏 */
+.sidebar {
+    flex: 0 0 350px;
+    display: flex;
+    flex-direction: column;
+    gap: 15px;
+}
+
+/* 面板共用样式 */
+.panel {
+    background: white;
+    border-radius: 12px;
+    padding: 15px;
+    box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08);
+    transition: box-shadow 0.3s;
+    position: relative;
+    overflow: hidden;
+}
+
+.panel:hover {
+    box-shadow: 0 15px 35px rgba(0, 0, 0, 0.12);
+}
+
+.panel::after {
+    content: '';
+    position: absolute;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 4px;
+    background: linear-gradient(90deg, #4568dc, #b06ab3);
+}
+
+.panel h2 {
+    font-size: 1.4rem;
+    font-weight: 600;
+    margin-bottom: 20px;
+    color: #333;
+    border-bottom: 1px solid #eee;
+    padding-bottom: 15px;
+    position: relative;
+}
+
+.panel h2::after {
+    content: '';
+    position: absolute;
+    bottom: -1px;
+    left: 0;
+    width: 60px;
+    height: 3px;
+    background: linear-gradient(90deg, #4568dc, #b06ab3);
+    border-radius: 3px;
+}
+
+/* 输入组样式 */
+.input-group {
+    margin-bottom: 20px;
+}
+
+.input-group label {
+    display: block;
+    margin-bottom: 8px;
+    font-weight: 500;
+    color: #444;
+}
+
+.input-group input[type="text"],
+.input-group select,
+.input-group textarea {
+    width: 100%;
+    padding: 12px;
+    border: 1px solid #ddd;
+    border-radius: 8px;
+    font-size: 12.6px;
+    transition: all 0.3s;
+    background-color: #f9f9f9;
+}
+
+.input-group input[type="text"]:focus,
+.input-group select:focus,
+.input-group textarea:focus {
+    border-color: #4568dc;
+    outline: none;
+    box-shadow: 0 0 0 3px rgba(69, 104, 220, 0.2);
+    background-color: white;
+}
+
+.checkbox-group {
+    display: flex;
+    align-items: center;
+    position: relative;
+    left: -260px;
+}
+
+.checkbox-group label {
+    display: flex;
+    align-items: center;
+    cursor: pointer;
+    margin-right: 10px;
+}
+
+.checkbox-group input[type="checkbox"] {
+    margin-right: 2px;
+    accent-color: #4568dc;
+    width: 16px;
+    height: 16px;
+}
+
+.csv-options {
+    display: flex;
+    gap: 15px;
+    margin-top: 10px;
+}
+
+/* 按钮样式 */
+.btn {
+    display: inline-block;
+    padding: 12px 20px;
+    border: none;
+    border-radius: 8px;
+    font-size: 13.5px;
+    font-weight: 500;
+    cursor: pointer;
+    transition: all 0.3s;
+    position: relative;
+    overflow: hidden;
+}
+
+.btn::after {
+    content: '';
+    position: absolute;
+    top: 0;
+    left: -100%;
+    width: 100%;
+    height: 100%;
+    background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
+    transition: left 0.5s;
+}
+
+.btn:hover::after {
+    left: 100%;
+}
+
+.btn-primary {
+    background: linear-gradient(135deg, #4568dc, #5c7be5);
+    color: white;
+    box-shadow: 0 5px 15px rgba(69, 104, 220, 0.3);
+}
+
+.btn-primary:hover {
+    background: linear-gradient(135deg, #3a56bb, #4d69cd);
+    transform: translateY(-2px);
+    box-shadow: 0 8px 20px rgba(69, 104, 220, 0.4);
+}
+
+.btn-primary:disabled {
+    background: #a5b1e2;
+    cursor: not-allowed;
+    box-shadow: none;
+}
+
+.btn-secondary {
+    background: linear-gradient(135deg, #f0f2f5, #e4e7f0);
+    color: #444;
+    box-shadow: 0 5px 15px rgba(0, 0, 0, 0.05);
+}
+
+.btn-secondary:hover {
+    background: linear-gradient(135deg, #e4e6e9, #d8dce6);
+    transform: translateY(-2px);
+    box-shadow: 0 8px 20px rgba(0, 0, 0, 0.1);
+}
+
+.input-group .btn {
+    margin-right: 10px;
+}
+
+/* 右侧主要内容区 */
+.main-content {
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+    gap: 25px;
+    max-width: 800px;
+    margin: 0 auto;
+}
+
+.chart-container {
+    background-color: #fff;
+    border-radius: 12px;
+    box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08);
+    padding: 25px;
+    margin-bottom: 6px;
+    transition: box-shadow 0.3s;
+    position: relative;
+    overflow: hidden;
+    max-width: 100%;
+    overflow-x: auto;
+}
+
+.chart-container::after {
+    content: '';
+    position: absolute;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 4px;
+    background: linear-gradient(90deg, #4568dc, #b06ab3);
+}
+
+.chart-container:hover {
+    box-shadow: 0 15px 35px rgba(0, 0, 0, 0.12);
+}
+
+#chart-wrapper {
+    flex: 1;
+    position: relative;
+    border-radius: 8px;
+    overflow: hidden;
+    margin: 0 0 20px 0;
+    border: 1px solid #e0e0e0;
+    padding: 15px;
+    background: white;
+    transition: all 0.3s ease;
+    box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.03);
+}
+
+/* 添加图表区域默认背景样式 */
+#chart-wrapper:empty {
+    background-image: url("data:image/svg+xml,%3Csvg width='100%25' height='100%25' xmlns='http://www.w3.org/2000/svg'%3E%3Cdefs%3E%3ClinearGradient id='grad' x1='0%25' y1='0%25' x2='100%25' y2='100%25'%3E%3Cstop offset='0%25' style='stop-color:%23f0f4ff;stop-opacity:1' /%3E%3Cstop offset='100%25' style='stop-color:%23eef4ff;stop-opacity:1' /%3E%3C/linearGradient%3E%3Cpattern id='grid' width='40' height='40' patternUnits='userSpaceOnUse'%3E%3Cpath d='M 0 10 L 40 10 M 10 0 L 10 40 M 0 20 L 40 20 M 20 0 L 20 40 M 0 30 L 40 30 M 30 0 L 30 40' fill='none' stroke='%23e0e0e0' stroke-width='0.5'/%3E%3C/pattern%3E%3C/defs%3E%3Crect width='100%25' height='100%25' fill='url(%23grad)'/%3E%3Crect width='100%25' height='100%25' fill='url(%23grid)'/%3E%3Ctext x='50%25' y='50%25' font-family='Arial, sans-serif' font-size='16' font-weight='bold' text-anchor='middle' fill='%23aaaaaa' dominant-baseline='middle'%3E选择数据并点击"生成图表"%3C/text%3E%3Cg transform='translate(50%25, 40%25)'%3E%3Cpath d='M-40,-30 L-40,30 L40,30' stroke='%234568dc' stroke-width='2' fill='none' stroke-opacity='0.3'/%3E%3Ccircle cx='-20' cy='10' r='5' fill='%234568dc' fill-opacity='0.3'/%3E%3Ccircle cx='0' cy='-10' r='5' fill='%234568dc' fill-opacity='0.3'/%3E%3Ccircle cx='20' cy='0' r='5' fill='%234568dc' fill-opacity='0.3'/%3E%3Cpath d='M-20,10 L0,-10 L20,0' stroke='%234568dc' stroke-width='2' fill='none' stroke-opacity='0.3'/%3E%3C/g%3E%3C/svg%3E");
+    background-size: cover;
+    background-position: center;
+    min-height: 300px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+}
+
+.chart-actions {
+    display: flex;
+    justify-content: flex-end;
+    gap: 12px;
+    margin-bottom: 20px;
+    padding: 0 0 15px 0;
+    border-bottom: 1px solid #e0e0e0;
+}
+
+/* 提示面板 */
+.tips-panel {
+    background: white;
+    border-radius: 12px;
+    padding: 25px;
+    box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08);
+    position: relative;
+    overflow: hidden;
+    max-width: 100%;
+    overflow-x: auto;
+}
+
+.tips-panel::after {
+    content: '';
+    position: absolute;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 4px;
+    background: linear-gradient(90deg, #4568dc, #b06ab3);
+}
+
+.tips-panel h3 {
+    font-size: 1.2rem;
+    margin-bottom: 15px;
+    color: #333;
+    position: relative;
+    padding-bottom: 10px;
+}
+
+.tips-panel h3::after {
+    content: '';
+    position: absolute;
+    bottom: 0;
+    left: 0;
+    width: 40px;
+    height: 3px;
+    background: linear-gradient(90deg, #4568dc, #b06ab3);
+    border-radius: 3px;
+}
+
+.tips-panel ul {
+    padding-left: 20px;
+}
+
+.tips-panel li {
+    margin-bottom: 10px;
+    color: #555;
+    position: relative;
+}
+
+.tips-panel li::before {
+    content: '';
+    position: absolute;
+    left: -20px;
+    top: 8px;
+    width: 8px;
+    height: 8px;
+    background: linear-gradient(135deg, #4568dc, #b06ab3);
+    border-radius: 50%;
+}
+
+/* 响应式设计 */
+@media (max-width: 900px) {
+    .app-container {
+        flex-direction: column;
+    }
+    
+    .sidebar {
+        flex: initial;
+        width: 100%;
+    }
+    
+    .chart-container {
+        height: 400px;
+    }
+}
+
+/* 数据格式切换相关样式 */
+#simple-data-container,
+#series-data-container,
+#csv-data-container {
+    transition: all 0.3s ease;
+}
+
+/* 图表导出loading效果 */
+.loading-overlay {
+    position: absolute;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    background: rgba(255, 255, 255, 0.8);
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    z-index: 10;
+    backdrop-filter: blur(3px);
+}
+
+.loading-spinner {
+    width: 45px;
+    height: 45px;
+    border: 4px solid rgba(69, 104, 220, 0.3);
+    border-radius: 50%;
+    border-top: 4px solid #4568dc;
+    animation: spin 1s linear infinite;
+    box-shadow: 0 0 10px rgba(69, 104, 220, 0.2);
+}
+
+@keyframes spin {
+    0% { transform: rotate(0deg); }
+    100% { transform: rotate(360deg); }
+}
+
+/* 通知弹窗样式 */
+.notification {
+    position: fixed;
+    top: 20px;
+    right: 20px;
+    padding: 15px 25px;
+    background: linear-gradient(135deg, #4caf50, #45a049);
+    color: white;
+    border-radius: 8px;
+    box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
+    z-index: 100;
+    opacity: 0;
+    transform: translateY(-20px);
+    transition: all 0.3s;
+}
+
+.notification.show {
+    opacity: 1;
+    transform: translateY(0);
+}
+
+.notification.error {
+    background: linear-gradient(135deg, #f44336, #e53935);
+}
+
+/* 添加引导动画 */
+@keyframes pulse {
+    0% { box-shadow: 0 0 0 0 rgba(69, 104, 220, 0.6); }
+    70% { box-shadow: 0 0 0 12px rgba(69, 104, 220, 0); }
+    100% { box-shadow: 0 0 0 0 rgba(69, 104, 220, 0); }
+}
+
+#generate-btn {
+    animation: pulse 2s infinite;
+}
+
+/* 图表类型画廊 */
+.chart-type-gallery {
+    margin-top: 25px;
+    border-top: 1px solid #e0e0e0;
+    padding-top: 20px;
+}
+
+.chart-type-gallery h3 {
+    font-size: 1.08rem;
+    margin-bottom: 15px;
+    color: #333;
+    position: relative;
+    display: inline-block;
+    padding-bottom: 8px;
+}
+
+.chart-type-gallery h3::after {
+    content: '';
+    position: absolute;
+    bottom: 0;
+    left: 0;
+    width: 100%;
+    height: 3px;
+    background: linear-gradient(90deg, #4568dc, #b06ab3);
+    border-radius: 3px;
+}
+
+.chart-type-scroller {
+    overflow-x: auto;
+    white-space: nowrap;
+    padding-bottom: 15px;
+    /* 自定义滚动条 */
+    scrollbar-width: thin;
+    scrollbar-color: #ccc #f0f0f0;
+}
+
+.chart-type-scroller::-webkit-scrollbar {
+    height: 8px;
+}
+
+.chart-type-scroller::-webkit-scrollbar-track {
+    background: #f0f0f0;
+    border-radius: 10px;
+}
+
+.chart-type-scroller::-webkit-scrollbar-thumb {
+    background: linear-gradient(90deg, #4568dc, #b06ab3);
+    border-radius: 10px;
+}
+
+.chart-type-group {
+    display: inline-block;
+    vertical-align: top;
+    margin-right: 25px;
+    min-width: 250px;
+}
+
+.chart-type-group h4 {
+    font-size: 15.3px;
+    margin: 0 0 12px 0;
+    color: #444;
+    border-bottom: 1px dashed #e0e0e0;
+    padding-bottom: 8px;
+}
+
+.chart-type-items {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 12px;
+}
+
+.chart-type-item {
+    width: 85px;
+    text-align: center;
+    cursor: pointer;
+    transition: box-shadow 0.3s, background-color 0.3s;
+    border-radius: 8px;
+    padding: 10px 5px;
+    background-color: #f9f9f9;
+    box-shadow: 0 3px 8px rgba(0,0,0,0.05);
+}
+
+.chart-type-item:hover {
+    background-color: #f0f8ff;
+    box-shadow: 0 8px 15px rgba(0,0,0,0.1);
+}
+
+.chart-type-item.active {
+    background: linear-gradient(135deg, #e6f2ff, #eef8ff);
+    border: 1px solid #b3d7ff;
+    box-shadow: 0 5px 15px rgba(69, 104, 220, 0.15);
+}
+
+.chart-type-item img {
+    width: 60px;
+    height: 60px;
+    object-fit: contain;
+    margin-bottom: 8px;
+    border-radius: 4px;
+    background: white;
+    padding: 3px;
+    border: 1px solid #eee;
+}
+
+.chart-type-item span {
+    display: block;
+    font-size: 11.7px;
+    color: #444;
+    white-space: normal;
+    line-height: 1.3;
+}
+
+/* 添加canvas样式 */
+canvas#chart-canvas {
+    height: 450px !important;
+}
+
+/* 为canvas添加默认背景,但在绘制图表时不显示背景 */
+canvas#chart-canvas:not([data-chart-rendered]) {
+    background-image: url("data:image/svg+xml,%3Csvg width='100%25' height='100%25' xmlns='http://www.w3.org/2000/svg'%3E%3Cdefs%3E%3ClinearGradient id='grad' x1='0%25' y1='0%25' x2='100%25' y2='100%25'%3E%3Cstop offset='0%25' style='stop-color:%23f0f4ff;stop-opacity:1' /%3E%3Cstop offset='100%25' style='stop-color:%23eef4ff;stop-opacity:1' /%3E%3C/linearGradient%3E%3Cpattern id='grid' width='40' height='40' patternUnits='userSpaceOnUse'%3E%3Cpath d='M 0 10 L 40 10 M 10 0 L 10 40 M 0 20 L 40 20 M 20 0 L 20 40 M 0 30 L 40 30 M 30 0 L 30 40' fill='none' stroke='%23e0e0e0' stroke-width='0.5'/%3E%3C/pattern%3E%3C/defs%3E%3Crect width='100%25' height='100%25' fill='url(%23grad)'/%3E%3Crect width='100%25' height='100%25' fill='url(%23grid)'/%3E%3Cg transform='translate(50%25, 50%25)'%3E%3Cpath d='M-40,-30 L-40,30 L40,30' stroke='%234568dc' stroke-width='2' fill='none' stroke-opacity='0.3'/%3E%3Ccircle cx='-20' cy='10' r='5' fill='%234568dc' fill-opacity='0.3'/%3E%3Ccircle cx='0' cy='-10' r='5' fill='%234568dc' fill-opacity='0.3'/%3E%3Ccircle cx='20' cy='0' r='5' fill='%234568dc' fill-opacity='0.3'/%3E%3Cpath d='M-20,10 L0,-10 L20,0' stroke='%234568dc' stroke-width='2' fill='none' stroke-opacity='0.3'/%3E%3C/g%3E%3C/svg%3E");
+    background-size: cover;
+    background-position: center;
+}
+
+/* 确保有data-chart-rendered属性的canvas没有背景图 */
+canvas#chart-canvas[data-chart-rendered] {
+    background-image: none !important;
+}
+
+/* 美化文件上传控件 */
+.input-group input[type="file"] {
+    width: 0.1px;
+    height: 0.1px;
+    opacity: 0;
+    overflow: hidden;
+    position: absolute;
+    z-index: -1;
+}
+
+.input-group input[type="file"] + label {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    padding: 12px 15px;
+    border-radius: 8px;
+    background: linear-gradient(135deg, #f0f2f5, #e4e7f0);
+    color: #444;
+    cursor: pointer;
+    font-weight: 500;
+    transition: all 0.3s;
+    box-shadow: 0 3px 10px rgba(0, 0, 0, 0.05);
+    margin-bottom: 0;
+    text-align: center;
+    width: 100%;
+}
+
+.input-group input[type="file"] + label:hover {
+    background: #e9ecef;
+    border-color: #adb5bd;
+}
+
+.input-group input[type="file"] + label::before {
+    content: '\f07c'; /* Use Font Awesome icon code for folder */
+    font-family: "Font Awesome 5 Free"; /* Ensure Font Awesome is loaded */
+    font-weight: 900;
+    margin-right: 8px;
+    display: inline-block; /* Ensure icon aligns properly */
+}
+
+/* 新增的页面头部样式 */
+.page-header {
+    background-color: #f8f9fa;
+    padding: 12px 20px;
+    border-bottom: 1px solid #e3e6f0;
+    display: flex;
+    align-items: center;
+    margin-bottom: 20px;
+}
+
+.page-header .header-link {
+    display: inline-flex;
+    align-items: center;
+    text-decoration: none;
+    color: #5a5c69;
+    font-weight: 500;
+    font-size: 1.2rem;
+    transition: color 0.2s ease-in-out;
+}
+
+.page-header .header-link:hover {
+    color: #4e73df;
+}
+
+.page-header .header-link img {
+    margin-right: 8px;
+    height: 18px;
+    width: 18px;
+}
+
+.page-header .header-link .fehelper-text {
+    font-weight: 700;
+}
+
+.page-header .page-title-suffix {
+    margin-left: 8px;
+    font-size: 1.2rem;
+    color: #858796;
+} 

部分文件因为文件数量过多而无法显示