// 常量定义
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);
});