提交
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
@@ -0,0 +1,140 @@
|
||||
# 数据大屏 Vue3 项目
|
||||
|
||||
这是使用 Vue 3 + Vite 重构的数据大屏项目,功能与原HTML版本完全一致。
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
vue-app/
|
||||
├── src/
|
||||
│ ├── components/
|
||||
│ │ ├── TopData.vue # 顶部数据区域(年度目标、销售额、进度条、季度进度)
|
||||
│ │ ├── GroupRanking.vue # 小组排名组件
|
||||
│ │ └── PersonRanking.vue # 个人排名组件
|
||||
│ ├── App.vue # 主应用组件
|
||||
│ ├── main.js # 入口文件
|
||||
│ └── style.css # 全局样式
|
||||
├── public/
|
||||
│ └── img/ # 图片资源(需要从原项目复制)
|
||||
├── index.html # HTML模板
|
||||
├── package.json # 项目配置
|
||||
└── vite.config.js # Vite配置
|
||||
```
|
||||
|
||||
## 安装步骤
|
||||
|
||||
### 1. 安装依赖
|
||||
|
||||
```bash
|
||||
cd vue-app
|
||||
npm install
|
||||
```
|
||||
|
||||
### 2. 复制图片资源
|
||||
|
||||
将原项目的图片文件夹复制到 `vue-app/public/` 目录:
|
||||
|
||||
```bash
|
||||
# Windows
|
||||
xcopy ..\img public\img\ /E /I
|
||||
|
||||
# Linux/Mac
|
||||
cp -r ../img public/img/
|
||||
```
|
||||
|
||||
### 3. 启动开发服务器
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
浏览器会自动打开 `http://localhost:3000`
|
||||
|
||||
### 4. 构建生产版本
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
构建产物在 `vue-app/dist/` 目录
|
||||
|
||||
## 功能特性
|
||||
|
||||
✅ **响应式设计**
|
||||
- PC端自动等比例缩放
|
||||
- 移动端自适应布局
|
||||
- 支持4K屏幕、普通PC、移动端
|
||||
|
||||
✅ **数据展示**
|
||||
- 年度目标和销售额动态加载
|
||||
- 进度条动画效果
|
||||
- 季度周数进度显示
|
||||
|
||||
✅ **图表功能**
|
||||
- 小组排名柱状图
|
||||
- 个人排名柱状图(移动端横向布局)
|
||||
- ECharts响应式图表
|
||||
|
||||
✅ **性能优化**
|
||||
- Vue 3 Composition API
|
||||
- 组件化开发
|
||||
- 防抖处理窗口resize
|
||||
|
||||
## 对接真实API
|
||||
|
||||
在 `src/components/TopData.vue` 的 `fetchYearData` 函数中修改:
|
||||
|
||||
```javascript
|
||||
const fetchYearData = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/year-data')
|
||||
const data = await response.json()
|
||||
return data
|
||||
} catch (error) {
|
||||
console.error('获取数据失败:', error)
|
||||
return { target: 0, actual: 0 }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 数据格式
|
||||
|
||||
### API返回格式
|
||||
|
||||
```json
|
||||
{
|
||||
"target": 3500,
|
||||
"actual": 2750
|
||||
}
|
||||
```
|
||||
|
||||
### 排名数据格式
|
||||
|
||||
在 `GroupRanking.vue` 和 `PersonRanking.vue` 中修改数据:
|
||||
|
||||
```javascript
|
||||
const groupData = ref([
|
||||
{ name: '名称', target: 目标值, actual: 实际值 }
|
||||
])
|
||||
```
|
||||
|
||||
## 技术栈
|
||||
|
||||
- Vue 3.4+ (Composition API)
|
||||
- Vite 5.0+
|
||||
- ECharts 5.4+
|
||||
- JavaScript ES6+
|
||||
|
||||
## 浏览器支持
|
||||
|
||||
- Chrome (推荐)
|
||||
- Firefox
|
||||
- Safari
|
||||
- Edge
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 确保 `public/img/` 目录包含所有图片资源
|
||||
2. PC端缩放范围为 0.6-1.0(缩小不放大)
|
||||
3. 移动端断点为 768px
|
||||
4. 图表使用防抖处理resize事件(300ms延迟)
|
||||
@@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>数据大屏</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Generated
+1233
File diff suppressed because it is too large
Load Diff
@@ -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 |
@@ -0,0 +1,105 @@
|
||||
<template>
|
||||
<div class="container" ref="containerRef">
|
||||
<!-- 头部图片 -->
|
||||
<div class="header-image">
|
||||
<img :src="headerImage" alt="头部装饰图片">
|
||||
</div>
|
||||
|
||||
<div class="cont">
|
||||
<!-- 顶部数据区域 -->
|
||||
<TopData @progress-rate="handleProgressRate" />
|
||||
|
||||
<!-- 小组排名区域 -->
|
||||
<h2 class="section-title">小组排名</h2>
|
||||
<GroupRanking />
|
||||
|
||||
<!-- 个人排名区域 -->
|
||||
<h2 class="section-title">个人排名</h2>
|
||||
<PersonRanking />
|
||||
|
||||
<!-- 尾部图片 -->
|
||||
<div class="footer-image">
|
||||
<img :src="footerImage" alt="尾部装饰图片">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import TopData from './components/TopData.vue'
|
||||
import GroupRanking from './components/GroupRanking.vue'
|
||||
import PersonRanking from './components/PersonRanking.vue'
|
||||
|
||||
// 导入图片
|
||||
import headerImage from '/img/heard.png'
|
||||
import footerImage from '/img/foot.png'
|
||||
|
||||
const MOBILE_BREAKPOINT = 768
|
||||
const DESIGN_WIDTH = 1920
|
||||
const containerRef = ref(null)
|
||||
|
||||
// PC端等比例缩放函数
|
||||
const setScale = () => {
|
||||
const screenWidth = window.innerWidth
|
||||
|
||||
// 移动端不缩放,保持响应式布局
|
||||
if (screenWidth <= MOBILE_BREAKPOINT) {
|
||||
document.body.style.transform = 'none'
|
||||
document.body.style.transformOrigin = 'left top'
|
||||
document.body.style.marginLeft = '0px'
|
||||
document.body.style.width = '100%'
|
||||
document.body.style.minWidth = '320px'
|
||||
document.body.style.height = 'auto'
|
||||
document.body.style.overflowX = 'hidden'
|
||||
return
|
||||
}
|
||||
|
||||
// PC端等比例缩放
|
||||
const scale = screenWidth / DESIGN_WIDTH
|
||||
const finalScale = Math.max(scale, 0.6)
|
||||
|
||||
// 重置样式以获取真实内容高度
|
||||
document.body.style.transform = 'none'
|
||||
document.body.style.height = 'auto'
|
||||
|
||||
// 获取真实内容高度
|
||||
const contentHeight = document.body.scrollHeight
|
||||
|
||||
// 计算缩放后的高度
|
||||
const scaledHeight = Math.ceil(contentHeight * finalScale)
|
||||
|
||||
// 应用缩放
|
||||
document.body.style.transform = `scale(${finalScale})`
|
||||
document.body.style.transformOrigin = 'left top'
|
||||
document.body.style.width = `${DESIGN_WIDTH}px`
|
||||
document.body.style.minWidth = `${DESIGN_WIDTH}px`
|
||||
document.body.style.height = `${scaledHeight}px`
|
||||
document.body.style.marginLeft = '0px'
|
||||
document.body.style.overflowX = 'hidden'
|
||||
}
|
||||
|
||||
let resizeTimer = null
|
||||
|
||||
const handleResize = () => {
|
||||
setScale()
|
||||
}
|
||||
|
||||
const handleProgressRate = (rate) => {
|
||||
// 可以在这里处理进度率数据
|
||||
console.log('进度率:', rate)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
setScale()
|
||||
window.addEventListener('resize', () => {
|
||||
clearTimeout(resizeTimer)
|
||||
resizeTimer = setTimeout(setScale, 100)
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', handleResize)
|
||||
clearTimeout(resizeTimer)
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,252 @@
|
||||
<template>
|
||||
<div class="group-ranking">
|
||||
<div class="ranking-content">
|
||||
<!-- 排名列表 -->
|
||||
<div class="ranking-list">
|
||||
<div
|
||||
v-for="(item, index) in groupData"
|
||||
:key="index"
|
||||
class="ranking-item"
|
||||
:class="index < 3 ? `top-${index + 1}` : 'other'"
|
||||
>
|
||||
<div class="rank-number">{{ index + 1 }}</div>
|
||||
<div class="rank-name">{{ item.name }}</div>
|
||||
<div class="rank-sales">销售额: {{ formatNumber(item.actual) }} | 目标: {{ formatNumber(item.target) }}</div>
|
||||
<div class="rank-rate">完成率: {{ ((item.actual / item.target) * 100).toFixed(1) }}%</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 图表容器 -->
|
||||
<div ref="chartRef" class="chart-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import * as echarts from 'echarts'
|
||||
|
||||
const chartRef = ref(null)
|
||||
let chart = null
|
||||
|
||||
const MOBILE_BREAKPOINT = 768
|
||||
|
||||
// 小组数据
|
||||
const groupData = ref([
|
||||
{ name: '华东一组', target: 50000000000, actual: 62000000000 },
|
||||
{ name: '华南二组', target: 45000000000, actual: 51000000000 },
|
||||
{ name: '华北三组', target: 40000000000, actual: 43500000000 },
|
||||
{ name: '西南四组', target: 35000000000, actual: 28000000000 },
|
||||
{ name: '东北五组', target: 30000000000, actual: 26500000000 }
|
||||
])
|
||||
|
||||
// 格式化数字(万元)
|
||||
const formatNumber = (num) => {
|
||||
return (num / 10000).toFixed(0) + '万'
|
||||
}
|
||||
|
||||
// 渲染图表
|
||||
const renderChart = () => {
|
||||
if (!chartRef.value) return
|
||||
|
||||
// 销毁旧图表
|
||||
if (chart) {
|
||||
chart.dispose()
|
||||
}
|
||||
|
||||
chart = echarts.init(chartRef.value)
|
||||
|
||||
const names = groupData.value.map(item => item.name)
|
||||
const actuals = groupData.value.map(item => item.actual / 100000000)
|
||||
const targets = groupData.value.map(item => item.target / 100000000)
|
||||
|
||||
const isMobile = window.innerWidth <= MOBILE_BREAKPOINT
|
||||
|
||||
const option = {
|
||||
backgroundColor: 'transparent',
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow'
|
||||
},
|
||||
formatter: function(params) {
|
||||
let result = params[0].name + '<br/>'
|
||||
params.forEach(param => {
|
||||
result += param.marker + param.seriesName + ': ' + param.value + '万<br/>'
|
||||
})
|
||||
return result
|
||||
},
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.9)',
|
||||
borderColor: '#ff6600',
|
||||
borderWidth: 1,
|
||||
textStyle: {
|
||||
color: '#fff',
|
||||
fontSize: 12
|
||||
}
|
||||
},
|
||||
grid: isMobile ? {
|
||||
left: '5%',
|
||||
right: '5%',
|
||||
bottom: '15%',
|
||||
top: '20%',
|
||||
containLabel: true
|
||||
} : {
|
||||
left: '15%',
|
||||
right: '10%',
|
||||
bottom: '10%',
|
||||
top: '20%',
|
||||
containLabel: true
|
||||
},
|
||||
legend: {
|
||||
data: ['目标销售额', '实际销售额'],
|
||||
textStyle: {
|
||||
color: '#fff',
|
||||
fontSize: 12
|
||||
},
|
||||
top: '2%'
|
||||
},
|
||||
xAxis: isMobile ? {
|
||||
type: 'category',
|
||||
data: names,
|
||||
name: '小组',
|
||||
nameTextStyle: {
|
||||
color: '#fff'
|
||||
},
|
||||
axisLabel: {
|
||||
color: '#fff',
|
||||
fontSize: 10,
|
||||
interval: 0,
|
||||
rotate: 30
|
||||
},
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: 'rgba(255, 102, 0, 0.5)'
|
||||
}
|
||||
}
|
||||
} : {
|
||||
type: 'value',
|
||||
name: '万元',
|
||||
nameTextStyle: {
|
||||
color: '#fff'
|
||||
},
|
||||
axisLabel: {
|
||||
color: '#fff',
|
||||
fontSize: 11,
|
||||
formatter: function(value) {
|
||||
return value * 100
|
||||
}
|
||||
},
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: 'rgba(255, 102, 0, 0.5)'
|
||||
}
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
color: 'rgba(255, 255, 255, 0.1)'
|
||||
}
|
||||
}
|
||||
},
|
||||
yAxis: isMobile ? {
|
||||
type: 'value',
|
||||
name: '万',
|
||||
nameTextStyle: {
|
||||
color: '#fff'
|
||||
},
|
||||
axisLabel: {
|
||||
color: '#fff',
|
||||
fontSize: 11
|
||||
},
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: 'rgba(255, 102, 0, 0.5)'
|
||||
}
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
color: 'rgba(255, 255, 255, 0.1)'
|
||||
}
|
||||
}
|
||||
} : {
|
||||
type: 'category',
|
||||
data: names,
|
||||
axisLabel: {
|
||||
color: '#fff',
|
||||
fontSize: 11
|
||||
},
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: 'rgba(255, 102, 0, 0.5)'
|
||||
}
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '目标销售额',
|
||||
type: 'bar',
|
||||
data: targets,
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(isMobile ? 0 : 0, isMobile ? 1 : 0, isMobile ? 0 : 1, isMobile ? 0 : 0, [
|
||||
{ offset: 0, color: '#F77436' },
|
||||
{ offset: 1, color: '#F77436' }
|
||||
]),
|
||||
borderRadius: isMobile ? [4, 4, 0, 0] : [0, 4, 4, 0]
|
||||
},
|
||||
barMaxWidth: 15,
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
shadowBlur: 10,
|
||||
shadowOffsetX: 0,
|
||||
shadowColor: 'rgba(102, 102, 102, 0.8)'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: '实际销售额',
|
||||
type: 'bar',
|
||||
data: actuals,
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(isMobile ? 0 : 0, isMobile ? 1 : 0, isMobile ? 0 : 1, isMobile ? 0 : 0, [
|
||||
{ offset: 0, color: '#65491D' },
|
||||
{ offset: 1, color: '#65491D' }
|
||||
]),
|
||||
borderRadius: isMobile ? [4, 4, 0, 0] : [0, 4, 4, 0]
|
||||
},
|
||||
barMaxWidth: 15,
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
shadowBlur: 10,
|
||||
shadowOffsetX: 0,
|
||||
shadowColor: 'rgba(255, 102, 0, 0.8)'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
chart.setOption(option)
|
||||
}
|
||||
|
||||
let resizeTimer = null
|
||||
|
||||
const handleResize = () => {
|
||||
clearTimeout(resizeTimer)
|
||||
resizeTimer = setTimeout(() => {
|
||||
renderChart()
|
||||
}, 300)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
renderChart()
|
||||
window.addEventListener('resize', handleResize)
|
||||
}, 500)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (chart) {
|
||||
chart.dispose()
|
||||
}
|
||||
window.removeEventListener('resize', handleResize)
|
||||
clearTimeout(resizeTimer)
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,251 @@
|
||||
<template>
|
||||
<div class="person-ranking">
|
||||
<div class="ranking-content">
|
||||
<!-- 排名列表 -->
|
||||
<div class="ranking-list">
|
||||
<div
|
||||
v-for="(item, index) in personData"
|
||||
:key="index"
|
||||
class="ranking-item"
|
||||
:class="index < 3 ? `top-${index + 1}` : 'other'"
|
||||
>
|
||||
<div class="rank-number">{{ index + 1 }}</div>
|
||||
<div class="rank-name">{{ item.name }}</div>
|
||||
<div class="rank-sales">销售额: {{ formatNumber(item.actual) }} | 目标: {{ formatNumber(item.target) }}</div>
|
||||
<div class="rank-rate">完成率: {{ ((item.actual / item.target) * 100).toFixed(1) }}%</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 图表容器 -->
|
||||
<div ref="chartRef" class="chart-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import * as echarts from 'echarts'
|
||||
|
||||
const chartRef = ref(null)
|
||||
let chart = null
|
||||
|
||||
const MOBILE_BREAKPOINT = 768
|
||||
|
||||
// 个人数据
|
||||
const personData = ref([
|
||||
{ name: '张伟', target: 5000000000, actual: 7200000000 },
|
||||
{ name: '李娜', target: 4500000000, actual: 5800000000 },
|
||||
{ name: '王强', target: 4000000000, actual: 4900000000 },
|
||||
{ name: '刘洋', target: 3800000000, actual: 4100000000 },
|
||||
{ name: '陈静', target: 3500000000, actual: 3600000000 },
|
||||
{ name: '杨明', target: 3200000000, actual: 2800000000 },
|
||||
{ name: '赵丽', target: 3000000000, actual: 2500000000 }
|
||||
])
|
||||
|
||||
// 格式化数字(万元)
|
||||
const formatNumber = (num) => {
|
||||
return (num / 10000).toFixed(0) + '万'
|
||||
}
|
||||
|
||||
// 渲染图表
|
||||
const renderChart = () => {
|
||||
if (!chartRef.value) return
|
||||
|
||||
// 销毁旧图表
|
||||
if (chart) {
|
||||
chart.dispose()
|
||||
}
|
||||
|
||||
chart = echarts.init(chartRef.value)
|
||||
|
||||
const names = personData.value.map(item => item.name)
|
||||
const actuals = personData.value.map(item => item.actual / 100000000)
|
||||
const targets = personData.value.map(item => item.target / 100000000)
|
||||
|
||||
const isMobile = window.innerWidth <= MOBILE_BREAKPOINT
|
||||
|
||||
const option = {
|
||||
backgroundColor: 'transparent',
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow'
|
||||
},
|
||||
formatter: function(params) {
|
||||
let result = params[0].name + '<br/>'
|
||||
params.forEach(param => {
|
||||
result += param.marker + param.seriesName + ': ' + param.value + '万<br/>'
|
||||
})
|
||||
return result
|
||||
},
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.9)',
|
||||
borderColor: '#ff6600',
|
||||
borderWidth: 1,
|
||||
textStyle: {
|
||||
color: '#fff',
|
||||
fontSize: 12
|
||||
}
|
||||
},
|
||||
grid: isMobile ? {
|
||||
left: '10%',
|
||||
right: '5%',
|
||||
bottom: '15%',
|
||||
top: '20%',
|
||||
containLabel: true
|
||||
} : {
|
||||
left: '15%',
|
||||
right: '10%',
|
||||
bottom: '10%',
|
||||
top: '20%',
|
||||
containLabel: true
|
||||
},
|
||||
legend: {
|
||||
data: ['目标销售额', '实际销售额'],
|
||||
textStyle: {
|
||||
color: '#fff',
|
||||
fontSize: 12
|
||||
},
|
||||
top: '2%'
|
||||
},
|
||||
xAxis: isMobile ? {
|
||||
type: 'value',
|
||||
name: '万',
|
||||
nameLocation: 'end',
|
||||
nameGap: 5,
|
||||
nameTextStyle: {
|
||||
color: '#fff',
|
||||
fontSize: 10
|
||||
},
|
||||
axisLabel: {
|
||||
color: '#fff',
|
||||
fontSize: 10
|
||||
},
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: 'rgba(255, 102, 0, 0.5)'
|
||||
}
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
color: 'rgba(255, 255, 255, 0.1)'
|
||||
}
|
||||
}
|
||||
} : {
|
||||
type: 'value',
|
||||
name: '万',
|
||||
nameTextStyle: {
|
||||
color: '#fff'
|
||||
},
|
||||
axisLabel: {
|
||||
color: '#fff',
|
||||
fontSize: 11,
|
||||
formatter: function(value) {
|
||||
return value * 100
|
||||
}
|
||||
},
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: 'rgba(255, 102, 0, 0.5)'
|
||||
}
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
color: 'rgba(255, 255, 255, 0.1)'
|
||||
}
|
||||
}
|
||||
},
|
||||
yAxis: isMobile ? {
|
||||
type: 'category',
|
||||
data: names,
|
||||
axisLabel: {
|
||||
color: '#fff',
|
||||
fontSize: 10
|
||||
},
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: 'rgba(255, 102, 0, 0.5)'
|
||||
}
|
||||
}
|
||||
} : {
|
||||
type: 'category',
|
||||
data: names,
|
||||
axisLabel: {
|
||||
color: '#fff',
|
||||
fontSize: 11
|
||||
},
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: 'rgba(255, 102, 0, 0.5)'
|
||||
}
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '目标销售额',
|
||||
type: 'bar',
|
||||
data: targets,
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
|
||||
{ offset: 0, color: '#F77436' },
|
||||
{ offset: 1, color: '#F77436' }
|
||||
]),
|
||||
borderRadius: [0, 4, 4, 0]
|
||||
},
|
||||
barMaxWidth: 15,
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
shadowBlur: 10,
|
||||
shadowOffsetX: 0,
|
||||
shadowColor: 'rgba(102, 102, 102, 0.8)'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: '实际销售额',
|
||||
type: 'bar',
|
||||
data: actuals,
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
|
||||
{ offset: 0, color: '#65491D' },
|
||||
{ offset: 1, color: '#65491D' }
|
||||
]),
|
||||
borderRadius: [0, 4, 4, 0]
|
||||
},
|
||||
barMaxWidth: 15,
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
shadowBlur: 10,
|
||||
shadowOffsetX: 0,
|
||||
shadowColor: 'rgba(255, 102, 0, 0.8)'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
chart.setOption(option)
|
||||
}
|
||||
|
||||
let resizeTimer = null
|
||||
|
||||
const handleResize = () => {
|
||||
clearTimeout(resizeTimer)
|
||||
resizeTimer = setTimeout(() => {
|
||||
renderChart()
|
||||
}, 300)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
renderChart()
|
||||
window.addEventListener('resize', handleResize)
|
||||
}, 500)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (chart) {
|
||||
chart.dispose()
|
||||
}
|
||||
window.removeEventListener('resize', handleResize)
|
||||
clearTimeout(resizeTimer)
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,166 @@
|
||||
<template>
|
||||
<div class="top-data">
|
||||
<!-- 销售信息 -->
|
||||
<div class="sales-info">
|
||||
<div class="sales-info-top">
|
||||
<div class="target-info">
|
||||
<h3>年度目标</h3>
|
||||
<div class="target-amount">{{ formatLargeNumber(yearData.target) }}</div>
|
||||
</div>
|
||||
<div class="total-info">
|
||||
<h3>年度总销售额</h3>
|
||||
<div class="total-amount">{{ formatLargeNumber(yearData.actual) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sales-info-cont">
|
||||
<div class="sales-info-bottom">
|
||||
<div class="progress-container">
|
||||
<div class="progress-bg">
|
||||
<div class="progress-bar" :style="{ width: progressWidth }">
|
||||
<div class="progress-info">
|
||||
<div class="play-btn"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="progress-text">{{ progressRate }}%</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="progress-label">完成率</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 年度进度 -->
|
||||
<div class="countdown">
|
||||
<h3>年度进度</h3>
|
||||
<div class="quarters-container">
|
||||
<div v-for="(quarter, index) in quarters" :key="quarter.id"
|
||||
class="quarter-item"
|
||||
:class="{ 'last-quarter': index === quarters.length - 1 }">
|
||||
<div class="quarter-label">{{ quarter.label }}</div>
|
||||
<div class="weeks-container">
|
||||
<div
|
||||
v-for="week in quarter.weeks"
|
||||
:key="week"
|
||||
class="week-circle"
|
||||
:class="{ 'current': week === currentWeek, 'passed': week < currentWeek }"
|
||||
:title="`第${week}周`"
|
||||
>
|
||||
{{ week }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
|
||||
const emit = defineEmits(['progress-rate'])
|
||||
|
||||
// 年度销售数据
|
||||
const yearData = ref({
|
||||
target: 3500,
|
||||
actual: 2750
|
||||
})
|
||||
|
||||
// 进度条宽度
|
||||
const progressWidth = ref('0%')
|
||||
const progressRate = ref('0.00')
|
||||
|
||||
// 获取当前周数
|
||||
const getCurrentWeek = () => {
|
||||
const now = new Date()
|
||||
const start = new Date(now.getFullYear(), 0, 1)
|
||||
const diff = now - start
|
||||
return Math.floor(diff / (7 * 24 * 60 * 60 * 1000)) + 1
|
||||
}
|
||||
|
||||
const currentWeek = ref(getCurrentWeek())
|
||||
|
||||
// 季度数据
|
||||
const quarters = ref([
|
||||
{ id: 'q1', label: 'Q1', start: 1, end: 13, weeks: [] },
|
||||
{ id: 'q2', label: 'Q2', start: 14, end: 26, weeks: [] },
|
||||
{ id: 'q3', label: 'Q3', start: 27, end: 39, weeks: [] },
|
||||
{ id: 'q4', label: 'Q4', start: 40, end: 52, weeks: [] }
|
||||
])
|
||||
|
||||
// 初始化季度周数
|
||||
quarters.value.forEach(quarter => {
|
||||
for (let i = quarter.start; i <= quarter.end; i++) {
|
||||
quarter.weeks.push(i)
|
||||
}
|
||||
})
|
||||
|
||||
// 获取年度数据(可替换为API调用)
|
||||
const fetchYearData = async () => {
|
||||
// TODO: 替换为实际的API调用
|
||||
// try {
|
||||
// const response = await fetch('/api/year-data')
|
||||
// const data = await response.json()
|
||||
// return data
|
||||
// } catch (error) {
|
||||
// console.error('获取数据失败:', error)
|
||||
// return { target: 0, actual: 0 }
|
||||
// }
|
||||
|
||||
// 当前使用模拟数据
|
||||
console.log('正在获取年度数据...')
|
||||
return {
|
||||
target: 3500,
|
||||
actual: 2750
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化大数字(万元)
|
||||
const formatLargeNumber = (num) => {
|
||||
return num.toLocaleString('zh-CN') + ' 万元'
|
||||
}
|
||||
|
||||
// 启动进度条动画
|
||||
const startProgressAnimation = (targetRate) => {
|
||||
const PROGRESS_STEP = 0.4
|
||||
const PROGRESS_INTERVAL = 20
|
||||
let value = 0
|
||||
let lastTime = 0
|
||||
|
||||
const animateProgress = (currentTime) => {
|
||||
if (!lastTime) lastTime = currentTime
|
||||
const deltaTime = currentTime - lastTime
|
||||
|
||||
if (deltaTime >= PROGRESS_INTERVAL) {
|
||||
value += PROGRESS_STEP
|
||||
if (value >= targetRate) {
|
||||
value = targetRate
|
||||
progressWidth.value = value + '%'
|
||||
return
|
||||
}
|
||||
progressWidth.value = value + '%'
|
||||
lastTime = currentTime
|
||||
}
|
||||
requestAnimationFrame(animateProgress)
|
||||
}
|
||||
|
||||
requestAnimationFrame(animateProgress)
|
||||
}
|
||||
|
||||
// 初始化数据
|
||||
onMounted(async () => {
|
||||
// 获取数据
|
||||
yearData.value = await fetchYearData()
|
||||
|
||||
// 计算完成率
|
||||
const rate = ((yearData.value.actual / yearData.value.target) * 100).toFixed(2)
|
||||
progressRate.value = rate
|
||||
|
||||
// 发射进度率事件
|
||||
emit('progress-rate', parseFloat(rate))
|
||||
|
||||
// 延迟启动进度条动画
|
||||
setTimeout(() => {
|
||||
startProgressAnimation(parseFloat(rate))
|
||||
}, 800)
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,5 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import './style.css'
|
||||
|
||||
createApp(App).mount('#app')
|
||||
@@ -0,0 +1,620 @@
|
||||
/* PC端等比例缩放,根字号固定 */
|
||||
html {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* 移动端基础字号 */
|
||||
@media screen and (max-width: 768px) {
|
||||
html {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Microsoft YaHei', Arial, sans-serif;
|
||||
color: #fff;
|
||||
min-width: 320px;
|
||||
overflow-x: hidden;
|
||||
/* PC端缩放相关样式 */
|
||||
transform-origin: left top;
|
||||
transition: transform 0.1s ease-out;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
background: #000;
|
||||
/* 固定最小宽度,避免缩放时内容挤压 */
|
||||
min-width: 1200px;
|
||||
}
|
||||
|
||||
.cont{
|
||||
width: 90%;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 0 1rem;
|
||||
/* 确保内容区域最小宽度 */
|
||||
min-width: 1080px;
|
||||
}
|
||||
|
||||
/* 移动端下调整宽度 */
|
||||
@media screen and (max-width: 768px) {
|
||||
body {
|
||||
transform: none !important;
|
||||
width: 100% !important;
|
||||
min-width: 320px !important;
|
||||
height: auto !important;
|
||||
overflow-x: hidden !important;
|
||||
}
|
||||
|
||||
.container {
|
||||
min-width: 320px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.cont {
|
||||
width: 95%;
|
||||
max-width: 100%;
|
||||
min-width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* 顶部数据区域 */
|
||||
.top-data {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1fr;
|
||||
gap: clamp(6px, 1.5vw, 14px);
|
||||
margin-bottom: clamp(10px, 1.5vw, 20px);
|
||||
}
|
||||
|
||||
.sales-info {
|
||||
background: url(/img/hicon.png);
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100% 100%;
|
||||
border-radius: clamp(4px, 0.5vw, 8px);
|
||||
padding: clamp(10px, 1.5vw, 20px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: clamp(10px, 1.5vw, 15px);
|
||||
}
|
||||
|
||||
.sales-info-top {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.target-info, .total-info {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.target-info h3,.total-info h3{
|
||||
font-size: clamp(0.75rem, 1vw, 1rem);
|
||||
color: #fff;
|
||||
margin-bottom: clamp(5px, 0.8vw, 8px);
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.target-amount {
|
||||
font-size: clamp(1.25rem, 2.5vw, 2.25rem);
|
||||
font-weight: bold;
|
||||
color: #F77436;
|
||||
margin-bottom: clamp(5px, 0.8vw, 10px);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.total-amount {
|
||||
font-size: clamp(1.25rem, 2.5vw, 2.25rem);
|
||||
font-weight: bold;
|
||||
color: #FA551F;
|
||||
margin-bottom: clamp(5px, 0.8vw, 10px);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.sales-info-cont{
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.sales-info-bottom {
|
||||
width: 90%;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
width: 100%;
|
||||
height: clamp(20px, 2.5vw, 30px);
|
||||
padding: 3px;
|
||||
border-radius: 20px;
|
||||
background: linear-gradient(180deg, #FF923C, #FF923C);
|
||||
box-shadow: 0px 0px 8px rgb(255 255 255 / 80%);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.progress-bg {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #DC7439;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 0%;
|
||||
border-radius: 16px;
|
||||
background: repeating-linear-gradient(45deg,rgba(255, 255, 255, 0.25) 0,rgba(255, 255, 255, 0.25) 8px,rgba(255, 255, 255, 0.12) 8px,rgba(255, 255, 255, 0.12) 16px),linear-gradient(180deg, #FFD85E, #FFBE25);
|
||||
animation: move 1.2s linear infinite;
|
||||
box-shadow: inset 0 0 6px rgba(255, 255, 255, 0.6);
|
||||
will-change: width;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.progress-info {
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: #fff;
|
||||
font-size: clamp(10px, 1.2vw, 13px);
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.play-btn {
|
||||
width: clamp(18px, 2vw, 24px);
|
||||
height: clamp(18px, 2vw, 24px);
|
||||
border-radius: 50%;
|
||||
background: #DC7439;
|
||||
border: 2px solid #FFBE25;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.play-btn::before {
|
||||
content: '';
|
||||
margin-left: 2px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-top: 5px solid transparent;
|
||||
border-bottom: 5px solid transparent;
|
||||
border-left: 7px solid #fff;
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
font-size: clamp(10px, 1.2vw, 13px);
|
||||
white-space: nowrap;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.progress-label {
|
||||
color: #DC7439;
|
||||
font-weight: bold;
|
||||
font-size: clamp(10px, 1.2vw, 13px);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 条纹滚动动画 */
|
||||
@keyframes move {
|
||||
from { background-position: 0 0; }
|
||||
to { background-position: 40px 0; }
|
||||
}
|
||||
|
||||
.completion-rate {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.countdown {
|
||||
border: 1px solid #523A14;
|
||||
border-radius: 8px;
|
||||
padding: clamp(8px, 1.5vw,10px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.countdown h3 {
|
||||
font-size: clamp(0.875rem, 1.25vw, 1.25rem);
|
||||
color: #e7552f;
|
||||
margin-bottom: clamp(10px, 1.5vw, 15px);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* 季度容器 */
|
||||
.quarters-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: clamp(5px, 1.5vw, 5px);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 单个季度 */
|
||||
.quarter-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: clamp(5px, 1vw, 8px);
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
border-bottom: 1px solid #523A14;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
.last-quarter{
|
||||
border-bottom: none;
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
|
||||
.quarter-label {
|
||||
background: #e7552f;
|
||||
color: #fff;
|
||||
font-size: clamp(0.875rem, 1.2vw, 1rem);
|
||||
font-weight: bold;
|
||||
padding: clamp(3px, 0.5vw, 5px) clamp(8px, 1vw, 12px);
|
||||
border-radius: 4px;
|
||||
min-width: clamp(35px, 5vw, 45px);
|
||||
text-align: center;
|
||||
box-shadow: 0 0 10px rgba(255, 102, 0, 0.5);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 周数圆圈容器 */
|
||||
.weeks-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: clamp(3px, 0.5vw, 5px);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* 单个周数圆圈 */
|
||||
.week-circle {
|
||||
width: clamp(18px, 2.5vw, 24px);
|
||||
height: clamp(18px, 2.5vw, 24px);
|
||||
border-radius: 50%;
|
||||
border: 2px solid #815B37;
|
||||
background: rgba(129, 91, 55, 0.2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: clamp(0.625rem, 1vw, 0.75rem);
|
||||
color: #f5bbab;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.week-circle:hover {
|
||||
background: rgba(231, 85, 47, 0.4);
|
||||
border-color: #e7552f;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
/* 当前周高亮 */
|
||||
.week-circle.current {
|
||||
background: #e7552f;
|
||||
border-color: #ff6600;
|
||||
box-shadow: 0 0 10px rgba(255, 102, 0, 0.8);
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
/* 已过去的周 */
|
||||
.week-circle.passed {
|
||||
background: #e7552f;
|
||||
border-color: #ff6600;
|
||||
box-shadow: 0 0 10px rgba(255, 102, 0, 0.8);
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
box-shadow: 0 0 10px rgba(255, 102, 0, 0.8);
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 0 20px rgba(255, 102, 0, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/* 小组排名区域 */
|
||||
.group-ranking {
|
||||
border: 1px solid #523A14;
|
||||
border-radius: 8px;
|
||||
padding: clamp(10px, 1.5vw, 20px);
|
||||
margin-bottom: clamp(10px, 1.5vw, 20px);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: clamp(0.875rem, 1.25vw, 1rem);
|
||||
color: #fff;
|
||||
background: #ff6600;
|
||||
padding: clamp(5px, 0.8vw, 5px) clamp(10px, 1.5vw, 15px);
|
||||
display: inline-block;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.ranking-content {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: clamp(10px, 1.5vw, 20px);
|
||||
}
|
||||
|
||||
.ranking-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: clamp(8px, 1vw, 10px);
|
||||
}
|
||||
|
||||
.ranking-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: clamp(8px, 1.2vw, 12px) clamp(10px, 1.5vw, 15px);
|
||||
border-radius: 4px;
|
||||
font-size: clamp(0.75rem, 1vw, 0.875rem);
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.ranking-item.top-1 {
|
||||
background: linear-gradient(90deg, rgba(199, 173, 61, 0.4), rgba(199, 173, 61, 0.1));
|
||||
border: 1px solid rgba(199, 173, 61, 0.4);
|
||||
}
|
||||
|
||||
.ranking-item.top-2 {
|
||||
background: linear-gradient(90deg, rgba(136, 136, 135, 0.5), rgba(136, 136, 135, 0.1));
|
||||
border: 1px solid rgba(136, 136, 135, 0.4);
|
||||
}
|
||||
|
||||
.ranking-item.top-3 {
|
||||
background: linear-gradient(90deg, rgba(59, 96, 93, 0.6), rgba(59, 96, 93,0.1));
|
||||
border: 1px solid rgba(59, 96, 93, 0.4);
|
||||
}
|
||||
|
||||
.ranking-item.other {
|
||||
background: linear-gradient(90deg, rgba(100, 100, 100, 0.3), rgba(100, 100, 100, 0.1));
|
||||
border: 1px solid rgba(100, 100, 100, 0.4);
|
||||
}
|
||||
|
||||
.rank-number {
|
||||
font-size: clamp(0.875rem, 1.25vw, 1.25rem);
|
||||
font-weight: bold;
|
||||
margin-right: clamp(10px, 1.5vw, 15px);
|
||||
min-width: 20px;
|
||||
width: clamp(20px, 3vw, 25px);
|
||||
height: clamp(20px, 3vw, 25px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.ranking-item.top-1 .rank-number {
|
||||
background: #C7AD3D;
|
||||
color: #000;
|
||||
box-shadow: 0 0 8px rgba(255, 204, 0, 0.8);
|
||||
}
|
||||
|
||||
.ranking-item.top-2 .rank-number {
|
||||
background: #888887;
|
||||
color: #000;
|
||||
box-shadow: 0 0 8px rgba(192, 192, 192, 0.8);
|
||||
}
|
||||
|
||||
.ranking-item.top-3 .rank-number {
|
||||
background: #3B605D;
|
||||
color: #fff;
|
||||
box-shadow: 0 0 8px rgba(205, 127, 50, 0.8);
|
||||
}
|
||||
|
||||
.ranking-item.other .rank-number {
|
||||
background: #815B37;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.rank-name {
|
||||
font-weight: bold;
|
||||
margin-right: clamp(10px, 1.5vw, 20px);
|
||||
min-width: clamp(60px, 8vw, 90px);
|
||||
color: #fff;
|
||||
font-size: clamp(0.75rem, 1.1vw, 0.875rem);
|
||||
}
|
||||
|
||||
.rank-sales {
|
||||
flex: 1;
|
||||
margin-right: clamp(10px, 1.5vw, 20px);
|
||||
color: #F77436;
|
||||
font-size: clamp(0.6875rem, 1vw, 0.8125rem);
|
||||
}
|
||||
|
||||
.rank-rate {
|
||||
font-weight: bold;
|
||||
color: #FA3925;
|
||||
font-size: clamp(0.6875rem, 1vw, 0.8125rem);
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
height: 100%;
|
||||
min-height: clamp(250px, 30vw, 300px);
|
||||
}
|
||||
|
||||
.ranking-content {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
/* 个人排名区域 */
|
||||
.person-ranking {
|
||||
border: 1px solid #523A14;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
|
||||
/* 移动端优化 (768px及以下) */
|
||||
@media (max-width: 768px) {
|
||||
.top-data {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.ranking-content {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.sales-info {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.sales-info-top {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.sales-info-cont {
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.sales-info-bottom {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: clamp(0.875rem, 2.5vw, 1.125rem);
|
||||
}
|
||||
|
||||
.quarter-label {
|
||||
font-size: clamp(0.75rem, 2vw, 0.875rem);
|
||||
padding: clamp(2px, 0.5vw, 4px) clamp(6px, 1.5vw, 10px);
|
||||
min-width: clamp(30px, 5vw, 40px);
|
||||
}
|
||||
|
||||
.week-circle {
|
||||
width: clamp(16px, 4vw, 20px);
|
||||
height: clamp(16px, 4vw, 20px);
|
||||
font-size: clamp(0.5rem, 1.5vw, 0.625rem);
|
||||
}
|
||||
|
||||
.ranking-item {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
grid-template-rows: auto auto;
|
||||
gap: clamp(5px, 1vw, 8px);
|
||||
font-size: clamp(0.6875rem, 2vw, 0.875rem);
|
||||
padding: clamp(10px, 2vw, 15px);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.ranking-item .rank-number {
|
||||
grid-column: 1;
|
||||
grid-row: 1 / 3;
|
||||
font-size: clamp(0.875rem, 2.5vw, 1.125rem);
|
||||
width: clamp(18px, 5vw, 25px);
|
||||
height: clamp(18px, 5vw, 25px);
|
||||
min-width: 18px;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.ranking-item .rank-name {
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
margin-bottom: 0;
|
||||
margin-right: 0;
|
||||
font-size: clamp(0.75rem, 2.2vw, 0.875rem);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.ranking-item .rank-sales {
|
||||
grid-column: 2;
|
||||
grid-row: 2;
|
||||
margin-right: 0;
|
||||
margin-bottom: 0;
|
||||
margin-left: 0;
|
||||
margin-top: 0;
|
||||
font-size: clamp(0.625rem, 1.8vw, 0.75rem);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.ranking-item .rank-rate {
|
||||
grid-column: 3;
|
||||
grid-row: 1 / 3;
|
||||
margin-left: 0;
|
||||
flex-shrink: 0;
|
||||
font-size: clamp(0.6875rem, 2vw, 0.8125rem);
|
||||
align-self: center;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
min-height: clamp(200px, 40vw, 250px);
|
||||
}
|
||||
}
|
||||
|
||||
/* 超小屏幕优化 (480px及以下) */
|
||||
@media (max-width: 480px) {
|
||||
.quarters-container {
|
||||
gap: clamp(5px, 2vw, 8px);
|
||||
}
|
||||
|
||||
.quarter-label {
|
||||
font-size: clamp(0.625rem, 2.5vw, 0.75rem);
|
||||
padding: clamp(2px, 0.5vw, 3px) clamp(5px, 1.5vw, 8px);
|
||||
min-width: clamp(25px, 6vw, 35px);
|
||||
}
|
||||
|
||||
.week-circle {
|
||||
width: clamp(14px, 4.5vw, 18px);
|
||||
height: clamp(14px, 4.5vw, 18px);
|
||||
font-size: clamp(0.4375rem, 2vw, 0.5625rem);
|
||||
gap: clamp(2px, 0.5vw, 3px);
|
||||
}
|
||||
}
|
||||
|
||||
/* 头部和尾部图片 */
|
||||
.header-image {
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.footer-image {
|
||||
margin-top: 20px;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.header-image img,
|
||||
.footer-image img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
/* 排名数字内联样式替代 */
|
||||
.ranking-item .rank-number {
|
||||
color: #000;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
server: {
|
||||
port: 3000,
|
||||
open: true
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user