提交
@@ -0,0 +1,667 @@
|
||||
/* PC端等比例缩放,根字号固定 */
|
||||
html {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* 移动端基础字号 */
|
||||
@media screen and (max-width: 768px) {
|
||||
html {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Microsoft YaHei', Arial, sans-serif;
|
||||
color: #fff;
|
||||
min-width: 320px;
|
||||
overflow-x: hidden;
|
||||
/* PC端缩放相关样式 */
|
||||
transform-origin: left top;
|
||||
transition: transform 0.1s ease-out;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
background: #000;
|
||||
/* 固定最小宽度,避免缩放时内容挤压 */
|
||||
min-width: 1200px;
|
||||
}
|
||||
|
||||
.cont{
|
||||
width: 90%;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 0 1rem;
|
||||
/* 确保内容区域最小宽度 */
|
||||
min-width: 1080px;
|
||||
}
|
||||
|
||||
/* 移动端下调整宽度 */
|
||||
@media screen and (max-width: 768px) {
|
||||
body {
|
||||
transform: none !important;
|
||||
width: 100% !important;
|
||||
min-width: 320px !important;
|
||||
height: auto !important;
|
||||
overflow-x: hidden !important;
|
||||
}
|
||||
|
||||
.container {
|
||||
min-width: 320px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.cont {
|
||||
width: 95%;
|
||||
max-width: 100%;
|
||||
min-width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* 顶部标题 */
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: clamp(1rem, 2vw, 2rem);
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: clamp(1.5rem, 3vw, 3rem);
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
text-shadow: 0 0 20px rgba(255, 102, 0, 0.8);
|
||||
margin-bottom: 0.5rem;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.header p {
|
||||
font-size: clamp(0.875rem, 1.5vw, 1.5rem);
|
||||
color: #ff6600;
|
||||
letter-spacing: clamp(1px, 0.1vw, 2px);
|
||||
}
|
||||
|
||||
/* 顶部数据区域 */
|
||||
.top-data {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1fr;
|
||||
gap: clamp(6px, 1.5vw, 14px);
|
||||
margin-bottom: clamp(10px, 1.5vw, 20px);
|
||||
}
|
||||
|
||||
.sales-info {
|
||||
background: url(../img/hicon.png);
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100% 100%;
|
||||
border-radius: clamp(4px, 0.5vw, 8px);
|
||||
padding: clamp(10px, 1.5vw, 20px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: clamp(10px, 1.5vw, 15px);
|
||||
}
|
||||
|
||||
.sales-info-top {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.target-info, .total-info {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.target-info h3,.total-info h3{
|
||||
font-size: clamp(0.75rem, 1vw, 1rem);
|
||||
color: #fff;
|
||||
margin-bottom: clamp(5px, 0.8vw, 8px);
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.target-amount {
|
||||
font-size: clamp(1.25rem, 2.5vw, 2.25rem);
|
||||
font-weight: bold;
|
||||
color: #F77436;
|
||||
margin-bottom: clamp(5px, 0.8vw, 10px);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.total-amount {
|
||||
font-size: clamp(1.25rem, 2.5vw, 2.25rem);
|
||||
font-weight: bold;
|
||||
color: #FA551F;
|
||||
margin-bottom: clamp(5px, 0.8vw, 10px);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.sales-info-cont{
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.sales-info-bottom {
|
||||
width: 90%;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
width: 100%;
|
||||
height: clamp(20px, 2.5vw, 30px);
|
||||
padding: 3px;
|
||||
border-radius: 20px;
|
||||
background: linear-gradient(180deg, #FF923C, #FF923C);
|
||||
box-shadow: 0px 0px 8px rgb(255 255 255 / 80%);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.progress-bg {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #DC7439;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 0%;
|
||||
border-radius: 16px;
|
||||
background: repeating-linear-gradient(45deg,rgba(255, 255, 255, 0.25) 0,rgba(255, 255, 255, 0.25) 8px,rgba(255, 255, 255, 0.12) 8px,rgba(255, 255, 255, 0.12) 16px),linear-gradient(180deg, #FFD85E, #FFBE25);
|
||||
animation: move 1.2s linear infinite;
|
||||
box-shadow: inset 0 0 6px rgba(255, 255, 255, 0.6);
|
||||
will-change: width;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.progress-info {
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: #fff;
|
||||
font-size: clamp(10px, 1.2vw, 13px);
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.play-btn {
|
||||
width: clamp(18px, 2vw, 24px);
|
||||
height: clamp(18px, 2vw, 24px);
|
||||
border-radius: 50%;
|
||||
background: #DC7439;
|
||||
border: 2px solid #FFBE25;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.play-btn::before {
|
||||
content: '';
|
||||
margin-left: 2px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-top: 5px solid transparent;
|
||||
border-bottom: 5px solid transparent;
|
||||
border-left: 7px solid #fff;
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
font-size: clamp(10px, 1.2vw, 13px);
|
||||
white-space: nowrap;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.progress-label {
|
||||
color: #DC7439;
|
||||
font-weight: bold;
|
||||
font-size: clamp(10px, 1.2vw, 13px);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 进度条加载动画 */
|
||||
@keyframes progressLoad {
|
||||
0% {
|
||||
width: 0;
|
||||
}
|
||||
100% {
|
||||
width: 77.05%;
|
||||
}
|
||||
}
|
||||
|
||||
/* 条纹滚动动画 */
|
||||
@keyframes move {
|
||||
from { background-position: 0 0; }
|
||||
to { background-position: 40px 0; }
|
||||
}
|
||||
|
||||
.completion-rate {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.countdown {
|
||||
border: 1px solid #523A14;
|
||||
border-radius: 8px;
|
||||
padding: clamp(8px, 1.5vw,10px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.countdown h3 {
|
||||
font-size: clamp(0.875rem, 1.25vw, 1.25rem);
|
||||
color: #e7552f;
|
||||
margin-bottom: clamp(10px, 1.5vw, 15px);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* 季度容器 */
|
||||
.quarters-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: clamp(5px, 1.5vw, 5px);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 单个季度 */
|
||||
.quarter-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: clamp(5px, 1vw, 8px);
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
border-bottom: 1px solid #523A14;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
.last-quarter{
|
||||
border-bottom: none;
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
|
||||
.quarter-label {
|
||||
background: #e7552f;
|
||||
color: #fff;
|
||||
font-size: clamp(0.875rem, 1.2vw, 1rem);
|
||||
font-weight: bold;
|
||||
padding: clamp(3px, 0.5vw, 5px) clamp(8px, 1vw, 12px);
|
||||
border-radius: 4px;
|
||||
min-width: clamp(35px, 5vw, 45px);
|
||||
text-align: center;
|
||||
box-shadow: 0 0 10px rgba(255, 102, 0, 0.5);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 周数圆圈容器 */
|
||||
.weeks-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: clamp(3px, 0.5vw, 5px);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* 单个周数圆圈 */
|
||||
.week-circle {
|
||||
width: clamp(18px, 2.5vw, 24px);
|
||||
height: clamp(18px, 2.5vw, 24px);
|
||||
border-radius: 50%;
|
||||
border: 2px solid #815B37;
|
||||
background: rgba(129, 91, 55, 0.2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: clamp(0.625rem, 1vw, 0.75rem);
|
||||
color: #f5bbab;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.week-circle:hover {
|
||||
background: rgba(231, 85, 47, 0.4);
|
||||
border-color: #e7552f;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
/* 当前周高亮 */
|
||||
.week-circle.current {
|
||||
background: #e7552f;
|
||||
border-color: #ff6600;
|
||||
box-shadow: 0 0 10px rgba(255, 102, 0, 0.8);
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
/* 已过去的周 */
|
||||
.week-circle.passed {
|
||||
background: #e7552f;
|
||||
border-color: #ff6600;
|
||||
box-shadow: 0 0 10px rgba(255, 102, 0, 0.8);
|
||||
/* opacity: 0.6; */
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
box-shadow: 0 0 10px rgba(255, 102, 0, 0.8);
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 0 20px rgba(255, 102, 0, 1);
|
||||
}
|
||||
}
|
||||
|
||||
.countdown-label {
|
||||
font-size: clamp(0.875rem, 1.25vw, 1.25rem);
|
||||
color: #ff6600;
|
||||
margin-top: 8px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* 小组排名区域 */
|
||||
.group-ranking {
|
||||
border: 1px solid #523A14;
|
||||
border-radius: 8px;
|
||||
padding: clamp(10px, 1.5vw, 20px);
|
||||
margin-bottom: clamp(10px, 1.5vw, 20px);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: clamp(0.875rem, 1.25vw, 1rem);
|
||||
color: #fff;
|
||||
background: #ff6600;
|
||||
padding: clamp(5px, 0.8vw, 5px) clamp(10px, 1.5vw, 15px);
|
||||
display: inline-block;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.ranking-content {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: clamp(10px, 1.5vw, 20px);
|
||||
}
|
||||
|
||||
.ranking-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: clamp(8px, 1vw, 10px);
|
||||
}
|
||||
|
||||
.ranking-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: clamp(8px, 1.2vw, 12px) clamp(10px, 1.5vw, 15px);
|
||||
border-radius: 4px;
|
||||
font-size: clamp(0.75rem, 1vw, 0.875rem);
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.ranking-item.top-1 {
|
||||
background: linear-gradient(90deg, rgba(199, 173, 61, 0.4), rgba(199, 173, 61, 0.1));
|
||||
border: 1px solid rgba(199, 173, 61, 0.4);
|
||||
}
|
||||
|
||||
.ranking-item.top-2 {
|
||||
background: linear-gradient(90deg, rgba(136, 136, 135, 0.5), rgba(136, 136, 135, 0.1));
|
||||
border: 1px solid rgba(136, 136, 135, 0.4);
|
||||
}
|
||||
|
||||
.ranking-item.top-3 {
|
||||
background: linear-gradient(90deg, rgba(59, 96, 93, 0.6), rgba(59, 96, 93,0.1));
|
||||
border: 1px solid rgba(59, 96, 93, 0.4);
|
||||
}
|
||||
|
||||
.ranking-item.other {
|
||||
background: linear-gradient(90deg, rgba(100, 100, 100, 0.3), rgba(100, 100, 100, 0.1));
|
||||
border: 1px solid rgba(100, 100, 100, 0.4);
|
||||
}
|
||||
|
||||
.rank-number {
|
||||
font-size: clamp(0.875rem, 1.25vw, 1.25rem);
|
||||
font-weight: bold;
|
||||
margin-right: clamp(10px, 1.5vw, 15px);
|
||||
min-width: 20px;
|
||||
width: clamp(20px, 3vw, 25px);
|
||||
height: clamp(20px, 3vw, 25px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.ranking-item.top-1 .rank-number {
|
||||
background: #C7AD3D;
|
||||
color: #000;
|
||||
box-shadow: 0 0 8px rgba(255, 204, 0, 0.8);
|
||||
}
|
||||
|
||||
.ranking-item.top-2 .rank-number {
|
||||
background: #888887;
|
||||
color: #000;
|
||||
box-shadow: 0 0 8px rgba(192, 192, 192, 0.8);
|
||||
}
|
||||
|
||||
.ranking-item.top-3 .rank-number {
|
||||
background: #3B605D;
|
||||
color: #fff;
|
||||
box-shadow: 0 0 8px rgba(205, 127, 50, 0.8);
|
||||
}
|
||||
|
||||
.ranking-item.other .rank-number {
|
||||
background: #815B37;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.rank-name {
|
||||
font-weight: bold;
|
||||
margin-right: clamp(10px, 1.5vw, 20px);
|
||||
min-width: clamp(60px, 8vw, 90px);
|
||||
color: #fff;
|
||||
font-size: clamp(0.75rem, 1.1vw, 0.875rem);
|
||||
}
|
||||
|
||||
.rank-sales {
|
||||
flex: 1;
|
||||
margin-right: clamp(10px, 1.5vw, 20px);
|
||||
color: #F77436;
|
||||
font-size: clamp(0.6875rem, 1vw, 0.8125rem);
|
||||
}
|
||||
|
||||
.rank-rate {
|
||||
font-weight: bold;
|
||||
color: #FA3925;
|
||||
font-size: clamp(0.6875rem, 1vw, 0.8125rem);
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
height: 100%;
|
||||
min-height: clamp(250px, 30vw, 300px);
|
||||
}
|
||||
|
||||
.ranking-content {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
/* 个人排名区域 */
|
||||
.person-ranking {
|
||||
border: 1px solid #523A14;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
|
||||
/* 移动端优化 (768px及以下) */
|
||||
@media (max-width: 768px) {
|
||||
.top-data {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.ranking-content {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.sales-info {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.sales-info-top {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.sales-info-cont {
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.sales-info-bottom {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: clamp(0.875rem, 2.5vw, 1.125rem);
|
||||
}
|
||||
|
||||
.quarter-label {
|
||||
font-size: clamp(0.75rem, 2vw, 0.875rem);
|
||||
padding: clamp(2px, 0.5vw, 4px) clamp(6px, 1.5vw, 10px);
|
||||
min-width: clamp(30px, 5vw, 40px);
|
||||
}
|
||||
|
||||
.week-circle {
|
||||
width: clamp(16px, 4vw, 20px);
|
||||
height: clamp(16px, 4vw, 20px);
|
||||
font-size: clamp(0.5rem, 1.5vw, 0.625rem);
|
||||
}
|
||||
|
||||
.ranking-item {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
grid-template-rows: auto auto;
|
||||
gap: clamp(5px, 1vw, 8px);
|
||||
font-size: clamp(0.6875rem, 2vw, 0.875rem);
|
||||
padding: clamp(10px, 2vw, 15px);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.ranking-item .rank-number {
|
||||
grid-column: 1;
|
||||
grid-row: 1 / 3;
|
||||
font-size: clamp(0.875rem, 2.5vw, 1.125rem);
|
||||
width: clamp(18px, 5vw, 25px);
|
||||
height: clamp(18px, 5vw, 25px);
|
||||
min-width: 18px;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.ranking-item .rank-name {
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
margin-bottom: 0;
|
||||
margin-right: 0;
|
||||
font-size: clamp(0.75rem, 2.2vw, 0.875rem);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.ranking-item .rank-sales {
|
||||
grid-column: 2;
|
||||
grid-row: 2;
|
||||
margin-right: 0;
|
||||
margin-bottom: 0;
|
||||
margin-left: 0;
|
||||
margin-top: 0;
|
||||
font-size: clamp(0.625rem, 1.8vw, 0.75rem);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.ranking-item .rank-rate {
|
||||
grid-column: 3;
|
||||
grid-row: 1 / 3;
|
||||
margin-left: 0;
|
||||
flex-shrink: 0;
|
||||
font-size: clamp(0.6875rem, 2vw, 0.8125rem);
|
||||
align-self: center;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
min-height: clamp(200px, 40vw, 250px);
|
||||
}
|
||||
}
|
||||
|
||||
/* 超小屏幕优化 (480px及以下) */
|
||||
@media (max-width: 480px) {
|
||||
.header h1 {
|
||||
font-size: clamp(1.25rem, 5vw, 1.5rem);
|
||||
}
|
||||
|
||||
.header p {
|
||||
font-size: clamp(0.75rem, 3vw, 1rem);
|
||||
}
|
||||
|
||||
.quarters-container {
|
||||
gap: clamp(5px, 2vw, 8px);
|
||||
}
|
||||
|
||||
.quarter-label {
|
||||
font-size: clamp(0.625rem, 2.5vw, 0.75rem);
|
||||
padding: clamp(2px, 0.5vw, 3px) clamp(5px, 1.5vw, 8px);
|
||||
min-width: clamp(25px, 6vw, 35px);
|
||||
}
|
||||
|
||||
.week-circle {
|
||||
width: clamp(14px, 4.5vw, 18px);
|
||||
height: clamp(14px, 4.5vw, 18px);
|
||||
font-size: clamp(0.4375rem, 2vw, 0.5625rem);
|
||||
gap: clamp(2px, 0.5vw, 3px);
|
||||
}
|
||||
}
|
||||
|
||||
/* 头部和尾部图片 */
|
||||
.header-image {
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.footer-image {
|
||||
margin-top: 20px;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.header-image img,
|
||||
.footer-image img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
/* 排名数字内联样式替代 */
|
||||
.ranking-item .rank-number {
|
||||
color: #000;
|
||||
}
|
||||
|
After Width: | Height: | Size: 113 KiB |
|
After Width: | Height: | Size: 226 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
@@ -0,0 +1,104 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>数据大屏</title>
|
||||
<!-- 使用本地CSS文件,提升加载性能和缓存效率 -->
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
<!-- 使用本地ECharts,加载更快,支持离线访问 -->
|
||||
<script src="js/echarts.min.js" defer></script>
|
||||
<!-- 使用本地JS文件,提升加载性能和缓存效率 -->
|
||||
<script src="js/main.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<!-- 头部图片 -->
|
||||
<div class="header-image">
|
||||
<img src="img/heard.png" alt="头部装饰图片">
|
||||
</div>
|
||||
|
||||
<div class="cont">
|
||||
<!-- 顶部数据区域 -->
|
||||
<div class="top-data">
|
||||
<div class="sales-info">
|
||||
<div class="sales-info-top">
|
||||
<div class="target-info">
|
||||
<h3>年度目标</h3>
|
||||
<div class="target-amount">加载中...</div>
|
||||
</div>
|
||||
<div class="total-info">
|
||||
<h3>年度总销售额</h3>
|
||||
<div class="total-amount">加载中...</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sales-info-cont">
|
||||
<div class="sales-info-bottom">
|
||||
<div class="progress-container">
|
||||
<div class="progress-bg">
|
||||
<div class="progress-bar">
|
||||
<div class="progress-info">
|
||||
<div class="play-btn"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="progress-text">0%</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="progress-label">完成率</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="countdown">
|
||||
<h3>年度进度</h3>
|
||||
<div class="quarters-container" id="quartersContainer">
|
||||
<!-- Q1 -->
|
||||
<div class="quarter-item">
|
||||
<div class="quarter-label">Q1</div>
|
||||
<div class="weeks-container" id="q1Weeks"></div>
|
||||
</div>
|
||||
<!-- Q2 -->
|
||||
<div class="quarter-item">
|
||||
<div class="quarter-label">Q2</div>
|
||||
<div class="weeks-container" id="q2Weeks"></div>
|
||||
</div>
|
||||
<!-- Q3 -->
|
||||
<div class="quarter-item">
|
||||
<div class="quarter-label">Q3</div>
|
||||
<div class="weeks-container" id="q3Weeks"></div>
|
||||
</div>
|
||||
<!-- Q4 -->
|
||||
<div class="quarter-item last-quarter">
|
||||
<div class="quarter-label">Q4</div>
|
||||
<div class="weeks-container" id="q4Weeks"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 小组排名区域 -->
|
||||
<h2 class="section-title">小组排名</h2>
|
||||
<div class="group-ranking">
|
||||
<div class="ranking-content">
|
||||
<div class="ranking-list" id="groupRankList"></div>
|
||||
<div class="chart-container" id="groupChart"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 个人排名区域 -->
|
||||
<h2 class="section-title">个人排名</h2>
|
||||
<div class="person-ranking">
|
||||
<div class="ranking-content">
|
||||
<div class="ranking-list" id="personRankList"></div>
|
||||
<div class="chart-container" id="personChart"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 尾部图片 -->
|
||||
<div class="footer-image">
|
||||
<img src="img/foot.png" alt="尾部装饰图片">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,125 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Progress</title>
|
||||
<style>
|
||||
body {
|
||||
background:#0b0f14;
|
||||
display:flex;
|
||||
justify-content:center;
|
||||
align-items:center;
|
||||
height:100vh;
|
||||
}
|
||||
|
||||
.progress-wrap {
|
||||
width:680px;
|
||||
height:30px;
|
||||
padding:3px;
|
||||
border-radius:20px;
|
||||
background:linear-gradient(180deg,#ffb15a,#ff8c2a);
|
||||
box-shadow:0 0 8px rgba(255,153,51,.6);
|
||||
}
|
||||
|
||||
.progress-bg {
|
||||
position:relative;
|
||||
width:100%;
|
||||
height:100%;
|
||||
background:#1a1f26;
|
||||
border-radius:16px;
|
||||
overflow:hidden;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
position:relative;
|
||||
height:100%;
|
||||
width:0%;
|
||||
border-radius:16px;
|
||||
background:
|
||||
repeating-linear-gradient(
|
||||
45deg,
|
||||
rgba(255,255,255,.25) 0,
|
||||
rgba(255,255,255,.25) 8px,
|
||||
rgba(255,255,255,.12) 8px,
|
||||
rgba(255,255,255,.12) 16px
|
||||
),
|
||||
linear-gradient(180deg,#ffd27d,#ff8c2a);
|
||||
animation:move 1.2s linear infinite;
|
||||
box-shadow:inset 0 0 6px rgba(255,255,255,.6);
|
||||
}
|
||||
|
||||
@keyframes move {
|
||||
from { background-position:0 0; }
|
||||
to { background-position:40px 0; }
|
||||
}
|
||||
|
||||
/* 箭头容器(圆形) */
|
||||
.progress-info {
|
||||
position:absolute;
|
||||
right: 0px;
|
||||
top:50%;
|
||||
transform:translateY(-50%);
|
||||
display:flex;
|
||||
align-items:center;
|
||||
gap:6px;
|
||||
color:#fff;
|
||||
font-size:13px;
|
||||
font-weight:600;
|
||||
white-space:nowrap;
|
||||
}
|
||||
|
||||
.play-btn {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius:50%;
|
||||
background: #dc7439;
|
||||
border: 2px solid #ffb521;
|
||||
/* box-shadow:
|
||||
0 0 6px rgba(255,170,60,.8),
|
||||
inset 0 0 3px rgba(255,255,255,.9); */
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:center;
|
||||
}
|
||||
|
||||
.play-btn::before {
|
||||
content:"";
|
||||
margin-left:2px;
|
||||
width:0;
|
||||
height:0;
|
||||
border-top:5px solid transparent;
|
||||
border-bottom:5px solid transparent;
|
||||
border-left:7px solid #fff;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="progress-wrap">
|
||||
<div class="progress-bg">
|
||||
<div class="progress-bar" id="bar">
|
||||
<div class="progress-info">
|
||||
<div class="play-btn"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let value = 0;
|
||||
const target = 77.05;
|
||||
const bar = document.getElementById('bar');
|
||||
const percent = document.getElementById('percent');
|
||||
|
||||
const timer = setInterval(() => {
|
||||
value += 0.4;
|
||||
if (value >= target) {
|
||||
value = target;
|
||||
clearInterval(timer);
|
||||
}
|
||||
bar.style.width = value + '%';
|
||||
}, 20);
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,629 @@
|
||||
<!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.4.3/dist/echarts.min.js"></script>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Microsoft YaHei', Arial, sans-serif;
|
||||
background: linear-gradient(135deg, #0a0e27 0%, #1a1f3a 100%);
|
||||
min-height: 100vh;
|
||||
color: #fff;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1920px;
|
||||
margin: 0 auto;
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
/* 顶部年度标题 */
|
||||
.year-title {
|
||||
text-align: center;
|
||||
font-size: 36px;
|
||||
color: #fff;
|
||||
margin-bottom: 30px;
|
||||
font-weight: bold;
|
||||
letter-spacing: 4px;
|
||||
}
|
||||
|
||||
/* 顶部销售额区域 */
|
||||
.sales-header {
|
||||
margin-bottom: 40px;
|
||||
padding: 40px;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border-radius: 20px;
|
||||
border: 1px solid rgba(240, 138, 49, 0.2);
|
||||
}
|
||||
|
||||
.sales-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
margin-bottom: 40px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sales-progress-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 15px;
|
||||
margin-bottom: 15px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sales-section .sales-info {
|
||||
text-align: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.progress-label {
|
||||
font-size: 18px;
|
||||
color: #f08a31;
|
||||
font-weight: bold;
|
||||
min-width: 70px;
|
||||
}
|
||||
|
||||
.goal-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.goal-section .sales-info {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.full-width {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.sales-info {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: center;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.sales-label {
|
||||
font-size: 28px;
|
||||
color: #f08a31;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.sales-value {
|
||||
font-size: 56px;
|
||||
font-weight: bold;
|
||||
color: #f08a31;
|
||||
text-shadow: 0 0 20px rgba(240, 138, 49, 0.5);
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
.sales-unit {
|
||||
font-size: 24px;
|
||||
color: #f08a31;
|
||||
}
|
||||
|
||||
.target-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.target-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.target-label {
|
||||
font-size: 18px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.target-label.completed {
|
||||
color: #f08a31;
|
||||
}
|
||||
|
||||
.target-label.goal {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
.progress-bar-container {
|
||||
position: relative;
|
||||
width: 800px;
|
||||
height: 20px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 15px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #f08a31, #ffb347);
|
||||
border-radius: 15px;
|
||||
transition: width 1s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.target-line {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 3px;
|
||||
background: #ff4d4f;
|
||||
box-shadow: 0 0 10px rgba(255, 77, 79, 0.8);
|
||||
}
|
||||
|
||||
.target-line::before,
|
||||
.target-line::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -6px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 8px solid #ff4d4f;
|
||||
border-top: 6px solid transparent;
|
||||
border-bottom: 6px solid transparent;
|
||||
}
|
||||
|
||||
.target-line::before {
|
||||
top: -6px;
|
||||
}
|
||||
|
||||
.target-line::after {
|
||||
bottom: -6px;
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.progress-value {
|
||||
font-size: 18px;
|
||||
color: #f08a31;
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.goal-text {
|
||||
font-size: 18px;
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
.sales-label.goal {
|
||||
color: #ff4d4f;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.sales-value.goal {
|
||||
color: #ff4d4f;
|
||||
text-shadow: 0 0 20px rgba(255, 77, 79, 0.5);
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.sales-unit.goal {
|
||||
color: #ff4d4f;
|
||||
|
||||
}
|
||||
|
||||
.goal-bar {
|
||||
background: linear-gradient(90deg, #ff4d4f, #ff7875);
|
||||
}
|
||||
|
||||
/* 内容区域 */
|
||||
.content-section {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 22px;
|
||||
color: #f08a31;
|
||||
margin-bottom: 20px;
|
||||
padding-left: 20px;
|
||||
border-left: 4px solid #f08a31;
|
||||
}
|
||||
|
||||
.rank-container {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border-radius: 15px;
|
||||
padding: 25px;
|
||||
border: 1px solid rgba(240, 138, 49, 0.15);
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
/* 排名列表 */
|
||||
.rank-list {
|
||||
width: 33.33%;
|
||||
min-width: 300px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.rank-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 15px 20px;
|
||||
margin-bottom: 12px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.rank-item:hover {
|
||||
background: rgba(240, 138, 49, 0.15);
|
||||
border-color: rgba(240, 138, 49, 0.4);
|
||||
transform: translateX(5px);
|
||||
}
|
||||
|
||||
.rank-item.top-1 {
|
||||
background: linear-gradient(135deg, rgba(255, 215, 0, 0.2), rgba(255, 215, 0, 0.05));
|
||||
border-color: rgba(255, 215, 0, 0.5);
|
||||
}
|
||||
|
||||
.rank-item.top-2 {
|
||||
background: linear-gradient(135deg, rgba(192, 192, 192, 0.2), rgba(192, 192, 192, 0.05));
|
||||
border-color: rgba(192, 192, 192, 0.5);
|
||||
}
|
||||
|
||||
.rank-item.top-3 {
|
||||
background: linear-gradient(135deg, rgba(205, 127, 50, 0.2), rgba(205, 127, 50, 0.05));
|
||||
border-color: rgba(205, 127, 50, 0.5);
|
||||
}
|
||||
|
||||
.rank-number {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
border-radius: 50%;
|
||||
margin-right: 15px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.rank-item.top-1 .rank-number {
|
||||
background: linear-gradient(135deg, #ffd700, #ffed4e);
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.rank-item.top-2 .rank-number {
|
||||
background: linear-gradient(135deg, #c0c0c0, #e8e8e8);
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.rank-item.top-3 .rank-number {
|
||||
background: linear-gradient(135deg, #cd7f32, #daa520);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.rank-item:not(.top-1):not(.top-2):not(.top-3) .rank-number {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.rank-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.rank-name {
|
||||
font-size: 18px;
|
||||
color: #fff;
|
||||
margin-bottom: 5px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.rank-sales {
|
||||
font-size: 14px;
|
||||
color: #f08a31;
|
||||
}
|
||||
|
||||
.rank-rate {
|
||||
font-size: 14px;
|
||||
color: #4ade80;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
/* 图表区域 */
|
||||
.chart-container {
|
||||
flex: 1;
|
||||
min-height: 300px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* 响应式 */
|
||||
@media (max-width: 1400px) {
|
||||
.sales-value {
|
||||
font-size: 42px;
|
||||
}
|
||||
.progress-bar-container {
|
||||
width: 500px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.rank-container {
|
||||
flex-direction: column;
|
||||
}
|
||||
.rank-list,
|
||||
.chart-container {
|
||||
width: 100%;
|
||||
}
|
||||
.sales-section,
|
||||
.goal-section {
|
||||
align-items: center;
|
||||
}
|
||||
.sales-info {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
.progress-bar-container {
|
||||
width: 100%;
|
||||
}
|
||||
.sales-progress-wrapper {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<!-- 年度标题 -->
|
||||
<div class="year-title">数据看板</div>
|
||||
|
||||
<!-- 顶部销售额区域 -->
|
||||
<div class="sales-header">
|
||||
<!-- 总销售额区域:进度条在上,金额在下,居左 -->
|
||||
<div class="sales-section">
|
||||
<div class="sales-progress-wrapper">
|
||||
<div class="progress-bar-container">
|
||||
<div class="progress-bar"></div>
|
||||
</div>
|
||||
<div class="progress-label">77.05%</div>
|
||||
</div>
|
||||
<div class="sales-info">
|
||||
<span class="sales-label">总销售额</span>
|
||||
<span class="sales-value">231,154,000,000</span>
|
||||
<span class="sales-unit">元</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 年度目标区域:金额居中,进度条在下 -->
|
||||
<div class="goal-section">
|
||||
<div class="sales-info">
|
||||
<span class="sales-label goal">年度目标</span>
|
||||
<span class="sales-value goal">300,000,000,000</span>
|
||||
<span class="sales-unit goal">元</span>
|
||||
</div>
|
||||
<div class="progress-bar-container full-width">
|
||||
<div class="progress-bar goal-bar" style="width: 100%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 小组排名区域 -->
|
||||
<div class="content-section">
|
||||
<div class="section-title">小组排名</div>
|
||||
<div class="rank-container">
|
||||
<div class="rank-list" id="groupRankList"></div>
|
||||
<div class="chart-container" id="groupChart"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 个人排名区域 -->
|
||||
<div class="content-section">
|
||||
<div class="section-title">个人排名</div>
|
||||
<div class="rank-container">
|
||||
<div class="rank-list" id="personRankList"></div>
|
||||
<div class="chart-container" id="personChart"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 模拟数据 - 小组排名
|
||||
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 }
|
||||
];
|
||||
|
||||
// 格式化数字
|
||||
function formatNumber(num) {
|
||||
return (num / 100000000).toFixed(1) + '亿';
|
||||
}
|
||||
|
||||
// 渲染排名列表
|
||||
function renderRankList(containerId, data, type) {
|
||||
const container = document.getElementById(containerId);
|
||||
container.innerHTML = data.map((item, index) => {
|
||||
const rate = ((item.actual / item.target) * 100).toFixed(1);
|
||||
const rankClass = index < 3 ? `top-${index + 1}` : '';
|
||||
return `
|
||||
<div class="rank-item ${rankClass}">
|
||||
<div class="rank-number">${index + 1}</div>
|
||||
<div class="rank-info">
|
||||
<div class="rank-name">${item.name}</div>
|
||||
<div class="rank-sales">销售额: ${formatNumber(item.actual)} | 目标: ${formatNumber(item.target)}
|
||||
<span class="rank-rate">完成率: ${rate}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// 渲染图表
|
||||
function renderChart(containerId, data, type) {
|
||||
const chartDom = document.getElementById(containerId);
|
||||
const chart = echarts.init(chartDom);
|
||||
const names = data.map(item => item.name);
|
||||
const targets = data.map(item => item.target / 100000000);
|
||||
const actuals = data.map(item => item.actual / 100000000);
|
||||
|
||||
const option = {
|
||||
backgroundColor: 'transparent',
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow'
|
||||
},
|
||||
formatter: function(params) {
|
||||
let result = params[0].name + '<br/>';
|
||||
params.forEach(item => {
|
||||
result += `${item.marker}${item.seriesName}: ${item.value}亿<br/>`;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
data: ['目标销售额', '实际销售额'],
|
||||
textStyle: {
|
||||
color: '#fff'
|
||||
},
|
||||
top: 10
|
||||
},
|
||||
grid: {
|
||||
left: '10%',
|
||||
right: '5%',
|
||||
bottom: '3%',
|
||||
top: '15%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'value',
|
||||
name: '销售额(亿)',
|
||||
nameTextStyle: {
|
||||
color: '#fff'
|
||||
},
|
||||
axisLabel: {
|
||||
color: '#fff',
|
||||
fontSize: 12
|
||||
},
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: 'rgba(255, 255, 255, 0.2)'
|
||||
}
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
color: 'rgba(255, 255, 255, 0.1)'
|
||||
}
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'category',
|
||||
data: names,
|
||||
axisLabel: {
|
||||
color: '#fff',
|
||||
fontSize: 14
|
||||
},
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: 'rgba(255, 255, 255, 0.2)'
|
||||
}
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '目标销售额',
|
||||
type: 'bar',
|
||||
data: targets,
|
||||
itemStyle: {
|
||||
color: 'rgba(255, 77, 79, 0.6)',
|
||||
borderRadius: [0, 0, 0, 0]
|
||||
},
|
||||
barMaxWidth: 30
|
||||
},
|
||||
{
|
||||
name: '实际销售额',
|
||||
type: 'bar',
|
||||
data: actuals,
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
|
||||
{ offset: 0, color: '#f08a31' },
|
||||
{ offset: 1, color: '#ffb347' }
|
||||
]),
|
||||
borderRadius: [0, 0, 0, 0]
|
||||
},
|
||||
barMaxWidth: 30
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
chart.setOption(option);
|
||||
chart.resize();
|
||||
|
||||
// 响应式
|
||||
window.addEventListener('resize', () => {
|
||||
chart.resize();
|
||||
});
|
||||
}
|
||||
|
||||
// 初始化
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
renderRankList('groupRankList', groupData, 'group');
|
||||
renderRankList('personRankList', personData, 'person');
|
||||
|
||||
// 确保DOM完全渲染后再初始化图表
|
||||
setTimeout(() => {
|
||||
// 确保图表容器高度与排名列表一致
|
||||
function syncChartHeight() {
|
||||
const groupRankList = document.getElementById('groupRankList');
|
||||
const groupChart = document.getElementById('groupChart');
|
||||
const personRankList = document.getElementById('personRankList');
|
||||
const personChart = document.getElementById('personChart');
|
||||
|
||||
if (groupRankList && groupChart) {
|
||||
groupChart.style.height = groupRankList.offsetHeight + 'px';
|
||||
}
|
||||
if (personRankList && personChart) {
|
||||
personChart.style.height = personRankList.offsetHeight + 'px';
|
||||
}
|
||||
}
|
||||
|
||||
syncChartHeight();
|
||||
renderChart('groupChart', groupData, 'group');
|
||||
renderChart('personChart', personData, 'person');
|
||||
|
||||
// 响应式调整
|
||||
window.addEventListener('resize', () => {
|
||||
syncChartHeight();
|
||||
// 重新调整图表大小
|
||||
const groupChart = echarts.getInstanceByDom(document.getElementById('groupChart'));
|
||||
const personChart = echarts.getInstanceByDom(document.getElementById('personChart'));
|
||||
if (groupChart) groupChart.resize();
|
||||
if (personChart) personChart.resize();
|
||||
});
|
||||
}, 200);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,656 @@
|
||||
// 常量定义
|
||||
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 `
|
||||
<div class="ranking-item ${rankClass}">
|
||||
<div class="rank-number">${index + 1}</div>
|
||||
<div class="rank-name">${item.name}</div>
|
||||
<div class="rank-sales">销售额: ${formatNumber(item.actual)} | 目标: ${formatNumber(item.target)}</div>
|
||||
<div class="rank-rate">完成率: ${rate}%</div>
|
||||
</div>
|
||||
`;
|
||||
}).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 `
|
||||
<div class="ranking-item ${rankClass}">
|
||||
<div class="rank-number">${index + 1}</div>
|
||||
<div class="rank-name">${item.name}</div>
|
||||
<div class="rank-sales">销售额: ${formatNumber(item.actual)} | 目标: ${formatNumber(item.target)}</div>
|
||||
<div class="rank-rate">完成率: ${rate}%</div>
|
||||
</div>
|
||||
`;
|
||||
}).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 + '<br/>';
|
||||
params.forEach(param => {
|
||||
result += param.marker + param.seriesName + ': ' + param.value + '万<br/>';
|
||||
});
|
||||
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 + '<br/>';
|
||||
params.forEach(param => {
|
||||
result += param.marker + param.seriesName + ': ' + param.value + '万<br/>';
|
||||
});
|
||||
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);
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
@@ -0,0 +1,149 @@
|
||||
# 数据大屏 React 项目
|
||||
|
||||
这是使用 React 18 + Vite 重构的数据大屏项目,功能与原HTML版本完全一致。
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
react-app/
|
||||
├── src/
|
||||
│ ├── components/
|
||||
│ │ ├── TopData.jsx # 顶部数据区域(年度目标、销售额、进度条、季度进度)
|
||||
│ │ ├── GroupRanking.jsx # 小组排名组件
|
||||
│ │ └── PersonRanking.jsx # 个人排名组件
|
||||
│ ├── App.jsx # 主应用组件
|
||||
│ ├── main.jsx # 入口文件
|
||||
│ └── style.css # 全局样式
|
||||
├── public/
|
||||
│ └── img/ # 图片资源
|
||||
├── index.html # HTML模板
|
||||
├── package.json # 项目配置
|
||||
└── vite.config.js # Vite配置
|
||||
```
|
||||
|
||||
## 安装步骤
|
||||
|
||||
### 1. 安装依赖
|
||||
|
||||
```bash
|
||||
cd react-app
|
||||
npm install
|
||||
```
|
||||
|
||||
### 2. 启动开发服务器
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
浏览器会自动打开 `http://localhost:3001`
|
||||
|
||||
### 3. 构建生产版本
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
构建产物在 `react-app/dist/` 目录
|
||||
|
||||
## 功能特性
|
||||
|
||||
✅ **响应式设计**
|
||||
- PC端自动等比例缩放
|
||||
- 移动端自适应布局
|
||||
- 支持4K屏幕、普通PC、移动端
|
||||
|
||||
✅ **数据展示**
|
||||
- 年度目标和销售额动态加载
|
||||
- 进度条动画效果
|
||||
- 季度周数进度显示
|
||||
|
||||
✅ **图表功能**
|
||||
- 小组排名柱状图
|
||||
- 个人排名柱状图(移动端横向布局)
|
||||
- ECharts响应式图表
|
||||
|
||||
✅ **性能优化**
|
||||
- React Hooks (useState, useEffect, useRef)
|
||||
- 组件化开发
|
||||
- 防抖处理窗口resize
|
||||
- 图表自动销毁重建
|
||||
|
||||
## 技术栈
|
||||
|
||||
- React 18.2+ (Hooks)
|
||||
- Vite 5.0+
|
||||
- ECharts 5.4+
|
||||
- JavaScript ES6+
|
||||
|
||||
## 对接真实API
|
||||
|
||||
在 `src/components/TopData.jsx` 的 `fetchYearData` 函数中修改:
|
||||
|
||||
```javascript
|
||||
const fetchYearData = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/year-data')
|
||||
const data = await response.json()
|
||||
return data
|
||||
} catch (error) {
|
||||
console.error('获取数据失败:', error)
|
||||
return { target: 0, actual: 0 }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 数据格式
|
||||
|
||||
### API返回格式
|
||||
|
||||
```json
|
||||
{
|
||||
"target": 3500,
|
||||
"actual": 2750
|
||||
}
|
||||
```
|
||||
|
||||
### 排名数据格式
|
||||
|
||||
在 `GroupRanking.jsx` 和 `PersonRanking.jsx` 中修改数据:
|
||||
|
||||
```javascript
|
||||
const groupData = [
|
||||
{ name: '名称', target: 目标值, actual: 实际值 }
|
||||
]
|
||||
```
|
||||
|
||||
## React Hooks 使用
|
||||
|
||||
- **useState**: 组件状态管理
|
||||
- **useEffect**: 副作用处理(图表初始化、事件监听)
|
||||
- **useRef**: DOM引用和图表实例存储
|
||||
- **useCallback**: 性能优化(可选)
|
||||
|
||||
## 浏览器支持
|
||||
|
||||
- Chrome (推荐)
|
||||
- Firefox
|
||||
- Safari
|
||||
- Edge
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 确保 `public/img/` 目录包含所有图片资源
|
||||
2. PC端缩放范围为 0.6-1.0(缩小不放大)
|
||||
3. 移动端断点为 768px
|
||||
4. 图表使用防抖处理resize事件(300ms延迟)
|
||||
5. React组件使用函数式组件 + Hooks
|
||||
|
||||
## 与Vue版本对比
|
||||
|
||||
| 特性 | React版本 | Vue版本 |
|
||||
|------|-----------|---------|
|
||||
| 状态管理 | useState | ref/reactive |
|
||||
| 副作用 | useEffect | onMounted/onUnmounted |
|
||||
| DOM引用 | useRef | ref |
|
||||
| 组件定义 | 函数式组件 | setup script |
|
||||
| 样式处理 | CSS Modules/全局CSS | scoped CSS |
|
||||
| 学习曲线 | 中等 | 较低 |
|
||||
| 生态成熟度 | 非常成熟 | 快速发展 |
|
||||
@@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>数据大屏</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "screen-dashboard-react",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"echarts": "^5.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-react": "^4.2.0",
|
||||
"vite": "^5.0.0"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 113 KiB |
|
After Width: | Height: | Size: 226 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
@@ -0,0 +1,102 @@
|
||||
import { useEffect, useRef } from 'react'
|
||||
import TopData from './components/TopData'
|
||||
import GroupRanking from './components/GroupRanking'
|
||||
import PersonRanking from './components/PersonRanking'
|
||||
import headerImage from '/img/heard.png'
|
||||
import footerImage from '/img/foot.png'
|
||||
|
||||
function App() {
|
||||
const containerRef = useRef(null)
|
||||
const MOBILE_BREAKPOINT = 768
|
||||
const DESIGN_WIDTH = 1920
|
||||
|
||||
// PC端等比例缩放函数
|
||||
const 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
|
||||
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 handleProgressRate = (rate) => {
|
||||
console.log('进度率:', rate)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setScale()
|
||||
|
||||
let resizeTimer = null
|
||||
const handleResize = () => {
|
||||
clearTimeout(resizeTimer)
|
||||
resizeTimer = setTimeout(setScale, 100)
|
||||
}
|
||||
|
||||
window.addEventListener('resize', handleResize)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize)
|
||||
clearTimeout(resizeTimer)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="container" ref={containerRef}>
|
||||
{/* 头部图片 */}
|
||||
<div className="header-image">
|
||||
<img src={headerImage} alt="头部装饰图片" />
|
||||
</div>
|
||||
|
||||
<div className="cont">
|
||||
{/* 顶部数据区域 */}
|
||||
<TopData onProgressRate={handleProgressRate} />
|
||||
|
||||
{/* 小组排名区域 */}
|
||||
<h2 className="section-title">小组排名</h2>
|
||||
<GroupRanking />
|
||||
|
||||
{/* 个人排名区域 */}
|
||||
<h2 className="section-title">个人排名</h2>
|
||||
<PersonRanking />
|
||||
|
||||
{/* 尾部图片 */}
|
||||
<div className="footer-image">
|
||||
<img src={footerImage} alt="尾部装饰图片" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
@@ -0,0 +1,257 @@
|
||||
import { useEffect, useRef } from 'react'
|
||||
import * as echarts from 'echarts'
|
||||
|
||||
function GroupRanking() {
|
||||
const chartRef = useRef(null)
|
||||
const chartInstance = useRef(null)
|
||||
|
||||
const MOBILE_BREAKPOINT = 768
|
||||
|
||||
// 小组数据
|
||||
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 formatNumber = (num) => {
|
||||
return (num / 10000).toFixed(0) + '万'
|
||||
}
|
||||
|
||||
// 渲染图表
|
||||
const renderChart = () => {
|
||||
if (!chartRef.current) return
|
||||
|
||||
// 销毁旧图表
|
||||
if (chartInstance.current) {
|
||||
chartInstance.current.dispose()
|
||||
}
|
||||
|
||||
chartInstance.current = echarts.init(chartRef.current)
|
||||
|
||||
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 + '<br/>'
|
||||
params.forEach(param => {
|
||||
result += param.marker + param.seriesName + ': ' + param.value + '万<br/>'
|
||||
})
|
||||
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)'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
chartInstance.current.setOption(option)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
renderChart()
|
||||
}, 500)
|
||||
|
||||
let resizeTimer = null
|
||||
const handleResize = () => {
|
||||
clearTimeout(resizeTimer)
|
||||
resizeTimer = setTimeout(() => {
|
||||
renderChart()
|
||||
}, 300)
|
||||
}
|
||||
|
||||
window.addEventListener('resize', handleResize)
|
||||
|
||||
return () => {
|
||||
clearTimeout(timer)
|
||||
window.removeEventListener('resize', handleResize)
|
||||
clearTimeout(resizeTimer)
|
||||
if (chartInstance.current) {
|
||||
chartInstance.current.dispose()
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="group-ranking">
|
||||
<div className="ranking-content">
|
||||
{/* 排名列表 */}
|
||||
<div className="ranking-list">
|
||||
{groupData.map((item, index) => {
|
||||
const rate = ((item.actual / item.target) * 100).toFixed(1)
|
||||
const rankClass = index < 3 ? `top-${index + 1}` : 'other'
|
||||
|
||||
return (
|
||||
<div key={index} className={`ranking-item ${rankClass}`}>
|
||||
<div className="rank-number">{index + 1}</div>
|
||||
<div className="rank-name">{item.name}</div>
|
||||
<div className="rank-sales">销售额: {formatNumber(item.actual)} | 目标: {formatNumber(item.target)}</div>
|
||||
<div className="rank-rate">完成率: {rate}%</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
{/* 图表容器 */}
|
||||
<div ref={chartRef} className="chart-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default GroupRanking
|
||||
@@ -0,0 +1,256 @@
|
||||
import { useEffect, useRef } from 'react'
|
||||
import * as echarts from 'echarts'
|
||||
|
||||
function PersonRanking() {
|
||||
const chartRef = useRef(null)
|
||||
const chartInstance = useRef(null)
|
||||
|
||||
const MOBILE_BREAKPOINT = 768
|
||||
|
||||
// 个人数据
|
||||
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 }
|
||||
]
|
||||
|
||||
// 格式化数字(万元)
|
||||
const formatNumber = (num) => {
|
||||
return (num / 10000).toFixed(0) + '万'
|
||||
}
|
||||
|
||||
// 渲染图表
|
||||
const renderChart = () => {
|
||||
if (!chartRef.current) return
|
||||
|
||||
// 销毁旧图表
|
||||
if (chartInstance.current) {
|
||||
chartInstance.current.dispose()
|
||||
}
|
||||
|
||||
chartInstance.current = echarts.init(chartRef.current)
|
||||
|
||||
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 + '<br/>'
|
||||
params.forEach(param => {
|
||||
result += param.marker + param.seriesName + ': ' + param.value + '万<br/>'
|
||||
})
|
||||
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)'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
chartInstance.current.setOption(option)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
renderChart()
|
||||
}, 500)
|
||||
|
||||
let resizeTimer = null
|
||||
const handleResize = () => {
|
||||
clearTimeout(resizeTimer)
|
||||
resizeTimer = setTimeout(() => {
|
||||
renderChart()
|
||||
}, 300)
|
||||
}
|
||||
|
||||
window.addEventListener('resize', handleResize)
|
||||
|
||||
return () => {
|
||||
clearTimeout(timer)
|
||||
window.removeEventListener('resize', handleResize)
|
||||
clearTimeout(resizeTimer)
|
||||
if (chartInstance.current) {
|
||||
chartInstance.current.dispose()
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="person-ranking">
|
||||
<div className="ranking-content">
|
||||
{/* 排名列表 */}
|
||||
<div className="ranking-list">
|
||||
{personData.map((item, index) => {
|
||||
const rate = ((item.actual / item.target) * 100).toFixed(1)
|
||||
const rankClass = index < 3 ? `top-${index + 1}` : 'other'
|
||||
|
||||
return (
|
||||
<div key={index} className={`ranking-item ${rankClass}`}>
|
||||
<div className="rank-number">{index + 1}</div>
|
||||
<div className="rank-name">{item.name}</div>
|
||||
<div className="rank-sales">销售额: {formatNumber(item.actual)} | 目标: {formatNumber(item.target)}</div>
|
||||
<div className="rank-rate">完成率: {rate}%</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
{/* 图表容器 */}
|
||||
<div ref={chartRef} className="chart-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PersonRanking
|
||||
@@ -0,0 +1,151 @@
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
|
||||
function TopData({ onProgressRate }) {
|
||||
const [yearData, setYearData] = useState({ target: 3500, actual: 2750 })
|
||||
const [progressWidth, setProgressWidth] = useState('0%')
|
||||
const [progressRate, setProgressRate] = useState('0.00')
|
||||
|
||||
const MOBILE_BREAKPOINT = 768
|
||||
|
||||
// 获取当前周数
|
||||
const getCurrentWeek = () => {
|
||||
const now = new Date()
|
||||
const start = new Date(now.getFullYear(), 0, 1)
|
||||
const diff = now - start
|
||||
return Math.floor(diff / (7 * 24 * 60 * 60 * 1000)) + 1
|
||||
}
|
||||
|
||||
const [currentWeek, setCurrentWeek] = useState(getCurrentWeek())
|
||||
|
||||
// 季度数据
|
||||
const [quarters] = useState([
|
||||
{ id: 'q1', label: 'Q1', start: 1, end: 13, weeks: Array.from({ length: 13 }, (_, i) => i + 1) },
|
||||
{ id: 'q2', label: 'Q2', start: 14, end: 26, weeks: Array.from({ length: 13 }, (_, i) => i + 14) },
|
||||
{ id: 'q3', label: 'Q3', start: 27, end: 39, weeks: Array.from({ length: 13 }, (_, i) => i + 27) },
|
||||
{ id: 'q4', label: 'Q4', start: 40, end: 52, weeks: Array.from({ length: 13 }, (_, i) => i + 40) }
|
||||
])
|
||||
|
||||
// 获取年度数据
|
||||
const fetchYearData = async () => {
|
||||
console.log('正在获取年度数据...')
|
||||
// TODO: 替换为实际的API调用
|
||||
return {
|
||||
target: 3500,
|
||||
actual: 2750
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化大数字
|
||||
const formatLargeNumber = (num) => {
|
||||
return num.toLocaleString('zh-CN') + ' 万元'
|
||||
}
|
||||
|
||||
// 启动进度条动画
|
||||
const startProgressAnimation = (targetRate) => {
|
||||
const PROGRESS_STEP = 0.4
|
||||
const PROGRESS_INTERVAL = 20
|
||||
let value = 0
|
||||
let lastTime = 0
|
||||
|
||||
const animateProgress = (currentTime) => {
|
||||
if (!lastTime) lastTime = currentTime
|
||||
const deltaTime = currentTime - lastTime
|
||||
|
||||
if (deltaTime >= PROGRESS_INTERVAL) {
|
||||
value += PROGRESS_STEP
|
||||
if (value >= targetRate) {
|
||||
value = targetRate
|
||||
setProgressWidth(value + '%')
|
||||
return
|
||||
}
|
||||
setProgressWidth(value + '%')
|
||||
lastTime = currentTime
|
||||
}
|
||||
requestAnimationFrame(animateProgress)
|
||||
}
|
||||
|
||||
requestAnimationFrame(animateProgress)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const initData = async () => {
|
||||
const data = await fetchYearData()
|
||||
setYearData(data)
|
||||
|
||||
const rate = ((data.actual / data.target) * 100).toFixed(2)
|
||||
setProgressRate(rate)
|
||||
|
||||
if (onProgressRate) {
|
||||
onProgressRate(parseFloat(rate))
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
startProgressAnimation(parseFloat(rate))
|
||||
}, 800)
|
||||
}
|
||||
|
||||
initData()
|
||||
}, [onProgressRate])
|
||||
|
||||
return (
|
||||
<div className="top-data">
|
||||
{/* 销售信息 */}
|
||||
<div className="sales-info">
|
||||
<div className="sales-info-top">
|
||||
<div className="target-info">
|
||||
<h3>年度目标</h3>
|
||||
<div className="target-amount">{formatLargeNumber(yearData.target)}</div>
|
||||
</div>
|
||||
<div className="total-info">
|
||||
<h3>年度总销售额</h3>
|
||||
<div className="total-amount">{formatLargeNumber(yearData.actual)}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="sales-info-cont">
|
||||
<div className="sales-info-bottom">
|
||||
<div className="progress-container">
|
||||
<div className="progress-bg">
|
||||
<div className="progress-bar" style={{ width: progressWidth }}>
|
||||
<div className="progress-info">
|
||||
<div className="play-btn"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="progress-text">{progressRate}%</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="progress-label">完成率</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 年度进度 */}
|
||||
<div className="countdown">
|
||||
<h3>年度进度</h3>
|
||||
<div className="quarters-container">
|
||||
{quarters.map((quarter, index) => (
|
||||
<div
|
||||
key={quarter.id}
|
||||
className="quarter-item"
|
||||
style={{ borderBottom: index === quarters.length - 1 ? 'none' : '1px solid #523A14' }}
|
||||
>
|
||||
<div className="quarter-label">{quarter.label}</div>
|
||||
<div className="weeks-container">
|
||||
{quarter.weeks.map((week) => (
|
||||
<div
|
||||
key={week}
|
||||
className={`week-circle ${week === currentWeek ? 'current' : ''} ${week < currentWeek ? 'passed' : ''}`}
|
||||
title={`第${week}周`}
|
||||
>
|
||||
{week}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TopData
|
||||
@@ -0,0 +1,10 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App'
|
||||
import './style.css'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
)
|
||||
@@ -0,0 +1,620 @@
|
||||
/* PC端等比例缩放,根字号固定 */
|
||||
html {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* 移动端基础字号 */
|
||||
@media screen and (max-width: 768px) {
|
||||
html {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Microsoft YaHei', Arial, sans-serif;
|
||||
color: #fff;
|
||||
min-width: 320px;
|
||||
overflow-x: hidden;
|
||||
/* PC端缩放相关样式 */
|
||||
transform-origin: left top;
|
||||
transition: transform 0.1s ease-out;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
background: #000;
|
||||
/* 固定最小宽度,避免缩放时内容挤压 */
|
||||
min-width: 1200px;
|
||||
}
|
||||
|
||||
.cont{
|
||||
width: 90%;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 0 1rem;
|
||||
/* 确保内容区域最小宽度 */
|
||||
min-width: 1080px;
|
||||
}
|
||||
|
||||
/* 移动端下调整宽度 */
|
||||
@media screen and (max-width: 768px) {
|
||||
body {
|
||||
transform: none !important;
|
||||
width: 100% !important;
|
||||
min-width: 320px !important;
|
||||
height: auto !important;
|
||||
overflow-x: hidden !important;
|
||||
}
|
||||
|
||||
.container {
|
||||
min-width: 320px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.cont {
|
||||
width: 95%;
|
||||
max-width: 100%;
|
||||
min-width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* 顶部数据区域 */
|
||||
.top-data {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1fr;
|
||||
gap: clamp(6px, 1.5vw, 14px);
|
||||
margin-bottom: clamp(10px, 1.5vw, 20px);
|
||||
}
|
||||
|
||||
.sales-info {
|
||||
background: url(/img/hicon.png);
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100% 100%;
|
||||
border-radius: clamp(4px, 0.5vw, 8px);
|
||||
padding: clamp(10px, 1.5vw, 20px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: clamp(10px, 1.5vw, 15px);
|
||||
}
|
||||
|
||||
.sales-info-top {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.target-info, .total-info {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.target-info h3,.total-info h3{
|
||||
font-size: clamp(0.75rem, 1vw, 1rem);
|
||||
color: #fff;
|
||||
margin-bottom: clamp(5px, 0.8vw, 8px);
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.target-amount {
|
||||
font-size: clamp(1.25rem, 2.5vw, 2.25rem);
|
||||
font-weight: bold;
|
||||
color: #F77436;
|
||||
margin-bottom: clamp(5px, 0.8vw, 10px);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.total-amount {
|
||||
font-size: clamp(1.25rem, 2.5vw, 2.25rem);
|
||||
font-weight: bold;
|
||||
color: #FA551F;
|
||||
margin-bottom: clamp(5px, 0.8vw, 10px);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.sales-info-cont{
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.sales-info-bottom {
|
||||
width: 90%;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
width: 100%;
|
||||
height: clamp(20px, 2.5vw, 30px);
|
||||
padding: 3px;
|
||||
border-radius: 20px;
|
||||
background: linear-gradient(180deg, #FF923C, #FF923C);
|
||||
box-shadow: 0px 0px 8px rgb(255 255 255 / 80%);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.progress-bg {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #DC7439;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 0%;
|
||||
border-radius: 16px;
|
||||
background: repeating-linear-gradient(45deg,rgba(255, 255, 255, 0.25) 0,rgba(255, 255, 255, 0.25) 8px,rgba(255, 255, 255, 0.12) 8px,rgba(255, 255, 255, 0.12) 16px),linear-gradient(180deg, #FFD85E, #FFBE25);
|
||||
animation: move 1.2s linear infinite;
|
||||
box-shadow: inset 0 0 6px rgba(255, 255, 255, 0.6);
|
||||
will-change: width;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.progress-info {
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: #fff;
|
||||
font-size: clamp(10px, 1.2vw, 13px);
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.play-btn {
|
||||
width: clamp(18px, 2vw, 24px);
|
||||
height: clamp(18px, 2vw, 24px);
|
||||
border-radius: 50%;
|
||||
background: #DC7439;
|
||||
border: 2px solid #FFBE25;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.play-btn::before {
|
||||
content: '';
|
||||
margin-left: 2px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-top: 5px solid transparent;
|
||||
border-bottom: 5px solid transparent;
|
||||
border-left: 7px solid #fff;
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
font-size: clamp(10px, 1.2vw, 13px);
|
||||
white-space: nowrap;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.progress-label {
|
||||
color: #DC7439;
|
||||
font-weight: bold;
|
||||
font-size: clamp(10px, 1.2vw, 13px);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 条纹滚动动画 */
|
||||
@keyframes move {
|
||||
from { background-position: 0 0; }
|
||||
to { background-position: 40px 0; }
|
||||
}
|
||||
|
||||
.completion-rate {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.countdown {
|
||||
border: 1px solid #523A14;
|
||||
border-radius: 8px;
|
||||
padding: clamp(8px, 1.5vw,10px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.countdown h3 {
|
||||
font-size: clamp(0.875rem, 1.25vw, 1.25rem);
|
||||
color: #e7552f;
|
||||
margin-bottom: clamp(10px, 1.5vw, 15px);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* 季度容器 */
|
||||
.quarters-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: clamp(5px, 1.5vw, 5px);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 单个季度 */
|
||||
.quarter-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: clamp(5px, 1vw, 8px);
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
border-bottom: 1px solid #523A14;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
.last-quarter{
|
||||
border-bottom: none;
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
|
||||
.quarter-label {
|
||||
background: #e7552f;
|
||||
color: #fff;
|
||||
font-size: clamp(0.875rem, 1.2vw, 1rem);
|
||||
font-weight: bold;
|
||||
padding: clamp(3px, 0.5vw, 5px) clamp(8px, 1vw, 12px);
|
||||
border-radius: 4px;
|
||||
min-width: clamp(35px, 5vw, 45px);
|
||||
text-align: center;
|
||||
box-shadow: 0 0 10px rgba(255, 102, 0, 0.5);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 周数圆圈容器 */
|
||||
.weeks-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: clamp(3px, 0.5vw, 5px);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* 单个周数圆圈 */
|
||||
.week-circle {
|
||||
width: clamp(18px, 2.5vw, 24px);
|
||||
height: clamp(18px, 2.5vw, 24px);
|
||||
border-radius: 50%;
|
||||
border: 2px solid #815B37;
|
||||
background: rgba(129, 91, 55, 0.2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: clamp(0.625rem, 1vw, 0.75rem);
|
||||
color: #f5bbab;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.week-circle:hover {
|
||||
background: rgba(231, 85, 47, 0.4);
|
||||
border-color: #e7552f;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
/* 当前周高亮 */
|
||||
.week-circle.current {
|
||||
background: #e7552f;
|
||||
border-color: #ff6600;
|
||||
box-shadow: 0 0 10px rgba(255, 102, 0, 0.8);
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
/* 已过去的周 */
|
||||
.week-circle.passed {
|
||||
background: #e7552f;
|
||||
border-color: #ff6600;
|
||||
box-shadow: 0 0 10px rgba(255, 102, 0, 0.8);
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
box-shadow: 0 0 10px rgba(255, 102, 0, 0.8);
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 0 20px rgba(255, 102, 0, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/* 小组排名区域 */
|
||||
.group-ranking {
|
||||
border: 1px solid #523A14;
|
||||
border-radius: 8px;
|
||||
padding: clamp(10px, 1.5vw, 20px);
|
||||
margin-bottom: clamp(10px, 1.5vw, 20px);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: clamp(0.875rem, 1.25vw, 1rem);
|
||||
color: #fff;
|
||||
background: #ff6600;
|
||||
padding: clamp(5px, 0.8vw, 5px) clamp(10px, 1.5vw, 15px);
|
||||
display: inline-block;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.ranking-content {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: clamp(10px, 1.5vw, 20px);
|
||||
}
|
||||
|
||||
.ranking-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: clamp(8px, 1vw, 10px);
|
||||
}
|
||||
|
||||
.ranking-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: clamp(8px, 1.2vw, 12px) clamp(10px, 1.5vw, 15px);
|
||||
border-radius: 4px;
|
||||
font-size: clamp(0.75rem, 1vw, 0.875rem);
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.ranking-item.top-1 {
|
||||
background: linear-gradient(90deg, rgba(199, 173, 61, 0.4), rgba(199, 173, 61, 0.1));
|
||||
border: 1px solid rgba(199, 173, 61, 0.4);
|
||||
}
|
||||
|
||||
.ranking-item.top-2 {
|
||||
background: linear-gradient(90deg, rgba(136, 136, 135, 0.5), rgba(136, 136, 135, 0.1));
|
||||
border: 1px solid rgba(136, 136, 135, 0.4);
|
||||
}
|
||||
|
||||
.ranking-item.top-3 {
|
||||
background: linear-gradient(90deg, rgba(59, 96, 93, 0.6), rgba(59, 96, 93,0.1));
|
||||
border: 1px solid rgba(59, 96, 93, 0.4);
|
||||
}
|
||||
|
||||
.ranking-item.other {
|
||||
background: linear-gradient(90deg, rgba(100, 100, 100, 0.3), rgba(100, 100, 100, 0.1));
|
||||
border: 1px solid rgba(100, 100, 100, 0.4);
|
||||
}
|
||||
|
||||
.rank-number {
|
||||
font-size: clamp(0.875rem, 1.25vw, 1.25rem);
|
||||
font-weight: bold;
|
||||
margin-right: clamp(10px, 1.5vw, 15px);
|
||||
min-width: 20px;
|
||||
width: clamp(20px, 3vw, 25px);
|
||||
height: clamp(20px, 3vw, 25px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.ranking-item.top-1 .rank-number {
|
||||
background: #C7AD3D;
|
||||
color: #000;
|
||||
box-shadow: 0 0 8px rgba(255, 204, 0, 0.8);
|
||||
}
|
||||
|
||||
.ranking-item.top-2 .rank-number {
|
||||
background: #888887;
|
||||
color: #000;
|
||||
box-shadow: 0 0 8px rgba(192, 192, 192, 0.8);
|
||||
}
|
||||
|
||||
.ranking-item.top-3 .rank-number {
|
||||
background: #3B605D;
|
||||
color: #fff;
|
||||
box-shadow: 0 0 8px rgba(205, 127, 50, 0.8);
|
||||
}
|
||||
|
||||
.ranking-item.other .rank-number {
|
||||
background: #815B37;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.rank-name {
|
||||
font-weight: bold;
|
||||
margin-right: clamp(10px, 1.5vw, 20px);
|
||||
min-width: clamp(60px, 8vw, 90px);
|
||||
color: #fff;
|
||||
font-size: clamp(0.75rem, 1.1vw, 0.875rem);
|
||||
}
|
||||
|
||||
.rank-sales {
|
||||
flex: 1;
|
||||
margin-right: clamp(10px, 1.5vw, 20px);
|
||||
color: #F77436;
|
||||
font-size: clamp(0.6875rem, 1vw, 0.8125rem);
|
||||
}
|
||||
|
||||
.rank-rate {
|
||||
font-weight: bold;
|
||||
color: #FA3925;
|
||||
font-size: clamp(0.6875rem, 1vw, 0.8125rem);
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
height: 100%;
|
||||
min-height: clamp(250px, 30vw, 300px);
|
||||
}
|
||||
|
||||
.ranking-content {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
/* 个人排名区域 */
|
||||
.person-ranking {
|
||||
border: 1px solid #523A14;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
|
||||
/* 移动端优化 (768px及以下) */
|
||||
@media (max-width: 768px) {
|
||||
.top-data {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.ranking-content {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.sales-info {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.sales-info-top {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.sales-info-cont {
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.sales-info-bottom {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: clamp(0.875rem, 2.5vw, 1.125rem);
|
||||
}
|
||||
|
||||
.quarter-label {
|
||||
font-size: clamp(0.75rem, 2vw, 0.875rem);
|
||||
padding: clamp(2px, 0.5vw, 4px) clamp(6px, 1.5vw, 10px);
|
||||
min-width: clamp(30px, 5vw, 40px);
|
||||
}
|
||||
|
||||
.week-circle {
|
||||
width: clamp(16px, 4vw, 20px);
|
||||
height: clamp(16px, 4vw, 20px);
|
||||
font-size: clamp(0.5rem, 1.5vw, 0.625rem);
|
||||
}
|
||||
|
||||
.ranking-item {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
grid-template-rows: auto auto;
|
||||
gap: clamp(5px, 1vw, 8px);
|
||||
font-size: clamp(0.6875rem, 2vw, 0.875rem);
|
||||
padding: clamp(10px, 2vw, 15px);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.ranking-item .rank-number {
|
||||
grid-column: 1;
|
||||
grid-row: 1 / 3;
|
||||
font-size: clamp(0.875rem, 2.5vw, 1.125rem);
|
||||
width: clamp(18px, 5vw, 25px);
|
||||
height: clamp(18px, 5vw, 25px);
|
||||
min-width: 18px;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.ranking-item .rank-name {
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
margin-bottom: 0;
|
||||
margin-right: 0;
|
||||
font-size: clamp(0.75rem, 2.2vw, 0.875rem);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.ranking-item .rank-sales {
|
||||
grid-column: 2;
|
||||
grid-row: 2;
|
||||
margin-right: 0;
|
||||
margin-bottom: 0;
|
||||
margin-left: 0;
|
||||
margin-top: 0;
|
||||
font-size: clamp(0.625rem, 1.8vw, 0.75rem);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.ranking-item .rank-rate {
|
||||
grid-column: 3;
|
||||
grid-row: 1 / 3;
|
||||
margin-left: 0;
|
||||
flex-shrink: 0;
|
||||
font-size: clamp(0.6875rem, 2vw, 0.8125rem);
|
||||
align-self: center;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
min-height: clamp(200px, 40vw, 250px);
|
||||
}
|
||||
}
|
||||
|
||||
/* 超小屏幕优化 (480px及以下) */
|
||||
@media (max-width: 480px) {
|
||||
.quarters-container {
|
||||
gap: clamp(5px, 2vw, 8px);
|
||||
}
|
||||
|
||||
.quarter-label {
|
||||
font-size: clamp(0.625rem, 2.5vw, 0.75rem);
|
||||
padding: clamp(2px, 0.5vw, 3px) clamp(5px, 1.5vw, 8px);
|
||||
min-width: clamp(25px, 6vw, 35px);
|
||||
}
|
||||
|
||||
.week-circle {
|
||||
width: clamp(14px, 4.5vw, 18px);
|
||||
height: clamp(14px, 4.5vw, 18px);
|
||||
font-size: clamp(0.4375rem, 2vw, 0.5625rem);
|
||||
gap: clamp(2px, 0.5vw, 3px);
|
||||
}
|
||||
}
|
||||
|
||||
/* 头部和尾部图片 */
|
||||
.header-image {
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.footer-image {
|
||||
margin-top: 20px;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.header-image img,
|
||||
.footer-image img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
/* 排名数字内联样式替代 */
|
||||
.ranking-item .rank-number {
|
||||
color: #000;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
port: 3001,
|
||||
open: true
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
@@ -0,0 +1,140 @@
|
||||
# 数据大屏 Vue3 项目
|
||||
|
||||
这是使用 Vue 3 + Vite 重构的数据大屏项目,功能与原HTML版本完全一致。
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
vue-app/
|
||||
├── src/
|
||||
│ ├── components/
|
||||
│ │ ├── TopData.vue # 顶部数据区域(年度目标、销售额、进度条、季度进度)
|
||||
│ │ ├── GroupRanking.vue # 小组排名组件
|
||||
│ │ └── PersonRanking.vue # 个人排名组件
|
||||
│ ├── App.vue # 主应用组件
|
||||
│ ├── main.js # 入口文件
|
||||
│ └── style.css # 全局样式
|
||||
├── public/
|
||||
│ └── img/ # 图片资源(需要从原项目复制)
|
||||
├── index.html # HTML模板
|
||||
├── package.json # 项目配置
|
||||
└── vite.config.js # Vite配置
|
||||
```
|
||||
|
||||
## 安装步骤
|
||||
|
||||
### 1. 安装依赖
|
||||
|
||||
```bash
|
||||
cd vue-app
|
||||
npm install
|
||||
```
|
||||
|
||||
### 2. 复制图片资源
|
||||
|
||||
将原项目的图片文件夹复制到 `vue-app/public/` 目录:
|
||||
|
||||
```bash
|
||||
# Windows
|
||||
xcopy ..\img public\img\ /E /I
|
||||
|
||||
# Linux/Mac
|
||||
cp -r ../img public/img/
|
||||
```
|
||||
|
||||
### 3. 启动开发服务器
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
浏览器会自动打开 `http://localhost:3000`
|
||||
|
||||
### 4. 构建生产版本
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
构建产物在 `vue-app/dist/` 目录
|
||||
|
||||
## 功能特性
|
||||
|
||||
✅ **响应式设计**
|
||||
- PC端自动等比例缩放
|
||||
- 移动端自适应布局
|
||||
- 支持4K屏幕、普通PC、移动端
|
||||
|
||||
✅ **数据展示**
|
||||
- 年度目标和销售额动态加载
|
||||
- 进度条动画效果
|
||||
- 季度周数进度显示
|
||||
|
||||
✅ **图表功能**
|
||||
- 小组排名柱状图
|
||||
- 个人排名柱状图(移动端横向布局)
|
||||
- ECharts响应式图表
|
||||
|
||||
✅ **性能优化**
|
||||
- Vue 3 Composition API
|
||||
- 组件化开发
|
||||
- 防抖处理窗口resize
|
||||
|
||||
## 对接真实API
|
||||
|
||||
在 `src/components/TopData.vue` 的 `fetchYearData` 函数中修改:
|
||||
|
||||
```javascript
|
||||
const fetchYearData = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/year-data')
|
||||
const data = await response.json()
|
||||
return data
|
||||
} catch (error) {
|
||||
console.error('获取数据失败:', error)
|
||||
return { target: 0, actual: 0 }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 数据格式
|
||||
|
||||
### API返回格式
|
||||
|
||||
```json
|
||||
{
|
||||
"target": 3500,
|
||||
"actual": 2750
|
||||
}
|
||||
```
|
||||
|
||||
### 排名数据格式
|
||||
|
||||
在 `GroupRanking.vue` 和 `PersonRanking.vue` 中修改数据:
|
||||
|
||||
```javascript
|
||||
const groupData = ref([
|
||||
{ name: '名称', target: 目标值, actual: 实际值 }
|
||||
])
|
||||
```
|
||||
|
||||
## 技术栈
|
||||
|
||||
- Vue 3.4+ (Composition API)
|
||||
- Vite 5.0+
|
||||
- ECharts 5.4+
|
||||
- JavaScript ES6+
|
||||
|
||||
## 浏览器支持
|
||||
|
||||
- Chrome (推荐)
|
||||
- Firefox
|
||||
- Safari
|
||||
- Edge
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 确保 `public/img/` 目录包含所有图片资源
|
||||
2. PC端缩放范围为 0.6-1.0(缩小不放大)
|
||||
3. 移动端断点为 768px
|
||||
4. 图表使用防抖处理resize事件(300ms延迟)
|
||||
@@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>数据大屏</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "screen-dashboard",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.4.0",
|
||||
"echarts": "^5.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.0.0",
|
||||
"vite": "^5.0.0"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 113 KiB |
|
After Width: | Height: | Size: 226 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
@@ -0,0 +1,105 @@
|
||||
<template>
|
||||
<div class="container" ref="containerRef">
|
||||
<!-- 头部图片 -->
|
||||
<div class="header-image">
|
||||
<img :src="headerImage" alt="头部装饰图片">
|
||||
</div>
|
||||
|
||||
<div class="cont">
|
||||
<!-- 顶部数据区域 -->
|
||||
<TopData @progress-rate="handleProgressRate" />
|
||||
|
||||
<!-- 小组排名区域 -->
|
||||
<h2 class="section-title">小组排名</h2>
|
||||
<GroupRanking />
|
||||
|
||||
<!-- 个人排名区域 -->
|
||||
<h2 class="section-title">个人排名</h2>
|
||||
<PersonRanking />
|
||||
|
||||
<!-- 尾部图片 -->
|
||||
<div class="footer-image">
|
||||
<img :src="footerImage" alt="尾部装饰图片">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import TopData from './components/TopData.vue'
|
||||
import GroupRanking from './components/GroupRanking.vue'
|
||||
import PersonRanking from './components/PersonRanking.vue'
|
||||
|
||||
// 导入图片
|
||||
import headerImage from '/img/heard.png'
|
||||
import footerImage from '/img/foot.png'
|
||||
|
||||
const MOBILE_BREAKPOINT = 768
|
||||
const DESIGN_WIDTH = 1920
|
||||
const containerRef = ref(null)
|
||||
|
||||
// PC端等比例缩放函数
|
||||
const 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
|
||||
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'
|
||||
}
|
||||
|
||||
let resizeTimer = null
|
||||
|
||||
const handleResize = () => {
|
||||
setScale()
|
||||
}
|
||||
|
||||
const handleProgressRate = (rate) => {
|
||||
// 可以在这里处理进度率数据
|
||||
console.log('进度率:', rate)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
setScale()
|
||||
window.addEventListener('resize', () => {
|
||||
clearTimeout(resizeTimer)
|
||||
resizeTimer = setTimeout(setScale, 100)
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', handleResize)
|
||||
clearTimeout(resizeTimer)
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,252 @@
|
||||
<template>
|
||||
<div class="group-ranking">
|
||||
<div class="ranking-content">
|
||||
<!-- 排名列表 -->
|
||||
<div class="ranking-list">
|
||||
<div
|
||||
v-for="(item, index) in groupData"
|
||||
:key="index"
|
||||
class="ranking-item"
|
||||
:class="index < 3 ? `top-${index + 1}` : 'other'"
|
||||
>
|
||||
<div class="rank-number">{{ index + 1 }}</div>
|
||||
<div class="rank-name">{{ item.name }}</div>
|
||||
<div class="rank-sales">销售额: {{ formatNumber(item.actual) }} | 目标: {{ formatNumber(item.target) }}</div>
|
||||
<div class="rank-rate">完成率: {{ ((item.actual / item.target) * 100).toFixed(1) }}%</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 图表容器 -->
|
||||
<div ref="chartRef" class="chart-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import * as echarts from 'echarts'
|
||||
|
||||
const chartRef = ref(null)
|
||||
let chart = null
|
||||
|
||||
const MOBILE_BREAKPOINT = 768
|
||||
|
||||
// 小组数据
|
||||
const groupData = ref([
|
||||
{ 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 formatNumber = (num) => {
|
||||
return (num / 10000).toFixed(0) + '万'
|
||||
}
|
||||
|
||||
// 渲染图表
|
||||
const renderChart = () => {
|
||||
if (!chartRef.value) return
|
||||
|
||||
// 销毁旧图表
|
||||
if (chart) {
|
||||
chart.dispose()
|
||||
}
|
||||
|
||||
chart = echarts.init(chartRef.value)
|
||||
|
||||
const names = groupData.value.map(item => item.name)
|
||||
const actuals = groupData.value.map(item => item.actual / 100000000)
|
||||
const targets = groupData.value.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 + '<br/>'
|
||||
params.forEach(param => {
|
||||
result += param.marker + param.seriesName + ': ' + param.value + '万<br/>'
|
||||
})
|
||||
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)
|
||||
}
|
||||
|
||||
let resizeTimer = null
|
||||
|
||||
const handleResize = () => {
|
||||
clearTimeout(resizeTimer)
|
||||
resizeTimer = setTimeout(() => {
|
||||
renderChart()
|
||||
}, 300)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
renderChart()
|
||||
window.addEventListener('resize', handleResize)
|
||||
}, 500)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (chart) {
|
||||
chart.dispose()
|
||||
}
|
||||
window.removeEventListener('resize', handleResize)
|
||||
clearTimeout(resizeTimer)
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,251 @@
|
||||
<template>
|
||||
<div class="person-ranking">
|
||||
<div class="ranking-content">
|
||||
<!-- 排名列表 -->
|
||||
<div class="ranking-list">
|
||||
<div
|
||||
v-for="(item, index) in personData"
|
||||
:key="index"
|
||||
class="ranking-item"
|
||||
:class="index < 3 ? `top-${index + 1}` : 'other'"
|
||||
>
|
||||
<div class="rank-number">{{ index + 1 }}</div>
|
||||
<div class="rank-name">{{ item.name }}</div>
|
||||
<div class="rank-sales">销售额: {{ formatNumber(item.actual) }} | 目标: {{ formatNumber(item.target) }}</div>
|
||||
<div class="rank-rate">完成率: {{ ((item.actual / item.target) * 100).toFixed(1) }}%</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 图表容器 -->
|
||||
<div ref="chartRef" class="chart-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import * as echarts from 'echarts'
|
||||
|
||||
const chartRef = ref(null)
|
||||
let chart = null
|
||||
|
||||
const MOBILE_BREAKPOINT = 768
|
||||
|
||||
// 个人数据
|
||||
const personData = ref([
|
||||
{ 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 }
|
||||
])
|
||||
|
||||
// 格式化数字(万元)
|
||||
const formatNumber = (num) => {
|
||||
return (num / 10000).toFixed(0) + '万'
|
||||
}
|
||||
|
||||
// 渲染图表
|
||||
const renderChart = () => {
|
||||
if (!chartRef.value) return
|
||||
|
||||
// 销毁旧图表
|
||||
if (chart) {
|
||||
chart.dispose()
|
||||
}
|
||||
|
||||
chart = echarts.init(chartRef.value)
|
||||
|
||||
const names = personData.value.map(item => item.name)
|
||||
const actuals = personData.value.map(item => item.actual / 100000000)
|
||||
const targets = personData.value.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 + '<br/>'
|
||||
params.forEach(param => {
|
||||
result += param.marker + param.seriesName + ': ' + param.value + '万<br/>'
|
||||
})
|
||||
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)
|
||||
}
|
||||
|
||||
let resizeTimer = null
|
||||
|
||||
const handleResize = () => {
|
||||
clearTimeout(resizeTimer)
|
||||
resizeTimer = setTimeout(() => {
|
||||
renderChart()
|
||||
}, 300)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
renderChart()
|
||||
window.addEventListener('resize', handleResize)
|
||||
}, 500)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (chart) {
|
||||
chart.dispose()
|
||||
}
|
||||
window.removeEventListener('resize', handleResize)
|
||||
clearTimeout(resizeTimer)
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,166 @@
|
||||
<template>
|
||||
<div class="top-data">
|
||||
<!-- 销售信息 -->
|
||||
<div class="sales-info">
|
||||
<div class="sales-info-top">
|
||||
<div class="target-info">
|
||||
<h3>年度目标</h3>
|
||||
<div class="target-amount">{{ formatLargeNumber(yearData.target) }}</div>
|
||||
</div>
|
||||
<div class="total-info">
|
||||
<h3>年度总销售额</h3>
|
||||
<div class="total-amount">{{ formatLargeNumber(yearData.actual) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sales-info-cont">
|
||||
<div class="sales-info-bottom">
|
||||
<div class="progress-container">
|
||||
<div class="progress-bg">
|
||||
<div class="progress-bar" :style="{ width: progressWidth }">
|
||||
<div class="progress-info">
|
||||
<div class="play-btn"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="progress-text">{{ progressRate }}%</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="progress-label">完成率</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 年度进度 -->
|
||||
<div class="countdown">
|
||||
<h3>年度进度</h3>
|
||||
<div class="quarters-container">
|
||||
<div v-for="(quarter, index) in quarters" :key="quarter.id"
|
||||
class="quarter-item"
|
||||
:class="{ 'last-quarter': index === quarters.length - 1 }">
|
||||
<div class="quarter-label">{{ quarter.label }}</div>
|
||||
<div class="weeks-container">
|
||||
<div
|
||||
v-for="week in quarter.weeks"
|
||||
:key="week"
|
||||
class="week-circle"
|
||||
:class="{ 'current': week === currentWeek, 'passed': week < currentWeek }"
|
||||
:title="`第${week}周`"
|
||||
>
|
||||
{{ week }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
|
||||
const emit = defineEmits(['progress-rate'])
|
||||
|
||||
// 年度销售数据
|
||||
const yearData = ref({
|
||||
target: 3500,
|
||||
actual: 2750
|
||||
})
|
||||
|
||||
// 进度条宽度
|
||||
const progressWidth = ref('0%')
|
||||
const progressRate = ref('0.00')
|
||||
|
||||
// 获取当前周数
|
||||
const getCurrentWeek = () => {
|
||||
const now = new Date()
|
||||
const start = new Date(now.getFullYear(), 0, 1)
|
||||
const diff = now - start
|
||||
return Math.floor(diff / (7 * 24 * 60 * 60 * 1000)) + 1
|
||||
}
|
||||
|
||||
const currentWeek = ref(getCurrentWeek())
|
||||
|
||||
// 季度数据
|
||||
const quarters = ref([
|
||||
{ id: 'q1', label: 'Q1', start: 1, end: 13, weeks: [] },
|
||||
{ id: 'q2', label: 'Q2', start: 14, end: 26, weeks: [] },
|
||||
{ id: 'q3', label: 'Q3', start: 27, end: 39, weeks: [] },
|
||||
{ id: 'q4', label: 'Q4', start: 40, end: 52, weeks: [] }
|
||||
])
|
||||
|
||||
// 初始化季度周数
|
||||
quarters.value.forEach(quarter => {
|
||||
for (let i = quarter.start; i <= quarter.end; i++) {
|
||||
quarter.weeks.push(i)
|
||||
}
|
||||
})
|
||||
|
||||
// 获取年度数据(可替换为API调用)
|
||||
const fetchYearData = async () => {
|
||||
// TODO: 替换为实际的API调用
|
||||
// try {
|
||||
// const response = await fetch('/api/year-data')
|
||||
// const data = await response.json()
|
||||
// return data
|
||||
// } catch (error) {
|
||||
// console.error('获取数据失败:', error)
|
||||
// return { target: 0, actual: 0 }
|
||||
// }
|
||||
|
||||
// 当前使用模拟数据
|
||||
console.log('正在获取年度数据...')
|
||||
return {
|
||||
target: 3500,
|
||||
actual: 2750
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化大数字(万元)
|
||||
const formatLargeNumber = (num) => {
|
||||
return num.toLocaleString('zh-CN') + ' 万元'
|
||||
}
|
||||
|
||||
// 启动进度条动画
|
||||
const startProgressAnimation = (targetRate) => {
|
||||
const PROGRESS_STEP = 0.4
|
||||
const PROGRESS_INTERVAL = 20
|
||||
let value = 0
|
||||
let lastTime = 0
|
||||
|
||||
const animateProgress = (currentTime) => {
|
||||
if (!lastTime) lastTime = currentTime
|
||||
const deltaTime = currentTime - lastTime
|
||||
|
||||
if (deltaTime >= PROGRESS_INTERVAL) {
|
||||
value += PROGRESS_STEP
|
||||
if (value >= targetRate) {
|
||||
value = targetRate
|
||||
progressWidth.value = value + '%'
|
||||
return
|
||||
}
|
||||
progressWidth.value = value + '%'
|
||||
lastTime = currentTime
|
||||
}
|
||||
requestAnimationFrame(animateProgress)
|
||||
}
|
||||
|
||||
requestAnimationFrame(animateProgress)
|
||||
}
|
||||
|
||||
// 初始化数据
|
||||
onMounted(async () => {
|
||||
// 获取数据
|
||||
yearData.value = await fetchYearData()
|
||||
|
||||
// 计算完成率
|
||||
const rate = ((yearData.value.actual / yearData.value.target) * 100).toFixed(2)
|
||||
progressRate.value = rate
|
||||
|
||||
// 发射进度率事件
|
||||
emit('progress-rate', parseFloat(rate))
|
||||
|
||||
// 延迟启动进度条动画
|
||||
setTimeout(() => {
|
||||
startProgressAnimation(parseFloat(rate))
|
||||
}, 800)
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,5 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import './style.css'
|
||||
|
||||
createApp(App).mount('#app')
|
||||
@@ -0,0 +1,620 @@
|
||||
/* PC端等比例缩放,根字号固定 */
|
||||
html {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* 移动端基础字号 */
|
||||
@media screen and (max-width: 768px) {
|
||||
html {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Microsoft YaHei', Arial, sans-serif;
|
||||
color: #fff;
|
||||
min-width: 320px;
|
||||
overflow-x: hidden;
|
||||
/* PC端缩放相关样式 */
|
||||
transform-origin: left top;
|
||||
transition: transform 0.1s ease-out;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
background: #000;
|
||||
/* 固定最小宽度,避免缩放时内容挤压 */
|
||||
min-width: 1200px;
|
||||
}
|
||||
|
||||
.cont{
|
||||
width: 90%;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 0 1rem;
|
||||
/* 确保内容区域最小宽度 */
|
||||
min-width: 1080px;
|
||||
}
|
||||
|
||||
/* 移动端下调整宽度 */
|
||||
@media screen and (max-width: 768px) {
|
||||
body {
|
||||
transform: none !important;
|
||||
width: 100% !important;
|
||||
min-width: 320px !important;
|
||||
height: auto !important;
|
||||
overflow-x: hidden !important;
|
||||
}
|
||||
|
||||
.container {
|
||||
min-width: 320px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.cont {
|
||||
width: 95%;
|
||||
max-width: 100%;
|
||||
min-width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* 顶部数据区域 */
|
||||
.top-data {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1fr;
|
||||
gap: clamp(6px, 1.5vw, 14px);
|
||||
margin-bottom: clamp(10px, 1.5vw, 20px);
|
||||
}
|
||||
|
||||
.sales-info {
|
||||
background: url(/img/hicon.png);
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100% 100%;
|
||||
border-radius: clamp(4px, 0.5vw, 8px);
|
||||
padding: clamp(10px, 1.5vw, 20px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: clamp(10px, 1.5vw, 15px);
|
||||
}
|
||||
|
||||
.sales-info-top {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.target-info, .total-info {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.target-info h3,.total-info h3{
|
||||
font-size: clamp(0.75rem, 1vw, 1rem);
|
||||
color: #fff;
|
||||
margin-bottom: clamp(5px, 0.8vw, 8px);
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.target-amount {
|
||||
font-size: clamp(1.25rem, 2.5vw, 2.25rem);
|
||||
font-weight: bold;
|
||||
color: #F77436;
|
||||
margin-bottom: clamp(5px, 0.8vw, 10px);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.total-amount {
|
||||
font-size: clamp(1.25rem, 2.5vw, 2.25rem);
|
||||
font-weight: bold;
|
||||
color: #FA551F;
|
||||
margin-bottom: clamp(5px, 0.8vw, 10px);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.sales-info-cont{
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.sales-info-bottom {
|
||||
width: 90%;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
width: 100%;
|
||||
height: clamp(20px, 2.5vw, 30px);
|
||||
padding: 3px;
|
||||
border-radius: 20px;
|
||||
background: linear-gradient(180deg, #FF923C, #FF923C);
|
||||
box-shadow: 0px 0px 8px rgb(255 255 255 / 80%);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.progress-bg {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #DC7439;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 0%;
|
||||
border-radius: 16px;
|
||||
background: repeating-linear-gradient(45deg,rgba(255, 255, 255, 0.25) 0,rgba(255, 255, 255, 0.25) 8px,rgba(255, 255, 255, 0.12) 8px,rgba(255, 255, 255, 0.12) 16px),linear-gradient(180deg, #FFD85E, #FFBE25);
|
||||
animation: move 1.2s linear infinite;
|
||||
box-shadow: inset 0 0 6px rgba(255, 255, 255, 0.6);
|
||||
will-change: width;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.progress-info {
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: #fff;
|
||||
font-size: clamp(10px, 1.2vw, 13px);
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.play-btn {
|
||||
width: clamp(18px, 2vw, 24px);
|
||||
height: clamp(18px, 2vw, 24px);
|
||||
border-radius: 50%;
|
||||
background: #DC7439;
|
||||
border: 2px solid #FFBE25;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.play-btn::before {
|
||||
content: '';
|
||||
margin-left: 2px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-top: 5px solid transparent;
|
||||
border-bottom: 5px solid transparent;
|
||||
border-left: 7px solid #fff;
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
font-size: clamp(10px, 1.2vw, 13px);
|
||||
white-space: nowrap;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.progress-label {
|
||||
color: #DC7439;
|
||||
font-weight: bold;
|
||||
font-size: clamp(10px, 1.2vw, 13px);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 条纹滚动动画 */
|
||||
@keyframes move {
|
||||
from { background-position: 0 0; }
|
||||
to { background-position: 40px 0; }
|
||||
}
|
||||
|
||||
.completion-rate {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.countdown {
|
||||
border: 1px solid #523A14;
|
||||
border-radius: 8px;
|
||||
padding: clamp(8px, 1.5vw,10px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.countdown h3 {
|
||||
font-size: clamp(0.875rem, 1.25vw, 1.25rem);
|
||||
color: #e7552f;
|
||||
margin-bottom: clamp(10px, 1.5vw, 15px);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* 季度容器 */
|
||||
.quarters-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: clamp(5px, 1.5vw, 5px);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 单个季度 */
|
||||
.quarter-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: clamp(5px, 1vw, 8px);
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
border-bottom: 1px solid #523A14;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
.last-quarter{
|
||||
border-bottom: none;
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
|
||||
.quarter-label {
|
||||
background: #e7552f;
|
||||
color: #fff;
|
||||
font-size: clamp(0.875rem, 1.2vw, 1rem);
|
||||
font-weight: bold;
|
||||
padding: clamp(3px, 0.5vw, 5px) clamp(8px, 1vw, 12px);
|
||||
border-radius: 4px;
|
||||
min-width: clamp(35px, 5vw, 45px);
|
||||
text-align: center;
|
||||
box-shadow: 0 0 10px rgba(255, 102, 0, 0.5);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 周数圆圈容器 */
|
||||
.weeks-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: clamp(3px, 0.5vw, 5px);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* 单个周数圆圈 */
|
||||
.week-circle {
|
||||
width: clamp(18px, 2.5vw, 24px);
|
||||
height: clamp(18px, 2.5vw, 24px);
|
||||
border-radius: 50%;
|
||||
border: 2px solid #815B37;
|
||||
background: rgba(129, 91, 55, 0.2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: clamp(0.625rem, 1vw, 0.75rem);
|
||||
color: #f5bbab;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.week-circle:hover {
|
||||
background: rgba(231, 85, 47, 0.4);
|
||||
border-color: #e7552f;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
/* 当前周高亮 */
|
||||
.week-circle.current {
|
||||
background: #e7552f;
|
||||
border-color: #ff6600;
|
||||
box-shadow: 0 0 10px rgba(255, 102, 0, 0.8);
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
/* 已过去的周 */
|
||||
.week-circle.passed {
|
||||
background: #e7552f;
|
||||
border-color: #ff6600;
|
||||
box-shadow: 0 0 10px rgba(255, 102, 0, 0.8);
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
box-shadow: 0 0 10px rgba(255, 102, 0, 0.8);
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 0 20px rgba(255, 102, 0, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/* 小组排名区域 */
|
||||
.group-ranking {
|
||||
border: 1px solid #523A14;
|
||||
border-radius: 8px;
|
||||
padding: clamp(10px, 1.5vw, 20px);
|
||||
margin-bottom: clamp(10px, 1.5vw, 20px);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: clamp(0.875rem, 1.25vw, 1rem);
|
||||
color: #fff;
|
||||
background: #ff6600;
|
||||
padding: clamp(5px, 0.8vw, 5px) clamp(10px, 1.5vw, 15px);
|
||||
display: inline-block;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.ranking-content {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: clamp(10px, 1.5vw, 20px);
|
||||
}
|
||||
|
||||
.ranking-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: clamp(8px, 1vw, 10px);
|
||||
}
|
||||
|
||||
.ranking-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: clamp(8px, 1.2vw, 12px) clamp(10px, 1.5vw, 15px);
|
||||
border-radius: 4px;
|
||||
font-size: clamp(0.75rem, 1vw, 0.875rem);
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.ranking-item.top-1 {
|
||||
background: linear-gradient(90deg, rgba(199, 173, 61, 0.4), rgba(199, 173, 61, 0.1));
|
||||
border: 1px solid rgba(199, 173, 61, 0.4);
|
||||
}
|
||||
|
||||
.ranking-item.top-2 {
|
||||
background: linear-gradient(90deg, rgba(136, 136, 135, 0.5), rgba(136, 136, 135, 0.1));
|
||||
border: 1px solid rgba(136, 136, 135, 0.4);
|
||||
}
|
||||
|
||||
.ranking-item.top-3 {
|
||||
background: linear-gradient(90deg, rgba(59, 96, 93, 0.6), rgba(59, 96, 93,0.1));
|
||||
border: 1px solid rgba(59, 96, 93, 0.4);
|
||||
}
|
||||
|
||||
.ranking-item.other {
|
||||
background: linear-gradient(90deg, rgba(100, 100, 100, 0.3), rgba(100, 100, 100, 0.1));
|
||||
border: 1px solid rgba(100, 100, 100, 0.4);
|
||||
}
|
||||
|
||||
.rank-number {
|
||||
font-size: clamp(0.875rem, 1.25vw, 1.25rem);
|
||||
font-weight: bold;
|
||||
margin-right: clamp(10px, 1.5vw, 15px);
|
||||
min-width: 20px;
|
||||
width: clamp(20px, 3vw, 25px);
|
||||
height: clamp(20px, 3vw, 25px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.ranking-item.top-1 .rank-number {
|
||||
background: #C7AD3D;
|
||||
color: #000;
|
||||
box-shadow: 0 0 8px rgba(255, 204, 0, 0.8);
|
||||
}
|
||||
|
||||
.ranking-item.top-2 .rank-number {
|
||||
background: #888887;
|
||||
color: #000;
|
||||
box-shadow: 0 0 8px rgba(192, 192, 192, 0.8);
|
||||
}
|
||||
|
||||
.ranking-item.top-3 .rank-number {
|
||||
background: #3B605D;
|
||||
color: #fff;
|
||||
box-shadow: 0 0 8px rgba(205, 127, 50, 0.8);
|
||||
}
|
||||
|
||||
.ranking-item.other .rank-number {
|
||||
background: #815B37;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.rank-name {
|
||||
font-weight: bold;
|
||||
margin-right: clamp(10px, 1.5vw, 20px);
|
||||
min-width: clamp(60px, 8vw, 90px);
|
||||
color: #fff;
|
||||
font-size: clamp(0.75rem, 1.1vw, 0.875rem);
|
||||
}
|
||||
|
||||
.rank-sales {
|
||||
flex: 1;
|
||||
margin-right: clamp(10px, 1.5vw, 20px);
|
||||
color: #F77436;
|
||||
font-size: clamp(0.6875rem, 1vw, 0.8125rem);
|
||||
}
|
||||
|
||||
.rank-rate {
|
||||
font-weight: bold;
|
||||
color: #FA3925;
|
||||
font-size: clamp(0.6875rem, 1vw, 0.8125rem);
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
height: 100%;
|
||||
min-height: clamp(250px, 30vw, 300px);
|
||||
}
|
||||
|
||||
.ranking-content {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
/* 个人排名区域 */
|
||||
.person-ranking {
|
||||
border: 1px solid #523A14;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
|
||||
/* 移动端优化 (768px及以下) */
|
||||
@media (max-width: 768px) {
|
||||
.top-data {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.ranking-content {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.sales-info {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.sales-info-top {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.sales-info-cont {
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.sales-info-bottom {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: clamp(0.875rem, 2.5vw, 1.125rem);
|
||||
}
|
||||
|
||||
.quarter-label {
|
||||
font-size: clamp(0.75rem, 2vw, 0.875rem);
|
||||
padding: clamp(2px, 0.5vw, 4px) clamp(6px, 1.5vw, 10px);
|
||||
min-width: clamp(30px, 5vw, 40px);
|
||||
}
|
||||
|
||||
.week-circle {
|
||||
width: clamp(16px, 4vw, 20px);
|
||||
height: clamp(16px, 4vw, 20px);
|
||||
font-size: clamp(0.5rem, 1.5vw, 0.625rem);
|
||||
}
|
||||
|
||||
.ranking-item {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
grid-template-rows: auto auto;
|
||||
gap: clamp(5px, 1vw, 8px);
|
||||
font-size: clamp(0.6875rem, 2vw, 0.875rem);
|
||||
padding: clamp(10px, 2vw, 15px);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.ranking-item .rank-number {
|
||||
grid-column: 1;
|
||||
grid-row: 1 / 3;
|
||||
font-size: clamp(0.875rem, 2.5vw, 1.125rem);
|
||||
width: clamp(18px, 5vw, 25px);
|
||||
height: clamp(18px, 5vw, 25px);
|
||||
min-width: 18px;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.ranking-item .rank-name {
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
margin-bottom: 0;
|
||||
margin-right: 0;
|
||||
font-size: clamp(0.75rem, 2.2vw, 0.875rem);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.ranking-item .rank-sales {
|
||||
grid-column: 2;
|
||||
grid-row: 2;
|
||||
margin-right: 0;
|
||||
margin-bottom: 0;
|
||||
margin-left: 0;
|
||||
margin-top: 0;
|
||||
font-size: clamp(0.625rem, 1.8vw, 0.75rem);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.ranking-item .rank-rate {
|
||||
grid-column: 3;
|
||||
grid-row: 1 / 3;
|
||||
margin-left: 0;
|
||||
flex-shrink: 0;
|
||||
font-size: clamp(0.6875rem, 2vw, 0.8125rem);
|
||||
align-self: center;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
min-height: clamp(200px, 40vw, 250px);
|
||||
}
|
||||
}
|
||||
|
||||
/* 超小屏幕优化 (480px及以下) */
|
||||
@media (max-width: 480px) {
|
||||
.quarters-container {
|
||||
gap: clamp(5px, 2vw, 8px);
|
||||
}
|
||||
|
||||
.quarter-label {
|
||||
font-size: clamp(0.625rem, 2.5vw, 0.75rem);
|
||||
padding: clamp(2px, 0.5vw, 3px) clamp(5px, 1.5vw, 8px);
|
||||
min-width: clamp(25px, 6vw, 35px);
|
||||
}
|
||||
|
||||
.week-circle {
|
||||
width: clamp(14px, 4.5vw, 18px);
|
||||
height: clamp(14px, 4.5vw, 18px);
|
||||
font-size: clamp(0.4375rem, 2vw, 0.5625rem);
|
||||
gap: clamp(2px, 0.5vw, 3px);
|
||||
}
|
||||
}
|
||||
|
||||
/* 头部和尾部图片 */
|
||||
.header-image {
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.footer-image {
|
||||
margin-top: 20px;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.header-image img,
|
||||
.footer-image img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
/* 排名数字内联样式替代 */
|
||||
.ranking-item .rank-number {
|
||||
color: #000;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
server: {
|
||||
port: 3000,
|
||||
open: true
|
||||
}
|
||||
})
|
||||