11 Commits

Author SHA1 Message Date
浅念 8f9997828d 调整样式 2026-02-13 15:59:30 +08:00
浅念 4165a9a78f 移动端样式 2026-02-13 15:53:06 +08:00
浅念 557deaaa84 修改移动端 2026-02-13 15:43:02 +08:00
浅念 7feef8e17a 响应式 2026-02-13 15:01:27 +08:00
浅念 95a7deac4c 响应式 2026-02-13 14:59:45 +08:00
浅念 14223c7543 换底部图片 2026-02-13 14:48:39 +08:00
浅念 081cbdeaed 响应式 2026-02-13 14:45:33 +08:00
浅念 082ad32437 年度进度调整 2026-02-13 14:36:11 +08:00
浅念 7624633183 响应式 2026-02-13 14:22:40 +08:00
mzl92 2bb619ddf7 feat: 前端对接销售数据API
- TopData组件对接/api/sales/year-data获取年度数据
- GroupRanking组件对接/api/sales/group-ranking获取小组排名
- PersonRanking组件对接/api/sales/person-ranking获取个人排名
- 添加.env配置文件,设置API_BASE_URL

🤖 Generated with Claude Code
https://claude.com/claude-code
2026-02-13 13:16:51 +08:00
mzl92 4917f2e01e feat: 接入协作框架
- 创建 ai/contracts.yaml 定义服务契约
- 创建 ai/CHANGELOG_AI.md 记录项目变更
- 定义数据接口格式(年度数据、排名数据、季度进度)
- 定义技术栈(React 18 + Vite 5 / Vue 3 + Vite 5 / 原生 HTML)
- 更新 CHANGES.md 记录项目加入

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-12 15:57:34 +08:00
16 changed files with 1552 additions and 273 deletions
+72
View File
@@ -0,0 +1,72 @@
# sales_data_dashboard 变更日志
> 记录销售数据仪表盘项目的所有重要变更。
---
## [Unreleased]
### Added
- 项目接入协作框架
- 创建 `ai/contracts.yaml` 定义服务契约
- 定义数据接口格式(年度数据、排名数据、季度进度)
- 定义技术栈(React 18 + Vite 5 / Vue 3 + Vite 5 / 原生 HTML)
---
## [1.0.0] - 2026-02-12
### Added
- 初始化销售数据仪表盘项目
- 实现三个版本:原生 HTML、React、Vue
- 实现年度销售目标和实际销售额展示
- 实现销售完成率进度条
- 实现年度季度进度显示(Q1-Q4)
- 实现小组销售排名(列表 + 柱状图)
- 实现个人销售排名(列表 + 柱状图)
- 支持响应式设计(PC 端等比例缩放,移动端自适应)
- 集成 ECharts 5.4 进行数据可视化
### Features (React 版本)
- React Hooks (useState, useEffect, useRef)
- 组件化开发(TopData、GroupRanking、PersonRanking)
- 防抖处理窗口 resize
- 图表自动销毁重建
### Features (Vue 版本)
- Vue 3 Composition API
- Setup script 语法
- 响应式数据绑定
- 生命周期钩子(onMounted、onUnmounted)
### Features (原生 HTML 版本)
- 原生 JavaScript 实现
- DOM 操作
- Fetch API 数据获取
### Technical Details
- **技术栈**:React 18 / Vue 3 / Vite 5 / ECharts 5.4
- **开发端口**:React (3001)、Vue (3002)
- **构建工具**:Vite 5
- **样式**:原生 CSS3
- **浏览器支持**:Chrome、Firefox、Safari、Edge
---
## 变更类型说明
- **Added**: 新增功能
- **Changed**: 功能变更
- **Deprecated**: 即将废弃的功能
- **Removed**: 已删除的功能
- **Fixed**: 问题修复
- **Security**: 安全性改进
---
## 发布流程
1. 更新 CHANGELOG_AI.md
2. 更新版本号(package.json)
3. 创建 Git tag
4. 推送到远程仓库
+189
View File
@@ -0,0 +1,189 @@
# sales_data_dashboard 服务契约
# 销售数据统计仪表盘 - 数据可视化大屏
# API 规范(遵循中控服务定义的规范)
api_spec: ../../_coordination/API_SPEC.md
# 服务描述
description: >
销售数据统计仪表盘,用于展示年度销售目标完成情况、小组排名和个人排名。
提供三个版本实现:原生 HTML、React 18 + Vite 5、Vue 3 + Vite 5。
支持响应式设计,适配 PC 端和移动端,使用 ECharts 5 进行数据可视化。
# 职责
# - 展示年度销售目标和实际销售额对比
# - 展示销售完成率进度条
# - 展示年度季度进度(Q1-Q4)
# - 展示小组销售排名(列表 + 柱状图)
# - 展示个人销售排名(列表 + 柱状图)
# - 支持数据动态加载和实时更新
# 自己拥有的(owns)
owns:
# API 接口(无后端 API,纯前端展示)
apis: []
# 数据存储(无数据库,使用 API 获取数据)
tables: []
# 领域事件(无事件发布)
events: []
# 枚举类型
enums:
- name: "RankingType"
description: "排名类型"
values: ["group", "person"]
module: "ranking"
- name: "ChartType"
description: "图表类型"
values: ["bar", "line", "pie"]
module: "chart"
# 业务流程
process:
- name: "数据加载流程"
description: "从后端 API 获取销售数据(年度目标、实际销售额、排名数据)"
module: "data"
- name: "数据可视化流程"
description: "使用 ECharts 渲染图表(柱状图、折线图、饼图)"
module: "chart"
- name: "响应式适配流程"
description: "根据屏幕尺寸自动调整布局和图表大小"
module: "ui"
# 依赖的外部服务(depends_on)
depends_on:
# 后端 API(需要对接)
apis:
- name: "GET /api/sales/year-data"
description: "获取年度销售数据(目标、实际)"
provider: "backend"
critical: true
- name: "GET /api/sales/group-ranking"
description: "获取小组销售排名"
provider: "backend"
critical: true
- name: "GET /api/sales/person-ranking"
description: "获取个人销售排名"
provider: "backend"
critical: true
- name: "GET /api/sales/quarter-progress"
description: "获取季度进度数据"
provider: "backend"
critical: false
# 外部事件
events: []
# 技术栈(React 版本)
tech_stack:
type: "web-app"
framework: "Vite 5"
language: "React 18 + JavaScript"
ui_library: "无(原生 CSS)"
styling: "CSS3"
build_tool: "Vite 5"
state_management: "React Hooks"
deployment: "Static Site"
api_client: "Fetch API"
chart_library: "ECharts 5.4"
# 技术栈(Vue 版本)
tech_stack_vue:
type: "web-app"
framework: "Vite 5"
language: "Vue 3 + JavaScript"
ui_library: "无(原生 CSS)"
styling: "CSS3(scoped)"
build_tool: "Vite 5"
state_management: "Vue 3 Composition API"
deployment: "Static Site"
api_client: "Fetch API"
chart_library: "ECharts 5.4"
# 技术栈(原生 HTML 版本)
tech_stack_vanilla:
type: "web-app"
framework: "无(原生 HTML/CSS/JS)"
language: "JavaScript ES6+"
ui_library: "无"
styling: "CSS3"
build_tool: "无"
state_management: "无(直接 DOM 操作)"
deployment: "Static Site"
api_client: "Fetch API"
chart_library: "ECharts 5.4"
# 部署配置
deployment:
type: "Static Site"
react_app_command: "cd react-app && npm run dev"
vue_app_command: "cd vue-app && npm run dev"
vanilla_command: "使用静态服务器(如 nginx)"
build_command: "cd react-app && npm run build"
output_dir: "react-app/dist"
dev_port_react: 3001
dev_port_vue: 3002
# 环境变量
env:
- name: "VITE_API_BASE_URL"
description: "后端 API 地址"
default: "http://localhost:8000/api"
required: true
# 数据接口格式(待对接)
data_formats:
# 年度数据格式
year_data:
target:
type: "number"
description: "年度销售目标"
example: 3500
actual:
type: "number"
description: "年度实际销售额"
example: 2750
# 排名数据格式
ranking_data:
type: "array"
items:
name:
type: "string"
description: "名称(小组名或个人姓名)"
target:
type: "number"
description: "销售目标"
actual:
type: "number"
description: "实际销售额"
example:
- name: "华东一组"
target: 500
actual: 450
- name: "华东二组"
target: 500
actual: 380
# 季度进度数据格式
quarter_progress:
type: "object"
description: "季度进度信息(可选,前端也可自动计算)"
properties:
current_quarter:
type: "number"
description: "当前季度(1-4)"
current_week:
type: "number"
description: "当前季度第几周"
total_weeks:
type: "number"
description: "当前季度总周数"
+335 -31
View File
@@ -21,49 +21,33 @@ body {
color: #fff;
min-width: 320px;
overflow-x: hidden;
/* PC端缩放相关样式 */
transform-origin: left top;
transition: transform 0.1s ease-out;
margin: 0;
padding: 0;
}
.container {
width: 100%;
min-height: 100vh;
height: auto;
display: flex;
flex-direction: column;
justify-content: space-between;
background: #000;
/* 固定最小宽度,避免缩放时内容挤压 */
min-width: 1200px;
}
.cont{
width: 90%;
max-width: 1400px;
max-width: 1600px;
margin: 0 auto;
padding: 0 1rem;
/* 确保内容区域最小宽度 */
min-width: 1080px;
/* padding: clamp(0.5rem, 1.5vw, 2rem); */
}
/* 移动端下调整宽度 */
/* 移动端优化 (768px及以下) */
@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;
padding: 0.5rem;
}
}
@@ -94,6 +78,7 @@ body {
grid-template-columns: 2fr 1fr;
gap: clamp(6px, 1.5vw, 14px);
margin-bottom: clamp(10px, 1.5vw, 20px);
min-height: 0; /* 允许grid子项收缩 */
}
.sales-info {
@@ -268,6 +253,7 @@ body {
flex-direction: column;
align-items: center;
justify-content: center;
overflow: hidden; /* 防止溢出 */
}
.countdown h3 {
@@ -275,6 +261,7 @@ body {
color: #e7552f;
margin-bottom: clamp(10px, 1.5vw, 15px);
font-weight: bold;
white-space: nowrap; /* 标题不换行 */
}
/* 季度容器 */
@@ -294,6 +281,7 @@ body {
min-width: 0;
border-bottom: 1px solid #523A14;
padding-bottom: 8px;
overflow: hidden; /* PC端防止内容溢出 */
}
.last-quarter{
border-bottom: none;
@@ -316,26 +304,34 @@ body {
/* 周数圆圈容器 */
.weeks-container {
display: flex;
flex-wrap: wrap;
gap: clamp(3px, 0.5vw, 5px);
flex-wrap: nowrap; /* PC端不换行 */
gap: clamp(2px, 0.4vw, 5px);
flex: 1;
overflow-x: hidden; /* 隐藏溢出内容 */
overflow-y: hidden;
min-width: 0; /* 允许flex子项缩小 */
justify-content: flex-start; /* 从左侧开始排列 */
}
/* 单个周数圆圈 */
.week-circle {
width: clamp(18px, 2.5vw, 24px);
height: clamp(18px, 2.5vw, 24px);
width: clamp(12px, 2.5vw, 24px);
height: clamp(12px, 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);
font-size: clamp(0.5rem, 1vw, 0.75rem);
color: #f5bbab;
font-weight: bold;
cursor: pointer;
transition: all 0.3s ease;
flex-shrink: 1; /* PC端允许周圈缩小以适应容器 */
min-width: 10px; /* 最小尺寸 */
min-height: 10px;
flex-basis: clamp(12px, 2.5vw, 24px); /* 弹性基础尺寸 */
}
.week-circle:hover {
@@ -544,6 +540,21 @@ body {
font-size: clamp(0.875rem, 2.5vw, 1.125rem);
}
/* 移动端年度进度允许换行 */
.countdown {
overflow: visible; /* 移动端允许溢出换行 */
}
.quarter-item {
overflow: visible; /* 移动端允许内容换行 */
}
.weeks-container {
flex-wrap: wrap; /* 移动端允许换行 */
overflow-x: visible;
overflow-y: visible;
}
.quarter-label {
font-size: clamp(0.75rem, 2vw, 0.875rem);
padding: clamp(2px, 0.5vw, 4px) clamp(6px, 1.5vw, 10px);
@@ -554,6 +565,10 @@ body {
width: clamp(16px, 4vw, 20px);
height: clamp(16px, 4vw, 20px);
font-size: clamp(0.5rem, 1.5vw, 0.625rem);
flex-shrink: 0; /* 移动端不缩小 */
min-width: auto;
min-height: auto;
flex-basis: auto;
}
.ranking-item {
@@ -639,15 +654,135 @@ body {
}
}
/* 笔记本小屏幕特别优化 (1024px - 1366px) */
@media screen and (min-width: 1024px) and (max-width: 1366px) {
/* 年度进度区域紧凑显示 */
.countdown {
padding: clamp(6px, 1vw, 8px);
}
.countdown h3 {
font-size: clamp(0.7rem, 1vw, 0.9rem);
margin-bottom: clamp(6px, 1vw, 10px);
}
.quarters-container {
gap: clamp(3px, 0.8vw, 4px);
}
.quarter-item {
gap: clamp(3px, 0.6vw, 5px);
padding-bottom: clamp(5px, 0.7vw, 6px);
}
.quarter-label {
font-size: clamp(0.7rem, 1vw, 0.85rem);
padding: clamp(2px, 0.3vw, 3px) clamp(5px, 0.8vw, 8px);
min-width: clamp(28px, 4vw, 35px);
}
.week-circle {
width: clamp(14px, 2vw, 18px);
height: clamp(14px, 2vw, 18px);
font-size: clamp(0.5rem, 0.8vw, 0.625rem);
border-width: 1px;
}
.weeks-container {
gap: clamp(1px, 0.3vw, 3px);
}
}
/* 高分辨率小笔记本优化 (125%-200% DPI缩放)
比如:13寸1920x1080 (150%缩放实际约1280px), 14寸2560x1440 (200%缩放实际约1280px) */
@media screen and (min-width: 1280px) and (max-width: 1600px) {
/* 当年度进度容器宽度不足时的自动缩放 */
.countdown {
padding: clamp(6px, 1vw, 10px);
/* 使用容器查询的方式,如果内容太多则整体缩小 */
min-width: 0;
}
.countdown h3 {
font-size: clamp(0.75rem, 1.1vw, 1.1rem);
margin-bottom: clamp(8px, 1.2vw, 12px);
}
.quarters-container {
gap: clamp(4px, 0.9vw, 6px);
}
.quarter-item {
gap: clamp(4px, 0.7vw, 7px);
padding-bottom: clamp(6px, 0.8vw, 8px);
}
.quarter-label {
font-size: clamp(0.72rem, 1.05vw, 0.9rem);
padding: clamp(2px, 0.35vw, 4px) clamp(6px, 0.85vw, 10px);
min-width: clamp(30px, 4.2vw, 38px);
}
.week-circle {
width: clamp(15px, 2.1vw, 20px);
height: clamp(15px, 2.1vw, 20px);
font-size: clamp(0.5rem, 0.85vw, 0.65rem);
border-width: 1.5px;
}
.weeks-container {
gap: clamp(2px, 0.35vw, 4px);
}
}
/* 极致紧凑模式 - 高DPI小屏幕 (13-14寸高分辨率笔记本)
检测16:9或16:10宽高比 + 中等分辨率 */
@media screen and (min-width: 1200px) and (max-width: 1440px) and (min-aspect-ratio: 16/10) {
.countdown {
padding: clamp(5px, 0.9vw, 8px);
}
.countdown h3 {
font-size: clamp(0.7rem, 1vw, 0.95rem);
margin-bottom: clamp(6px, 1vw, 10px);
}
.quarters-container {
gap: clamp(3px, 0.7vw, 5px);
}
.quarter-item {
gap: clamp(3px, 0.6vw, 6px);
padding-bottom: clamp(5px, 0.7vw, 7px);
}
.quarter-label {
font-size: clamp(0.68rem, 1vw, 0.82rem);
padding: clamp(2px, 0.3vw, 4px) clamp(5px, 0.8vw, 9px);
min-width: clamp(28px, 4vw, 35px);
}
.week-circle {
width: clamp(14px, 2vw, 18px);
height: clamp(14px, 2vw, 18px);
font-size: clamp(0.48rem, 0.8vw, 0.6rem);
border-width: 1px;
}
.weeks-container {
gap: clamp(1.5px, 0.3vw, 3px);
}
}
/* 头部和尾部图片 */
.header-image {
margin-bottom: 20px;
margin-bottom: clamp(10px, 1.5vw, 20px);
text-align: center;
width: 100%;
}
.footer-image {
margin-top: 20px;
margin-top: clamp(10px, 1.5vw, 20px);
text-align: center;
width: 100%;
}
@@ -665,3 +800,172 @@ body {
.ranking-item .rank-number {
color: #000;
}
/* 窄容器优化 - 适配所有高DPI小笔记本
当容器宽度小于400px时(比如高分辨率小笔记本的系统缩放) */
@media screen and (min-width: 769px) and (max-width: 1400px) {
.top-data {
/* 如果年度进度区域太窄,调整比例 */
grid-template-columns: 2fr minmax(280px, 1fr);
}
}
/* 极窄容器优化 - 当容器实际显示宽度很小 */
@media screen and (min-width: 769px) and (max-width: 1200px) {
.top-data {
grid-template-columns: 1fr 1fr;
}
}
/* ========== 响应式布局 - 不同屏幕尺寸优化 ========== */
/* 小屏幕笔记本 (1366x768, 1440x900) */
@media screen and (min-width: 769px) and (max-width: 1600px) {
.cont {
width: 95%;
/* padding: clamp(0.5rem, 1.5vw, 1.5rem); */
}
.section-title {
font-size: clamp(0.8rem, 1.1vw, 0.95rem);
}
.chart-container {
min-height: clamp(220px, 28vw, 280px);
}
/* 年度进度区域优化 */
.countdown {
padding: clamp(6px, 1vw, 10px);
}
.countdown h3 {
font-size: clamp(0.75rem, 1.1vw, 1.1rem);
margin-bottom: clamp(8px, 1.2vw, 12px);
}
.quarter-item {
gap: clamp(4px, 0.8vw, 6px);
padding-bottom: clamp(6px, 0.8vw, 8px);
}
.quarter-label {
font-size: clamp(0.75rem, 1.1vw, 0.95rem);
padding: clamp(2px, 0.4vw, 4px) clamp(6px, 0.9vw, 10px);
min-width: clamp(32px, 4.5vw, 40px);
}
.week-circle {
width: clamp(16px, 2.2vw, 22px);
height: clamp(16px, 2.2vw, 22px);
font-size: clamp(0.5625rem, 0.9vw, 0.6875rem);
border-width: 1.5px;
}
.weeks-container {
gap: clamp(2px, 0.4vw, 4px);
}
}
/* 标准全高清 (1920x1080) */
@media screen and (min-width: 1601px) and (max-width: 2100px) {
.cont {
width: 90%;
max-width: 1700px;
}
.chart-container {
min-height: clamp(250px, 28vw, 320px);
}
}
/* 2K 屏幕 (2560x1440) */
@media screen and (min-width: 2101px) and (max-width: 3200px) {
.cont {
width: 85%;
max-width: 2200px;
}
.section-title {
font-size: clamp(0.9rem, 1.2vw, 1.1rem);
padding: clamp(6px, 0.9vw, 8px) clamp(12px, 1.6vw, 18px);
}
.top-data {
gap: clamp(10px, 1.5vw, 18px);
margin-bottom: clamp(15px, 1.8vw, 25px);
}
.chart-container {
min-height: clamp(280px, 28vw, 380px);
}
.ranking-item {
padding: clamp(10px, 1.3vw, 14px) clamp(12px, 1.6vw, 18px);
font-size: clamp(0.8rem, 1.1vw, 0.95rem);
}
.header-image,
.footer-image {
margin: clamp(12px, 1.8vw, 25px) auto;
}
}
/* 4K 屏幕 (3840x2160 及以上) */
@media screen and (min-width: 3201px) {
.cont {
width: 80%;
max-width: 3000px;
padding: clamp(1.5rem, 2vw, 3rem);
}
.section-title {
font-size: clamp(1rem, 1.5vw, 1.3rem);
padding: clamp(8px, 1vw, 10px) clamp(15px, 2vw, 25px);
}
.top-data {
gap: clamp(15px, 2vw, 25px);
margin-bottom: clamp(20px, 2.5vw, 30px);
}
.sales-info {
padding: clamp(15px, 2vw, 25px);
gap: clamp(15px, 2vw, 20px);
}
.countdown {
padding: clamp(12px, 1.8vw, 15px);
}
.group-ranking,
.person-ranking {
padding: clamp(15px, 2vw, 30px);
margin-bottom: clamp(15px, 2vw, 25px);
}
.chart-container {
min-height: clamp(350px, 30vw, 500px);
}
.ranking-item {
padding: clamp(12px, 1.5vw, 18px) clamp(15px, 2vw, 22px);
font-size: clamp(0.85rem, 1.2vw, 1rem);
}
.rank-number {
width: clamp(22px, 3.5vw, 28px);
height: clamp(22px, 3.5vw, 28px);
}
.header-image,
.footer-image {
margin: clamp(15px, 2.5vw, 30px) auto;
}
/* 4K屏幕下的文字大小优化 */
.target-amount,
.total-amount {
font-size: clamp(1.5rem, 3vw, 2.5rem);
}
}
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 101 KiB

+84 -50
View File
@@ -5,7 +5,6 @@ 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 = {
@@ -13,49 +12,6 @@ let yearData = {
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 },
@@ -556,9 +512,6 @@ function renderPersonChart() {
// 初始化
document.addEventListener('DOMContentLoaded', async () => {
// PC端等比例缩放
setScale();
// 获取年度数据并更新顶部显示
yearData = await fetchYearData();
const progressRate = updateTopData();
@@ -591,9 +544,6 @@ document.addEventListener('DOMContentLoaded', async () => {
// 响应式调整 - 使用防抖优化性能
window.addEventListener('resize', () => {
// 立即更新缩放(不防抖,保证视觉效果流畅)
setScale();
// 图表重新渲染使用防抖
clearTimeout(resizeTimer);
resizeTimer = setTimeout(() => {
@@ -643,6 +593,90 @@ document.addEventListener('DOMContentLoaded', async () => {
initQuarters();
// 动态调整周圈大小以适应容器宽度
function adjustWeekCirclesSize() {
const weekContainers = document.querySelectorAll('.weeks-container');
// 只在PC端(>768px)时执行动态调整,移动端清除内联样式
const isMobile = window.innerWidth <= 768;
weekContainers.forEach(container => {
// 获取所有周圈
const weekCircles = container.querySelectorAll('.week-circle');
if (weekCircles.length === 0) return;
// 移动端:清除所有内联样式,让 CSS 样式生效
if (isMobile) {
weekCircles.forEach(circle => {
circle.style.width = '';
circle.style.height = '';
circle.style.fontSize = '';
circle.style.borderWidth = '';
});
container.style.transform = '';
return;
}
// PC端:动态调整周圈大小
// 获取容器的实际宽度
const containerWidth = container.offsetWidth;
// 计算所有周圈的总宽度(包括间距)
const gap = 3; // 默认间距
const totalGap = gap * (weekCircles.length - 1);
const circleCount = weekCircles.length;
// 计算理想的周圈大小
const idealSize = (containerWidth - totalGap) / circleCount;
// 设置最小和最大尺寸限制
const minSize = 10;
const maxSize = 24;
// 计算最终尺寸
let finalSize = Math.max(minSize, Math.min(idealSize, maxSize));
// 如果内容仍然溢出,计算缩放比例
const totalContentWidth = (finalSize * circleCount) + totalGap;
let scale = 1;
if (totalContentWidth > containerWidth) {
scale = containerWidth / totalContentWidth;
// 限制最小缩放比例,避免过小
scale = Math.max(0.5, scale);
}
// 应用样式到所有周圈
weekCircles.forEach(circle => {
circle.style.width = finalSize + 'px';
circle.style.height = finalSize + 'px';
circle.style.fontSize = (finalSize * 0.4) + 'px'; // 字体大小随圆圈缩放
circle.style.borderWidth = Math.max(1, finalSize / 8) + 'px'; // 边框随圆圈缩放
});
// 如果需要缩放整个容器
if (scale < 1) {
container.style.transform = `scaleX(${scale})`;
container.style.transformOrigin = 'left';
} else {
container.style.transform = 'none';
}
});
}
// 初始调整
setTimeout(() => {
adjustWeekCirclesSize();
}, 100);
// 监听窗口大小变化并重新调整
window.addEventListener('resize', () => {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(() => {
adjustWeekCirclesSize();
}, RESIZE_DEBOUNCE_TIME);
});
// 延迟启动进度条动画,使用动态计算的完成率
setTimeout(() => {
startProgressAnimation(progressRate);
+1
View File
@@ -0,0 +1 @@
VITE_API_BASE_URL=http://localhost:8000/api
Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 101 KiB

+1 -56
View File
@@ -7,68 +7,13 @@ 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 (
+33 -5
View File
@@ -1,4 +1,4 @@
import { useEffect, useRef } from 'react'
import { useEffect, useRef, useState } from 'react'
import * as echarts from 'echarts'
function GroupRanking() {
@@ -7,14 +7,36 @@ function GroupRanking() {
const MOBILE_BREAKPOINT = 768
// 小组数据
const groupData = [
// 小组数据(从 API 获取)
const [groupData, setGroupData] = useState([
{ 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 fetchGroupRanking = async () => {
try {
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000/api'
const response = await fetch(`${API_BASE_URL}/sales/group-ranking?year=2024`)
const result = await response.json()
console.log('小组排名 API 响应:', result)
// 转换单位:API 返回的是万元,需要转换为元
const rankings = result.rankings.map(item => ({
name: item.name,
target: item.target * 10000, // 万元转元
actual: item.actual * 10000
}))
setGroupData(rankings)
} catch (error) {
console.error('获取小组排名失败:', error)
// 保持默认数据
}
}
// 格式化数字(万元)
const formatNumber = (num) => {
@@ -203,6 +225,12 @@ function GroupRanking() {
chartInstance.current.setOption(option)
}
// 初始加载数据
useEffect(() => {
fetchGroupRanking()
}, [])
// 数据变化时重新渲染图表
useEffect(() => {
const timer = setTimeout(() => {
renderChart()
@@ -226,7 +254,7 @@ function GroupRanking() {
chartInstance.current.dispose()
}
}
}, [])
}, [groupData])
return (
<div className="group-ranking">
+33 -5
View File
@@ -1,4 +1,4 @@
import { useEffect, useRef } from 'react'
import { useEffect, useRef, useState } from 'react'
import * as echarts from 'echarts'
function PersonRanking() {
@@ -7,8 +7,8 @@ function PersonRanking() {
const MOBILE_BREAKPOINT = 768
// 个人数据
const personData = [
// 个人数据(从 API 获取)
const [personData, setPersonData] = useState([
{ name: '张伟', target: 5000000000, actual: 7200000000 },
{ name: '李娜', target: 4500000000, actual: 5800000000 },
{ name: '王强', target: 4000000000, actual: 4900000000 },
@@ -16,7 +16,29 @@ function PersonRanking() {
{ name: '陈静', target: 3500000000, actual: 3600000000 },
{ name: '杨明', target: 3200000000, actual: 2800000000 },
{ name: '赵丽', target: 3000000000, actual: 2500000000 }
]
])
// 获取个人排名数据
const fetchPersonRanking = async () => {
try {
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000/api'
const response = await fetch(`${API_BASE_URL}/sales/person-ranking?year=2024`)
const result = await response.json()
console.log('个人排名 API 响应:', result)
// 转换单位:API 返回的是万元,需要转换为元
const rankings = result.rankings.map(item => ({
name: item.name,
target: item.target * 10000, // 万元转元
actual: item.actual * 10000
}))
setPersonData(rankings)
} catch (error) {
console.error('获取个人排名失败:', error)
// 保持默认数据
}
}
// 格式化数字(万元)
const formatNumber = (num) => {
@@ -202,6 +224,12 @@ function PersonRanking() {
chartInstance.current.setOption(option)
}
// 初始加载数据
useEffect(() => {
fetchPersonRanking()
}, [])
// 数据变化时重新渲染图表
useEffect(() => {
const timer = setTimeout(() => {
renderChart()
@@ -225,7 +253,7 @@ function PersonRanking() {
chartInstance.current.dispose()
}
}
}, [])
}, [personData])
return (
<div className="person-ranking">
+94 -4
View File
@@ -6,6 +6,66 @@ function TopData({ onProgressRate }) {
const [progressRate, setProgressRate] = useState('0.00')
const MOBILE_BREAKPOINT = 768
const weekContainersRef = useRef(null)
// 动态调整周圈大小以适应容器宽度(仅PC端)
const adjustWeekCirclesSize = () => {
const weekContainers = document.querySelectorAll('.weeks-container')
// 只在PC端(>768px)时执行动态调整,移动端清除内联样式
const isMobile = window.innerWidth <= 768
weekContainers.forEach(container => {
const weekCircles = container.querySelectorAll('.week-circle')
if (weekCircles.length === 0) return
// 移动端:清除所有内联样式,让 CSS 样式生效
if (isMobile) {
weekCircles.forEach(circle => {
circle.style.width = ''
circle.style.height = ''
circle.style.fontSize = ''
circle.style.borderWidth = ''
})
container.style.transform = ''
return
}
// PC端:动态调整周圈大小
const containerWidth = container.offsetWidth
const gap = 3
const totalGap = gap * (weekCircles.length - 1)
const circleCount = weekCircles.length
const idealSize = (containerWidth - totalGap) / circleCount
const minSize = 10
const maxSize = 24
let finalSize = Math.max(minSize, Math.min(idealSize, maxSize))
const totalContentWidth = (finalSize * circleCount) + totalGap
let scale = 1
if (totalContentWidth > containerWidth) {
scale = containerWidth / totalContentWidth
scale = Math.max(0.5, scale)
}
weekCircles.forEach(circle => {
circle.style.width = finalSize + 'px'
circle.style.height = finalSize + 'px'
circle.style.fontSize = (finalSize * 0.4) + 'px'
circle.style.borderWidth = Math.max(1, finalSize / 8) + 'px'
})
if (scale < 1) {
container.style.transform = `scaleX(${scale})`
container.style.transformOrigin = 'left'
} else {
container.style.transform = 'none'
}
})
}
// 获取当前周数
const getCurrentWeek = () => {
@@ -28,10 +88,19 @@ function TopData({ onProgressRate }) {
// 获取年度数据
const fetchYearData = async () => {
console.log('正在获取年度数据...')
// TODO: 替换为实际的API调用
return {
target: 3500,
actual: 2750
try {
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000/api'
const response = await fetch(`${API_BASE_URL}/sales/year-data?year=2024`)
const result = await response.json()
console.log('API 响应:', result)
return result
} catch (error) {
console.error('获取年度数据失败:', error)
// 返回默认数据
return {
target: 3500,
actual: 2750
}
}
}
@@ -85,6 +154,27 @@ function TopData({ onProgressRate }) {
}
initData()
// 初始调整周圈大小 - 延迟到数据渲染后
setTimeout(() => {
adjustWeekCirclesSize()
}, 500) // 增加延迟确保 DOM 已渲染
// 监听窗口大小变化
let resizeTimer
const handleResize = () => {
clearTimeout(resizeTimer)
resizeTimer = setTimeout(() => {
adjustWeekCirclesSize()
}, 300)
}
window.addEventListener('resize', handleResize)
return () => {
window.removeEventListener('resize', handleResize)
clearTimeout(resizeTimer)
}
}, [onProgressRate])
return (
+312 -32
View File
@@ -1,15 +1,3 @@
/* PC端等比例缩放,根字号固定 */
html {
font-size: 16px;
}
/* 移动端基础字号 */
@media screen and (max-width: 768px) {
html {
font-size: 14px;
}
}
* {
margin: 0;
padding: 0;
@@ -21,9 +9,6 @@ body {
color: #fff;
min-width: 320px;
overflow-x: hidden;
/* PC端缩放相关样式 */
transform-origin: left top;
transition: transform 0.1s ease-out;
}
.container {
@@ -32,8 +17,6 @@ body {
flex-direction: column;
justify-content: space-between;
background: #000;
/* 固定最小宽度,避免缩放时内容挤压 */
min-width: 1200px;
}
.cont{
@@ -41,20 +24,10 @@ body {
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%;
@@ -73,6 +46,7 @@ body {
grid-template-columns: 2fr 1fr;
gap: clamp(6px, 1.5vw, 14px);
margin-bottom: clamp(10px, 1.5vw, 20px);
min-height: 0; /* 允许grid子项收缩 */
}
.sales-info {
@@ -237,6 +211,7 @@ body {
flex-direction: column;
align-items: center;
justify-content: center;
overflow: hidden; /* PC端防止溢出 */
}
.countdown h3 {
@@ -244,6 +219,7 @@ body {
color: #e7552f;
margin-bottom: clamp(10px, 1.5vw, 15px);
font-weight: bold;
white-space: nowrap; /* 标题不换行 */
}
/* 季度容器 */
@@ -263,6 +239,7 @@ body {
min-width: 0;
border-bottom: 1px solid #523A14;
padding-bottom: 8px;
overflow: hidden; /* PC端防止内容溢出 */
}
.last-quarter{
border-bottom: none;
@@ -285,26 +262,34 @@ body {
/* 周数圆圈容器 */
.weeks-container {
display: flex;
flex-wrap: wrap;
gap: clamp(3px, 0.5vw, 5px);
flex-wrap: nowrap; /* PC端不换行 */
gap: clamp(2px, 0.4vw, 5px);
flex: 1;
overflow-x: hidden; /* 隐藏溢出内容 */
overflow-y: hidden;
min-width: 0; /* 允许flex子项缩小 */
justify-content: flex-start; /* 从左侧开始排列 */
}
/* 单个周数圆圈 */
.week-circle {
width: clamp(18px, 2.5vw, 24px);
height: clamp(18px, 2.5vw, 24px);
width: clamp(12px, 2.5vw, 24px);
height: clamp(12px, 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);
font-size: clamp(0.5rem, 1vw, 0.75rem);
color: #f5bbab;
font-weight: bold;
cursor: pointer;
transition: all 0.3s ease;
flex-shrink: 1; /* PC端允许周圈缩小以适应容器 */
min-width: 10px; /* 最小尺寸 */
min-height: 10px;
flex-basis: clamp(12px, 2.5vw, 24px); /* 弹性基础尺寸 */
}
.week-circle:hover {
@@ -505,6 +490,21 @@ body {
font-size: clamp(0.875rem, 2.5vw, 1.125rem);
}
/* 移动端年度进度允许换行 */
.countdown {
overflow: visible; /* 移动端允许溢出换行 */
}
.quarter-item {
overflow: visible; /* 移动端允许内容换行 */
}
.weeks-container {
flex-wrap: wrap; /* 移动端允许换行 */
overflow-x: visible;
overflow-y: visible;
}
.quarter-label {
font-size: clamp(0.75rem, 2vw, 0.875rem);
padding: clamp(2px, 0.5vw, 4px) clamp(6px, 1.5vw, 10px);
@@ -515,6 +515,10 @@ body {
width: clamp(16px, 4vw, 20px);
height: clamp(16px, 4vw, 20px);
font-size: clamp(0.5rem, 1.5vw, 0.625rem);
flex-shrink: 0; /* 移动端不缩小 */
min-width: auto;
min-height: auto;
flex-basis: auto;
}
.ranking-item {
@@ -574,6 +578,14 @@ body {
/* 超小屏幕优化 (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);
}
@@ -592,6 +604,274 @@ body {
}
}
/* 笔记本小屏幕特别优化 (1024px - 1366px) */
@media screen and (min-width: 1024px) and (max-width: 1366px) {
/* 年度进度区域紧凑显示 */
.countdown {
padding: clamp(6px, 1vw, 8px);
}
.countdown h3 {
font-size: clamp(0.7rem, 1vw, 0.9rem);
margin-bottom: clamp(6px, 1vw, 10px);
}
.quarters-container {
gap: clamp(3px, 0.8vw, 4px);
}
.quarter-item {
gap: clamp(3px, 0.6vw, 5px);
padding-bottom: clamp(5px, 0.7vw, 6px);
}
.quarter-label {
font-size: clamp(0.7rem, 1vw, 0.85rem);
padding: clamp(2px, 0.3vw, 3px) clamp(5px, 0.8vw, 8px);
min-width: clamp(28px, 4vw, 35px);
}
.week-circle {
width: clamp(14px, 2vw, 18px);
height: clamp(14px, 2vw, 18px);
font-size: clamp(0.5rem, 0.8vw, 0.625rem);
border-width: 1px;
}
.weeks-container {
gap: clamp(1px, 0.3vw, 3px);
}
}
/* 高分辨率小笔记本优化 (125%-200% DPI缩放) */
@media screen and (min-width: 1280px) and (max-width: 1600px) {
.countdown {
padding: clamp(6px, 1vw, 10px);
min-width: 0;
}
.countdown h3 {
font-size: clamp(0.75rem, 1.1vw, 1.1rem);
margin-bottom: clamp(8px, 1.2vw, 12px);
}
.quarters-container {
gap: clamp(4px, 0.9vw, 6px);
}
.quarter-item {
gap: clamp(4px, 0.7vw, 7px);
padding-bottom: clamp(6px, 0.8vw, 8px);
}
.quarter-label {
font-size: clamp(0.72rem, 1.05vw, 0.9rem);
padding: clamp(2px, 0.35vw, 4px) clamp(6px, 0.85vw, 10px);
min-width: clamp(30px, 4.2vw, 38px);
}
.week-circle {
width: clamp(15px, 2.1vw, 20px);
height: clamp(15px, 2.1vw, 20px);
font-size: clamp(0.5rem, 0.85vw, 0.65rem);
border-width: 1.5px;
}
.weeks-container {
gap: clamp(2px, 0.35vw, 4px);
}
}
/* 极致紧凑模式 - 高DPI小屏幕 */
@media screen and (min-width: 1200px) and (max-width: 1440px) and (min-aspect-ratio: 16/10) {
.countdown {
padding: clamp(5px, 0.9vw, 8px);
}
.countdown h3 {
font-size: clamp(0.7rem, 1vw, 0.95rem);
margin-bottom: clamp(6px, 1vw, 10px);
}
.quarters-container {
gap: clamp(3px, 0.7vw, 5px);
}
.quarter-item {
gap: clamp(3px, 0.6vw, 6px);
padding-bottom: clamp(5px, 0.7vw, 7px);
}
.quarter-label {
font-size: clamp(0.68rem, 1vw, 0.82rem);
padding: clamp(2px, 0.3vw, 4px) clamp(5px, 0.8vw, 9px);
min-width: clamp(28px, 4vw, 35px);
}
.week-circle {
width: clamp(14px, 2vw, 18px);
height: clamp(14px, 2vw, 18px);
font-size: clamp(0.48rem, 0.8vw, 0.6rem);
border-width: 1px;
}
.weeks-container {
gap: clamp(1.5px, 0.3vw, 3px);
}
}
/* 窄容器优化 - 适配所有高DPI小笔记本 */
@media screen and (min-width: 769px) and (max-width: 1400px) {
.top-data {
grid-template-columns: 2fr minmax(280px, 1fr);
}
}
/* 极窄容器优化 */
@media screen and (min-width: 769px) and (max-width: 1200px) {
.top-data {
grid-template-columns: 1fr 1fr;
}
}
/* ========== 响应式布局 - 不同屏幕尺寸优化 ========== */
/* 小屏幕笔记本 (1366x768, 1440x900) */
@media screen and (min-width: 769px) and (max-width: 1600px) {
.cont {
width: 95%;
}
.section-title {
font-size: clamp(0.8rem, 1.1vw, 0.95rem);
}
.chart-container {
min-height: clamp(220px, 28vw, 280px);
}
/* 年度进度区域优化 */
.countdown {
padding: clamp(6px, 1vw, 10px);
}
.countdown h3 {
font-size: clamp(0.75rem, 1.1vw, 1.1rem);
margin-bottom: clamp(8px, 1.2vw, 12px);
}
.quarter-item {
gap: clamp(4px, 0.8vw, 6px);
padding-bottom: clamp(6px, 0.8vw, 8px);
}
.quarter-label {
font-size: clamp(0.75rem, 1.1vw, 0.95rem);
padding: clamp(2px, 0.4vw, 4px) clamp(6px, 0.9vw, 10px);
min-width: clamp(32px, 4.5vw, 40px);
}
.week-circle {
width: clamp(16px, 2.2vw, 22px);
height: clamp(16px, 2.2vw, 22px);
font-size: clamp(0.5625rem, 0.9vw, 0.6875rem);
border-width: 1.5px;
}
.weeks-container {
gap: clamp(2px, 0.4vw, 4px);
}
}
/* 标准全高清 (1920x1080) */
@media screen and (min-width: 1601px) and (max-width: 2100px) {
.cont {
width: 90%;
max-width: 1700px;
}
.chart-container {
min-height: clamp(250px, 28vw, 320px);
}
}
/* 2K 屏幕 (2560x1440) */
@media screen and (min-width: 2101px) and (max-width: 3200px) {
.cont {
width: 85%;
max-width: 2200px;
}
.section-title {
font-size: clamp(0.9rem, 1.2vw, 1.1rem);
padding: clamp(6px, 0.9vw, 8px) clamp(12px, 1.6vw, 18px);
}
.top-data {
gap: clamp(10px, 1.5vw, 18px);
margin-bottom: clamp(15px, 1.8vw, 25px);
}
.chart-container {
min-height: clamp(280px, 28vw, 380px);
}
}
/* 4K 屏幕 (3840x2160 及以上) */
@media screen and (min-width: 3201px) {
.cont {
width: 80%;
max-width: 3000px;
padding: clamp(1.5rem, 2vw, 3rem);
}
.section-title {
font-size: clamp(1rem, 1.5vw, 1.3rem);
padding: clamp(8px, 1vw, 10px) clamp(15px, 2vw, 25px);
}
.top-data {
gap: clamp(15px, 2vw, 25px);
margin-bottom: clamp(20px, 2.5vw, 30px);
}
.sales-info {
padding: clamp(15px, 2vw, 25px);
gap: clamp(15px, 2vw, 20px);
}
.countdown {
padding: clamp(12px, 1.8vw, 15px);
}
.group-ranking,
.person-ranking {
padding: clamp(15px, 2vw, 30px);
margin-bottom: clamp(15px, 2vw, 25px);
}
.chart-container {
min-height: clamp(350px, 30vw, 500px);
}
.ranking-item {
padding: clamp(12px, 1.5vw, 18px) clamp(15px, 2vw, 22px);
font-size: clamp(0.85rem, 1.2vw, 1rem);
}
.rank-number {
width: clamp(22px, 3.5vw, 28px);
height: clamp(22px, 3.5vw, 28px);
}
/* 4K屏幕下的文字大小优化 */
.target-amount,
.total-amount {
font-size: clamp(1.5rem, 3vw, 2.5rem);
}
}
/* 头部和尾部图片 */
.header-image {
margin-bottom: 20px;
Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 101 KiB

+4 -57
View File
@@ -35,71 +35,18 @@ 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)
// 可以在这里处理进度率数据
console.log('进度率:', rate)
}
onMounted(() => {
setScale()
window.addEventListener('resize', () => {
clearTimeout(resizeTimer)
resizeTimer = setTimeout(setScale, 100)
})
// 移除窗口缩放监听,使用纯响应式布局
})
onUnmounted(() => {
window.removeEventListener('resize', handleResize)
clearTimeout(resizeTimer)
// 清理工作
})
</script>
+82 -1
View File
@@ -55,7 +55,7 @@
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { ref, onMounted, onUnmounted } from 'vue'
const emit = defineEmits(['progress-rate'])
@@ -79,6 +79,65 @@ const getCurrentWeek = () => {
const currentWeek = ref(getCurrentWeek())
// 动态调整周圈大小以适应容器宽度
const adjustWeekCirclesSize = () => {
const weekContainers = document.querySelectorAll('.weeks-container')
// 只在PC端(>768px)时执行动态调整,移动端清除内联样式
const isMobile = window.innerWidth <= 768
weekContainers.forEach(container => {
const weekCircles = container.querySelectorAll('.week-circle')
if (weekCircles.length === 0) return
// 移动端:清除所有内联样式,让 CSS 样式生效
if (isMobile) {
weekCircles.forEach(circle => {
circle.style.width = ''
circle.style.height = ''
circle.style.fontSize = ''
circle.style.borderWidth = ''
})
container.style.transform = ''
return
}
// PC端:动态调整周圈大小
const containerWidth = container.offsetWidth
const gap = 3
const totalGap = gap * (weekCircles.length - 1)
const circleCount = weekCircles.length
const idealSize = (containerWidth - totalGap) / circleCount
const minSize = 10
const maxSize = 24
let finalSize = Math.max(minSize, Math.min(idealSize, maxSize))
const totalContentWidth = (finalSize * circleCount) + totalGap
let scale = 1
if (totalContentWidth > containerWidth) {
scale = containerWidth / totalContentWidth
scale = Math.max(0.5, scale)
}
weekCircles.forEach(circle => {
circle.style.width = finalSize + 'px'
circle.style.height = finalSize + 'px'
circle.style.fontSize = (finalSize * 0.4) + 'px'
circle.style.borderWidth = Math.max(1, finalSize / 8) + 'px'
})
if (scale < 1) {
container.style.transform = `scaleX(${scale})`
container.style.transformOrigin = 'left'
} else {
container.style.transform = 'none'
}
})
}
// 季度数据
const quarters = ref([
{ id: 'q1', label: 'Q1', start: 1, end: 13, weeks: [] },
@@ -162,5 +221,27 @@ onMounted(async () => {
setTimeout(() => {
startProgressAnimation(parseFloat(rate))
}, 800)
// 初始调整周圈大小 - 延迟到数据渲染后
setTimeout(() => {
adjustWeekCirclesSize()
}, 500) // 增加延迟确保 DOM 已渲染
// 监听窗口大小变化
let resizeTimer
const handleResize = () => {
clearTimeout(resizeTimer)
resizeTimer = setTimeout(() => {
adjustWeekCirclesSize()
}, 300)
}
window.addEventListener('resize', handleResize)
// 组件卸载时清理
onUnmounted(() => {
window.removeEventListener('resize', handleResize)
clearTimeout(resizeTimer)
})
})
</script>
+312 -32
View File
@@ -1,15 +1,3 @@
/* PC端等比例缩放,根字号固定 */
html {
font-size: 16px;
}
/* 移动端基础字号 */
@media screen and (max-width: 768px) {
html {
font-size: 14px;
}
}
* {
margin: 0;
padding: 0;
@@ -21,9 +9,6 @@ body {
color: #fff;
min-width: 320px;
overflow-x: hidden;
/* PC端缩放相关样式 */
transform-origin: left top;
transition: transform 0.1s ease-out;
}
.container {
@@ -32,8 +17,6 @@ body {
flex-direction: column;
justify-content: space-between;
background: #000;
/* 固定最小宽度,避免缩放时内容挤压 */
min-width: 1200px;
}
.cont{
@@ -41,20 +24,10 @@ body {
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%;
@@ -73,6 +46,7 @@ body {
grid-template-columns: 2fr 1fr;
gap: clamp(6px, 1.5vw, 14px);
margin-bottom: clamp(10px, 1.5vw, 20px);
min-height: 0; /* 允许grid子项收缩 */
}
.sales-info {
@@ -237,6 +211,7 @@ body {
flex-direction: column;
align-items: center;
justify-content: center;
overflow: hidden; /* PC端防止溢出 */
}
.countdown h3 {
@@ -244,6 +219,7 @@ body {
color: #e7552f;
margin-bottom: clamp(10px, 1.5vw, 15px);
font-weight: bold;
white-space: nowrap; /* 标题不换行 */
}
/* 季度容器 */
@@ -263,6 +239,7 @@ body {
min-width: 0;
border-bottom: 1px solid #523A14;
padding-bottom: 8px;
overflow: hidden; /* PC端防止内容溢出 */
}
.last-quarter{
border-bottom: none;
@@ -285,26 +262,34 @@ body {
/* 周数圆圈容器 */
.weeks-container {
display: flex;
flex-wrap: wrap;
gap: clamp(3px, 0.5vw, 5px);
flex-wrap: nowrap; /* PC端不换行 */
gap: clamp(2px, 0.4vw, 5px);
flex: 1;
overflow-x: hidden; /* 隐藏溢出内容 */
overflow-y: hidden;
min-width: 0; /* 允许flex子项缩小 */
justify-content: flex-start; /* 从左侧开始排列 */
}
/* 单个周数圆圈 */
.week-circle {
width: clamp(18px, 2.5vw, 24px);
height: clamp(18px, 2.5vw, 24px);
width: clamp(12px, 2.5vw, 24px);
height: clamp(12px, 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);
font-size: clamp(0.5rem, 1vw, 0.75rem);
color: #f5bbab;
font-weight: bold;
cursor: pointer;
transition: all 0.3s ease;
flex-shrink: 1; /* PC端允许周圈缩小以适应容器 */
min-width: 10px; /* 最小尺寸 */
min-height: 10px;
flex-basis: clamp(12px, 2.5vw, 24px); /* 弹性基础尺寸 */
}
.week-circle:hover {
@@ -505,6 +490,21 @@ body {
font-size: clamp(0.875rem, 2.5vw, 1.125rem);
}
/* 移动端年度进度允许换行 */
.countdown {
overflow: visible; /* 移动端允许溢出换行 */
}
.quarter-item {
overflow: visible; /* 移动端允许内容换行 */
}
.weeks-container {
flex-wrap: wrap; /* 移动端允许换行 */
overflow-x: visible;
overflow-y: visible;
}
.quarter-label {
font-size: clamp(0.75rem, 2vw, 0.875rem);
padding: clamp(2px, 0.5vw, 4px) clamp(6px, 1.5vw, 10px);
@@ -515,6 +515,10 @@ body {
width: clamp(16px, 4vw, 20px);
height: clamp(16px, 4vw, 20px);
font-size: clamp(0.5rem, 1.5vw, 0.625rem);
flex-shrink: 0; /* 移动端不缩小 */
min-width: auto;
min-height: auto;
flex-basis: auto;
}
.ranking-item {
@@ -574,6 +578,14 @@ body {
/* 超小屏幕优化 (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);
}
@@ -592,6 +604,274 @@ body {
}
}
/* 笔记本小屏幕特别优化 (1024px - 1366px) */
@media screen and (min-width: 1024px) and (max-width: 1366px) {
/* 年度进度区域紧凑显示 */
.countdown {
padding: clamp(6px, 1vw, 8px);
}
.countdown h3 {
font-size: clamp(0.7rem, 1vw, 0.9rem);
margin-bottom: clamp(6px, 1vw, 10px);
}
.quarters-container {
gap: clamp(3px, 0.8vw, 4px);
}
.quarter-item {
gap: clamp(3px, 0.6vw, 5px);
padding-bottom: clamp(5px, 0.7vw, 6px);
}
.quarter-label {
font-size: clamp(0.7rem, 1vw, 0.85rem);
padding: clamp(2px, 0.3vw, 3px) clamp(5px, 0.8vw, 8px);
min-width: clamp(28px, 4vw, 35px);
}
.week-circle {
width: clamp(14px, 2vw, 18px);
height: clamp(14px, 2vw, 18px);
font-size: clamp(0.5rem, 0.8vw, 0.625rem);
border-width: 1px;
}
.weeks-container {
gap: clamp(1px, 0.3vw, 3px);
}
}
/* 高分辨率小笔记本优化 (125%-200% DPI缩放) */
@media screen and (min-width: 1280px) and (max-width: 1600px) {
.countdown {
padding: clamp(6px, 1vw, 10px);
min-width: 0;
}
.countdown h3 {
font-size: clamp(0.75rem, 1.1vw, 1.1rem);
margin-bottom: clamp(8px, 1.2vw, 12px);
}
.quarters-container {
gap: clamp(4px, 0.9vw, 6px);
}
.quarter-item {
gap: clamp(4px, 0.7vw, 7px);
padding-bottom: clamp(6px, 0.8vw, 8px);
}
.quarter-label {
font-size: clamp(0.72rem, 1.05vw, 0.9rem);
padding: clamp(2px, 0.35vw, 4px) clamp(6px, 0.85vw, 10px);
min-width: clamp(30px, 4.2vw, 38px);
}
.week-circle {
width: clamp(15px, 2.1vw, 20px);
height: clamp(15px, 2.1vw, 20px);
font-size: clamp(0.5rem, 0.85vw, 0.65rem);
border-width: 1.5px;
}
.weeks-container {
gap: clamp(2px, 0.35vw, 4px);
}
}
/* 极致紧凑模式 - 高DPI小屏幕 */
@media screen and (min-width: 1200px) and (max-width: 1440px) and (min-aspect-ratio: 16/10) {
.countdown {
padding: clamp(5px, 0.9vw, 8px);
}
.countdown h3 {
font-size: clamp(0.7rem, 1vw, 0.95rem);
margin-bottom: clamp(6px, 1vw, 10px);
}
.quarters-container {
gap: clamp(3px, 0.7vw, 5px);
}
.quarter-item {
gap: clamp(3px, 0.6vw, 6px);
padding-bottom: clamp(5px, 0.7vw, 7px);
}
.quarter-label {
font-size: clamp(0.68rem, 1vw, 0.82rem);
padding: clamp(2px, 0.3vw, 4px) clamp(5px, 0.8vw, 9px);
min-width: clamp(28px, 4vw, 35px);
}
.week-circle {
width: clamp(14px, 2vw, 18px);
height: clamp(14px, 2vw, 18px);
font-size: clamp(0.48rem, 0.8vw, 0.6rem);
border-width: 1px;
}
.weeks-container {
gap: clamp(1.5px, 0.3vw, 3px);
}
}
/* 窄容器优化 - 适配所有高DPI小笔记本 */
@media screen and (min-width: 769px) and (max-width: 1400px) {
.top-data {
grid-template-columns: 2fr minmax(280px, 1fr);
}
}
/* 极窄容器优化 */
@media screen and (min-width: 769px) and (max-width: 1200px) {
.top-data {
grid-template-columns: 1fr 1fr;
}
}
/* ========== 响应式布局 - 不同屏幕尺寸优化 ========== */
/* 小屏幕笔记本 (1366x768, 1440x900) */
@media screen and (min-width: 769px) and (max-width: 1600px) {
.cont {
width: 95%;
}
.section-title {
font-size: clamp(0.8rem, 1.1vw, 0.95rem);
}
.chart-container {
min-height: clamp(220px, 28vw, 280px);
}
/* 年度进度区域优化 */
.countdown {
padding: clamp(6px, 1vw, 10px);
}
.countdown h3 {
font-size: clamp(0.75rem, 1.1vw, 1.1rem);
margin-bottom: clamp(8px, 1.2vw, 12px);
}
.quarter-item {
gap: clamp(4px, 0.8vw, 6px);
padding-bottom: clamp(6px, 0.8vw, 8px);
}
.quarter-label {
font-size: clamp(0.75rem, 1.1vw, 0.95rem);
padding: clamp(2px, 0.4vw, 4px) clamp(6px, 0.9vw, 10px);
min-width: clamp(32px, 4.5vw, 40px);
}
.week-circle {
width: clamp(16px, 2.2vw, 22px);
height: clamp(16px, 2.2vw, 22px);
font-size: clamp(0.5625rem, 0.9vw, 0.6875rem);
border-width: 1.5px;
}
.weeks-container {
gap: clamp(2px, 0.4vw, 4px);
}
}
/* 标准全高清 (1920x1080) */
@media screen and (min-width: 1601px) and (max-width: 2100px) {
.cont {
width: 90%;
max-width: 1700px;
}
.chart-container {
min-height: clamp(250px, 28vw, 320px);
}
}
/* 2K 屏幕 (2560x1440) */
@media screen and (min-width: 2101px) and (max-width: 3200px) {
.cont {
width: 85%;
max-width: 2200px;
}
.section-title {
font-size: clamp(0.9rem, 1.2vw, 1.1rem);
padding: clamp(6px, 0.9vw, 8px) clamp(12px, 1.6vw, 18px);
}
.top-data {
gap: clamp(10px, 1.5vw, 18px);
margin-bottom: clamp(15px, 1.8vw, 25px);
}
.chart-container {
min-height: clamp(280px, 28vw, 380px);
}
}
/* 4K 屏幕 (3840x2160 及以上) */
@media screen and (min-width: 3201px) {
.cont {
width: 80%;
max-width: 3000px;
padding: clamp(1.5rem, 2vw, 3rem);
}
.section-title {
font-size: clamp(1rem, 1.5vw, 1.3rem);
padding: clamp(8px, 1vw, 10px) clamp(15px, 2vw, 25px);
}
.top-data {
gap: clamp(15px, 2vw, 25px);
margin-bottom: clamp(20px, 2.5vw, 30px);
}
.sales-info {
padding: clamp(15px, 2vw, 25px);
gap: clamp(15px, 2vw, 20px);
}
.countdown {
padding: clamp(12px, 1.8vw, 15px);
}
.group-ranking,
.person-ranking {
padding: clamp(15px, 2vw, 30px);
margin-bottom: clamp(15px, 2vw, 25px);
}
.chart-container {
min-height: clamp(350px, 30vw, 500px);
}
.ranking-item {
padding: clamp(12px, 1.5vw, 18px) clamp(15px, 2vw, 22px);
font-size: clamp(0.85rem, 1.2vw, 1rem);
}
.rank-number {
width: clamp(22px, 3.5vw, 28px);
height: clamp(22px, 3.5vw, 28px);
}
/* 4K屏幕下的文字大小优化 */
.target-amount,
.total-amount {
font-size: clamp(1.5rem, 3vw, 2.5rem);
}
}
/* 头部和尾部图片 */
.header-image {
margin-bottom: 20px;