This commit is contained in:
浅念
2026-02-12 09:38:27 +08:00
commit 5ba8456a88
42 changed files with 8372 additions and 0 deletions
+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
}
})