// 常量定义
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)
const DESIGN_WIDTH = 1920; // 设计稿宽度(px)
// 年度销售数据(可通过API动态获取)
let yearData = {
target: 300000000, // 年度目标(万元)
actual: 231154000 // 年度实际销售额(万元)
};
// PC端等比例缩放函数
function setScale() {
const screenWidth = window.innerWidth;
// 移动端不缩放,保持响应式布局
if (screenWidth <= MOBILE_BREAKPOINT) {
document.body.style.transform = 'none';
document.body.style.transformOrigin = 'left top';
document.body.style.marginLeft = '0px';
document.body.style.width = '100%';
document.body.style.minWidth = '320px';
document.body.style.height = 'auto';
document.body.style.overflowX = 'hidden';
return;
}
// PC端等比例缩放
const scale = screenWidth / DESIGN_WIDTH;
// 只限制最小缩放比例,允许放大
// 最小缩放到60%,最大可以放大填满屏幕
const finalScale = Math.max(scale, 0.6);
// 重置样式以获取真实内容高度
document.body.style.transform = 'none';
document.body.style.height = 'auto';
// 获取真实内容高度
const contentHeight = document.body.scrollHeight;
// 计算缩放后的高度
const scaledHeight = Math.ceil(contentHeight * finalScale);
// 应用缩放
document.body.style.transform = `scale(${finalScale})`;
document.body.style.transformOrigin = 'left top';
document.body.style.width = `${DESIGN_WIDTH}px`;
document.body.style.minWidth = `${DESIGN_WIDTH}px`;
document.body.style.height = `${scaledHeight}px`;
document.body.style.marginLeft = '0px';
document.body.style.overflowX = 'hidden';
}
// 模拟数据 - 小组排名
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 () => {
// PC端等比例缩放
setScale();
// 获取年度数据并更新顶部显示
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', () => {
// 立即更新缩放(不防抖,保证视觉效果流畅)
setScale();
// 图表重新渲染使用防抖
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();
// 延迟启动进度条动画,使用动态计算的完成率
setTimeout(() => {
startProgressAnimation(progressRate);
}, PROGRESS_ANIM_DELAY);
// 可选:定时刷新数据(每30秒)
// setInterval(async () => {
// yearData = await fetchYearData();
// updateTopData();
// }, 30000);
});