diff --git a/public/demo.json b/public/demo.json new file mode 100644 index 0000000..c8b20d6 --- /dev/null +++ b/public/demo.json @@ -0,0 +1,175 @@ +{ + "name": "demo", + "description": "luopan demo config with named color palettes", + "background": "#fff000", + "outerRadius": 500, + "theme": { + "name": "五行配色主题", + "colorPalettes": { + "黑": "#000000", + "灰": "#757575", + "白": "#ffffff", + "木": "#43A047", + "火": "#E53935", + "土": "#8D6E63", + "金": "#78909C", + "水": "#0288D1", + "热": "#FF8F00", + "冷": "#1976D2", + "强": "#D32F2F", + "\u8f6f": "#FFE0B2" + } + }, + "layers": [ + { + "type": "centerIcon", + "centerIcon": { + "rIcon": 50, + "opacity": 0.8, + "name": "centericon.svg" + } + }, + { + "divisions": 2, + "rInner": 90, + "rOuter": 120, + "startAngle": 0, + + "sectors": [ + { + "content": "阴", + "innerFill": 1 + }, + { + "content": "阳", + "colorRef": "White", + "innerFill": 0 + } + ] + }, + { + "divisions": 8, + "rInner": 120, + "rOuter": 160, + "startAngle": 0, + "sectors": [ + { "content": "乾", "innerFill": 1 }, + { "content": "兑", "innerFill": 0 }, + { "content": "离", "innerFill": 1 }, + { "content": "震", "innerFill": 0 }, + { "content": "巽", "innerFill": 1 }, + { "content": "坎", "innerFill": 0 }, + { "content": "艮", "innerFill": 1 }, + { "content": "坤", "innerFill": 0 } + ] + }, + { + "divisions": 12, + "rInner": 160, + "rOuter": 200, + "startAngle": 0, + "colorRef": "土", + "innerFill": 1, + "num": 3, + "interval": 1, + "sectors": [ + { "content": "子", "colorRef": "水", "innerFill": 1 }, + { "content": "丑" }, + { "content": "寅", "colorRef": "木", "innerFill": 0 }, + { "content": "卯", "colorRef": "木", "innerFill": 1 }, + { "content": "辰" }, + { "content": "巳", "colorRef": "火", "innerFill": 1 }, + { "content": "午", "colorRef": "火", "innerFill": 0 }, + { "content": "未", "innerFill": 1 }, + { "content": "申", "colorRef": "金", "innerFill": 0 }, + { "content": "酉", "colorRef": "金", "innerFill": 1 }, + { "content": "戌" }, + { "content": "亥", "innerFill": 0 } + ] + }, + { + "divisions": 24, + "rInner": 210, + "rOuter": 240, + "startAngle": 0, + "sectors": [ + { "content": "甲乙|子|丙丁", "colorRef": "木", "innerFill": 1 }, + { "content": "丑" }, + { "content": "戊己|寅|庚辛", "colorRef": "土", "innerFill": 0 }, + { "content": "卯" }, + { "content": "壬癸|辰|甲乙", "innerFill": 1 }, + { "content": "巳" }, + { "content": "丙丁|午|戊己", "colorRef": "火", "innerFill": 0 }, + { "content": "未" }, + { "content": "庚辛|申|壬癸", "colorRef": "金", "innerFill": 1 }, + { "content": "酉" }, + { "content": "甲乙|戌|丙丁", "innerFill": 0 }, + { "content": "亥" }, + { "content": "戊己|子|庚辛" }, + { "content": "丑" }, + { "content": "壬癸|寅|甲乙" }, + { "content": "卯" }, + { "content": "丙丁|辰|戊己" }, + { "content": "巳" }, + { "content": "庚辛|午|壬癸" }, + { "content": "未" }, + { "content": "甲乙|申|丙丁" }, + { "content": "酉" }, + { "content": "戊己|戌|庚辛" }, + { "content": "亥" } + ] + }, + { + "divisions": 16, + "rInner": 240, + "rOuter": 270, + "startAngle": 0, + "sectors": [ + { "content": "甲乙|丙丁|戊己|庚辛", "colorRef": "木", "innerFill": 1 }, + { "content": "壬癸|甲乙|丙丁|戊己", "colorRef": "土", "innerFill": 0 }, + { "content": "庚辛|壬癸|甲乙|丙丁", "innerFill": 1 }, + { "content": "戊己|庚辛|壬癸|甲乙", "colorRef": "火", "innerFill": 0 }, + { "content": "丙丁|戊己|庚辛|壬癸", "colorRef": "金", "innerFill": 1 }, + { "content": "子丑|寅卯|辰巳|午未", "innerFill": 0 }, + { "content": "申酉|戌亥|子丑|寅卯", "colorRef": "木", "innerFill": 1 }, + { "content": "辰巳|午未|申酉|戌亥", "colorRef": "水", "innerFill": 0 }, + { "content": "甲乙|丙丁|戊己|庚辛", "innerFill": 1 }, + { "content": "壬癸|甲乙|丙丁|戊己", "colorRef": "土", "innerFill": 0 }, + { "content": "庚辛|壬癸|甲乙|丙丁", "colorRef": "木", "innerFill": 1 }, + { "content": "戊己|庚辛|壬癸|甲乙", "innerFill": 0 }, + { "content": "丙丁|戊己|庚辛|壬癸", "colorRef": "火", "innerFill": 1 }, + { "content": "子丑|寅卯|辰巳|午未", "colorRef": "金", "innerFill": 0 }, + { "content": "申酉|戌亥|子丑|寅卯", "innerFill": 1 }, + { "content": "辰巳|午未|申酉|戌亥", "colorRef": "水", "innerFill": 0 } + ] + }, + { + "divisions": 120, + "rInner": 300, + "rOuter": 310, + "startAngle": -4.5, + "innerFill": 0, + "colorRef": "火", + "num": 3, + "interval": 2, + "groupSplit": false + }, + { + "type": "degreeRing", + "degreeRing": { + "rInner": 350, + "rOuter": 380, + "showDegree": 1, + "mode": "both", + "opacity": 1, + "tickLength": 6, + "tickLengthStep": 2, + "majorTick": 10, + "minorTick": 5, + "microTick": 1, + "tickColor": "#000000", + "ringColor": "#000000" + } + } + ] +} diff --git a/public/demo.json.conf b/public/demo.json.conf index 1cb16c3..d73c2f6 100644 --- a/public/demo.json.conf +++ b/public/demo.json.conf @@ -48,33 +48,7 @@ "冷": "#1976D2", -- 冷蓝(冷色调) "强": "#D32F2F", -- 强烈红(高饱和度) "\u8f6f": "#FFE0B2" -- 柔和杏(低饱和度) - } - }, - - -- ======================================== - -- 中心图标配置 (Center Icon Configuration) - -- ======================================== - "centerIcon": { - "rIcon": 50, -- 图标半径,单位:像素 - "opacity": 0.8, -- 图标透明度(0.0-1.0,0为完全透明,1为完全不透明) - "name": "centericon.svg" -- SVG图标文件名,路径固定为 /icons/ 目录 - }, - - -- ======================================== - -- 360度刻度环配置 (360 Degree Scale Ring) - -- ======================================== - "degreeRing": { - "rInner": 450, -- 刻度环内半径 - "rOuter": 500, -- 刻度环外半径 - "showDegree": 1, -- 是否显示度数:0=不显示,1=显示(按 10° 间隔) - "mode": "both", -- 刻度线模式:"inner"(在rInner外侧)、"outer"(在rOuter内侧)、"both"(两侧都有,度数居中) - "opacity": 0.3, -- 圆环透明度(0.0-1.0,设置为0可以只显示刻度而不显示圆圈) - "tickLength": 6, -- 刻度线长度,单位:像素, minorTick比majorTick短1px, microTick比minorTick短1px - "majorTick": 10, -- 主刻度间隔(度),如 10 表示每 10° 一个主刻度 - "minorTick": 5, -- 次刻度间隔(度),如 2 表示每 2° 一个次刻度 - "microTick": 1, -- 微刻度间隔(度),如 1 表示每 1° 一个微刻度 - "tickColor": "#ffffff",-- 刻度线颜色 - "ringColor": "#ffffff" -- 圆环颜色 + }, }, -- ======================================== @@ -82,6 +56,17 @@ -- ======================================== -- 从内向外定义每一层的配置 "layers": [ + -- ======================================== + -- 中心图标层 (Center Icon Layer) + -- ======================================== + { + "type": "centerIcon", + "centerIcon": { + "rIcon": 50, -- 图标半径,单位:像素 + "opacity": 0.8, -- 图标透明度(0.0-1.0,0为完全透明,1为完全不透明) + "name": "centericon.svg" -- SVG图标文件名,路径固定为 /icons/ 目录 + } + }, -- ======================================== -- 阴阳 (2等分) -- ======================================== @@ -246,6 +231,26 @@ "num": 3, -- 连续着色3个扇区,每个区域跨3度 "interval": 2, -- 着色后间隔1个扇区 "groupSplit": false -- 新增:隐藏同组扇区之间的分割线, false表示不显示group中间分割线,该参数不设置,默认显示。 + }, + + -- ======================================== + -- 360度刻度环层 (360 Degree Scale Ring Layer) + -- ======================================== + { + "type": "degreeRing", + "degreeRing": { + "rInner": 450, -- 刻度环内半径 + "rOuter": 500, -- 刻度环外半径 + "showDegree": 1, -- 是否显示度数:0=不显示,1=显示(按 10° 间隔) + "mode": "both", -- 刻度线模式:"inner"(在rInner外侧)、"outer"(在rOuter内侧)、"both"(两侧都有,度数居中) + "opacity": 0.3, -- 圆环透明度(0.0-1.0,设置为0可以只显示刻度而不显示圆圈) + "tickLength": 6, -- 刻度线长度,单位:像素, minorTick比majorTick短1px, microTick比minorTick短1px + "majorTick": 10, -- 主刻度间隔(度),如 10 表示每 10° 一个主刻度 + "minorTick": 5, -- 次刻度间隔(度),如 2 表示每 2° 一个次刻度 + "microTick": 1, -- 微刻度间隔(度),如 1 表示每 1° 一个微刻度 + "tickColor": "#ffffff",-- 刻度线颜色 + "ringColor": "#ffffff" -- 圆环颜色 + } } ] } @@ -281,4 +286,3 @@ -- 示例:interval=0,表示该layer的所有扇区使用同样的colorRef -- -- ======================================== - diff --git a/public/demo2.json b/public/demo2.json new file mode 100644 index 0000000..484e9ad --- /dev/null +++ b/public/demo2.json @@ -0,0 +1,174 @@ +{ + "name": "demo2", + "description": "luopan demo config with named color palettes", + "background": "#000000", + "outerRadius": 500, + "theme": { + "name": "五行配色主题", + "colorPalettes": { + "黑": "#000000", + "灰": "#757575", + "白": "#ffffff", + "木": "#43A047", + "火": "#E53935", + "土": "#8D6E63", + "金": "#78909C", + "水": "#0288D1", + "热": "#FF8F00", + "冷": "#1976D2", + "强": "#D32F2F", + "\u8f6f": "#FFE0B2" + } + }, + "layers": [ + { + "type": "centerIcon", + "centerIcon": { + "rIcon": 50, + "opacity": 0.8, + "name": "centericon.svg" + } + }, + { + "divisions": 2, + "rInner": 60, + "rOuter": 90, + "startAngle": 0, + + "sectors": [ + { + "content": "阴", + "innerFill": 1 + }, + { + "content": "阳", + "colorRef": "White", + "innerFill": 0 + } + ] + }, + { + "divisions": 8, + "rInner": 120, + "rOuter": 160, + "startAngle": 0, + "sectors": [ + { "content": "乾", "innerFill": 1 }, + { "content": "兑", "innerFill": 0 }, + { "content": "离", "innerFill": 1 }, + { "content": "震", "innerFill": 0 }, + { "content": "巽", "innerFill": 1 }, + { "content": "坎", "innerFill": 0 }, + { "content": "艮", "innerFill": 1 }, + { "content": "坤", "innerFill": 0 } + ] + }, + { + "divisions": 12, + "rInner": 160, + "rOuter": 200, + "startAngle": 0, + "colorRef": "土", + "innerFill": 1, + "num": 3, + "interval": 1, + "sectors": [ + { "content": "子", "colorRef": "水", "innerFill": 1 }, + { "content": "丑" }, + { "content": "寅", "colorRef": "木", "innerFill": 0 }, + { "content": "卯", "colorRef": "木", "innerFill": 1 }, + { "content": "辰" }, + { "content": "巳", "colorRef": "火", "innerFill": 1 }, + { "content": "午", "colorRef": "火", "innerFill": 0 }, + { "content": "未", "innerFill": 1 }, + { "content": "申", "colorRef": "金", "innerFill": 0 }, + { "content": "酉", "colorRef": "金", "innerFill": 1 }, + { "content": "戌" }, + { "content": "亥", "innerFill": 0 } + ] + }, + { + "divisions": 24, + "rInner": 210, + "rOuter": 240, + "startAngle": 0, + "sectors": [ + { "content": "甲乙|子|丙丁", "colorRef": "木", "innerFill": 1 }, + { "content": "丑" }, + { "content": "戊己|寅|庚辛", "colorRef": "土", "innerFill": 0 }, + { "content": "卯" }, + { "content": "壬癸|辰|甲乙", "innerFill": 1 }, + { "content": "巳" }, + { "content": "丙丁|午|戊己", "colorRef": "火", "innerFill": 0 }, + { "content": "未" }, + { "content": "庚辛|申|壬癸", "colorRef": "金", "innerFill": 1 }, + { "content": "酉" }, + { "content": "甲乙|戌|丙丁", "innerFill": 0 }, + { "content": "亥" }, + { "content": "戊己|子|庚辛" }, + { "content": "丑" }, + { "content": "壬癸|寅|甲乙" }, + { "content": "卯" }, + { "content": "丙丁|辰|戊己" }, + { "content": "巳" }, + { "content": "庚辛|午|壬癸" }, + { "content": "未" }, + { "content": "甲乙|申|丙丁" }, + { "content": "酉" }, + { "content": "戊己|戌|庚辛" }, + { "content": "亥" } + ] + }, + { + "divisions": 16, + "rInner": 240, + "rOuter": 270, + "startAngle": 0, + "sectors": [ + { "content": "甲乙|丙丁|戊己|庚辛", "colorRef": "木", "innerFill": 1 }, + { "content": "壬癸|甲乙|丙丁|戊己", "colorRef": "土", "innerFill": 0 }, + { "content": "庚辛|壬癸|甲乙|丙丁", "innerFill": 1 }, + { "content": "戊己|庚辛|壬癸|甲乙", "colorRef": "火", "innerFill": 0 }, + { "content": "丙丁|戊己|庚辛|壬癸", "colorRef": "金", "innerFill": 1 }, + { "content": "子丑|寅卯|辰巳|午未", "innerFill": 0 }, + { "content": "申酉|戌亥|子丑|寅卯", "colorRef": "木", "innerFill": 1 }, + { "content": "辰巳|午未|申酉|戌亥", "colorRef": "水", "innerFill": 0 }, + { "content": "甲乙|丙丁|戊己|庚辛", "innerFill": 1 }, + { "content": "壬癸|甲乙|丙丁|戊己", "colorRef": "土", "innerFill": 0 }, + { "content": "庚辛|壬癸|甲乙|丙丁", "colorRef": "木", "innerFill": 1 }, + { "content": "戊己|庚辛|壬癸|甲乙", "innerFill": 0 }, + { "content": "丙丁|戊己|庚辛|壬癸", "colorRef": "火", "innerFill": 1 }, + { "content": "子丑|寅卯|辰巳|午未", "colorRef": "金", "innerFill": 0 }, + { "content": "申酉|戌亥|子丑|寅卯", "innerFill": 1 }, + { "content": "辰巳|午未|申酉|戌亥", "colorRef": "水", "innerFill": 0 } + ] + }, + { + "divisions": 120, + "rInner": 300, + "rOuter": 310, + "startAngle": -4.5, + "innerFill": 0, + "colorRef": "火", + "num": 3, + "interval": 2, + "groupSplit": false + }, + { + "type": "degreeRing", + "degreeRing": { + "rInner": 450, + "rOuter": 500, + "showDegree": 1, + "mode": "both", + "opacity": 0.3, + "tickLength": 6, + "majorTick": 10, + "minorTick": 5, + "microTick": 1, + "tickColor": "#ffffff", + "ringColor": "#ffffff" + } + } + ] +} diff --git a/refactor-plan.md b/refactor-plan.md index b5db4b9..3b9cc57 100644 --- a/refactor-plan.md +++ b/refactor-plan.md @@ -10,18 +10,21 @@ - **内置逻辑**:颜色、文字、填充等逻辑写死在代码中 - **测试导向**:现有实现主要为测试工具函数正确性而设计 -### 1.2 目标需求分析(基于 todolist.md 和 demo.json.conf) +### 1.2 目标需求分析(基于 todolist.md 和 demo.json) + +说明:`public/*.json` 为实际加载配置,`.json.conf` 仅作解释说明,不参与加载。 **核心变化:** 1. **配置驱动**:从 JSON 配置文件完全定义罗盘结构 -2. **复杂着色规则**:支持三级着色优先级(全局 → 层级规律填色 → 扇区独立) -3. **多文本单元**:扇区内容支持 `|` 分隔的多个文本单元,角度智能分配 -4. **SVG 图标支持**:扇区内容可以是 SVG 文件 -5. **中心图标**:支持可旋转的中心 SVG 图标 -6. **360度刻度环**:支持多种刻度模式的度数环 -7. **命名配色方案**:通过 theme.colorPalettes 定义可复用颜色 -8. **规律填色机制**:通过 num + interval 实现周期性着色 -9. **同组分割线控制**:groupSplit 参数控制组内分割线显示 +2. **新增罗盘零改码**:新增罗盘只需在 `public/` 下增加 JSON 配置文件,无需修改代码 +3. **复杂着色规则**:支持三级着色优先级(全局 → 层级规律填色 → 扇区独立) +4. **多文本单元**:扇区内容支持 `|` 分隔的多个文本单元,角度智能分配 +5. **SVG 图标支持**:扇区内容可以是 SVG 文件 +6. **中心图标**:作为 layer 类型,支持可旋转的中心 SVG 图标 +7. **360度刻度环**:作为 layer 类型,支持多种刻度模式的度数环 +8. **命名配色方案**:通过 theme.colorPalettes 定义可复用颜色 +9. **规律填色机制**:通过 num + interval 实现周期性着色 +10. **同组分割线控制**:groupSplit 参数控制组内分割线显示 --- @@ -95,18 +98,15 @@ JSON 文本 (带注释) [sectorBuilder] 遍历 layers ↓ 对每个 layer: - 1. 计算规律填色模式 (num, interval) - 2. 生成所有扇区 - 3. 对每个扇区: - - 应用颜色优先级 - - 解析多文本单元 - - 计算 SVG 路径 - ↓ -完整的 Sector 数组 - ↓ -[degreeRing] 生成刻度环 - ↓ -[centerIcon] 加载中心图标 + - type=sectors: + 1. 计算规律填色模式 (num, interval) + 2. 生成所有扇区 + 3. 对每个扇区: + - 应用颜色优先级 + - 解析多文本单元 + - 计算 SVG 路径 + - type=degreeRing:生成刻度环数据 + - type=centerIcon:加载中心图标 ↓ 最终渲染数据 ``` @@ -147,8 +147,6 @@ function applyPatternColoring( } ``` -CG: 角度分配还需结合扇区高度,就能计算出字的大小和布局 - **多文本单元角度分配:** ```typescript function splitMultiTextUnits( @@ -192,9 +190,7 @@ export interface LuopanConfig { description?: string; background: string; // 全局背景色 theme: ThemeConfig; - centerIcon?: CenterIconConfig; - degreeRing?: DegreeRingConfig; - layers: LayerConfig[]; + layers: LayerConfig[]; // 扇区层 + 中心图标层 + 刻度环层 } /** 主题配置 */ @@ -228,7 +224,14 @@ export interface DegreeRingConfig { } /** 层配置 */ -export interface LayerConfig { +export type LayerConfig = + | SectorLayerConfig + | CenterIconLayerConfig + | DegreeRingLayerConfig; + +/** 普通扇区层配置 */ +export interface SectorLayerConfig { + type?: 'sectors'; divisions: number; rInner: number; rOuter: number; @@ -241,6 +244,18 @@ export interface LayerConfig { sectors?: SectorConfig[]; } +/** 中心图标层配置 */ +export interface CenterIconLayerConfig { + type: 'centerIcon'; + centerIcon: CenterIconConfig; +} + +/** 刻度环层配置 */ +export interface DegreeRingLayerConfig { + type: 'degreeRing'; + degreeRing: DegreeRingConfig; +} + /** 扇区配置 */ export interface SectorConfig { content?: string; // 支持 "|" 分隔 @@ -256,7 +271,6 @@ export interface TextUnit { isSvg: boolean; // 是否为 SVG 文件名 } -CG: ??? /** 刻度线数据 */ export interface TickMark { angle: number; @@ -267,13 +281,12 @@ export interface TickMark { label?: string; // 度数标签(仅主刻度) } -CG:??? /** 扩展现有 Sector 接口 */ export interface Sector { // ... 现有字段保持 ... // 新增字段 - textUnits?: TextUnit[]; // 多文本单元 ??? + textUnits?: TextUnit[]; // 多文本单元 groupSplitVisible?: boolean; // 是否显示与下一个扇区的分割线 isSvgContent?: boolean; // 内容是否为 SVG svgPath?: string; // SVG 文件路径 @@ -284,7 +297,7 @@ export interface Sector { ```typescript // 输入:JSON 配置字符串 -const jsonText = await fetch('/demo.json.conf').then(r => r.text()); +const jsonText = await fetch('/demo.json').then(r => r.text()); // 步骤1:解析配置 const config = parseConfig(jsonText); @@ -294,18 +307,22 @@ 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) +const sectors = config.layers.flatMap((layer, layerIndex) => + layer.type === 'centerIcon' || layer.type === 'degreeRing' + ? [] + : sectorBuilder.buildLayer(layer, layerIndex) ); -// 步骤4:生成刻度环 -const degreeRingData = config.degreeRing - ? buildDegreeRing(config.degreeRing) +// 步骤4:生成刻度环(从 layers 中提取) +const degreeRingLayer = config.layers.find(layer => layer.type === 'degreeRing'); +const degreeRingData = degreeRingLayer + ? buildDegreeRing(degreeRingLayer.degreeRing) : null; -// 步骤5:加载中心图标 -const centerIconData = config.centerIcon - ? await loadCenterIcon(config.centerIcon) +// 步骤5:加载中心图标(从 layers 中提取) +const centerIconLayer = config.layers.find(layer => layer.type === 'centerIcon'); +const centerIconData = centerIconLayer + ? await loadCenterIcon(centerIconLayer.centerIcon) : null; // 输出:完整渲染数据 @@ -332,10 +349,10 @@ const renderData = { - JSON 解析 - 基础验证(必填字段检查) 3. ✅ 编写单元测试:`configParser.test.ts` -4. ✅ 测试用例:解析 `demo.json.conf` +4. ✅ 测试用例:解析 `demo.json` **验收标准:** -- 能成功解析 `demo.json.conf` 为 LuopanConfig 对象 +- 能成功解析 `demo.json` 为 LuopanConfig 对象 - 所有必填字段验证通过 - 测试覆盖率 > 80% @@ -417,34 +434,15 @@ export class ColorResolver { **核心代码:** ```typescript -export function parseMultiText( +// 仅保留名称,具体实现参考前文 splitMultiTextUnits +export function splitMultiTextUnits( 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; + // 参考前面代码 + return []; } ``` @@ -503,7 +501,7 @@ export class SectorBuilder { // 解析多文本单元 const content = sectorConfig?.content ?? ''; const textUnits = content.includes('|') - ? parseMultiText(content, aStart, aEnd) + ? splitMultiTextUnits(content, aStart, aEnd) : [{ content, aStart, aEnd, isSvg: content.endsWith('.svg') }]; // 确定是否显示分割线 @@ -553,7 +551,7 @@ export class SectorBuilder { ``` **验收标准:** -- 完整解析 `demo.json.conf` 所有 layer +- 完整解析 `demo.json` 所有 layer - 颜色优先级正确 - groupSplit 逻辑正确 - 多文本单元正确拆分 @@ -624,14 +622,17 @@ export function buildDegreeRing(config: DegreeRingConfig): { ticks.push({ angle, type, length, startR, endR }); } - // 生成度数标签 + // 生成度数标签(使用 textPath 保持方向一致) const labels: DegreeLabel[] = []; if (config.showDegree === 1) { + const r = (rInner + rOuter) / 2; for (let angle = 0; angle < 360; angle += config.majorTick) { labels.push({ angle, text: angle.toString(), - r: (rInner + rOuter) / 2 + r, + textPathId: `degree-label-${angle}`, + textPath: generateTextPath(r, r, angle - 4, angle + 4) }); } } @@ -652,7 +653,7 @@ export function buildDegreeRing(config: DegreeRingConfig): { **验收标准:** - 刻度线长度正确(major > minor > micro) - mode 模式正确(inner, outer, both) -- 度数标签位置准确 +- 度数标签方向与扇区文字一致(自动翻转) - 中心图标加载和旋转正常 --- @@ -711,17 +712,21 @@ export function useLuopan(configPathOrObject: string | LuopanConfig) { // 构建扇区 const sectorBuilder = new SectorBuilder(colorResolver); sectors.value = configObj.layers.flatMap((layer, i) => - sectorBuilder.buildLayer(layer, i) + layer.type === 'centerIcon' || layer.type === 'degreeRing' + ? [] + : sectorBuilder.buildLayer(layer, i) ); - // 构建刻度环 - if (configObj.degreeRing) { - degreeRing.value = buildDegreeRing(configObj.degreeRing); + // 构建刻度环(从 layers 中提取) + const degreeRingLayer = configObj.layers.find(layer => layer.type === 'degreeRing'); + if (degreeRingLayer) { + degreeRing.value = buildDegreeRing(degreeRingLayer.degreeRing); } - // 加载中心图标 - if (configObj.centerIcon) { - centerIcon.value = await loadCenterIcon(configObj.centerIcon); + // 加载中心图标(从 layers 中提取) + const centerIconLayer = configObj.layers.find(layer => layer.type === 'centerIcon'); + if (centerIconLayer) { + centerIcon.value = await loadCenterIcon(centerIconLayer.centerIcon); } } catch (e) { @@ -807,12 +812,12 @@ export function useLuopan(configPathOrObject: string | LuopanConfig) { ``` **验收标准:** -- 成功加载和渲染 `demo.json.conf` +- 成功加载和渲染 `demo.json` - 所有 layer 正确显示 - 多文本单元正确拆分显示 - 刻度环和中心图标正常渲染 @@ -844,8 +849,8 @@ const { config, sectors, degreeRing, centerIcon, loading, error } = **测试策略:** ```typescript describe('完整流程测试', () => { - it('应正确解析和渲染 demo.json.conf', async () => { - const config = await loadConfig('/demo.json.conf'); + it('应正确解析和渲染 demo.json', async () => { + const config = await loadConfig('/demo.json'); const sectors = buildAllSectors(config); // 验证扇区数量 @@ -927,6 +932,7 @@ describe('完整流程测试', () => { 2. 每个 `TextUnit` 包含独立的 `aStart`, `aEnd`, `content` 3. 渲染时为每个单元生成独立的 `` 元素 4. 使用 `getLayoutRatio()` 确保角度分配准确 +5. 字体大小与布局按单元独立计算:每个 `TextUnit` 用自身 `aStart/aEnd` + `content.length` 调用 `calculateSectorFontSize`,并基于相同角度范围生成 `generateTextPath` / `generateVerticalTextPath` **代码示例:** ```typescript @@ -1091,7 +1097,7 @@ describe('ColorResolver', () => { ```typescript describe('完整渲染流程', () => { it('应从 JSON 配置生成完整罗盘', async () => { - const config = await loadConfig('/demo.json.conf'); + const config = await loadConfig('/demo.json'); const { sectors, degreeRing, centerIcon } = buildLuopan(config); // 验证总扇区数 diff --git a/src/Luopan.vue b/src/Luopan.vue index 4ddb8dd..a8f8e25 100644 --- a/src/Luopan.vue +++ b/src/Luopan.vue @@ -2,15 +2,6 @@ - - 示例 {{ idx + 1 }}:{{ ex.name }} - - 显示辅助线 @@ -31,8 +22,12 @@ + 加载中... + 错误: {{ error.message }} + - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{ unit.content }} + + + + + + + + + {{ s.label }} + + + + + + + + + + + + - - - - - - - - + - - - + + + + + + + - - - - - {{ s.label }} + {{ label.text }} + - - + + @@ -166,90 +282,168 @@