This commit is contained in:
浅念
2026-02-12 09:38:27 +08:00
commit 5ba8456a88
42 changed files with 8372 additions and 0 deletions
+667
View File
@@ -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;
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 226 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

+104
View File
@@ -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>
+125
View File
@@ -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>
+629
View File
@@ -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>
+45
View File
File diff suppressed because one or more lines are too long
+656
View File
@@ -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);
});
+24
View File
@@ -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?
+149
View File
@@ -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 |
| 学习曲线 | 中等 | 较低 |
| 生态成熟度 | 非常成熟 | 快速发展 |
+12
View File
@@ -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>
+1700
View File
File diff suppressed because it is too large Load Diff
+19
View File
@@ -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"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

+102
View File
@@ -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
+257
View File
@@ -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
+256
View File
@@ -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
+151
View File
@@ -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
+10
View File
@@ -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>
)
+620
View File
@@ -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;
}
+10
View File
@@ -0,0 +1,10 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
server: {
port: 3001,
open: true
}
})
+24
View File
@@ -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?
+140
View File
@@ -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延迟)
+12
View File
@@ -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>
+1233
View File
File diff suppressed because it is too large Load Diff
+18
View File
@@ -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"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

+105
View File
@@ -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>
+252
View File
@@ -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>
+251
View File
@@ -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>
+166
View File
@@ -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>
+5
View File
@@ -0,0 +1,5 @@
import { createApp } from 'vue'
import App from './App.vue'
import './style.css'
createApp(App).mount('#app')
+620
View File
@@ -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;
}
+10
View File
@@ -0,0 +1,10 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
server: {
port: 3000,
open: true
}
})