// 常量定义 const CHART_INIT_DELAY = 500; // 图表初始化延迟(毫秒) const PROGRESS_ANIM_DELAY = 800; // 进度条动画延迟(毫秒) const RESIZE_DEBOUNCE_TIME = 300; // 防抖时间(毫秒) const PROGRESS_STEP = 0.4; // 进度条每帧增量 const PROGRESS_INTERVAL = 20; // 进度条更新间隔(毫秒) const MOBILE_BREAKPOINT = 768; // 移动端断点(px) // 年度销售数据(可通过API动态获取) let yearData = { target: 300000000, // 年度目标(万元) actual: 231154000 // 年度实际销售额(万元) }; // 模拟数据 - 小组排名 const groupData = [ { name: '华东一组', target: 50000000000, actual: 62000000000 }, { name: '华南二组', target: 45000000000, actual: 51000000000 }, { name: '华北三组', target: 40000000000, actual: 43500000000 }, { name: '西南四组', target: 35000000000, actual: 28000000000 }, { name: '东北五组', target: 30000000000, actual: 26500000000 } ]; // 模拟数据 - 个人排名 const personData = [ { name: '张伟', target: 5000000000, actual: 7200000000 }, { name: '李娜', target: 4500000000, actual: 5800000000 }, { name: '王强', target: 4000000000, actual: 4900000000 }, { name: '刘洋', target: 3800000000, actual: 4100000000 }, { name: '陈静', target: 3500000000, actual: 3600000000 }, { name: '杨明', target: 3200000000, actual: 2800000000 }, { name: '赵丽', target: 3000000000, actual: 2500000000 } ]; // 格式化数字(万元) function formatNumber(num) { return (num / 10000).toFixed(0) + '万'; } // 格式化大数字(万元) function formatLargeNumber(num) { return num.toLocaleString('zh-CN') + ' 万元'; } // 获取年度销售数据(可替换为API调用) async function fetchYearData() { // TODO: 替换为实际的API调用 // 示例: // try { // const response = await fetch('/api/year-data'); // const data = await response.json(); // return data; // } catch (error) { // console.error('获取数据失败:', error); // return null; // } // 当前使用模拟数据 console.log('正在获取年度数据...'); return { target: 3500, // 年度目标(万元)- 可修改此值测试 actual: 2750 // 年度实际销售额(万元)- 可修改此值测试 }; } // 更新顶部数据区域 function updateTopData() { const targetEl = document.querySelector('.target-amount'); const actualEl = document.querySelector('.total-amount'); const progressTextEl = document.querySelector('.progress-text'); console.log('正在更新顶部数据:', yearData); if (targetEl) { targetEl.textContent = formatLargeNumber(yearData.target); console.log('年度目标已更新:', formatLargeNumber(yearData.target)); } else { console.error('找不到 .target-amount 元素'); } if (actualEl) { actualEl.textContent = formatLargeNumber(yearData.actual); console.log('年度总销售额已更新:', formatLargeNumber(yearData.actual)); } else { console.error('找不到 .total-amount 元素'); } // 计算完成率 const progressRate = ((yearData.actual / yearData.target) * 100).toFixed(2); if (progressTextEl) { progressTextEl.textContent = progressRate + '%'; console.log('完成率已更新:', progressRate + '%'); } else { console.error('找不到 .progress-text 元素'); } return parseFloat(progressRate); } // 启动进度条动画 function startProgressAnimation(targetRate) { const progressBar = document.querySelector('.progress-bar'); if (!progressBar) return; let value = 0; let lastTime = 0; function animateProgress(currentTime) { if (!lastTime) lastTime = currentTime; const deltaTime = currentTime - lastTime; if (deltaTime >= PROGRESS_INTERVAL) { value += PROGRESS_STEP; if (value >= targetRate) { value = targetRate; progressBar.style.width = value + '%'; return; // 动画完成 } progressBar.style.width = value + '%'; lastTime = currentTime; } requestAnimationFrame(animateProgress); } requestAnimationFrame(animateProgress); } // 渲染小组排名列表 function renderGroupRankList() { const container = document.getElementById('groupRankList'); container.innerHTML = groupData.map((item, index) => { const rate = ((item.actual / item.target) * 100).toFixed(1); const rankClass = index < 3 ? `top-${index + 1}` : 'other'; return `
${index + 1}
${item.name}
销售额: ${formatNumber(item.actual)} | 目标: ${formatNumber(item.target)}
完成率: ${rate}%
`; }).join(''); } // 渲染个人排名列表 function renderPersonRankList() { const container = document.getElementById('personRankList'); container.innerHTML = personData.map((item, index) => { const rate = ((item.actual / item.target) * 100).toFixed(1); const rankClass = index < 3 ? `top-${index + 1}` : 'other'; return `
${index + 1}
${item.name}
销售额: ${formatNumber(item.actual)} | 目标: ${formatNumber(item.target)}
完成率: ${rate}%
`; }).join(''); } // 渲染小组图表 function renderGroupChart() { const chartDom = document.getElementById('groupChart'); const chart = echarts.init(chartDom); const names = groupData.map(item => item.name); const actuals = groupData.map(item => item.actual / 100000000); const targets = groupData.map(item => item.target / 100000000); // 判断是否为移动端 const isMobile = window.innerWidth <= MOBILE_BREAKPOINT; const option = { backgroundColor: 'transparent', tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' }, formatter: function(params) { let result = params[0].name + '
'; params.forEach(param => { result += param.marker + param.seriesName + ': ' + param.value + '万
'; }); return result; }, backgroundColor: 'rgba(0, 0, 0, 0.9)', borderColor: '#ff6600', borderWidth: 1, textStyle: { color: '#fff', fontSize: 12 } }, grid: isMobile ? { left: '5%', right: '5%', bottom: '15%', top: '20%', containLabel: true } : { left: '15%', right: '10%', bottom: '10%', top: '20%', containLabel: true }, legend: { data: ['目标销售额', '实际销售额'], textStyle: { color: '#fff', fontSize: 12 }, top: '2%' }, xAxis: isMobile ? { type: 'category', data: names, name: '小组', nameTextStyle: { color: '#fff' }, axisLabel: { color: '#fff', fontSize: 10, interval: 0, rotate: 30 }, axisLine: { lineStyle: { color: 'rgba(255, 102, 0, 0.5)' } } } : { type: 'value', name: '万元', nameTextStyle: { color: '#fff' }, axisLabel: { color: '#fff', fontSize: 11, formatter: function(value) { return value * 100; } }, axisLine: { lineStyle: { color: 'rgba(255, 102, 0, 0.5)' } }, splitLine: { lineStyle: { color: 'rgba(255, 255, 255, 0.1)' } } }, yAxis: isMobile ? { type: 'value', name: '万', nameTextStyle: { color: '#fff' }, axisLabel: { color: '#fff', fontSize: 11 }, axisLine: { lineStyle: { color: 'rgba(255, 102, 0, 0.5)' } }, splitLine: { lineStyle: { color: 'rgba(255, 255, 255, 0.1)' } } } : { type: 'category', data: names, axisLabel: { color: '#fff', fontSize: 11 }, axisLine: { lineStyle: { color: 'rgba(255, 102, 0, 0.5)' } } }, series: [ { name: '目标销售额', type: 'bar', data: targets, itemStyle: { color: new echarts.graphic.LinearGradient(isMobile ? 0 : 0, isMobile ? 1 : 0, isMobile ? 0 : 1, isMobile ? 0 : 0, [ { offset: 0, color: '#F77436' }, { offset: 1, color: '#F77436' } ]), borderRadius: isMobile ? [4, 4, 0, 0] : [0, 4, 4, 0] }, barMaxWidth: 15, emphasis: { itemStyle: { shadowBlur: 10, shadowOffsetX: 0, shadowColor: 'rgba(102, 102, 102, 0.8)' } } }, { name: '实际销售额', type: 'bar', data: actuals, itemStyle: { color: new echarts.graphic.LinearGradient(isMobile ? 0 : 0, isMobile ? 1 : 0, isMobile ? 0 : 1, isMobile ? 0 : 0, [ { offset: 0, color: '#65491D' }, { offset: 1, color: '#65491D' } ]), borderRadius: isMobile ? [4, 4, 0, 0] : [0, 4, 4, 0] }, barMaxWidth: 15, emphasis: { itemStyle: { shadowBlur: 10, shadowOffsetX: 0, shadowColor: 'rgba(255, 102, 0, 0.8)' } } } ] }; chart.setOption(option); return chart; } // 渲染个人图表 function renderPersonChart() { const chartDom = document.getElementById('personChart'); const chart = echarts.init(chartDom); const names = personData.map(item => item.name); const actuals = personData.map(item => item.actual / 100000000); const targets = personData.map(item => item.target / 100000000); // 判断是否为移动端 const isMobile = window.innerWidth <= MOBILE_BREAKPOINT; const option = { backgroundColor: 'transparent', tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' }, formatter: function(params) { let result = params[0].name + '
'; params.forEach(param => { result += param.marker + param.seriesName + ': ' + param.value + '万
'; }); return result; }, backgroundColor: 'rgba(0, 0, 0, 0.9)', borderColor: '#ff6600', borderWidth: 1, textStyle: { color: '#fff', fontSize: 12 } }, grid: isMobile ? { left: '10%', right: '5%', bottom: '15%', top: '20%', containLabel: true } : { left: '15%', right: '10%', bottom: '10%', top: '20%', containLabel: true }, legend: { data: ['目标销售额', '实际销售额'], textStyle: { color: '#fff', fontSize: 12 }, top: '2%' }, xAxis: isMobile ? { type: 'value', name: '万', nameLocation: 'end', nameGap: 5, nameTextStyle: { color: '#fff', fontSize: 10 }, axisLabel: { color: '#fff', fontSize: 10 }, axisLine: { lineStyle: { color: 'rgba(255, 102, 0, 0.5)' } }, splitLine: { lineStyle: { color: 'rgba(255, 255, 255, 0.1)' } } } : { type: 'value', name: '万', nameTextStyle: { color: '#fff' }, axisLabel: { color: '#fff', fontSize: 11, formatter: function(value) { return value * 100; } }, axisLine: { lineStyle: { color: 'rgba(255, 102, 0, 0.5)' } }, splitLine: { lineStyle: { color: 'rgba(255, 255, 255, 0.1)' } } }, yAxis: isMobile ? { type: 'category', data: names, axisLabel: { color: '#fff', fontSize: 10 }, axisLine: { lineStyle: { color: 'rgba(255, 102, 0, 0.5)' } } } : { type: 'category', data: names, axisLabel: { color: '#fff', fontSize: 11 }, axisLine: { lineStyle: { color: 'rgba(255, 102, 0, 0.5)' } } }, series: [ { name: '目标销售额', type: 'bar', data: targets, itemStyle: { color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [ { offset: 0, color: '#F77436' }, { offset: 1, color: '#F77436' } ]), borderRadius: [0, 4, 4, 0] }, barMaxWidth: 15, emphasis: { itemStyle: { shadowBlur: 10, shadowOffsetX: 0, shadowColor: 'rgba(102, 102, 102, 0.8)' } } }, { name: '实际销售额', type: 'bar', data: actuals, itemStyle: { color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [ { offset: 0, color: '#65491D' }, { offset: 1, color: '#65491D' } ]), borderRadius: [0, 4, 4, 0] }, barMaxWidth: 15, emphasis: { itemStyle: { shadowBlur: 10, shadowOffsetX: 0, shadowColor: 'rgba(255, 102, 0, 0.8)' } } } ] }; chart.setOption(option); return chart; } // 初始化 document.addEventListener('DOMContentLoaded', async () => { // 获取年度数据并更新顶部显示 yearData = await fetchYearData(); const progressRate = updateTopData(); // 渲染排名列表 renderGroupRankList(); renderPersonRankList(); // 初始化图表 let groupChart = null; let personChart = null; let resizeTimer = null; function initCharts() { // 销毁旧图表 if (groupChart) { groupChart.dispose(); } if (personChart) { personChart.dispose(); } // 重新渲染图表 groupChart = renderGroupChart(); personChart = renderPersonChart(); } setTimeout(() => { initCharts(); // 响应式调整 - 使用防抖优化性能 window.addEventListener('resize', () => { // 图表重新渲染使用防抖 clearTimeout(resizeTimer); resizeTimer = setTimeout(() => { initCharts(); }, RESIZE_DEBOUNCE_TIME); }); }, CHART_INIT_DELAY); // 初始化季度和周数显示 function initQuarters() { // 获取当前周数(1-52) const now = new Date(); const start = new Date(now.getFullYear(), 0, 1); const diff = now - start; const currentWeek = Math.floor(diff / (7 * 24 * 60 * 60 * 1000)) + 1; // 定义每个季度的周数范围 const quarters = [ { id: 'q1Weeks', start: 1, end: 13 }, { id: 'q2Weeks', start: 14, end: 26 }, { id: 'q3Weeks', start: 27, end: 39 }, { id: 'q4Weeks', start: 40, end: 52 } ]; // 为每个季度生成周数圆圈 quarters.forEach(quarter => { const container = document.getElementById(quarter.id); for (let i = quarter.start; i <= quarter.end; i++) { const weekCircle = document.createElement('div'); weekCircle.className = 'week-circle'; weekCircle.textContent = i; // 添加样式类 if (i < currentWeek) { weekCircle.classList.add('passed'); } else if (i === currentWeek) { weekCircle.classList.add('current'); } // 添加悬停提示 weekCircle.title = `第${i}周`; container.appendChild(weekCircle); } }); } initQuarters(); // 动态调整周圈大小以适应容器宽度 function adjustWeekCirclesSize() { const weekContainers = document.querySelectorAll('.weeks-container'); // 只在PC端(>768px)时执行动态调整,移动端清除内联样式 const isMobile = window.innerWidth <= 768; weekContainers.forEach(container => { // 获取所有周圈 const weekCircles = container.querySelectorAll('.week-circle'); if (weekCircles.length === 0) return; // 移动端:清除所有内联样式,让 CSS 样式生效 if (isMobile) { weekCircles.forEach(circle => { circle.style.width = ''; circle.style.height = ''; circle.style.fontSize = ''; circle.style.borderWidth = ''; }); container.style.transform = ''; return; } // PC端:动态调整周圈大小 // 获取容器的实际宽度 const containerWidth = container.offsetWidth; // 计算所有周圈的总宽度(包括间距) const gap = 3; // 默认间距 const totalGap = gap * (weekCircles.length - 1); const circleCount = weekCircles.length; // 计算理想的周圈大小 const idealSize = (containerWidth - totalGap) / circleCount; // 设置最小和最大尺寸限制 const minSize = 10; const maxSize = 24; // 计算最终尺寸 let finalSize = Math.max(minSize, Math.min(idealSize, maxSize)); // 如果内容仍然溢出,计算缩放比例 const totalContentWidth = (finalSize * circleCount) + totalGap; let scale = 1; if (totalContentWidth > containerWidth) { scale = containerWidth / totalContentWidth; // 限制最小缩放比例,避免过小 scale = Math.max(0.5, scale); } // 应用样式到所有周圈 weekCircles.forEach(circle => { circle.style.width = finalSize + 'px'; circle.style.height = finalSize + 'px'; circle.style.fontSize = (finalSize * 0.4) + 'px'; // 字体大小随圆圈缩放 circle.style.borderWidth = Math.max(1, finalSize / 8) + 'px'; // 边框随圆圈缩放 }); // 如果需要缩放整个容器 if (scale < 1) { container.style.transform = `scaleX(${scale})`; container.style.transformOrigin = 'left'; } else { container.style.transform = 'none'; } }); } // 初始调整 setTimeout(() => { adjustWeekCirclesSize(); }, 100); // 监听窗口大小变化并重新调整 window.addEventListener('resize', () => { clearTimeout(resizeTimer); resizeTimer = setTimeout(() => { adjustWeekCirclesSize(); }, RESIZE_DEBOUNCE_TIME); }); // 延迟启动进度条动画,使用动态计算的完成率 setTimeout(() => { startProgressAnimation(progressRate); }, PROGRESS_ANIM_DELAY); // 可选:定时刷新数据(每30秒) // setInterval(async () => { // yearData = await fetchYearData(); // updateTopData(); // }, 30000); });