feat: 初始化数据可视化图表生成器
功能特性: - 支持柱状图、折线图、混合图 - 支持多系列数据对比 - 支持拖拽调整系列顺序和自定义颜色 - 支持区域分割(左右对比) - 5种主题风格可选 - 支持导出PNG/SVG - 响应式布局,适配移动端
This commit is contained in:
@@ -0,0 +1,607 @@
|
|||||||
|
// ===== 全局状态 =====
|
||||||
|
let chartInstance = null;
|
||||||
|
let parsedData = null;
|
||||||
|
let seriesColors = [];
|
||||||
|
let seriesOrder = [];
|
||||||
|
|
||||||
|
// ===== 预设颜色方案 =====
|
||||||
|
const colorPalettes = {
|
||||||
|
default: ['#5470c6', '#91cc75', '#fac858', '#ee6666', '#73c0de', '#3ba272', '#fc8452', '#9a60b4', '#ea7ccc'],
|
||||||
|
dark: ['#4fc3f7', '#81c784', '#fff176', '#ff8a65', '#ba68c8', '#4dd0e1', '#ffab91', '#aed581', '#f48fb1'],
|
||||||
|
macarons: ['#2ec7c9', '#b6a2de', '#5ab1ef', '#ffb980', '#d87a80', '#8d98b3', '#e5cf0d', '#97b552', '#95706d'],
|
||||||
|
gradient: ['#7f7fd5', '#86a8e7', '#91eae4', '#ff6b6b', '#feca57', '#48dbfb', '#ff9ff3', '#54a0ff', '#5f27cd'],
|
||||||
|
retro: ['#d4a5a5', '#95b9c7', '#f6e8c3', '#dfceb4', '#a4c3b5', '#c9b1ff', '#f5c7b8', '#b8d4e3', '#e8c8a0']
|
||||||
|
};
|
||||||
|
|
||||||
|
// ===== 示例数据 =====
|
||||||
|
const sampleDataSets = [
|
||||||
|
{
|
||||||
|
name: '季度销售对比',
|
||||||
|
data: `产品, Q1, Q2, Q3, Q4
|
||||||
|
手机, 1200, 1800, 2100, 2500
|
||||||
|
平板, 800, 950, 1100, 1300
|
||||||
|
笔记本, 600, 750, 900, 1050
|
||||||
|
耳机, 400, 520, 680, 800`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '年度增长趋势',
|
||||||
|
data: `指标, 2020年, 2021年, 2022年, 2023年, 2024年
|
||||||
|
营收(万), 500, 680, 820, 1050, 1380
|
||||||
|
利润(万), 80, 120, 160, 230, 350
|
||||||
|
用户(千), 50, 85, 130, 200, 320`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '多指标对比(适合区域分割)',
|
||||||
|
data: `月份, 方案A-效率, 方案A-成本, 方案B-效率, 方案B-成本
|
||||||
|
1月, 85, 120, 78, 135
|
||||||
|
2月, 88, 115, 82, 128
|
||||||
|
3月, 92, 108, 88, 120
|
||||||
|
4月, 90, 112, 95, 110
|
||||||
|
5月, 95, 105, 98, 105
|
||||||
|
6月, 98, 98, 102, 95`
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// ===== 初始化 =====
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
// 默认加载第一个示例
|
||||||
|
document.getElementById('dataInput').value = sampleDataSets[0].data;
|
||||||
|
generateChart();
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===== 数据解析 =====
|
||||||
|
function parseData(rawText) {
|
||||||
|
const lines = rawText.trim().split('\n').filter(l => l.trim());
|
||||||
|
if (lines.length < 2) {
|
||||||
|
alert('数据至少需要包含表头和一行数据');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自动检测分隔符
|
||||||
|
let delimiter = ',';
|
||||||
|
if (lines[0].includes('\t')) {
|
||||||
|
delimiter = '\t';
|
||||||
|
} else if (lines[0].split('|').length > lines[0].split(',').length) {
|
||||||
|
delimiter = '|';
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = lines.map(line => {
|
||||||
|
return line.split(delimiter).map(cell => cell.trim());
|
||||||
|
});
|
||||||
|
|
||||||
|
const headers = rows[0];
|
||||||
|
const categories = [];
|
||||||
|
const seriesData = {};
|
||||||
|
|
||||||
|
// 第一列是横坐标,其余列是系列
|
||||||
|
for (let i = 1; i < headers.length; i++) {
|
||||||
|
seriesData[headers[i]] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 1; i < rows.length; i++) {
|
||||||
|
categories.push(rows[i][0]);
|
||||||
|
for (let j = 1; j < rows[i].length && j < headers.length; j++) {
|
||||||
|
const val = parseFloat(rows[i][j]);
|
||||||
|
seriesData[headers[j]].push(isNaN(val) ? 0 : val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
categories,
|
||||||
|
seriesNames: headers.slice(1),
|
||||||
|
seriesData
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 生成图表 =====
|
||||||
|
function generateChart() {
|
||||||
|
const rawText = document.getElementById('dataInput').value;
|
||||||
|
if (!rawText.trim()) {
|
||||||
|
alert('请输入数据');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedData = parseData(rawText);
|
||||||
|
if (!parsedData) return;
|
||||||
|
|
||||||
|
// 初始化系列顺序和颜色
|
||||||
|
seriesOrder = parsedData.seriesNames.map((_, i) => i);
|
||||||
|
const palette = colorPalettes[document.getElementById('themeStyle').value] || colorPalettes.default;
|
||||||
|
seriesColors = parsedData.seriesNames.map((_, i) => palette[i % palette.length]);
|
||||||
|
|
||||||
|
// 渲染系列配置
|
||||||
|
renderSeriesConfig();
|
||||||
|
|
||||||
|
// 初始化图表
|
||||||
|
initChart();
|
||||||
|
updateChart();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 初始化图表实例 =====
|
||||||
|
function initChart() {
|
||||||
|
const chartDom = document.getElementById('chartArea');
|
||||||
|
chartDom.innerHTML = '';
|
||||||
|
|
||||||
|
if (chartInstance) {
|
||||||
|
chartInstance.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
chartInstance = echarts.init(chartDom, null, { renderer: 'canvas' });
|
||||||
|
|
||||||
|
// 响应式
|
||||||
|
window.addEventListener('resize', () => {
|
||||||
|
chartInstance && chartInstance.resize();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 更新图表 =====
|
||||||
|
function updateChart() {
|
||||||
|
if (!parsedData || !chartInstance) return;
|
||||||
|
|
||||||
|
const chartType = document.getElementById('chartType').value;
|
||||||
|
const title = document.getElementById('chartTitle').value;
|
||||||
|
const theme = document.getElementById('themeStyle').value;
|
||||||
|
const showLegend = document.getElementById('showLegend').checked;
|
||||||
|
const showGrid = document.getElementById('showGrid').checked;
|
||||||
|
const showLabel = document.getElementById('showLabel').checked;
|
||||||
|
const stackMode = document.getElementById('stackMode').checked;
|
||||||
|
const smoothLine = document.getElementById('smoothLine').checked;
|
||||||
|
const enableSplit = document.getElementById('enableSplit').checked;
|
||||||
|
|
||||||
|
// 显示/隐藏分割配置
|
||||||
|
document.getElementById('splitConfig').style.display = enableSplit ? 'block' : 'none';
|
||||||
|
|
||||||
|
// 背景色
|
||||||
|
const bgColor = theme === 'dark' ? '#1a1a2e' : '#ffffff';
|
||||||
|
const textColor = theme === 'dark' ? '#e0e0e0' : '#333333';
|
||||||
|
const axisLineColor = theme === 'dark' ? '#444' : '#ddd';
|
||||||
|
|
||||||
|
// 按当前顺序构建系列
|
||||||
|
const palette = colorPalettes[theme] || colorPalettes.default;
|
||||||
|
const series = seriesOrder.map((origIdx, displayIdx) => {
|
||||||
|
const name = parsedData.seriesNames[origIdx];
|
||||||
|
const data = parsedData.seriesData[name];
|
||||||
|
const color = seriesColors[origIdx] || palette[origIdx % palette.length];
|
||||||
|
|
||||||
|
let type = chartType === 'bar-line'
|
||||||
|
? (displayIdx % 2 === 0 ? 'bar' : 'line')
|
||||||
|
: chartType;
|
||||||
|
|
||||||
|
const seriesItem = {
|
||||||
|
name: name,
|
||||||
|
type: type,
|
||||||
|
data: [...data],
|
||||||
|
itemStyle: { color: color },
|
||||||
|
emphasis: {
|
||||||
|
focus: 'series',
|
||||||
|
itemStyle: {
|
||||||
|
shadowBlur: 10,
|
||||||
|
shadowColor: 'rgba(0,0,0,0.3)'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 堆叠
|
||||||
|
if (stackMode) {
|
||||||
|
seriesItem.stack = 'total';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 折线图特有
|
||||||
|
if (type === 'line') {
|
||||||
|
seriesItem.smooth = smoothLine;
|
||||||
|
seriesItem.lineStyle = { width: 3 };
|
||||||
|
seriesItem.symbolSize = 8;
|
||||||
|
seriesItem.areaStyle = theme === 'gradient' ? {
|
||||||
|
opacity: 0.15
|
||||||
|
} : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 柱状图特有
|
||||||
|
if (type === 'bar') {
|
||||||
|
seriesItem.barMaxWidth = 40;
|
||||||
|
seriesItem.itemStyle.borderRadius = stackMode ? [0, 0, 0, 0] : [4, 4, 0, 0];
|
||||||
|
if (theme === 'gradient') {
|
||||||
|
seriesItem.itemStyle.color = new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||||
|
{ offset: 0, color: color },
|
||||||
|
{ offset: 1, color: adjustColor(color, 40) }
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 数据标签
|
||||||
|
if (showLabel) {
|
||||||
|
seriesItem.label = {
|
||||||
|
show: true,
|
||||||
|
position: type === 'bar' ? 'top' : 'top',
|
||||||
|
fontSize: 11,
|
||||||
|
color: textColor,
|
||||||
|
formatter: (params) => {
|
||||||
|
if (params.value >= 10000) return (params.value / 10000).toFixed(1) + 'w';
|
||||||
|
if (params.value >= 1000) return (params.value / 1000).toFixed(1) + 'k';
|
||||||
|
return params.value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return seriesItem;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 区域分割 - 使用 graphic 组件精确画分割线在柱子间隙中
|
||||||
|
const graphicElements = [];
|
||||||
|
if (enableSplit) {
|
||||||
|
const splitIdx = parseInt(document.getElementById('splitIndex').value) || 3;
|
||||||
|
const leftLabel = document.getElementById('leftLabel').value || '左侧';
|
||||||
|
const rightLabel = document.getElementById('rightLabel').value || '右侧';
|
||||||
|
const splitStyle = document.getElementById('splitStyle').value;
|
||||||
|
|
||||||
|
if (splitIdx < parsedData.categories.length) {
|
||||||
|
// 区域背景色用 markArea
|
||||||
|
const splitLineColor = theme === 'dark' ? '#ff6b6b' : '#e74c3c';
|
||||||
|
|
||||||
|
if (series.length > 0) {
|
||||||
|
series[0].markArea = {
|
||||||
|
silent: true,
|
||||||
|
data: [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: leftLabel,
|
||||||
|
xAxis: parsedData.categories[0],
|
||||||
|
itemStyle: {
|
||||||
|
color: theme === 'dark' ? 'rgba(79,195,247,0.06)' : 'rgba(79,70,229,0.05)',
|
||||||
|
borderWidth: 0
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
show: true,
|
||||||
|
position: 'insideTop',
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
color: theme === 'dark' ? '#4fc3f7' : '#4f46e5',
|
||||||
|
offset: [0, 10]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ xAxis: parsedData.categories[splitIdx - 1] }
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: rightLabel,
|
||||||
|
xAxis: parsedData.categories[splitIdx],
|
||||||
|
itemStyle: {
|
||||||
|
color: theme === 'dark' ? 'rgba(255,107,107,0.06)' : 'rgba(239,68,68,0.05)',
|
||||||
|
borderWidth: 0
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
show: true,
|
||||||
|
position: 'insideTop',
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
color: theme === 'dark' ? '#ff6b6b' : '#ef4444',
|
||||||
|
offset: [0, 10]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ xAxis: parsedData.categories[parsedData.categories.length - 1] }
|
||||||
|
]
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用 graphic 组件在渲染后画分割线(在柱子间隙中)
|
||||||
|
// 先存到全局,在 setOption 后通过 rendered 事件绘制
|
||||||
|
window._splitConfig = { splitIdx, splitLineColor, splitStyle, theme };
|
||||||
|
} else {
|
||||||
|
window._splitConfig = null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
window._splitConfig = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 图表配置
|
||||||
|
const option = {
|
||||||
|
backgroundColor: bgColor,
|
||||||
|
title: title ? {
|
||||||
|
text: title,
|
||||||
|
left: 'center',
|
||||||
|
top: 15,
|
||||||
|
textStyle: {
|
||||||
|
color: textColor,
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: 600
|
||||||
|
}
|
||||||
|
} : undefined,
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
backgroundColor: theme === 'dark' ? 'rgba(30,30,50,0.95)' : 'rgba(255,255,255,0.95)',
|
||||||
|
borderColor: theme === 'dark' ? '#555' : '#eee',
|
||||||
|
textStyle: { color: textColor },
|
||||||
|
axisPointer: {
|
||||||
|
type: chartType === 'bar' ? 'shadow' : 'cross',
|
||||||
|
crossStyle: { color: '#999' }
|
||||||
|
},
|
||||||
|
formatter: function(params) {
|
||||||
|
let html = `<div style="font-weight:600;margin-bottom:6px">${params[0].axisValue}</div>`;
|
||||||
|
params.forEach(p => {
|
||||||
|
html += `<div style="display:flex;align-items:center;gap:6px;margin:3px 0">
|
||||||
|
<span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:${p.color}"></span>
|
||||||
|
<span>${p.seriesName}:</span>
|
||||||
|
<span style="font-weight:600">${p.value.toLocaleString()}</span>
|
||||||
|
</div>`;
|
||||||
|
});
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
legend: showLegend ? {
|
||||||
|
show: true,
|
||||||
|
top: title ? 50 : 15,
|
||||||
|
textStyle: { color: textColor, fontSize: 12 },
|
||||||
|
itemGap: 20,
|
||||||
|
icon: 'roundRect'
|
||||||
|
} : { show: false },
|
||||||
|
grid: {
|
||||||
|
left: '3%',
|
||||||
|
right: '4%',
|
||||||
|
bottom: '3%',
|
||||||
|
top: showLegend ? (title ? 90 : 60) : (title ? 60 : 30),
|
||||||
|
containLabel: true
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
data: parsedData.categories,
|
||||||
|
axisLine: { lineStyle: { color: axisLineColor } },
|
||||||
|
axisLabel: {
|
||||||
|
color: textColor,
|
||||||
|
fontSize: 12,
|
||||||
|
interval: 0,
|
||||||
|
rotate: parsedData.categories.length > 10 ? 30 : 0
|
||||||
|
},
|
||||||
|
axisTick: { show: false }
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'value',
|
||||||
|
axisLine: { show: false },
|
||||||
|
axisTick: { show: false },
|
||||||
|
axisLabel: { color: textColor, fontSize: 11 },
|
||||||
|
splitLine: {
|
||||||
|
show: showGrid,
|
||||||
|
lineStyle: {
|
||||||
|
color: theme === 'dark' ? '#333' : '#f0f0f0',
|
||||||
|
type: 'dashed'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
series: series,
|
||||||
|
animationDuration: 800,
|
||||||
|
animationEasing: 'cubicOut'
|
||||||
|
};
|
||||||
|
|
||||||
|
chartInstance.setOption(option, true);
|
||||||
|
|
||||||
|
// 绘制分割线(在 category 间隙中)
|
||||||
|
drawSplitLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 在柱子间隙中精确绘制分割线 =====
|
||||||
|
function drawSplitLine() {
|
||||||
|
if (!chartInstance || !window._splitConfig) {
|
||||||
|
if (chartInstance) {
|
||||||
|
chartInstance.setOption({ graphic: { elements: [] } });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { splitIdx, splitLineColor, splitStyle } = window._splitConfig;
|
||||||
|
|
||||||
|
// 使用 convertToPixel 获取 category 的像素坐标(x位置)
|
||||||
|
const leftX = chartInstance.convertToPixel({ xAxisIndex: 0 }, splitIdx - 1);
|
||||||
|
const rightX = chartInstance.convertToPixel({ xAxisIndex: 0 }, splitIdx);
|
||||||
|
const midX = (leftX + rightX) / 2;
|
||||||
|
|
||||||
|
// 使用内部 coordinateSystem 获取精确的绘图区域边界
|
||||||
|
// 这是ECharts内部稳定可用的接口
|
||||||
|
const gridRect = chartInstance.getModel()
|
||||||
|
.getComponent('grid', 0)
|
||||||
|
.coordinateSystem.getRect();
|
||||||
|
|
||||||
|
const yTop = gridRect.y;
|
||||||
|
const yBottom = gridRect.y + gridRect.height;
|
||||||
|
|
||||||
|
// 虚线样式
|
||||||
|
let lineDash = null;
|
||||||
|
if (splitStyle === 'dashed') lineDash = [8, 4];
|
||||||
|
else if (splitStyle === 'dotted') lineDash = [3, 3];
|
||||||
|
|
||||||
|
chartInstance.setOption({
|
||||||
|
graphic: {
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
type: 'line',
|
||||||
|
shape: {
|
||||||
|
x1: midX,
|
||||||
|
y1: yTop,
|
||||||
|
x2: midX,
|
||||||
|
y2: yBottom
|
||||||
|
},
|
||||||
|
style: {
|
||||||
|
stroke: splitLineColor,
|
||||||
|
lineWidth: 2,
|
||||||
|
lineDash: lineDash
|
||||||
|
},
|
||||||
|
z: 100
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 渲染系列配置 =====
|
||||||
|
function renderSeriesConfig() {
|
||||||
|
const container = document.getElementById('seriesConfig');
|
||||||
|
container.innerHTML = '';
|
||||||
|
|
||||||
|
seriesOrder.forEach((origIdx, displayIdx) => {
|
||||||
|
const name = parsedData.seriesNames[origIdx];
|
||||||
|
const color = seriesColors[origIdx];
|
||||||
|
|
||||||
|
const item = document.createElement('div');
|
||||||
|
item.className = 'series-item';
|
||||||
|
item.draggable = true;
|
||||||
|
item.dataset.index = displayIdx;
|
||||||
|
|
||||||
|
item.innerHTML = `
|
||||||
|
<span class="drag-handle">⠿</span>
|
||||||
|
<span class="series-name">${name}</span>
|
||||||
|
<input type="color" value="${color}" onchange="updateSeriesColor(${origIdx}, this.value)">
|
||||||
|
<select onchange="updateSeriesType(${origIdx}, this.value)">
|
||||||
|
<option value="auto">自动</option>
|
||||||
|
<option value="bar">柱状</option>
|
||||||
|
<option value="line">折线</option>
|
||||||
|
</select>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// 拖拽事件
|
||||||
|
item.addEventListener('dragstart', handleDragStart);
|
||||||
|
item.addEventListener('dragover', handleDragOver);
|
||||||
|
item.addEventListener('drop', handleDrop);
|
||||||
|
item.addEventListener('dragend', handleDragEnd);
|
||||||
|
|
||||||
|
container.appendChild(item);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 拖拽排序 =====
|
||||||
|
let draggedItem = null;
|
||||||
|
|
||||||
|
function handleDragStart(e) {
|
||||||
|
draggedItem = this;
|
||||||
|
this.classList.add('dragging');
|
||||||
|
e.dataTransfer.effectAllowed = 'move';
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDragOver(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.dataTransfer.dropEffect = 'move';
|
||||||
|
|
||||||
|
const rect = this.getBoundingClientRect();
|
||||||
|
const midY = rect.top + rect.height / 2;
|
||||||
|
|
||||||
|
if (e.clientY < midY) {
|
||||||
|
this.style.borderTopColor = 'var(--primary)';
|
||||||
|
this.style.borderTopWidth = '2px';
|
||||||
|
} else {
|
||||||
|
this.style.borderBottomColor = 'var(--primary)';
|
||||||
|
this.style.borderBottomWidth = '2px';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDrop(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
// 清除样式
|
||||||
|
document.querySelectorAll('.series-item').forEach(item => {
|
||||||
|
item.style.borderTopColor = '';
|
||||||
|
item.style.borderTopWidth = '';
|
||||||
|
item.style.borderBottomColor = '';
|
||||||
|
item.style.borderBottomWidth = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
if (draggedItem === this) return;
|
||||||
|
|
||||||
|
const fromIdx = parseInt(draggedItem.dataset.index);
|
||||||
|
const toIdx = parseInt(this.dataset.index);
|
||||||
|
|
||||||
|
// 交换顺序
|
||||||
|
const temp = seriesOrder[fromIdx];
|
||||||
|
seriesOrder[fromIdx] = seriesOrder[toIdx];
|
||||||
|
seriesOrder[toIdx] = temp;
|
||||||
|
|
||||||
|
renderSeriesConfig();
|
||||||
|
updateChart();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDragEnd() {
|
||||||
|
this.classList.remove('dragging');
|
||||||
|
document.querySelectorAll('.series-item').forEach(item => {
|
||||||
|
item.style.borderTopColor = '';
|
||||||
|
item.style.borderTopWidth = '';
|
||||||
|
item.style.borderBottomColor = '';
|
||||||
|
item.style.borderBottomWidth = '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 系列操作 =====
|
||||||
|
function updateSeriesColor(origIdx, color) {
|
||||||
|
seriesColors[origIdx] = color;
|
||||||
|
updateChart();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSeriesType(origIdx, type) {
|
||||||
|
// 存储自定义类型覆盖
|
||||||
|
if (!window.seriesTypeOverrides) window.seriesTypeOverrides = {};
|
||||||
|
window.seriesTypeOverrides[origIdx] = type;
|
||||||
|
updateChart();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 工具函数 =====
|
||||||
|
function adjustColor(hex, amount) {
|
||||||
|
hex = hex.replace('#', '');
|
||||||
|
const num = parseInt(hex, 16);
|
||||||
|
let r = Math.min(255, ((num >> 16) & 0xff) + amount);
|
||||||
|
let g = Math.min(255, ((num >> 8) & 0xff) + amount);
|
||||||
|
let b = Math.min(255, (num & 0xff) + amount);
|
||||||
|
return `rgb(${r},${g},${b})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadSampleData() {
|
||||||
|
// 循环切换示例
|
||||||
|
const currentIdx = sampleDataSets.findIndex(s => s.data === document.getElementById('dataInput').value);
|
||||||
|
const nextIdx = (currentIdx + 1) % sampleDataSets.length;
|
||||||
|
document.getElementById('dataInput').value = sampleDataSets[nextIdx].data;
|
||||||
|
generateChart();
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearData() {
|
||||||
|
document.getElementById('dataInput').value = '';
|
||||||
|
document.getElementById('chartArea').innerHTML = `
|
||||||
|
<div class="placeholder">
|
||||||
|
<p>📊</p>
|
||||||
|
<p>输入数据后点击"生成图表"</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
if (chartInstance) {
|
||||||
|
chartInstance.dispose();
|
||||||
|
chartInstance = null;
|
||||||
|
}
|
||||||
|
parsedData = null;
|
||||||
|
document.getElementById('seriesConfig').innerHTML = '<p class="hint-text">生成图表后可在此调整各系列的顺序和颜色</p>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 导出图表 =====
|
||||||
|
function exportChart(format) {
|
||||||
|
if (!chartInstance) {
|
||||||
|
alert('请先生成图表');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (format === 'png') {
|
||||||
|
const url = chartInstance.getDataURL({
|
||||||
|
type: 'png',
|
||||||
|
pixelRatio: 2,
|
||||||
|
backgroundColor: document.getElementById('themeStyle').value === 'dark' ? '#1a1a2e' : '#fff'
|
||||||
|
});
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = 'chart.png';
|
||||||
|
a.click();
|
||||||
|
} else if (format === 'svg') {
|
||||||
|
// 重新用 SVG 渲染
|
||||||
|
const svgChart = echarts.init(document.createElement('div'), null, { renderer: 'svg' });
|
||||||
|
svgChart.setOption(chartInstance.getOption());
|
||||||
|
const url = svgChart.getDataURL({
|
||||||
|
type: 'svg',
|
||||||
|
pixelRatio: 2
|
||||||
|
});
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = 'chart.svg';
|
||||||
|
a.click();
|
||||||
|
svgChart.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
+144
@@ -0,0 +1,144 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>数据可视化图表生成器</title>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/echarts@5.5.0/dist/echarts.min.js"></script>
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="app-container">
|
||||||
|
<!-- 顶部标题 -->
|
||||||
|
<header class="app-header">
|
||||||
|
<h1>📊 数据可视化图表生成器</h1>
|
||||||
|
<p class="subtitle">粘贴数据,生成精美图表</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="main-content">
|
||||||
|
<!-- 左侧面板:数据输入和配置 -->
|
||||||
|
<div class="left-panel">
|
||||||
|
<!-- 数据输入区 -->
|
||||||
|
<div class="panel-section">
|
||||||
|
<h2>📝 数据输入</h2>
|
||||||
|
<div class="input-format-hint">
|
||||||
|
<p><strong>格式说明:</strong></p>
|
||||||
|
<p>第一行为表头(系列名称),第一列为横坐标值</p>
|
||||||
|
<pre>类别, 系列1, 系列2, 系列3
|
||||||
|
A, 10, 20, 30
|
||||||
|
B, 15, 25, 35
|
||||||
|
C, 20, 30, 40</pre>
|
||||||
|
</div>
|
||||||
|
<textarea id="dataInput" placeholder="在此粘贴数据... 支持逗号、制表符分隔 示例: 产品, 2023年, 2024年, 2025年 Q1, 120, 150, 180 Q2, 200, 220, 260 Q3, 180, 210, 240 Q4, 250, 280, 320"></textarea>
|
||||||
|
<div class="btn-group">
|
||||||
|
<button class="btn btn-primary" onclick="generateChart()">🎨 生成图表</button>
|
||||||
|
<button class="btn btn-secondary" onclick="loadSampleData()">📋 加载示例</button>
|
||||||
|
<button class="btn btn-secondary" onclick="clearData()">🗑️ 清空</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 图表配置区 -->
|
||||||
|
<div class="panel-section">
|
||||||
|
<h2>⚙️ 图表配置</h2>
|
||||||
|
|
||||||
|
<div class="config-group">
|
||||||
|
<label>图表类型</label>
|
||||||
|
<select id="chartType" onchange="updateChart()">
|
||||||
|
<option value="bar">柱状图</option>
|
||||||
|
<option value="line">折线图</option>
|
||||||
|
<option value="bar-line">柱状图+折线图混合</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="config-group">
|
||||||
|
<label>标题</label>
|
||||||
|
<input type="text" id="chartTitle" placeholder="图表标题" oninput="updateChart()">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="config-group">
|
||||||
|
<label>主题风格</label>
|
||||||
|
<select id="themeStyle" onchange="updateChart()">
|
||||||
|
<option value="default">默认</option>
|
||||||
|
<option value="dark">深色</option>
|
||||||
|
<option value="macarons">马卡龙</option>
|
||||||
|
<option value="gradient">渐变</option>
|
||||||
|
<option value="retro">复古</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="config-group">
|
||||||
|
<label>显示选项</label>
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<label><input type="checkbox" id="showLegend" checked onchange="updateChart()"> 图例</label>
|
||||||
|
<label><input type="checkbox" id="showGrid" checked onchange="updateChart()"> 网格线</label>
|
||||||
|
<label><input type="checkbox" id="showLabel" onchange="updateChart()"> 数据标签</label>
|
||||||
|
<label><input type="checkbox" id="stackMode" onchange="updateChart()"> 堆叠模式</label>
|
||||||
|
<label><input type="checkbox" id="smoothLine" checked onchange="updateChart()"> 平滑曲线</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 系列配置区 -->
|
||||||
|
<div class="panel-section">
|
||||||
|
<h2>🎨 系列配置</h2>
|
||||||
|
<div id="seriesConfig" class="series-config">
|
||||||
|
<p class="hint-text">生成图表后可在此调整各系列的顺序和颜色</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 区域分割配置 -->
|
||||||
|
<div class="panel-section">
|
||||||
|
<h2>📐 区域分割</h2>
|
||||||
|
<div class="config-group">
|
||||||
|
<label><input type="checkbox" id="enableSplit" onchange="updateChart()"> 启用区域分割</label>
|
||||||
|
</div>
|
||||||
|
<div id="splitConfig" class="split-config" style="display:none;">
|
||||||
|
<div class="config-group">
|
||||||
|
<label>分割位置(横坐标索引,如:3 表示在第3个数据后分割)</label>
|
||||||
|
<input type="number" id="splitIndex" value="3" min="1" oninput="updateChart()">
|
||||||
|
</div>
|
||||||
|
<div class="config-group">
|
||||||
|
<label>左侧区域标签</label>
|
||||||
|
<input type="text" id="leftLabel" placeholder="如:2023年" oninput="updateChart()">
|
||||||
|
</div>
|
||||||
|
<div class="config-group">
|
||||||
|
<label>右侧区域标签</label>
|
||||||
|
<input type="text" id="rightLabel" placeholder="如:2024年" oninput="updateChart()">
|
||||||
|
</div>
|
||||||
|
<div class="config-group">
|
||||||
|
<label>分割线样式</label>
|
||||||
|
<select id="splitStyle" onchange="updateChart()">
|
||||||
|
<option value="solid">实线</option>
|
||||||
|
<option value="dashed">虚线</option>
|
||||||
|
<option value="dotted">点线</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 导出按钮 -->
|
||||||
|
<div class="panel-section">
|
||||||
|
<div class="btn-group">
|
||||||
|
<button class="btn btn-success" onclick="exportChart('png')">📥 导出PNG</button>
|
||||||
|
<button class="btn btn-success" onclick="exportChart('svg')">📥 导出SVG</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧面板:图表预览 -->
|
||||||
|
<div class="right-panel">
|
||||||
|
<div class="chart-container">
|
||||||
|
<div id="chartArea" class="chart-area">
|
||||||
|
<div class="placeholder">
|
||||||
|
<p>📊</p>
|
||||||
|
<p>输入数据后点击"生成图表"</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="app.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,380 @@
|
|||||||
|
/* ===== 全局样式 ===== */
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--primary: #4f46e5;
|
||||||
|
--primary-hover: #4338ca;
|
||||||
|
--success: #10b981;
|
||||||
|
--success-hover: #059669;
|
||||||
|
--bg: #f8fafc;
|
||||||
|
--panel-bg: #ffffff;
|
||||||
|
--border: #e2e8f0;
|
||||||
|
--text: #1e293b;
|
||||||
|
--text-light: #64748b;
|
||||||
|
--shadow: 0 1px 3px rgba(0,0,0,0.1), 0 1px 2px rgba(0,0,0,0.06);
|
||||||
|
--shadow-lg: 0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -2px rgba(0,0,0,0.05);
|
||||||
|
--radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 布局 ===== */
|
||||||
|
.app-container {
|
||||||
|
max-width: 1600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-header {
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px 0 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-header h1 {
|
||||||
|
font-size: 2rem;
|
||||||
|
background: linear-gradient(135deg, var(--primary), #7c3aed);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
color: var(--text-light);
|
||||||
|
margin-top: 5px;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 420px 1fr;
|
||||||
|
gap: 20px;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 左侧面板 ===== */
|
||||||
|
.left-panel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
max-height: calc(100vh - 140px);
|
||||||
|
overflow-y: auto;
|
||||||
|
padding-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-panel::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-panel::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-panel::-webkit-scrollbar-thumb {
|
||||||
|
background: #cbd5e1;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-section {
|
||||||
|
background: var(--panel-bg);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-section h2 {
|
||||||
|
font-size: 1.05rem;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
color: var(--text);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 数据输入 ===== */
|
||||||
|
.input-format-hint {
|
||||||
|
background: #f1f5f9;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
color: var(--text-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-format-hint pre {
|
||||||
|
margin-top: 6px;
|
||||||
|
font-family: 'Fira Code', monospace;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
color: var(--primary);
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
#dataInput {
|
||||||
|
width: 100%;
|
||||||
|
height: 180px;
|
||||||
|
border: 2px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px;
|
||||||
|
font-family: 'Fira Code', 'Courier New', monospace;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
resize: vertical;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
#dataInput:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--primary);
|
||||||
|
box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 按钮 ===== */
|
||||||
|
.btn-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 8px 16px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: var(--primary);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background: var(--primary-hover);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: #f1f5f9;
|
||||||
|
color: var(--text);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-success {
|
||||||
|
background: var(--success);
|
||||||
|
color: white;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-success:hover {
|
||||||
|
background: var(--success-hover);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 配置区 ===== */
|
||||||
|
.config-group {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-group > label {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-light);
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-group select,
|
||||||
|
.config-group input[type="text"],
|
||||||
|
.config-group input[type="number"] {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
background: white;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-group select:focus,
|
||||||
|
.config-group input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--primary);
|
||||||
|
box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-group {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-group label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
font-size: 0.83rem;
|
||||||
|
color: var(--text);
|
||||||
|
cursor: pointer;
|
||||||
|
background: #f8fafc;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-group label:hover {
|
||||||
|
background: #f1f5f9;
|
||||||
|
border-color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-group input[type="checkbox"] {
|
||||||
|
accent-color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 系列配置 ===== */
|
||||||
|
.series-config {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.series-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: #f8fafc;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
cursor: grab;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.series-item:hover {
|
||||||
|
border-color: var(--primary);
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.series-item.dragging {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
.series-item .drag-handle {
|
||||||
|
color: #94a3b8;
|
||||||
|
cursor: grab;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.series-item .series-name {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.series-item input[type="color"] {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.series-item select {
|
||||||
|
padding: 4px 8px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint-text {
|
||||||
|
font-size: 0.83rem;
|
||||||
|
color: var(--text-light);
|
||||||
|
text-align: center;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 右侧图表区 ===== */
|
||||||
|
.right-panel {
|
||||||
|
position: sticky;
|
||||||
|
top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-container {
|
||||||
|
background: var(--panel-bg);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-area {
|
||||||
|
width: 100%;
|
||||||
|
height: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: var(--text-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder p:first-child {
|
||||||
|
font-size: 4rem;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder p:last-child {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 响应式 ===== */
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.main-content {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-panel {
|
||||||
|
max-height: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-area {
|
||||||
|
height: 450px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 动画 ===== */
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; transform: translateY(10px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-section {
|
||||||
|
animation: fadeIn 0.3s ease-out;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user