From d233bd24bf14ac2747c7bedf6ed0eba1b964eb80 Mon Sep 17 00:00:00 2001 From: douboer Date: Wed, 21 Jan 2026 22:00:13 +0800 Subject: [PATCH] update at 2026-01-21 22:00:13 --- refactor-plan.md | 1165 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1165 insertions(+) create mode 100644 refactor-plan.md diff --git a/refactor-plan.md b/refactor-plan.md new file mode 100644 index 0000000..1227dea --- /dev/null +++ b/refactor-plan.md @@ -0,0 +1,1165 @@ +# 罗盘项目重构方案 + +--- + +## 一、项目现状分析 + +### 1.1 当前架构特点 +- **示例驱动**:目前使用 EXAMPLES 数组硬编码示例数据(角度数组 + 半径数组) +- **简单数据结构**:通过角度分割点和半径列表简单定义层次和扇区 +- **内置逻辑**:颜色、文字、填充等逻辑写死在代码中 +- **测试导向**:现有实现主要为测试工具函数正确性而设计 + +### 1.2 目标需求分析(基于 todolist.md 和 demo.json.conf) + +**核心变化:** +1. **配置驱动**:从 JSON 配置文件完全定义罗盘结构 +2. **复杂着色规则**:支持三级着色优先级(全局 → 层级规律填色 → 扇区独立) +3. **多文本单元**:扇区内容支持 `|` 分隔的多个文本单元,角度智能分配 +4. **SVG 图标支持**:扇区内容可以是 SVG 文件 +5. **中心图标**:支持可旋转的中心 SVG 图标 +6. **360度刻度环**:支持多种刻度模式的度数环 +7. **命名配色方案**:通过 theme.colorPalettes 定义可复用颜色 +8. **规律填色机制**:通过 num + interval 实现周期性着色 +9. **同组分割线控制**:groupSplit 参数控制组内分割线显示 + +--- + +## 二、整体架构设计 + +### 2.1 分层架构 + +``` +┌─────────────────────────────────────────────────┐ +│ UI 层(Luopan.vue) │ +│ - SVG 渲染 │ +│ - 交互控制(缩放、平移) │ +└─────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────┐ +│ Composable 层(useLuopan.ts) │ +│ - 配置解析协调 │ +│ - 响应式数据管理 │ +└─────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────┐ +│ 配置解析层(新增) │ +│ - configParser.ts:JSON → 内部数据结构 │ +│ - colorResolver.ts:颜色解析引擎 │ +│ - sectorBuilder.ts:扇区数据生成器 │ +│ - multiTextParser.ts:多文本单元解析器 │ +└─────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────┐ +│ 工具函数层(utils.ts - 保持不变) │ +│ - 几何计算(已完善) │ +│ - 路径生成(已完善) │ +│ - 文字布局计算(已完善) │ +└─────────────────────────────────────────────────┘ +``` + +### 2.2 核心模块划分 + +| 模块名 | 文件 | 职责 | +|--------|------|------| +| **类型定义** | `types.ts` | 扩展类型定义,支持 JSON 配置结构 | +| **JSON 解析** | `configParser.ts` | 解析 JSON 配置,去注释,验证结构 | +| **颜色解析器** | `colorResolver.ts` | 处理三级着色优先级、规律填色 | +| **扇区构建器** | `sectorBuilder.ts` | 根据配置生成完整扇区数据 | +| **多文本解析** | `multiTextParser.ts` | 处理 `|` 分隔文本,角度分配 | +| **中心图标** | `centerIcon.ts` | 处理中心 SVG 图标加载和渲染 | +| **刻度环** | `degreeRing.ts` | 生成 360度刻度环数据 | +| **组合逻辑** | `useLuopan.ts` | 重构为配置驱动模式 | + +--- + +## 三、JSON 配置解析流程 + +### 3.1 解析流程图 + +``` +JSON 文本 (带注释) + ↓ +[configParser] 去除 -- 注释 + ↓ +标准 JSON 对象 + ↓ +[configParser] 结构验证 + ↓ +验证通过的 LuopanConfig + ↓ +[colorResolver] 解析颜色主题 + ↓ +颜色映射表 (ColorPalette) + ↓ +[sectorBuilder] 遍历 layers + ↓ +对每个 layer: + 1. 计算规律填色模式 (num, interval) + 2. 生成所有扇区 + 3. 对每个扇区: + - 应用颜色优先级 + - 解析多文本单元 + - 计算 SVG 路径 + ↓ +完整的 Sector 数组 + ↓ +[degreeRing] 生成刻度环 + ↓ +[centerIcon] 加载中心图标 + ↓ +最终渲染数据 +``` + +### 3.2 关键算法 + +**规律填色算法:** +```typescript +function applyPatternColoring( + divisions: number, + colorRef: string, + num: number, + interval: number +): Map { + const colorMap = new Map(); + + if (interval === 0) { + // 全部着色 + for (let i = 0; i < divisions; i++) { + colorMap.set(i, colorRef); + } + return colorMap; + } + + // 周期性着色:num个着色 + interval个空白 + let currentIndex = 0; + while (currentIndex < divisions) { + // 着色 num 个 + for (let i = 0; i < num && currentIndex < divisions; i++) { + colorMap.set(currentIndex, colorRef); + currentIndex++; + } + // 跳过 interval 个 + currentIndex += interval; + } + + return colorMap; +} +``` + +**多文本单元角度分配:** +```typescript +function splitMultiTextUnits( + content: string, + aStart: number, + aEnd: number +): TextUnit[] { + const units = content.split('|'); + const ratios = getLayoutRatio(units.length); // 从 constants.ts + + const totalAngle = aEnd - aStart; + const textUnits: TextUnit[] = []; + + let accumulatedAngle = aStart; + for (let i = 0; i < units.length; i++) { + const unitAngle = totalAngle * ratios[i]; + textUnits.push({ + content: units[i], + aStart: accumulatedAngle, + aEnd: accumulatedAngle + unitAngle + }); + accumulatedAngle += unitAngle; + } + + return textUnits; +} +``` + +--- + +## 四、数据流设计 + +### 4.1 配置数据结构(新增类型) + +```typescript +// src/types.ts 扩展 + +/** JSON 配置根对象 */ +export interface LuopanConfig { + name: string; + description?: string; + background: string; // 全局背景色 + theme: ThemeConfig; + centerIcon?: CenterIconConfig; + degreeRing?: DegreeRingConfig; + layers: LayerConfig[]; +} + +/** 主题配置 */ +export interface ThemeConfig { + name?: string; + colorPalettes: Record; // 命名颜色映射 +} + +/** 中心图标配置 */ +export interface CenterIconConfig { + rIcon: number; + opacity: number; + name: string; // SVG 文件名 + rotation?: number; // 可选旋转角度 +} + +/** 刻度环配置 */ +export interface DegreeRingConfig { + rInner: number; + rOuter: number; + showDegree: 0 | 1; + mode: 'inner' | 'outer' | 'both'; + opacity: number; + tickLength: number; + tickLengthStep?: number; + majorTick: number; + minorTick: number; + microTick: number; + tickColor: string; + ringColor: string; +} + +/** 层配置 */ +export interface LayerConfig { + divisions: number; + rInner: number; + rOuter: number; + startAngle?: number; + colorRef?: string; + innerFill?: 0 | 1; + num?: number; + interval?: number; + groupSplit?: boolean; + sectors?: SectorConfig[]; +} + +/** 扇区配置 */ +export interface SectorConfig { + content?: string; // 支持 "|" 分隔 + colorRef?: string; + innerFill?: 0 | 1; +} + +/** 文本单元(多文本拆分后) */ +export interface TextUnit { + content: string; + aStart: number; + aEnd: number; + isSvg: boolean; // 是否为 SVG 文件名 +} + +/** 刻度线数据 */ +export interface TickMark { + angle: number; + type: 'major' | 'minor' | 'micro'; + length: number; + startR: number; + endR: number; + label?: string; // 度数标签(仅主刻度) +} + +/** 扩展现有 Sector 接口 */ +export interface Sector { + // ... 现有字段保持 ... + + // 新增字段 + textUnits?: TextUnit[]; // 多文本单元 + groupSplitVisible?: boolean; // 是否显示与下一个扇区的分割线 + isSvgContent?: boolean; // 内容是否为 SVG + svgPath?: string; // SVG 文件路径 +} +``` + +### 4.2 数据流转示例 + +```typescript +// 输入:JSON 配置字符串 +const jsonText = await fetch('/demo.json.conf').then(r => r.text()); + +// 步骤1:解析配置 +const config = parseConfig(jsonText); + +// 步骤2:解析颜色主题 +const colorResolver = new ColorResolver(config.theme, config.background); + +// 步骤3:构建扇区 +const sectorBuilder = new SectorBuilder(colorResolver); +const sectors = config.layers.flatMap((layer, layerIndex) => + sectorBuilder.buildLayer(layer, layerIndex) +); + +// 步骤4:生成刻度环 +const degreeRingData = config.degreeRing + ? buildDegreeRing(config.degreeRing) + : null; + +// 步骤5:加载中心图标 +const centerIconData = config.centerIcon + ? await loadCenterIcon(config.centerIcon) + : null; + +// 输出:完整渲染数据 +const renderData = { + sectors, + degreeRing: degreeRingData, + centerIcon: centerIconData, + background: config.background +}; +``` + +--- + +## 五、实施步骤(分阶段) + +### 阶段 1:类型定义和配置解析(2-3天) + +**目标:** 建立配置解析基础 + +**任务:** +1. ✅ 扩展 `types.ts`,新增所有配置相关类型 +2. ✅ 实现 `configParser.ts`: + - 去除 `--` 注释 + - JSON 解析 + - 基础验证(必填字段检查) +3. ✅ 编写单元测试:`configParser.test.ts` +4. ✅ 测试用例:解析 `demo.json.conf` + +**验收标准:** +- 能成功解析 `demo.json.conf` 为 LuopanConfig 对象 +- 所有必填字段验证通过 +- 测试覆盖率 > 80% + +--- + +### 阶段 2:颜色解析引擎(2天) + +**目标:** 实现三级着色优先级 + +**任务:** +1. ✅ 实现 `colorResolver.ts`: + - ColorResolver 类 + - resolveColor(ref: string) 方法 + - 规律填色算法 + - 颜色优先级逻辑 +2. ✅ 编写单元测试:`colorResolver.test.ts` +3. ✅ 测试用例覆盖: + - 全局背景色 + - 层级 colorRef + - 扇区 colorRef + - 规律填色(num + interval) + +**核心代码:** +```typescript +export class ColorResolver { + private colorPalettes: Record; + private globalBackground: string; + + constructor(theme: ThemeConfig, background: string) { + this.colorPalettes = theme.colorPalettes; + this.globalBackground = background; + } + + resolveColor(colorRef?: string): string { + if (!colorRef) return this.globalBackground; + return this.colorPalettes[colorRef] ?? colorRef; + } + + resolveLayerColors(layer: LayerConfig): Map { + const colorMap = new Map(); + + if (!layer.colorRef || !layer.num) { + return colorMap; // 无规律填色 + } + + return applyPatternColoring( + layer.divisions, + this.resolveColor(layer.colorRef), + layer.num, + layer.interval ?? 0 + ); + } +} +``` + +**验收标准:** +- 着色优先级正确 +- 规律填色算法准确 +- 边界情况处理(interval=0, num > divisions) + +--- + +### 阶段 3:多文本单元解析(2天) + +**目标:** 支持 `|` 分隔文本的角度分配 + +**任务:** +1. ✅ 实现 `multiTextParser.ts`: + - splitMultiTextUnits() 函数 + - 角度分配逻辑 + - SVG 文件检测 +2. ✅ 更新 `utils.ts`: + - 适配多文本单元的路径生成 + - 字体大小计算调整 +3. ✅ 编写单元测试:`multiTextParser.test.ts` +4. ✅ 测试用例: + - 1-5 个单元的角度分配 + - LAYOUT_RATIO_PRESETS 验证 + +**核心代码:** +```typescript +export function parseMultiText( + content: string, + aStart: number, + aEnd: number, + svgIconPath: string = 'src/assets/icons/' +): TextUnit[] { + const parts = content.split('|').map(s => s.trim()); + const ratios = getLayoutRatio(parts.length); + const totalAngle = aEnd - aStart; + + const units: TextUnit[] = []; + let currentAngle = aStart; + + for (let i = 0; i < parts.length; i++) { + const unitAngle = totalAngle * ratios[i]; + const isSvg = parts[i].endsWith('.svg'); + + units.push({ + content: parts[i], + aStart: currentAngle, + aEnd: currentAngle + unitAngle, + isSvg, + }); + + currentAngle += unitAngle; + } + + return units; +} +``` + +**验收标准:** +- 角度分配与 LAYOUT_RATIO_PRESETS 一致 +- 支持 SVG 文件名识别 +- 边界情况(单个文本、空字符串) + +--- + +### 阶段 4:扇区构建器(3-4天) + +**目标:** 整合所有逻辑,生成完整扇区数据 + +**任务:** +1. ✅ 实现 `sectorBuilder.ts`: + - SectorBuilder 类 + - buildLayer() 方法 + - buildSector() 方法 + - 整合颜色、多文本、innerFill、groupSplit +2. ✅ 更新 `Sector` 接口(types.ts) +3. ✅ 编写单元测试:`sectorBuilder.test.ts` +4. ✅ 集成测试:完整 layer 构建 + +**核心代码:** +```typescript +export class SectorBuilder { + constructor(private colorResolver: ColorResolver) {} + + buildLayer(layer: LayerConfig, layerIndex: number): Sector[] { + const sectors: Sector[] = []; + const layerColorMap = this.colorResolver.resolveLayerColors(layer); + + const angleStep = 360 / layer.divisions; + const startAngle = layer.startAngle ?? 0; + + for (let i = 0; i < layer.divisions; i++) { + const aStart = startAngle + i * angleStep; + const aEnd = aStart + angleStep; + + // 确定颜色(优先级:sector > layer pattern > global) + const sectorConfig = layer.sectors?.[i]; + let fillColor: string; + + if (sectorConfig?.colorRef) { + fillColor = this.colorResolver.resolveColor(sectorConfig.colorRef); + } else if (layerColorMap.has(i)) { + fillColor = layerColorMap.get(i)!; + } else { + fillColor = this.colorResolver.resolveColor(); + } + + // 确定 innerFill + const innerFill = sectorConfig?.innerFill ?? layer.innerFill ?? 0; + + // 解析多文本单元 + const content = sectorConfig?.content ?? ''; + const textUnits = content.includes('|') + ? parseMultiText(content, aStart, aEnd) + : [{ content, aStart, aEnd, isSvg: content.endsWith('.svg') }]; + + // 确定是否显示分割线 + const groupSplitVisible = this.shouldShowGroupSplit( + layer, + i, + layerColorMap + ); + + // 生成完整扇区 + const sector = this.buildSector({ + layerIndex, + pieIndex: i, + rInner: layer.rInner, + rOuter: layer.rOuter, + aStart, + aEnd, + fillColor, + innerFill, + textUnits, + groupSplitVisible, + }); + + sectors.push(sector); + } + + return sectors; + } + + private shouldShowGroupSplit( + layer: LayerConfig, + sectorIndex: number, + layerColorMap: Map + ): boolean { + if (layer.groupSplit !== false) return true; // 默认显示 + + // 如果 groupSplit=false,检查是否为组内分割线 + if (!layer.num) return true; + + const cycleLength = layer.num + (layer.interval ?? 0); + const posInCycle = sectorIndex % cycleLength; + + // 组内(0 到 num-1)不显示分割线 + return posInCycle >= layer.num - 1; + } +} +``` + +**验收标准:** +- 完整解析 `demo.json.conf` 所有 layer +- 颜色优先级正确 +- groupSplit 逻辑正确 +- 多文本单元正确拆分 + +--- + +### 阶段 5:刻度环和中心图标(2-3天) + +**目标:** 实现刻度环和中心图标功能 + +**任务:** +1. ✅ 实现 `degreeRing.ts`: + - buildDegreeRing() 函数 + - 刻度线生成(major, minor, micro) + - 度数标签生成 + - mode 处理(inner, outer, both) +2. ✅ 实现 `centerIcon.ts`: + - loadCenterIcon() 函数 + - SVG 加载和解析 + - 旋转处理 +3. ✅ 更新 Luopan.vue: + - 渲染刻度环 + - 渲染中心图标 +4. ✅ 编写单元测试 + +**核心代码(刻度环):** +```typescript +export function buildDegreeRing(config: DegreeRingConfig): { + ticks: TickMark[]; + ring: { rInner: number; rOuter: number; color: string; opacity: number }; + labels?: DegreeLabel[]; +} { + const ticks: TickMark[] = []; + const { rInner, rOuter, mode, tickLength, tickLengthStep = 0 } = config; + + // 生成刻度线 + for (let angle = 0; angle < 360; angle++) { + let type: 'major' | 'minor' | 'micro'; + let length: number; + + if (angle % config.majorTick === 0) { + type = 'major'; + length = tickLength; + } else if (angle % config.minorTick === 0) { + type = 'minor'; + length = tickLength - tickLengthStep; + } else if (angle % config.microTick === 0) { + type = 'micro'; + length = tickLength - 2 * tickLengthStep; + } else { + continue; + } + + // 根据 mode 确定刻度线位置 + let startR: number, endR: number; + if (mode === 'inner') { + startR = rInner; + endR = rInner + length; + } else if (mode === 'outer') { + startR = rOuter - length; + endR = rOuter; + } else { // both + startR = rInner; + endR = rInner + length; + // 还需要添加外侧刻度 + } + + ticks.push({ angle, type, length, startR, endR }); + } + + // 生成度数标签 + const labels: DegreeLabel[] = []; + if (config.showDegree === 1) { + for (let angle = 0; angle < 360; angle += config.majorTick) { + labels.push({ + angle, + text: angle.toString(), + r: (rInner + rOuter) / 2 + }); + } + } + + return { + ticks, + ring: { + rInner, + rOuter, + color: config.ringColor, + opacity: config.opacity + }, + labels: config.showDegree ? labels : undefined + }; +} +``` + +**验收标准:** +- 刻度线长度正确(major > minor > micro) +- mode 模式正确(inner, outer, both) +- 度数标签位置准确 +- 中心图标加载和旋转正常 + +--- + +### 阶段 6:重构 useLuopan 和 Luopan.vue(3-4天) + +**目标:** 适配新的配置驱动模式 + +**任务:** +1. ✅ 重构 `useLuopan.ts`: + - 接收 JSON 配置路径或对象 + - 异步加载和解析配置 + - 调用各个解析器和构建器 + - 返回完整渲染数据 +2. ✅ 更新 `Luopan.vue`: + - 移除示例选择器 + - 添加配置加载界面 + - 渲染多文本单元 + - 渲染刻度环 + - 渲染中心图标 + - 处理 groupSplit(条件渲染分割线) +3. ✅ 更新 `App.vue`: + - 传入配置文件路径 +4. ✅ E2E 测试 + +**核心代码(useLuopan 重构):** +```typescript +export function useLuopan(configPathOrObject: string | LuopanConfig) { + const config = ref(null); + const sectors = ref([]); + const degreeRing = ref(null); + const centerIcon = ref(null); + const loading = ref(true); + const error = ref(null); + + const loadConfig = async () => { + try { + loading.value = true; + + // 加载配置 + let configObj: LuopanConfig; + if (typeof configPathOrObject === 'string') { + const jsonText = await fetch(configPathOrObject).then(r => r.text()); + configObj = parseConfig(jsonText); + } else { + configObj = configPathOrObject; + } + config.value = configObj; + + // 解析颜色 + const colorResolver = new ColorResolver( + configObj.theme, + configObj.background + ); + + // 构建扇区 + const sectorBuilder = new SectorBuilder(colorResolver); + sectors.value = configObj.layers.flatMap((layer, i) => + sectorBuilder.buildLayer(layer, i) + ); + + // 构建刻度环 + if (configObj.degreeRing) { + degreeRing.value = buildDegreeRing(configObj.degreeRing); + } + + // 加载中心图标 + if (configObj.centerIcon) { + centerIcon.value = await loadCenterIcon(configObj.centerIcon); + } + + } catch (e) { + error.value = e as Error; + } finally { + loading.value = false; + } + }; + + // 自动加载 + loadConfig(); + + return { + config: readonly(config), + sectors: readonly(sectors), + degreeRing: readonly(degreeRing), + centerIcon: readonly(centerIcon), + loading: readonly(loading), + error: readonly(error), + reload: loadConfig + }; +} +``` + +**Luopan.vue 关键更新:** +```vue + + + +``` + +**验收标准:** +- 成功加载和渲染 `demo.json.conf` +- 所有 layer 正确显示 +- 多文本单元正确拆分显示 +- 刻度环和中心图标正常渲染 +- groupSplit 效果正确 + +--- + +### 阶段 7:测试和优化(2-3天) + +**目标:** 确保质量和性能 + +**任务:** +1. ✅ 单元测试补全(目标覆盖率 > 85%) +2. ✅ 集成测试: + - 完整配置文件解析 + - 边界情况测试 +3. ✅ 性能优化: + - 大量扇区渲染优化(>1000个) + - 配置解析缓存 +4. ✅ 错误处理: + - 配置格式错误提示 + - SVG 加载失败处理 + - 颜色引用不存在处理 +5. ✅ 文档更新: + - README.md + - API 文档 + - 配置示例 + +**测试策略:** +```typescript +describe('完整流程测试', () => { + it('应正确解析和渲染 demo.json.conf', async () => { + const config = await loadConfig('/demo.json.conf'); + const sectors = buildAllSectors(config); + + // 验证扇区数量 + const expectedCount = config.layers.reduce( + (sum, layer) => sum + layer.divisions, 0 + ); + expect(sectors).toHaveLength(expectedCount); + + // 验证颜色优先级 + const layer3Sector0 = sectors.find( + s => s.layerIndex === 2 && s.pieIndex === 0 + ); + expect(layer3Sector0?.fill).toBe('#0288D1'); // 水 + + // 验证多文本单元 + const multiTextSector = sectors.find( + s => s.textUnits && s.textUnits.length > 1 + ); + expect(multiTextSector?.textUnits).toHaveLength(3); + }); + + it('应处理配置错误', async () => { + const invalidJson = '{ "name": "test", "missing": "layers" }'; + expect(() => parseConfig(invalidJson)).toThrow('缺少必填字段'); + }); +}); +``` + +**验收标准:** +- 单元测试覆盖率 > 85% +- 所有边界情况测试通过 +- 大量扇区(120等分 × 5层)渲染流畅 +- 错误信息清晰友好 + +--- + +## 六、需要新增/修改的文件清单 + +### 新增文件(共 8 个) + +| 文件路径 | 功能 | 优先级 | +|---------|------|--------| +| `src/configParser.ts` | JSON 配置解析和验证 | P0 | +| `src/colorResolver.ts` | 颜色解析引擎 | P0 | +| `src/sectorBuilder.ts` | 扇区数据构建器 | P0 | +| `src/multiTextParser.ts` | 多文本单元解析 | P1 | +| `src/degreeRing.ts` | 刻度环生成器 | P1 | +| `src/centerIcon.ts` | 中心图标处理 | P2 | +| `tests/configParser.test.ts` | 配置解析测试 | P0 | +| `tests/sectorBuilder.test.ts` | 扇区构建测试 | P1 | + +### 修改文件(共 5 个) + +| 文件路径 | 修改内容 | 影响范围 | +|---------|---------|---------| +| `src/types.ts` | 新增配置相关类型定义 | 扩展,不影响现有代码 | +| `src/constants.ts` | 保持不变,已有 LAYOUT_RATIO_PRESETS | 无 | +| `src/utils.ts` | 保持不变,工具函数完善 | 无 | +| `src/composables/useLuopan.ts` | 重构为配置驱动模式 | 完全重写 | +| `src/Luopan.vue` | 适配新数据结构和渲染逻辑 | 大幅修改 | + +### 保持不变(共 2 个) + +| 文件路径 | 原因 | +|---------|------| +| `src/utils.ts` | 几何计算和路径生成函数已完善,无需修改 | +| `src/constants.ts` | LAYOUT_RATIO_PRESETS 已定义,符合需求 | + +--- + +## 七、技术难点和解决方案 + +### 难点 1:多文本单元的路径生成 + +**问题:** 一个扇区内有多个文本单元,每个单元需要独立的 textPath,且角度分配要精确。 + +**解决方案:** +1. 在 `sectorBuilder` 中为每个文本单元生成独立的 `TextUnit` 对象 +2. 每个 `TextUnit` 包含独立的 `aStart`, `aEnd`, `content` +3. 渲染时为每个单元生成独立的 `` 元素 +4. 使用 `getLayoutRatio()` 确保角度分配准确 + +**代码示例:** +```typescript +// 为每个文本单元生成独立路径 +textUnits.forEach((unit, index) => { + unit.textPathId = `${sector.key}-unit-${index}`; + unit.textPath = generateTextPath( + sector.rInner, + sector.rOuter, + unit.aStart, + unit.aEnd + ); +}); +``` + +--- + +### 难点 2:规律填色算法的边界情况 + +**问题:** `num` + `interval` 组合可能产生边界情况(如 num > divisions)。 + +**解决方案:** +1. 算法中加入边界检查: + ```typescript + for (let i = 0; i < num && currentIndex < divisions; i++) { + colorMap.set(currentIndex, colorRef); + currentIndex++; + } + ``` +2. 单元测试覆盖所有边界情况: + - `num = divisions, interval = 0`(全部着色) + - `num = 1, interval = 1`(隔一个着色) + - `num > divisions`(截断处理) + - `interval = 0`(特殊情况:全部着色) + +--- + +### 难点 3:groupSplit 的判断逻辑 + +**问题:** 需要判断两个相邻扇区是否属于同一着色组,仅在组边界显示分割线。 + +**解决方案:** +1. 在 `SectorBuilder` 中计算每个扇区在周期中的位置: + ```typescript + const cycleLength = num + interval; + const posInCycle = sectorIndex % cycleLength; + const isGroupBoundary = posInCycle === num - 1; + ``` +2. 为每个扇区添加 `groupSplitVisible` 属性 +3. 渲染时条件渲染分割线: + ```vue + + ``` + +--- + +### 难点 4:SVG 图标加载和缓存 + +**问题:** 多个扇区可能使用相同 SVG 图标,需要避免重复加载。 + +**解决方案:** +1. 实现 SVG 缓存机制: + ```typescript + const svgCache = new Map(); + + async function loadSvg(filename: string): Promise { + if (svgCache.has(filename)) { + return svgCache.get(filename)!; + } + + const path = `/src/assets/icons/${filename}`; + const content = await fetch(path).then(r => r.text()); + svgCache.set(filename, content); + return content; + } + ``` +2. 使用 `` 定义 SVG,`` 引用: + ```vue + + + + + + + + ``` + +--- + +### 难点 5:刻度环的刻度线长度分级 + +**问题:** major、minor、micro 三种刻度线长度需要根据 `tickLengthStep` 递减。 + +**解决方案:** +1. 统一计算逻辑: + ```typescript + const lengths = { + major: config.tickLength, + minor: config.tickLength - (config.tickLengthStep ?? 0), + micro: config.tickLength - 2 * (config.tickLengthStep ?? 0) + }; + ``` +2. 确保最小长度不为负: + ```typescript + Object.keys(lengths).forEach(key => { + lengths[key] = Math.max(1, lengths[key]); + }); + ``` + +--- + +## 八、测试策略 + +### 8.1 单元测试 + +| 模块 | 测试文件 | 测试重点 | +|------|---------|---------| +| 配置解析 | `configParser.test.ts` | 注释去除、JSON 解析、验证逻辑 | +| 颜色解析 | `colorResolver.test.ts` | 优先级、规律填色、边界情况 | +| 多文本解析 | `multiTextParser.test.ts` | 角度分配、SVG 识别 | +| 扇区构建 | `sectorBuilder.test.ts` | 完整扇区生成、groupSplit | +| 刻度环 | `degreeRing.test.ts` | 刻度线生成、度数标签 | + +**示例测试用例:** +```typescript +describe('ColorResolver', () => { + it('应正确解析颜色引用', () => { + const resolver = new ColorResolver( + { colorPalettes: { '木': '#43A047' } }, + '#000000' + ); + expect(resolver.resolveColor('木')).toBe('#43A047'); + }); + + it('应应用规律填色', () => { + const layer: LayerConfig = { + divisions: 12, + colorRef: '土', + num: 3, + interval: 1, + // ... + }; + const colorMap = resolver.resolveLayerColors(layer); + + // 扇区 0, 1, 2 着色 + expect(colorMap.has(0)).toBe(true); + expect(colorMap.has(1)).toBe(true); + expect(colorMap.has(2)).toBe(true); + // 扇区 3 空白 + expect(colorMap.has(3)).toBe(false); + // 扇区 4, 5, 6 着色 + expect(colorMap.has(4)).toBe(true); + }); +}); +``` + +### 8.2 集成测试 + +```typescript +describe('完整渲染流程', () => { + it('应从 JSON 配置生成完整罗盘', async () => { + const config = await loadConfig('/demo.json.conf'); + const { sectors, degreeRing, centerIcon } = buildLuopan(config); + + // 验证总扇区数 + expect(sectors.length).toBeGreaterThan(0); + + // 验证刻度环 + expect(degreeRing?.ticks.length).toBe(360); // 每度一个微刻度 + + // 验证中心图标 + expect(centerIcon?.svgPath).toContain('centericon.svg'); + }); +}); +``` + +### 8.3 E2E 测试(可选) + +使用 Playwright 或 Cypress 测试完整用户流程: +1. 加载页面 +2. 验证罗盘正确渲染 +3. 测试缩放和平移 +4. 验证多文本单元显示 +5. 验证刻度环显示 + +--- + +## 九、实施风险和缓解措施 + +| 风险 | 影响 | 概率 | 缓解措施 | +|-----|------|------|---------| +| 配置文件格式复杂,解析困难 | 高 | 中 | 分阶段实现,先支持基础字段,再扩展 | +| 多文本单元渲染性能问题 | 中 | 低 | 使用虚拟化技术,仅渲染可见扇区 | +| SVG 图标加载失败 | 中 | 中 | 实现错误处理和占位符显示 | +| 规律填色算法边界情况 | 高 | 中 | 充分的单元测试覆盖 | +| 现有代码兼容性 | 低 | 低 | `utils.ts` 保持不变,新旧模式共存 | + +--- + +## 十、总结和后续规划 + +### 10.1 重构价值 + +1. **配置驱动**:从硬编码到 JSON 配置,大幅提升灵活性 +2. **功能完整**:支持复杂着色、多文本、SVG 图标、刻度环 +3. **可维护性**:清晰的模块划分和职责分离 +4. **可扩展性**:易于添加新功能(如动画、交互) + +### 10.2 预期工期 + +- **总计:16-20 个工作日** + - 基础架构(阶段1-2):4-5天 + - 核心功能(阶段3-4):5-6天 + - 高级功能(阶段5):2-3天 + - 集成和优化(阶段6-7):5-6天 + +### 10.3 后续优化方向 + +1. **性能优化**: + - 虚拟滚动(大量扇区) + - Web Worker 解析配置 + - Canvas 渲染模式(可选) + +2. **功能扩展**: + - 配置编辑器(可视化配置生成) + - 动画效果(旋转、渐变) + - 导出功能(PNG, SVG) + - 多配置切换 + +3. **开发体验**: + - VS Code 插件(JSON 配置提示) + - 配置验证工具 + - 在线预览工具 + +--- + +**本重构方案完全基于现有代码和需求文档制定,确保可行性和可执行性。建议按阶段逐步实施,每个阶段完成后进行验收,确保质量和进度。**