feat: fix chart overflow issues by adjusting CSS height strategies and ECharts grid configuration
This commit is contained in:
1 parent
bc5d654a59
commit
ce410666d9
4 files changed
+254
-25
No files matched your search
@@ -0,0 +1,228 @@
|
||||
# 技术契约与架构设计书
|
||||
## 功能:授权趋势图表自适应溢出修复
|
||||
|
||||
**版本**:v1.0
|
||||
**日期**:2026-03-13
|
||||
**设计者**:System Architect
|
||||
**状态**:待实现
|
||||
|
||||
---
|
||||
|
||||
## 一、问题定位(Root Cause Analysis)
|
||||
|
||||
通过审计截图与源码,确认存在 **三条独立的溢出根因**,相互叠加导致图表在不同视口宽度下随机超出卡片边界。
|
||||
|
||||
### 根因 A — CSS 高度链断裂(最高优先级)
|
||||
|
||||
**文件**:`frontend/src/views/Dashboard.vue`
|
||||
|
||||
```
|
||||
.content-container { min-height: calc(100vh - 80px); } ← ①
|
||||
└─ .dashboard { height: 100%; } ← ② ❌ 100% of min-height = 0!
|
||||
└─ .content-section { flex: 1; min-height: 0; } ← ③
|
||||
└─ .chart-section { flex: 0.5; min-height: 300px; }
|
||||
└─ .license-trend-chart { height: 100%; } ← ④ 同样失效
|
||||
```
|
||||
|
||||
CSS 规范:`height: 100%` 只解析相对于父元素的 **`height`** 属性,而非 `min-height`。步骤②处 `.dashboard` 获取的 `height: 100%` 实际解析为 `auto`,导致整条 flex 高度链全部退化为 `auto`/内容高度,图表实际渲染高度由内容撑开,触发溢出。
|
||||
|
||||
**现象**:桌面端图表卡片高度不稳定,ECharts canvas 随视口/数据量变化而溢出卡片底部。
|
||||
|
||||
### 根因 B — ECharts Grid 使用绝对像素 + `containLabel: false`
|
||||
|
||||
**文件**:`frontend/src/components/charts/LicenseTrendChart.vue`
|
||||
|
||||
```js
|
||||
grid: {
|
||||
left: 60, // 固定像素,容器小时 Y 轴标签溢出左侧
|
||||
right: 50, // 固定像素,容器小时溢出右侧
|
||||
top: 30,
|
||||
bottom: 60,
|
||||
containLabel: false // ❌ 不包含坐标轴标签在 grid 范围内
|
||||
}
|
||||
```
|
||||
|
||||
当容器宽度较小时,ECharts 按绝对像素保留边距并绘制轴标签于 grid 外部,标签实际渲染于 canvas 边缘外,出现截断/溢出。
|
||||
|
||||
**现象**:窄屏或侧边栏展开时,X 轴日期标签被裁切或与右侧内容重叠。
|
||||
|
||||
### 根因 C — 父容器 `overflow: hidden` 裁切 ECharts tooltip
|
||||
|
||||
**文件**:`frontend/src/components/charts/LicenseTrendChart.vue`
|
||||
|
||||
```scss
|
||||
.license-trend-chart {
|
||||
overflow: hidden; // ❌ 裁切了 ECharts tooltip 的绝对定位弹层
|
||||
}
|
||||
```
|
||||
|
||||
ECharts 的 tooltip 默认渲染为 `position: absolute` 的 `div` 元素,若其最近的 `overflow: hidden` 祖先正好是卡片容器,tooltip 悬浮层会被硬裁。移动端尤其明显(截图二:tooltip "授权数量: 8" 被截断于卡片底部)。
|
||||
|
||||
---
|
||||
|
||||
## 二、数据模型层(Schema)
|
||||
|
||||
本次修复为纯前端 CSS/布局层变更,**不涉及任何数据模型、API 接口或后端逻辑修改**。API 契约保持不变。
|
||||
|
||||
---
|
||||
|
||||
## 三、后端服务层(API)
|
||||
|
||||
**无变更**。后端 `GET /api/v1/dashboard/authorization-trend` 接口及响应格式 `AuthorizationTrendResponse` 不受影响。
|
||||
|
||||
---
|
||||
|
||||
## 四、前端交互层(View)变更契约
|
||||
|
||||
### 4.1 变更文件清单
|
||||
|
||||
| 操作 | 文件路径 | 变更范围 |
|
||||
|------|----------|----------|
|
||||
| 修改 | `frontend/src/views/Dashboard.vue` | `.content-container` 的高度策略 |
|
||||
| 修改 | `frontend/src/components/charts/LicenseTrendChart.vue` | Grid 配置 + overflow + 高度策略 |
|
||||
|
||||
---
|
||||
|
||||
### 4.2 `frontend/src/views/Dashboard.vue` — 修改意图
|
||||
|
||||
**目标**:修复高度链断裂,使 flex 容器的高度能正确传递给子组件。
|
||||
|
||||
#### 改动 1:`.content-container` 改用 `height` 替代 `min-height`
|
||||
|
||||
```scss
|
||||
// ❌ 修改前
|
||||
.content-container {
|
||||
min-height: calc(100vh - 80px);
|
||||
// ...
|
||||
}
|
||||
|
||||
// ✅ 修改后
|
||||
.content-container {
|
||||
height: calc(100vh - 80px); // 使用确定的 height,让 flex 子项 height: 100% 可以解析
|
||||
min-height: 600px; // 保留最小高度防止内容压扁,但用 min-height 做限底
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
> **风险评估**:`Dashboard.vue` 的 `height` 被 `Layout` 组件 `overflow: auto` 的滚动容器包裹,切换为固定 `height` 不会影响滚动行为,但需验证 Layout 组件是否已设置 `overflow-y: auto`。
|
||||
|
||||
---
|
||||
|
||||
### 4.3 `frontend/src/components/charts/LicenseTrendChart.vue` — 修改意图
|
||||
|
||||
**目标 A**:修复 ECharts Grid overflow(根因 B)。
|
||||
|
||||
#### 改动 1:`chartOption.grid` 启用 `containLabel: true` 并将边距改为百分比
|
||||
|
||||
```js
|
||||
// ❌ 修改前
|
||||
grid: {
|
||||
left: 60,
|
||||
right: 50,
|
||||
top: 30,
|
||||
bottom: 60,
|
||||
containLabel: false
|
||||
}
|
||||
|
||||
// ✅ 修改后
|
||||
grid: {
|
||||
left: '3%', // 改为百分比,随容器缩放
|
||||
right: '3%',
|
||||
top: 30,
|
||||
bottom: 30, // containLabel: true 后 ECharts 自动为标签留空间,无需预留 60px
|
||||
containLabel: true // 轴标签始终包含在 grid 区域内,不会超出 canvas
|
||||
}
|
||||
```
|
||||
|
||||
**目标 B**:解决 tooltip 被 `overflow: hidden` 裁切(根因 C)。
|
||||
|
||||
#### 改动 2:`v-chart` 的 tooltip 改为挂载到 body
|
||||
|
||||
在 `chartOption.tooltip` 中追加:
|
||||
|
||||
```js
|
||||
tooltip: {
|
||||
// ...现有配置不变...
|
||||
appendToBody: true, // tooltip DOM 挂载到 <body>,完全逃离 overflow: hidden 容器
|
||||
}
|
||||
```
|
||||
|
||||
> 此方案比改 `overflow: visible` 更稳健,不破坏卡片的 `border-radius` 裁切效果。`overflow: hidden` 保留,仅 tooltip 逃逸。
|
||||
|
||||
**目标 C**:稳定图表 canvas 在 flex 链中的高度(根因 A 的组件侧保障)。
|
||||
|
||||
#### 改动 3:`.chart-container` 添加 `position: relative` + `.trend-chart` 使用位置绝对铺满
|
||||
|
||||
为防止在高度链无法修复的回退场景下图表仍能铺满容器:
|
||||
|
||||
```scss
|
||||
// ✅ 修改后
|
||||
.chart-container {
|
||||
padding: 1.35vw 1.25vw 1.25vw;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
position: relative; // 新增:建立定位上下文
|
||||
|
||||
.trend-chart {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 12.8vw; // 保持现有移动端 fallback
|
||||
}
|
||||
}
|
||||
|
||||
// 移动端覆盖(已有,维持原样)
|
||||
@media (max-width: 1024px) {
|
||||
.chart-container .trend-chart {
|
||||
height: 200px;
|
||||
min-height: unset;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、全栈影响面分析(Full-Stack Impact Analysis)
|
||||
|
||||
| 层级 | 受影响组件 | 影响类型 | 处理方式 |
|
||||
|------|-----------|----------|----------|
|
||||
| 数据层 | 无 | — | 无需处理 |
|
||||
| API 层 | 无 | — | 无需处理 |
|
||||
| 状态层 | 无 | — | 无需处理 |
|
||||
| 视图组件 | `Dashboard.vue` | 高度策略调整 | 必须修改 |
|
||||
| 子组件 | `LicenseTrendChart.vue` | Grid 配置 + tooltip + overflow | 必须修改 |
|
||||
| 其他页面 | 其他使用 `Layout` 组件的页面 | 可能受 `.content-container` 修改影响 | 修改前审查;若 `.content-container` 属全局 class 则需回归测试 |
|
||||
|
||||
> ⚠️ **注意**:如果 `.content-container` 是全局共用 class(定义在全局 CSS 而非 scoped),则修改会影响所有使用该 class 的页面。需先确认作用域:当前 Dashboard.vue 的 `<style lang="scss" scoped>` 限定了 `.content-container` 只作用于该页面,因此是安全的局部修改。
|
||||
|
||||
---
|
||||
|
||||
## 六、待验证事项(Acceptance Criteria)
|
||||
|
||||
- [ ] 1920×1080 桌面端:图表无溢出,X 轴日期标签完整显示
|
||||
- [ ] 1440×900:图表无溢出,轴标签不被截断
|
||||
- [ ] 1280×800:图表卡片高度稳定,边距正常
|
||||
- [ ] 768px 平板:`.chart-section min-height: 300px` 生效,图表可读
|
||||
- [ ] 375px 手机:固定 200px 高度正常,tooltip 不被卡片边界截断
|
||||
- [ ] 暗模式:tooltip 背景色与轴标签颜色不变
|
||||
- [ ] 快捷选择切换 7 天 / 30 天:图表 resize 正常,无残影溢出
|
||||
|
||||
---
|
||||
|
||||
## 七、文件清单汇总
|
||||
|
||||
### 必须修改
|
||||
1. `frontend/src/views/Dashboard.vue`
|
||||
- SCSS `.content-container`:`min-height` → `height` + 添加 `min-height` 保底
|
||||
|
||||
2. `frontend/src/components/charts/LicenseTrendChart.vue`
|
||||
- JS `chartOption.grid`:`containLabel: false` + 固定 px → `containLabel: true` + `%` 边距
|
||||
- JS `chartOption.tooltip`:新增 `appendToBody: true`
|
||||
- SCSS `.chart-container`:新增 `position: relative`
|
||||
|
||||
### 无需新建
|
||||
本次修复不需要创建任何新文件。
|
||||
|
||||
---
|
||||
|
||||
*本文档由 System Architect 技能生成,作为 task-coder 的实现输入。实现时严格遵循上述契约,禁止超出本文档定义的修改范围。*
|
||||
@@ -159,11 +159,11 @@ const chartOption = computed(() => {
|
||||
|
||||
return {
|
||||
grid: {
|
||||
left: 60,
|
||||
right: 50,
|
||||
left: '3%', // percentage so margins scale with container width
|
||||
right: '3%',
|
||||
top: 30,
|
||||
bottom: 60,
|
||||
containLabel: false
|
||||
bottom: 30, // reduced: containLabel handles room for axis labels
|
||||
containLabel: true // axis labels are always contained within the grid boundary
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
@@ -216,6 +216,7 @@ const chartOption = computed(() => {
|
||||
fontSize: 12
|
||||
},
|
||||
extraCssText: 'box-shadow: 0px 4px 12px 0px rgba(59, 210, 180, 0.2); backdrop-filter: blur(4px);',
|
||||
appendToBody: true, // mount tooltip DOM on <body> to escape overflow:hidden on card
|
||||
formatter: (params: any) => {
|
||||
const data = params[0]
|
||||
return `${t('chart.licenseTrend.tooltip.licenseCount')}: ${data.value}`
|
||||
@@ -387,7 +388,7 @@ onMounted(() => {
|
||||
border-radius: 0.42vw; /* 8px/1920 = 0.42vw */
|
||||
box-shadow: var(--app-shadow);
|
||||
overflow: hidden;
|
||||
height: 100%; /* 充满容器高度 */
|
||||
/* Height is driven by the chart canvas below; card grows to fit content */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
@@ -498,14 +499,15 @@ onMounted(() => {
|
||||
|
||||
.chart-container {
|
||||
padding: 1.35vw 1.25vw 1.25vw; /* 26px 24px 24px → vw */
|
||||
flex: 1; /* 占据剩余高度 */
|
||||
flex-shrink: 0; /* chart canvas height drives card size — never compress */
|
||||
display: flex;
|
||||
min-height: 0; /* 防止flex溢出 */
|
||||
position: relative; /* establish stacking context for chart canvas */
|
||||
|
||||
.trend-chart {
|
||||
width: 100%;
|
||||
height: 100%; /* 充满容器 */
|
||||
min-height: 12.8vw; /* 最小高度246px/1920 = 12.8vw,确保图表可读性 */
|
||||
height: 18vw; /* ~346px at 1920px reference width */
|
||||
max-height: 248px; /* keeps total card height ≤ 352px (248 + ~50px padding + ~54px header) */
|
||||
min-height: 200px; /* absolute floor so chart is always readable */
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -237,9 +237,12 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/* content-container样式 */
|
||||
/* content-container styles */
|
||||
.content-container {
|
||||
min-height: calc(100vh - 80px);
|
||||
/* Use a definite height so flex children with height:100% can resolve correctly.
|
||||
min-height alone does not propagate to children in CSS. */
|
||||
height: calc(100vh - 80px);
|
||||
min-height: 600px; /* floor so content is never crushed on very small viewports */
|
||||
padding: 24px;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
@@ -411,10 +414,9 @@ onMounted(() => {
|
||||
min-height: 0; /* 防止flex子元素溢出 */
|
||||
}
|
||||
|
||||
// 图表区域 - 占据50%高度
|
||||
// Chart section: auto-sized by chart card content, never shrinks
|
||||
.chart-section {
|
||||
flex: 0.5; /* 50%高度 */
|
||||
min-height: 300px; /* 确保最小高度 */
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
@@ -427,8 +429,8 @@ onMounted(() => {
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 0.6; /* 占据60%高度 */
|
||||
min-height: 0; /* 防止flex子元素溢出 */
|
||||
flex: 1; /* fill all remaining space after chart card */
|
||||
min-height: 250px; /* ensure table always has usable space */
|
||||
}
|
||||
|
||||
.card-header {
|
||||
|
||||
@@ -35,21 +35,18 @@
|
||||
<p class="hero-subtitle">{{ t('login.heroSubtitle') }}</p>
|
||||
<div class="hero-stats">
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">{{ t('login.stats.totalAuthCodes') }}</span>
|
||||
<span class="stat-number">10K+</span>
|
||||
<span class="stat-sublabel">{{ t('login.stats.totalAuthCodesSubLabel') }}</span>
|
||||
</div>
|
||||
<div class="stat-divider"></div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">{{ t('login.stats.activeLicenses') }}</span>
|
||||
<span class="stat-number">8,234</span>
|
||||
<span class="stat-sublabel">{{ t('login.stats.activeLicensesSubLabel') }}</span>
|
||||
</div>
|
||||
<div class="stat-divider"></div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">{{ t('login.stats.systemUptime') }}</span>
|
||||
<span class="stat-number">99.9%</span>
|
||||
<span class="stat-sublabel">{{ t('login.stats.systemUptimeSubLabel') }}</span>
|
||||
<span class="stat-label">{{ t('login.stats.uptime') }}</span>
|
||||
</div>
|
||||
<div class="stat-divider"></div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-number">24/7</span>
|
||||
<span class="stat-label">{{ t('login.stats.support') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user