feat(chart): add multi-axis overlay controls
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
## 📊 当前结构分析
|
||||
|
||||
### 当前组件列表
|
||||
|
||||
```
|
||||
src/components/
|
||||
├── WaveformChart.vue # 主容器组件 (~1143 行)
|
||||
@@ -105,7 +106,9 @@ src/components/
|
||||
**职责**:提供共享的类型、常量和工具
|
||||
|
||||
**文件**:
|
||||
|
||||
- `core/types.ts` - 基础类型定义
|
||||
|
||||
```typescript
|
||||
export interface DisplaySeries { ... }
|
||||
export interface TrackLayout { ... }
|
||||
@@ -128,7 +131,9 @@ src/components/
|
||||
**职责**:数据规范化、轨道布局计算
|
||||
|
||||
**文件**:
|
||||
|
||||
- `data/types.ts` - 数据类型
|
||||
|
||||
```typescript
|
||||
export type WaveformData = ...
|
||||
export type WaveformDisplayMode = ...
|
||||
@@ -136,6 +141,7 @@ src/components/
|
||||
```
|
||||
|
||||
- `data/normalize.ts` - 数据规范化
|
||||
|
||||
```typescript
|
||||
export function normalizeWaveformData(data: WaveformData): WaveformSeries[]
|
||||
export function normalizeWaveformSeries(data: WaveformData): DisplaySeries[]
|
||||
@@ -153,6 +159,7 @@ src/components/
|
||||
```
|
||||
|
||||
**来源**:
|
||||
|
||||
- `waveform.ts` → `data/types.ts` + `data/normalize.ts`
|
||||
- `WaveformChart.vue` 中的 `trackLayouts` 计算逻辑 → `data/layout.ts`
|
||||
|
||||
@@ -163,6 +170,7 @@ src/components/
|
||||
**职责**:渲染波形轨道、网格、坐标轴、波形线
|
||||
|
||||
**文件**:
|
||||
|
||||
- `rendering/WaveformTrack.vue` - 波形轨道组件(已存在)
|
||||
- `rendering/types.ts` - 渲染相关类型
|
||||
```typescript
|
||||
@@ -171,10 +179,12 @@ src/components/
|
||||
```
|
||||
|
||||
**可选优化**:
|
||||
|
||||
- `rendering/Grid.vue` - 独立网格组件
|
||||
- `rendering/Axis.vue` - 独立坐标轴组件
|
||||
|
||||
**来源**:
|
||||
|
||||
- `WaveformTrack.vue` → `rendering/WaveformTrack.vue`
|
||||
|
||||
---
|
||||
@@ -184,17 +194,19 @@ src/components/
|
||||
**职责**:管理缩放行为、变换状态
|
||||
|
||||
**文件**:
|
||||
|
||||
- `zoom/useZoom.ts` - 缩放组合式函数
|
||||
|
||||
```typescript
|
||||
export function useZoom(options: ZoomOptions) {
|
||||
const sharedTransform = shallowRef<ZoomTransform>(zoomIdentity)
|
||||
const independentTransforms = shallowRef<ZoomTransform[]>([])
|
||||
|
||||
|
||||
function configureZoom() { ... }
|
||||
function resetViewport() { ... }
|
||||
function handleSharedZoom(event: D3ZoomEvent) { ... }
|
||||
function handleIndependentZoom(event: D3ZoomEvent, trackIndex: number) { ... }
|
||||
|
||||
|
||||
return {
|
||||
sharedTransform,
|
||||
independentTransforms,
|
||||
@@ -221,17 +233,19 @@ src/components/
|
||||
**职责**:处理用户交互(悬浮、点击、工具栏)
|
||||
|
||||
**文件**:
|
||||
|
||||
- `interaction/WaveformToolbar.vue` - 工具栏(已存在)
|
||||
- `interaction/WaveformTooltip.vue` - 悬浮提示(已存在)
|
||||
|
||||
- `interaction/useInteraction.ts` - 交互管理
|
||||
|
||||
```typescript
|
||||
export function useInteraction(options: InteractionOptions) {
|
||||
const interactionMode = ref<WaveformInteractionMode>('zoom')
|
||||
|
||||
|
||||
function setInteractionMode(mode: WaveformInteractionMode) { ... }
|
||||
function handleOverlayClick(event: PointerEvent, trackIndex?: number) { ... }
|
||||
|
||||
|
||||
return {
|
||||
interactionMode,
|
||||
setInteractionMode,
|
||||
@@ -241,16 +255,17 @@ src/components/
|
||||
```
|
||||
|
||||
- `interaction/useHover.ts` - 悬浮逻辑
|
||||
|
||||
```typescript
|
||||
export function useHover(options: HoverOptions) {
|
||||
const hoveredSeriesPoints = ref<HoveredSeriesPoint[]>([])
|
||||
const hoveredTrackIndex = ref<number | null>(null)
|
||||
const hoverPosition = ref({ x: 0, y: 0 })
|
||||
|
||||
|
||||
function handlePointerMove(event: PointerEvent, trackIndex?: number) { ... }
|
||||
function clearHover() { ... }
|
||||
function nearestPoint(series: DisplaySeries, xValue: number) { ... }
|
||||
|
||||
|
||||
return {
|
||||
hoveredSeriesPoints,
|
||||
hoveredTrackIndex,
|
||||
@@ -270,6 +285,7 @@ src/components/
|
||||
```
|
||||
|
||||
**来源**:
|
||||
|
||||
- `WaveformToolbar.vue` → `interaction/WaveformToolbar.vue`
|
||||
- `WaveformTooltip.vue` → `interaction/WaveformTooltip.vue`
|
||||
- `WaveformChart.vue` 中的交互逻辑 → `interaction/useInteraction.ts` + `interaction/useHover.ts`
|
||||
@@ -281,21 +297,23 @@ src/components/
|
||||
**职责**:管理标注和图形(创建、编辑、删除、渲染)
|
||||
|
||||
**文件**:
|
||||
|
||||
- `annotation/WaveformAnnotationLayer.vue` - 标注渲染层(已存在)
|
||||
- `annotation/WaveformEditor.vue` - 标注编辑器(已存在)
|
||||
|
||||
- `annotation/useAnnotation.ts` - 标注管理逻辑
|
||||
|
||||
```typescript
|
||||
export function useAnnotation(options: AnnotationOptions) {
|
||||
const selection = ref<WaveformMarkupSelection>(null)
|
||||
const editingDraft = ref<EditingDraft | null>(null)
|
||||
const rangeDraft = ref<RangeDraft | null>(null)
|
||||
|
||||
|
||||
function createAnnotation(point: WaveformPoint, seriesId: string) { ... }
|
||||
function editAnnotation(id: string) { ... }
|
||||
function deleteAnnotation(id: string) { ... }
|
||||
function selectMarkup(kind: 'annotation' | 'shape', id: string) { ... }
|
||||
|
||||
|
||||
return {
|
||||
selection,
|
||||
editingDraft,
|
||||
@@ -309,6 +327,7 @@ src/components/
|
||||
```
|
||||
|
||||
- `annotation/markup.ts` - 标注工具函数
|
||||
|
||||
```typescript
|
||||
export function layoutAnnotationBox(...) { ... }
|
||||
export function resolveAnnotationStyle(...) { ... }
|
||||
@@ -325,6 +344,7 @@ src/components/
|
||||
```
|
||||
|
||||
**来源**:
|
||||
|
||||
- `WaveformAnnotationLayer.vue` → `annotation/WaveformAnnotationLayer.vue`
|
||||
- `WaveformEditor.vue` → `annotation/WaveformEditor.vue`
|
||||
- `waveform-markup.ts` → `annotation/markup.ts` + `annotation/types.ts`
|
||||
@@ -464,26 +484,20 @@ watch(() => props.data, resetViewport)
|
||||
:track="track"
|
||||
@pointer-move="handlePointerMove($event, track.index)"
|
||||
/>
|
||||
|
||||
|
||||
<!-- 标注层 -->
|
||||
<WaveformAnnotationLayer
|
||||
:annotations="renderedAnnotations"
|
||||
:shapes="renderedShapes"
|
||||
/>
|
||||
<WaveformAnnotationLayer :annotations="renderedAnnotations" :shapes="renderedShapes" />
|
||||
</svg>
|
||||
|
||||
|
||||
<!-- 工具栏 -->
|
||||
<WaveformToolbar
|
||||
:interaction-mode="interactionMode"
|
||||
@update:interaction-mode="setInteractionMode"
|
||||
/>
|
||||
|
||||
|
||||
<!-- 编辑器 -->
|
||||
<WaveformEditor
|
||||
v-if="editingDraft"
|
||||
:draft="editingDraft"
|
||||
/>
|
||||
|
||||
<WaveformEditor v-if="editingDraft" :draft="editingDraft" />
|
||||
|
||||
<!-- Tooltip -->
|
||||
<WaveformTooltip
|
||||
:visible="hoveredSeriesPoints.length > 0"
|
||||
@@ -539,6 +553,7 @@ describe('useZoom', () => {
|
||||
**风险**:移动文件可能导致测试失败、功能损坏
|
||||
|
||||
**缓解措施**:
|
||||
|
||||
- 分阶段重构,每个阶段运行测试
|
||||
- 保持向后兼容
|
||||
- 使用 Git 分支,可以随时回滚
|
||||
@@ -548,6 +563,7 @@ describe('useZoom', () => {
|
||||
**风险**:大量文件的导入路径需要更新
|
||||
|
||||
**缓解措施**:
|
||||
|
||||
- 使用 IDE 的重构功能
|
||||
- 在新目录的 `index.ts` 中保持导出一致
|
||||
- 逐步迁移,保留旧路径的重导出
|
||||
@@ -557,6 +573,7 @@ describe('useZoom', () => {
|
||||
**风险**:类型定义分散后可能产生循环依赖
|
||||
|
||||
**缓解措施**:
|
||||
|
||||
- 明确类型依赖关系
|
||||
- 共享类型放在 `core/types.ts`
|
||||
- 避免系统之间直接依赖类型
|
||||
@@ -566,6 +583,7 @@ describe('useZoom', () => {
|
||||
**风险**:拆分可能影响打包体积和加载性能
|
||||
|
||||
**缓解措施**:
|
||||
|
||||
- 使用 Tree-shaking 优化
|
||||
- 合理使用动态导入
|
||||
- 监控打包体积变化
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
## 目标
|
||||
|
||||
将 `WaveformChart.vue`(1743 行)拆分为更小的、职责单一的子组件:
|
||||
|
||||
1. **WaveformTooltip.vue** - 悬浮提示组件
|
||||
2. **WaveformTrack.vue** - 单个波形轨道组件
|
||||
3. **WaveformAnnotationLayer.vue** - 标注层组件
|
||||
@@ -10,6 +11,7 @@
|
||||
## 当前代码分析
|
||||
|
||||
### 主组件职责(过多)
|
||||
|
||||
- ✅ 数据管理和状态协调
|
||||
- ✅ 缩放和交互事件处理
|
||||
- 🔴 渲染波形轨道(网格、轴、波形线、十字线)
|
||||
@@ -19,13 +21,14 @@
|
||||
- ✅ 编辑器管理(已拆分)
|
||||
|
||||
### 模板结构(1055-1743 行)
|
||||
|
||||
```vue
|
||||
<div class="waveform-chart">
|
||||
<svg>
|
||||
<g transform="translate(margin)">
|
||||
<!-- 1. 轨道循环(100+ 行)包含:网格、轴、标签、波形线、十字线 -->
|
||||
<g v-for="track in trackLayouts">...</g>
|
||||
|
||||
|
||||
<!-- 2. 标注和图形层(135 行) -->
|
||||
<g class="waveform-chart__markup-layer">
|
||||
<g v-for="shape in renderedShapes">...</g>
|
||||
@@ -34,10 +37,10 @@
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
||||
<!-- 3. Tooltip(15 行) -->
|
||||
<div v-if="showTooltip && hoveredPoint" class="waveform-chart__tooltip">...</div>
|
||||
|
||||
|
||||
<!-- 4. 已拆分组件 -->
|
||||
<WaveformToolbar />
|
||||
<WaveformEditor />
|
||||
@@ -51,10 +54,11 @@
|
||||
**职责**:显示鼠标悬浮时的数据点信息
|
||||
|
||||
**Props**:
|
||||
|
||||
```typescript
|
||||
interface Props {
|
||||
visible: boolean
|
||||
position: { x: number, y: number }
|
||||
position: { x: number; y: number }
|
||||
timeUnit: 's' | 'ms'
|
||||
hoveredPoint: WaveformPoint | null
|
||||
seriesPoints: Array<{
|
||||
@@ -68,6 +72,7 @@ interface Props {
|
||||
```
|
||||
|
||||
**提取内容**:
|
||||
|
||||
- 模板:1463-1478 行(16 行)
|
||||
- 样式:1682-1742 行(61 行)
|
||||
- 计算属性:`tooltipStyle`(471-477 行)
|
||||
@@ -79,11 +84,12 @@ interface Props {
|
||||
**职责**:渲染单个波形轨道(网格、坐标轴、波形线、十字线、overlay)
|
||||
|
||||
**Props**:
|
||||
|
||||
```typescript
|
||||
interface Props {
|
||||
track: TrackLayout
|
||||
clipPathId: string
|
||||
margin: { top: number, right: number, bottom: number, left: number }
|
||||
margin: { top: number; right: number; bottom: number; left: number }
|
||||
innerWidth: number
|
||||
showTooltip: boolean
|
||||
zoomable: boolean
|
||||
@@ -91,11 +97,12 @@ interface Props {
|
||||
activeInteractionMode: WaveformInteractionMode
|
||||
frameNumber?: string | number
|
||||
timeUnit: 's' | 'ms'
|
||||
hoveredPoint?: HoveredSeriesPoint // 用于显示十字线
|
||||
hoveredPoint?: HoveredSeriesPoint // 用于显示十字线
|
||||
}
|
||||
```
|
||||
|
||||
**Emits**:
|
||||
|
||||
```typescript
|
||||
interface Emits {
|
||||
(e: 'pointer-move', event: PointerEvent): void
|
||||
@@ -108,6 +115,7 @@ interface Emits {
|
||||
```
|
||||
|
||||
**提取内容**:
|
||||
|
||||
- 模板:1099-1265 行(166 行)
|
||||
- 相关函数:
|
||||
- `shouldShowYAxisLabel` (247-262)
|
||||
@@ -119,6 +127,7 @@ interface Emits {
|
||||
- 样式:部分轨道相关样式
|
||||
|
||||
**注意事项**:
|
||||
|
||||
- 轨道组件需要在父组件中接收 D3 渲染的坐标轴
|
||||
- 或者在 `onMounted` 中自己调用 D3 渲染坐标轴
|
||||
|
||||
@@ -129,6 +138,7 @@ interface Emits {
|
||||
**职责**:渲染所有标注和图形(annotations + shapes)
|
||||
|
||||
**Props**:
|
||||
|
||||
```typescript
|
||||
interface Props {
|
||||
renderedAnnotations: RenderedAnnotation[]
|
||||
@@ -142,6 +152,7 @@ interface Props {
|
||||
```
|
||||
|
||||
**Emits**:
|
||||
|
||||
```typescript
|
||||
interface Emits {
|
||||
(e: 'select-markup', kind: 'annotation' | 'shape', id: string): void
|
||||
@@ -150,6 +161,7 @@ interface Emits {
|
||||
```
|
||||
|
||||
**提取内容**:
|
||||
|
||||
- 模板:1285-1419 行(135 行)
|
||||
- 相关函数:
|
||||
- `isSelected` (491-493)
|
||||
@@ -166,12 +178,14 @@ interface Emits {
|
||||
## 实施步骤
|
||||
|
||||
### 阶段 1: 创建 WaveformTooltip.vue ✅
|
||||
|
||||
1. 创建组件文件
|
||||
2. 提取模板和样式
|
||||
3. 实现计算属性 `tooltipStyle`
|
||||
4. 更新主组件使用新组件
|
||||
|
||||
### 阶段 2: 创建 WaveformAnnotationLayer.vue ✅
|
||||
|
||||
1. 创建组件文件
|
||||
2. 提取标注层模板
|
||||
3. 提取相关工具函数
|
||||
@@ -179,6 +193,7 @@ interface Emits {
|
||||
5. 更新主组件
|
||||
|
||||
### 阶段 3: 创建 WaveformTrack.vue ✅
|
||||
|
||||
1. 创建组件文件
|
||||
2. 提取轨道渲染逻辑
|
||||
3. 处理 D3 坐标轴渲染(使用 `ref` + `onMounted`)
|
||||
@@ -186,6 +201,7 @@ interface Emits {
|
||||
5. 更新主组件
|
||||
|
||||
### 阶段 4: 验证和测试 ✅
|
||||
|
||||
1. 运行类型检查 `pnpm typecheck`
|
||||
2. 运行代码规范检查 `pnpm lint`
|
||||
3. 运行单元测试 `pnpm test`
|
||||
@@ -196,20 +212,25 @@ interface Emits {
|
||||
## 设计决策
|
||||
|
||||
### 1. 类型共享
|
||||
|
||||
将 `DisplaySeries`, `HoveredSeriesPoint`, `TrackLayout`, `RenderedAnnotation`, `RenderedShape` 等接口移到独立的类型文件中,供多个组件使用。
|
||||
|
||||
**创建** `src/components/waveform-chart-types.ts`
|
||||
|
||||
### 2. D3 渲染策略
|
||||
|
||||
对于 WaveformTrack 中的坐标轴渲染:
|
||||
|
||||
- **方案 A(推荐)**:在 Track 组件内部使用 `ref` + `onMounted` 调用 D3
|
||||
- **方案 B**:父组件渲染后通知子组件
|
||||
- **选择 A**:更符合组件封装原则
|
||||
|
||||
### 3. 事件冒泡
|
||||
|
||||
所有交互事件(click, pointer-move 等)通过 emit 向上传递,保持主组件的事件协调职责。
|
||||
|
||||
### 4. 样式隔离
|
||||
|
||||
每个组件使用 `<style scoped>`,但共享的样式变量可以提取到 CSS 变量中。
|
||||
|
||||
---
|
||||
@@ -217,6 +238,7 @@ interface Emits {
|
||||
## 预期收益
|
||||
|
||||
### 代码规模
|
||||
|
||||
- **主组件**:1743 → ~1100 行(-37%)
|
||||
- **新组件**:
|
||||
- WaveformTooltip: ~80 行
|
||||
@@ -224,11 +246,13 @@ interface Emits {
|
||||
- WaveformTrack: ~300 行
|
||||
|
||||
### 可维护性
|
||||
|
||||
- 每个组件职责清晰
|
||||
- 修改轨道渲染不影响标注层
|
||||
- Tooltip 可独立测试和复用
|
||||
|
||||
### 可测试性
|
||||
|
||||
- 每个子组件可独立单元测试
|
||||
- 减少主组件测试的复杂度
|
||||
|
||||
@@ -237,19 +261,23 @@ interface Emits {
|
||||
## 风险和注意事项
|
||||
|
||||
### 1. D3 上下文问题
|
||||
|
||||
坐标轴渲染依赖 D3 操作 DOM,需要确保 ref 正确传递和挂载时机。
|
||||
|
||||
**解决方案**:在 Track 组件中使用 `watch` 监听 `track` prop 变化,触发重新渲染。
|
||||
|
||||
### 2. 性能影响
|
||||
|
||||
拆分组件可能增加 Vue 的更新开销。
|
||||
|
||||
**解决方案**:
|
||||
|
||||
- 使用 `shallowRef` 存储 D3 对象
|
||||
- 对于大数组(tracks, annotations),使用稳定的 `:key`
|
||||
- 如有性能问题,可使用 `v-memo` 指令
|
||||
|
||||
### 3. 向后兼容
|
||||
|
||||
确保 props 和 emits 接口不变。
|
||||
|
||||
**验证方法**:运行现有的 24 个单元测试。
|
||||
|
||||
Reference in New Issue
Block a user