update at 2026-01-23 23:20:39
This commit is contained in:
1
.codex/.gitignore
vendored
Normal file
1
.codex/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
tmp/
|
||||
@@ -6,23 +6,7 @@
|
||||
"strokeOpacity": 1,
|
||||
"strokeColor": "黑",
|
||||
"insetDistance": 1,
|
||||
"theme": {
|
||||
"name": "五行配色主题",
|
||||
"colorPalettes": {
|
||||
"黑": "#000000",
|
||||
"灰": "#757575",
|
||||
"白": "#ffffff",
|
||||
"木": "#43A047",
|
||||
"火": "#E53935",
|
||||
"土": "#8D6E63",
|
||||
"金": "#D4AF37",
|
||||
"水": "#0288D1",
|
||||
"热": "#FF8F00",
|
||||
"冷": "#1976D2",
|
||||
"强": "#D32F2F",
|
||||
"软": "#FFE0B2"
|
||||
}
|
||||
},
|
||||
"themeRef": "五行强",
|
||||
"layers": [
|
||||
{
|
||||
"type": "centerIcon",
|
||||
|
||||
@@ -26,33 +26,9 @@
|
||||
-- ========================================
|
||||
-- 主题配置
|
||||
-- ========================================
|
||||
"theme": {
|
||||
"name": "五行配色主题",
|
||||
|
||||
-- 文字颜色自动计算:根据背景色明暗度自动选择黑/白文字,确保高对比度
|
||||
-- "textOnLight": "#1a1a1a", 已删除,由系统自动计算
|
||||
-- "textOnDark": "#ffffff", 已删除,由系统自动计算
|
||||
|
||||
-- ========================================
|
||||
-- 命名配色方案 (Named Color Palettes)
|
||||
-- ========================================
|
||||
-- 定义可复用的命名颜色,在 layers 中通过名称引用
|
||||
-- 包含10种配色:黑、灰、白、五行(木火土金水)、热、冷、强、软
|
||||
"colorPalettes": {
|
||||
"黑": "#000000", -- 纯黑
|
||||
"灰": "#757575", -- 中灰
|
||||
"白": "#ffffff", -- 纯白
|
||||
"木": "#43A047", -- 生机绿(五行:木)
|
||||
"火": "#E53935", -- 烈焰红(五行:火)
|
||||
"土": "#8D6E63", -- 大地棕(五行:土)
|
||||
"金": "#78909C", -- 金属灰(五行:金)
|
||||
"水": "#0288D1", -- 水蓝(五行:水)
|
||||
"热": "#FF8F00", -- 暖橙(暖色调)
|
||||
"冷": "#1976D2", -- 冷蓝(冷色调)
|
||||
"强": "#D32F2F", -- 强烈红(高饱和度)
|
||||
"\u8f6f": "#FFE0B2" -- 柔和杏(低饱和度)
|
||||
},
|
||||
},
|
||||
-- 主题统一放在 public/themes.json 中管理
|
||||
-- 通过 themeRef 引用主题名称;不填则使用 themes.json.default
|
||||
"themeRef": "五行弱",
|
||||
|
||||
-- ========================================
|
||||
-- 层配置 (Layers Configuration)
|
||||
@@ -129,7 +105,7 @@
|
||||
"startAngle": 0, -- 第一个扇区的起始角度(度,0度为正北方向,顺时针)
|
||||
"colorRef": "土", -- 着色使用的颜色引用
|
||||
"innerFill": 1, -- 着色区域的内缩设置
|
||||
-- "start": 1, -- 从第1个扇区开始着色(已废弃,统一从第1个扇区开始)
|
||||
-- "patternOffset": 2, -- 第一个着色单元偏移1个扇区(可选)
|
||||
"num": 3, -- 连续着色3个扇区
|
||||
"interval": 1, -- 着色后间隔1个扇区
|
||||
"sectors": [
|
||||
@@ -265,17 +241,18 @@
|
||||
-- 示例:{ "content": "木", "colorRef": "木" }
|
||||
--
|
||||
-- 优先级2:层级规律填色配置
|
||||
-- 通过 colorRef + start + num + interval 实现规律着色
|
||||
-- 通过 colorRef + patternOffset + num + interval 实现规律着色
|
||||
-- 未着色的扇区使用全局 background 颜色
|
||||
--
|
||||
-- ========================================
|
||||
-- 规律填色算法说明
|
||||
-- ========================================
|
||||
-- startAngle: 第一个扇区的起始角度(度,0度为正北方向,顺时针增加)
|
||||
-- patternOffset: 规律填色起始偏移(扇区序号,从1开始,默认1,未指定时与startAngle对齐)
|
||||
-- num: 连续着色扇区数量
|
||||
-- interval: 着色后间隔的扇区数量
|
||||
--
|
||||
-- 算法:从第1个扇区开始,循环执行 "着色num个" → "跳过interval个"
|
||||
-- 算法:从第 patternOffset 个扇区开始,循环执行 "着色num个" → "跳过interval个"
|
||||
-- 特殊情况:interval=0 表示无间隔,全部着色
|
||||
--
|
||||
-- 示例:divisions=12, startAngle=0, num=3, interval=1
|
||||
|
||||
@@ -1,25 +1,13 @@
|
||||
{
|
||||
"name": "demo2",
|
||||
"description": "luopan demo config with named color palettes",
|
||||
"description": "luopan demo2 config with named color palettes",
|
||||
"background": "#000000",
|
||||
"strokeWidth": 1,
|
||||
"strokeOpacity": 1,
|
||||
"strokeColor": "白",
|
||||
"insetDistance": 1,
|
||||
"outerRadius": 500,
|
||||
"theme": {
|
||||
"name": "五行配色主题",
|
||||
"colorPalettes": {
|
||||
"黑": "#000000",
|
||||
"灰": "#757575",
|
||||
"白": "#ffffff",
|
||||
"木": "#43A047",
|
||||
"火": "#E53935",
|
||||
"土": "#8D6E63",
|
||||
"金": "#78909C",
|
||||
"水": "#0288D1",
|
||||
"热": "#FF8F00",
|
||||
"冷": "#1976D2",
|
||||
"强": "#D32F2F",
|
||||
"\u8f6f": "#FFE0B2"
|
||||
}
|
||||
},
|
||||
"themeRef": "五行弱",
|
||||
"layers": [
|
||||
{
|
||||
"type": "centerIcon",
|
||||
@@ -38,11 +26,12 @@
|
||||
"sectors": [
|
||||
{
|
||||
"content": "阴",
|
||||
"colorRef": "黑",
|
||||
"innerFill": 1
|
||||
},
|
||||
{
|
||||
"content": "阳",
|
||||
"colorRef": "White",
|
||||
"colorRef": "白",
|
||||
"innerFill": 0
|
||||
}
|
||||
]
|
||||
@@ -150,8 +139,8 @@
|
||||
"startAngle": -4.5,
|
||||
"innerFill": 0,
|
||||
"colorRef": "火",
|
||||
"num": 3,
|
||||
"interval": 2,
|
||||
"num": 4,
|
||||
"interval": 1,
|
||||
"groupSplit": false
|
||||
},
|
||||
{
|
||||
|
||||
160
public/demo3.json
Normal file
160
public/demo3.json
Normal file
@@ -0,0 +1,160 @@
|
||||
{
|
||||
"name": "demo",
|
||||
"description": "luopan demo config with named color palettes",
|
||||
"background": "白",
|
||||
"strokeWidth": 1,
|
||||
"strokeOpacity": 1,
|
||||
"strokeColor": "黑",
|
||||
"insetDistance": 1,
|
||||
"themeRef": "五行弱",
|
||||
"layers": [
|
||||
{
|
||||
"type": "centerIcon",
|
||||
"centerIcon": {
|
||||
"rIcon": 50,
|
||||
"opacity": 0.8,
|
||||
"name": "centericon.svg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"divisions": 2,
|
||||
"rInner": 60,
|
||||
"rOuter": 100,
|
||||
"startAngle": 0,
|
||||
"sectors": [
|
||||
{
|
||||
"content": "阴",
|
||||
"colorRef": "黑"
|
||||
},
|
||||
{
|
||||
"content": "阳",
|
||||
"colorRef": "白"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"divisions": 8,
|
||||
"rInner": 120,
|
||||
"rOuter": 160,
|
||||
"startAngle": 0,
|
||||
"colorRef": "水",
|
||||
"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": "土",
|
||||
"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": 200,
|
||||
"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": 250,
|
||||
"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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
21
public/luopan-configs.json
Normal file
21
public/luopan-configs.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"default": "demo.json",
|
||||
"items": [
|
||||
{
|
||||
"name": "样式测试1",
|
||||
"path": "/demo.json"
|
||||
},
|
||||
{
|
||||
"name": "样式测试2",
|
||||
"path": "/demo2.json"
|
||||
},
|
||||
{
|
||||
"name": "样式测试3",
|
||||
"path": "/demo3.json"
|
||||
},
|
||||
{
|
||||
"name": "入门罗盘",
|
||||
"path": "/rumen.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
162
public/rumen.json
Normal file
162
public/rumen.json
Normal file
@@ -0,0 +1,162 @@
|
||||
{
|
||||
"name": "rumen",
|
||||
"description": "luopan rumen config",
|
||||
"background": "金",
|
||||
"strokeWidth": 1,
|
||||
"strokeOpacity": 1,
|
||||
"strokeColor": "黑",
|
||||
"insetDistance": 1,
|
||||
"themeRef": "五行弱",
|
||||
"layers": [
|
||||
{
|
||||
"type": "centerIcon",
|
||||
"centerIcon": {
|
||||
"rIcon": 50,
|
||||
"opacity": 0.8,
|
||||
"name": "centericon.svg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"divisions": 2,
|
||||
"rInner": 60,
|
||||
"rOuter": 100,
|
||||
"startAngle": 0,
|
||||
"sectors": [
|
||||
{
|
||||
"content": "阴",
|
||||
"colorRef": "黑"
|
||||
},
|
||||
{
|
||||
"content": "阳",
|
||||
"colorRef": "白"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"divisions": 8,
|
||||
"rInner": 120,
|
||||
"rOuter": 160,
|
||||
"startAngle": 0,
|
||||
"sectors": [
|
||||
{ "content": "乾", "colorRef": "火", "innerFill": 1 },
|
||||
{ "content": "兑", "colorRef": "冷", "innerFill": 0 },
|
||||
{ "content": "离", "colorRef": "火", "innerFill": 1 },
|
||||
{ "content": "震", "colorRef": "木", "innerFill": 0 },
|
||||
{ "content": "巽", "colorRef": "木", "innerFill": 1 },
|
||||
{ "content": "坎", "colorRef": "冷", "innerFill": 0 },
|
||||
{ "content": "艮", "colorRef": "强", "innerFill": 1 },
|
||||
{ "content": "坤", "colorRef": "土", "innerFill": 0 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"divisions": 12,
|
||||
"rInner": 160,
|
||||
"rOuter": 200,
|
||||
"startAngle": 0,
|
||||
"colorRef": "金",
|
||||
"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": 200,
|
||||
"rOuter": 240,
|
||||
"startAngle": 0,
|
||||
"colorRef": "金",
|
||||
"num": 2,
|
||||
"interval": 1,
|
||||
"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": 250,
|
||||
"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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
174
public/themes.json
Normal file
174
public/themes.json
Normal file
@@ -0,0 +1,174 @@
|
||||
{
|
||||
"default": "五行强",
|
||||
"items": [
|
||||
{
|
||||
"name": "五行强",
|
||||
"colorPalettes": {
|
||||
"黑": "#000000",
|
||||
"灰": "#757575",
|
||||
"白": "#ffffff",
|
||||
"木": "#43A047",
|
||||
"火": "#E53935",
|
||||
"土": "#8D6E63",
|
||||
"金": "#D4AF37",
|
||||
"水": "#0288D1",
|
||||
"热": "#FF8F00",
|
||||
"冷": "#1976D2",
|
||||
"强": "#D32F2F",
|
||||
"软": "#FFE0B2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "五行弱",
|
||||
"colorPalettes": {
|
||||
"黑": "#000000",
|
||||
"灰": "#DADADA",
|
||||
"白": "#ffffff",
|
||||
"木": "#2A6B2D",
|
||||
"火": "#C44846",
|
||||
"土": "#A57563",
|
||||
"金": "#E7D238",
|
||||
"水": "#1E7BAC",
|
||||
"热": "#FF8F00",
|
||||
"冷": "#1976D2",
|
||||
"强": "#FF0000",
|
||||
"弱": "#EFD272"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "晨曦",
|
||||
"colorPalettes": {
|
||||
"霞光": "#FFB997",
|
||||
"暖金": "#FFC857",
|
||||
"薄雾": "#E6EFF6",
|
||||
"嫩绿": "#A8E6CE",
|
||||
"朝蓝": "#7AC7E3",
|
||||
"珊红": "#FF6B6B",
|
||||
"砂色": "#D9B99B",
|
||||
"云白": "#FFFFFF",
|
||||
"影灰": "#7D7F87",
|
||||
"墨蓝": "#2A3D66"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "暮色",
|
||||
"colorPalettes": {
|
||||
"暮紫": "#6C5B7B",
|
||||
"深蓝": "#283149",
|
||||
"暮橙": "#FF914D",
|
||||
"黯灰": "#5D5D66",
|
||||
"暗玉": "#2E8B7C",
|
||||
"烟蓝": "#4B6F86",
|
||||
"月白": "#F2F6F8",
|
||||
"铜黄": "#C08F35",
|
||||
"绛红": "#A23E48",
|
||||
"夜黑": "#0B0F1A"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "海风",
|
||||
"colorPalettes": {
|
||||
"海蓝": "#0077B6",
|
||||
"浅滩": "#EAF6F6",
|
||||
"浪白": "#F7FCFF",
|
||||
"蔚蓝": "#00B4D8",
|
||||
"青绿": "#2EC4B6",
|
||||
"碧玉": "#118AB2",
|
||||
"珊粉": "#FF7A7A",
|
||||
"深海": "#023E8A",
|
||||
"泡沫": "#CFF7F3",
|
||||
"夕珊": "#FFB4A2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "砂岩",
|
||||
"colorPalettes": {
|
||||
"砂黄": "#DDB892",
|
||||
"土褐": "#A67C52",
|
||||
"岩灰": "#8A8776",
|
||||
"日黄": "#FFDA77",
|
||||
"褐红": "#A65E3B",
|
||||
"朽木": "#C7B299",
|
||||
"石白": "#F5F3EE",
|
||||
"暮砂": "#BFAE9D",
|
||||
"土灰": "#9E8F80",
|
||||
"暗褐": "#5C3A21"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "森林",
|
||||
"colorPalettes": {
|
||||
"树绿": "#2F7A1F",
|
||||
"苔绿": "#69995B",
|
||||
"深绿": "#1B4636",
|
||||
"叶黄": "#C8D44B",
|
||||
"泥褐": "#7C5A3A",
|
||||
"树皮": "#6A4C3B",
|
||||
"莹绿": "#9FD89B",
|
||||
"林影": "#254117",
|
||||
"嫩叶": "#B7E4A6",
|
||||
"藤蔓": "#3B6B35"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "霓虹",
|
||||
"colorPalettes": {
|
||||
"电青": "#00F5D4",
|
||||
"霓粉": "#FF6AC1",
|
||||
"紫光": "#9D4EDD",
|
||||
"亮黄": "#FFD60A",
|
||||
"霓橙": "#FF7A00",
|
||||
"洋红": "#FF007F",
|
||||
"亮蓝": "#00A3FF",
|
||||
"霓绿": "#41F582",
|
||||
"黑底": "#0A0A0A",
|
||||
"白晕": "#F8F8FF"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "芙蓉",
|
||||
"colorPalettes": {
|
||||
"芙粉": "#FADCD9",
|
||||
"荷绿": "#9BD7C6",
|
||||
"莲白": "#FFF7F3",
|
||||
"柔紫": "#E9D6FF",
|
||||
"粉橙": "#FFD1BA",
|
||||
"浅灰": "#E6E6E6",
|
||||
"香黄": "#FFE9A8",
|
||||
"薄荷": "#BCEBCB",
|
||||
"桃红": "#FF94A6",
|
||||
"雾蓝": "#DDECF7"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "墨韵",
|
||||
"colorPalettes": {
|
||||
"墨黑": "#0B0B0B",
|
||||
"浓灰": "#4A4A4A",
|
||||
"烟白": "#F5F5F5",
|
||||
"石墨": "#2E2E2E",
|
||||
"青黛": "#1F6F8B",
|
||||
"皓白": "#FFFFFF",
|
||||
"墨蓝": "#14213D",
|
||||
"暗灰": "#6B6B6B",
|
||||
"笔褐": "#5B4B3A",
|
||||
"玄青": "#0F4C5C"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "灰阶",
|
||||
"colorPalettes": {
|
||||
"墨灰": "#0B0B0B",
|
||||
"深灰": "#2F2F2F",
|
||||
"石灰": "#595959",
|
||||
"烟灰": "#808080",
|
||||
"中灰": "#A0A0A0",
|
||||
"银灰": "#C0C0C0",
|
||||
"雾灰": "#D9D9D9",
|
||||
"亮灰": "#EAEAEA",
|
||||
"浅灰": "#F5F5F5",
|
||||
"乳白": "#FFFFFF"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
128
refactor-plan.md
128
refactor-plan.md
@@ -17,15 +17,91 @@
|
||||
**核心变化:**
|
||||
1. **配置驱动**:从 JSON 配置文件完全定义罗盘结构
|
||||
2. **新增罗盘零改码**:新增罗盘只需在 `public/` 下增加 JSON 配置文件,无需修改代码
|
||||
3. **复杂着色规则**:支持三级着色优先级(全局 → 层级规律填色 → 扇区独立)
|
||||
4. **多文本单元**:扇区内容支持 `|` 分隔的多个文本单元,角度智能分配
|
||||
5. **SVG 图标支持**:扇区内容可以是 SVG 文件
|
||||
6. **中心图标**:作为 layer 类型,支持可旋转的中心 SVG 图标
|
||||
7. **360度刻度环**:作为 layer 类型,支持多种刻度模式的度数环
|
||||
8. **命名配色方案**:通过 theme.colorPalettes 定义可复用颜色
|
||||
9. **规律填色机制**:通过 num + interval 实现周期性着色
|
||||
10. **同组分割线控制**:groupSplit 参数控制组内分割线显示
|
||||
11. **外半径兜底**:默认使用 layers 最大 rOuter,outerRadius 仅作为无层配置时的兜底
|
||||
3. **配置下拉切换**:下拉菜单可选择 `public/` 下的配置(如 `demo.json`、`demo2.json`)切换罗盘
|
||||
4. **复杂着色规则**:支持三级着色优先级(全局 → 层级规律填色 → 扇区独立)
|
||||
5. **多文本单元**:扇区内容支持 `|` 分隔的多个文本单元,角度智能分配
|
||||
6. **多文本比例可配**:在每个 layer 中可配置多文本比例(用于 `|` 分隔内容的角度分配)
|
||||
7. **SVG 图标支持**:扇区内容可以是 SVG 文件
|
||||
8. **中心图标**:作为 layer 类型,支持可旋转的中心 SVG 图标
|
||||
9. **360度刻度环**:作为 layer 类型,支持多种刻度模式的度数环
|
||||
10. **命名配色方案**:通过 theme.colorPalettes 定义可复用颜色
|
||||
11. **规律填色机制**:通过 num + interval 实现周期性着色
|
||||
12. **同组分割线控制**:groupSplit 参数控制组内分割线显示
|
||||
13. **外半径兜底**:默认使用 layers 最大 rOuter,outerRadius 仅作为无层配置时的兜底
|
||||
|
||||
### 1.3 配置清单与切换机制(新增)
|
||||
|
||||
**问题:** 运行时无法直接读取 `public/` 目录文件列表,因此下拉菜单需要“配置清单”或“构建期生成清单”。
|
||||
|
||||
**推荐方案(满足“新增罗盘只需增加 JSON 文件”):**
|
||||
1. **清单文件:** 构建期生成 `public/luopan-configs.json`,包含可选配置列表与默认项。
|
||||
2. **生成逻辑:** 扫描 `public/` 下的 `*.json`,排除 `*.json.conf` 和 `luopan-configs.json` 本身。
|
||||
3. **示例格式:**
|
||||
```json
|
||||
{
|
||||
"default": "demo.json",
|
||||
"items": [
|
||||
{ "name": "示例罗盘一", "path": "/demo.json" },
|
||||
{ "name": "示例罗盘二", "path": "/demo2.json" }
|
||||
]
|
||||
}
|
||||
```
|
||||
4. **前端加载:** `Luopan.vue` 启动时拉取该清单,渲染下拉选项。
|
||||
5. **切换逻辑:** 选择项变化时更新 `configPath` 并触发重新加载;同步更新 URL `?config=xxx.json` 便于分享。
|
||||
6. **新增配置:** 仅需在 `public/` 放入新的 `*.json`,清单会在构建/开发启动时自动更新。
|
||||
|
||||
**备用方案(不启用生成脚本):** 手动维护 `public/luopan-configs.json`,仍然无需改代码,但需追加清单条目。
|
||||
|
||||
### 1.4 主题抽离与引用(新增)
|
||||
|
||||
**目标:** 罗盘配置通过 `themeRef` 引用统一的 `themes.json`,不再内嵌 theme。
|
||||
|
||||
**主题文件结构(public/themes.json):**
|
||||
```json
|
||||
{
|
||||
"default": "五行",
|
||||
"items": [
|
||||
{
|
||||
"name": "五行",
|
||||
"colorPalettes": {
|
||||
"黑": "#000000",
|
||||
"灰": "#757575"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**罗盘 JSON 使用方式:**
|
||||
```json
|
||||
{
|
||||
"name": "demo",
|
||||
"background": "白",
|
||||
"themeRef": "五行",
|
||||
"layers": []
|
||||
}
|
||||
```
|
||||
|
||||
**解析规则:**
|
||||
1. 如果配置包含 `themeRef`,优先使用对应主题。
|
||||
2. 若未指定 `themeRef`,使用 `themes.json.default`。
|
||||
3. 若 `themeRef` 或 `default` 不存在:抛出配置错误(或回退到空主题,需在实现中约定)。
|
||||
|
||||
**实现改动点:**
|
||||
1. `types.ts`:
|
||||
- `LuopanConfig.theme` 变为可选
|
||||
- 新增 `themeRef?: string`
|
||||
2. `configParser.ts`:
|
||||
- 支持解析 `themeRef`
|
||||
- 当 `theme` 缺失时暂不报错,由上层注入主题
|
||||
3. `useLuopan.ts`:
|
||||
- 读取 `public/themes.json`
|
||||
- 根据 `themeRef/default` 注入主题到配置
|
||||
- 处理错误与回退
|
||||
4. 示例与文档:
|
||||
- `public/demo*.json` 替换为 `themeRef`
|
||||
- 新增 `public/themes.json`
|
||||
- 更新 `demo.json.conf` 说明
|
||||
|
||||
---
|
||||
|
||||
@@ -148,15 +224,17 @@ function applyPatternColoring(
|
||||
}
|
||||
```
|
||||
|
||||
**多文本单元角度分配:**
|
||||
**多文本单元角度分配(支持 unitRatios):**
|
||||
```typescript
|
||||
function splitMultiTextUnits(
|
||||
content: string,
|
||||
aStart: number,
|
||||
aEnd: number
|
||||
aEnd: number,
|
||||
unitRatios?: number[]
|
||||
): TextUnit[] {
|
||||
const units = content.split('|');
|
||||
const ratios = getLayoutRatio(units.length); // 从 constants.ts
|
||||
const ratios = normalizeRatios(unitRatios, units.length)
|
||||
?? getLayoutRatio(units.length); // 从 constants.ts
|
||||
|
||||
const totalAngle = aEnd - aStart;
|
||||
const textUnits: TextUnit[] = [];
|
||||
@@ -174,6 +252,27 @@ function splitMultiTextUnits(
|
||||
|
||||
return textUnits;
|
||||
}
|
||||
|
||||
function normalizeRatios(
|
||||
ratios: number[] | undefined,
|
||||
count: number
|
||||
): number[] | null {
|
||||
if (!ratios || ratios.length !== count) return null;
|
||||
if (ratios.some((value) => typeof value !== 'number' || value <= 0)) return null;
|
||||
const sum = ratios.reduce((acc, value) => acc + value, 0);
|
||||
if (sum <= 0) return null;
|
||||
return ratios.map((value) => value / sum);
|
||||
}
|
||||
```
|
||||
|
||||
**示例(layer 级别配置比例):**
|
||||
```json
|
||||
{
|
||||
"divisions": 24,
|
||||
"rInner": 200,
|
||||
"rOuter": 240,
|
||||
"unitRatios": [0.25, 0.5, 0.25]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
@@ -243,6 +342,7 @@ export interface SectorLayerConfig {
|
||||
startAngle?: number;
|
||||
colorRef?: string;
|
||||
innerFill?: 0 | 1;
|
||||
unitRatios?: number[]; // 多文本单元比例(对应 content 用 "|" 分隔)
|
||||
num?: number;
|
||||
interval?: number;
|
||||
groupSplit?: boolean;
|
||||
@@ -429,6 +529,7 @@ export class ColorResolver {
|
||||
- splitMultiTextUnits() 函数
|
||||
- 角度分配逻辑
|
||||
- SVG 文件检测
|
||||
- 支持 layer.unitRatios 覆盖默认比例
|
||||
2. ✅ 更新 `utils.ts`:
|
||||
- 适配多文本单元的路径生成
|
||||
- 字体大小计算调整
|
||||
@@ -674,7 +775,7 @@ export function buildDegreeRing(config: DegreeRingConfig): {
|
||||
- 调用各个解析器和构建器
|
||||
- 返回完整渲染数据
|
||||
2. ✅ 更新 `Luopan.vue`:
|
||||
- 移除示例选择器
|
||||
- 使用配置下拉替换示例选择器(可切换 `public/*.json`)
|
||||
- 添加配置加载界面
|
||||
- 渲染多文本单元
|
||||
- 渲染刻度环
|
||||
@@ -1179,4 +1280,3 @@ describe('完整渲染流程', () => {
|
||||
---
|
||||
|
||||
**本重构方案完全基于现有代码和需求文档制定,确保可行性和可执行性。建议按阶段逐步实施,每个阶段完成后进行验收,确保质量和进度。**
|
||||
|
||||
|
||||
100
src/Luopan.vue
100
src/Luopan.vue
@@ -2,6 +2,19 @@
|
||||
<div class="luopan-wrap">
|
||||
<!-- 工具栏 -->
|
||||
<div class="toolbar">
|
||||
<label class="toggle" v-if="configOptions.length">
|
||||
<span>罗盘配置</span>
|
||||
<select v-model="selectedConfigPath" :disabled="Boolean(props.config)">
|
||||
<option
|
||||
v-for="item in configOptions"
|
||||
:key="item.path"
|
||||
:value="item.path"
|
||||
>
|
||||
{{ item.name }}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label class="toggle">
|
||||
<input type="checkbox" v-model="showGuides" />
|
||||
显示辅助线
|
||||
@@ -298,10 +311,10 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
import { useLuopan } from './composables/useLuopan';
|
||||
import { DEFAULT_SIZE, DEFAULT_TEXT_RADIAL_POSITION, SECTOR_STROKE_WIDTH } from './constants';
|
||||
import type { LuopanConfig, Sector, TextRadialPosition, TextUnit } from './types';
|
||||
import type { LuopanConfigInput, Sector, TextRadialPosition, TextUnit } from './types';
|
||||
import { annularSectorCentroid } from './utils';
|
||||
|
||||
/**
|
||||
@@ -310,7 +323,7 @@ import { annularSectorCentroid } from './utils';
|
||||
interface Props {
|
||||
size?: number;
|
||||
configPath?: string;
|
||||
config?: LuopanConfig;
|
||||
config?: LuopanConfigInput;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
@@ -338,13 +351,81 @@ const dragStartY = ref(0);
|
||||
const dragStartPanX = ref(0);
|
||||
const dragStartPanY = ref(0);
|
||||
|
||||
const resolveConfigPath = () => {
|
||||
if (typeof window === 'undefined') return props.configPath;
|
||||
interface ConfigListItem {
|
||||
name: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
interface ConfigList {
|
||||
default?: string;
|
||||
items?: ConfigListItem[];
|
||||
}
|
||||
|
||||
const normalizeConfigPath = (value: string) =>
|
||||
value.startsWith('/') ? value : `/${value}`;
|
||||
|
||||
const resolveQueryConfig = () => {
|
||||
if (typeof window === 'undefined') return undefined;
|
||||
const param = new URLSearchParams(window.location.search).get('config');
|
||||
return param ? `/${param}` : props.configPath;
|
||||
return param ? normalizeConfigPath(param) : undefined;
|
||||
};
|
||||
|
||||
const configInput = props.config ?? resolveConfigPath();
|
||||
const resolveConfigPath = () => resolveQueryConfig() ?? props.configPath;
|
||||
|
||||
const configOptions = ref<ConfigListItem[]>([]);
|
||||
const selectedConfigPath = ref(resolveConfigPath());
|
||||
|
||||
const updateUrlConfigParam = (path: string) => {
|
||||
if (props.config) return;
|
||||
if (typeof window === 'undefined') return;
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set('config', path.replace(/^\//, ''));
|
||||
window.history.replaceState({}, '', url.toString());
|
||||
};
|
||||
|
||||
const applySelectedConfig = (path: string) => {
|
||||
const normalized = normalizeConfigPath(path);
|
||||
selectedConfigPath.value = normalized;
|
||||
};
|
||||
|
||||
const loadConfigList = async () => {
|
||||
if (props.config) return;
|
||||
try {
|
||||
const response = await fetch('/luopan-configs.json', { cache: 'no-store' });
|
||||
if (!response.ok) return;
|
||||
const configList = (await response.json()) as ConfigList;
|
||||
const items = Array.isArray(configList.items) ? configList.items : [];
|
||||
const normalizedItems = items
|
||||
.filter((item) => item && typeof item.path === 'string')
|
||||
.map((item) => ({
|
||||
name: item.name ?? item.path.replace(/^\//, ''),
|
||||
path: normalizeConfigPath(item.path),
|
||||
}));
|
||||
|
||||
configOptions.value = normalizedItems;
|
||||
|
||||
const queryPath = resolveQueryConfig();
|
||||
const availablePaths = new Set(normalizedItems.map((item) => item.path));
|
||||
if (queryPath && availablePaths.has(queryPath)) {
|
||||
applySelectedConfig(queryPath);
|
||||
return;
|
||||
}
|
||||
|
||||
const defaultPath = configList.default
|
||||
? normalizeConfigPath(configList.default)
|
||||
: normalizedItems[0]?.path;
|
||||
if (defaultPath) applySelectedConfig(defaultPath);
|
||||
} catch (err) {
|
||||
console.error('加载配置清单失败', err);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(loadConfigList);
|
||||
|
||||
watch(selectedConfigPath, (value, previous) => {
|
||||
if (value === previous) return;
|
||||
updateUrlConfigParam(value);
|
||||
});
|
||||
|
||||
/**
|
||||
* 使用罗盘逻辑
|
||||
@@ -360,7 +441,10 @@ const {
|
||||
toXY,
|
||||
loading,
|
||||
error,
|
||||
} = useLuopan(configInput, textRadialPosition);
|
||||
} = useLuopan(
|
||||
computed(() => props.config ?? selectedConfigPath.value),
|
||||
textRadialPosition
|
||||
);
|
||||
|
||||
// 以实际外半径作为 `viewBox`,确保完整显示配置中的大半径罗盘
|
||||
const viewBoxSize = computed(() => {
|
||||
|
||||
@@ -6,7 +6,8 @@ export const applyPatternColoring = (
|
||||
divisions: number,
|
||||
color: string,
|
||||
num: number,
|
||||
interval: number
|
||||
interval: number,
|
||||
patternOffset: number = 1
|
||||
): Map<number, string> => {
|
||||
const colorMap = new Map<number, string>();
|
||||
|
||||
@@ -19,14 +20,18 @@ export const applyPatternColoring = (
|
||||
return colorMap;
|
||||
}
|
||||
|
||||
const cycleLength = num + interval;
|
||||
if (cycleLength <= 0) return colorMap;
|
||||
const offsetBase = Number.isFinite(patternOffset) ? Math.trunc(patternOffset) - 1 : 0;
|
||||
const normalizedOffset = ((offsetBase % divisions) + divisions) % divisions;
|
||||
|
||||
// 规律填色:连续着色 `num` 个扇区,然后跳过 `interval` 个扇区。
|
||||
let currentIndex = 0;
|
||||
while (currentIndex < divisions) {
|
||||
for (let i = 0; i < num && currentIndex < divisions; i++) {
|
||||
colorMap.set(currentIndex, color);
|
||||
currentIndex++;
|
||||
for (let i = 0; i < divisions; i++) {
|
||||
const relativeIndex = i - normalizedOffset;
|
||||
const posInCycle = ((relativeIndex % cycleLength) + cycleLength) % cycleLength;
|
||||
if (posInCycle < num) {
|
||||
colorMap.set(i, color);
|
||||
}
|
||||
currentIndex += interval;
|
||||
}
|
||||
|
||||
return colorMap;
|
||||
@@ -57,7 +62,8 @@ export class ColorResolver {
|
||||
const color = this.resolveColor(layer.colorRef);
|
||||
if (typeof layer.num === 'number' && layer.num > 0) {
|
||||
const interval = layer.interval ?? 0;
|
||||
return applyPatternColoring(layer.divisions, color, layer.num, interval);
|
||||
const patternOffset = layer.patternOffset ?? 1;
|
||||
return applyPatternColoring(layer.divisions, color, layer.num, interval, patternOffset);
|
||||
}
|
||||
|
||||
for (let i = 0; i < layer.divisions; i++) {
|
||||
|
||||
@@ -2,14 +2,18 @@
|
||||
* 罗盘业务逻辑组合函数
|
||||
*/
|
||||
|
||||
import { computed, ref, readonly, watch, type Ref } from 'vue';
|
||||
import { computed, ref, readonly, watch, type Ref, isRef } from 'vue';
|
||||
import type {
|
||||
CenterIconData,
|
||||
DegreeRingData,
|
||||
LayerConfig,
|
||||
LuopanConfig,
|
||||
LuopanConfigInput,
|
||||
Sector,
|
||||
SectorLayerConfig,
|
||||
ThemeConfig,
|
||||
ThemeItem,
|
||||
ThemesConfig,
|
||||
TextRadialPosition,
|
||||
} from '../types';
|
||||
import { polarToXY } from '../utils';
|
||||
@@ -26,7 +30,7 @@ const isSectorLayer = (layer: LayerConfig): layer is SectorLayerConfig =>
|
||||
const HEX_COLOR_RE = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
|
||||
|
||||
const resolveThemeColor = (
|
||||
theme: LuopanConfig['theme'],
|
||||
theme: ThemeConfig,
|
||||
value: string | undefined,
|
||||
fallback: string
|
||||
) => {
|
||||
@@ -41,14 +45,84 @@ const findDegreeRingLayer = (layers: LayerConfig[]) =>
|
||||
const findCenterIconLayer = (layers: LayerConfig[]) =>
|
||||
layers.find((layer) => layer.type === 'centerIcon');
|
||||
|
||||
const THEMES_PATH = '/themes.json';
|
||||
|
||||
let cachedThemes: ThemesConfig | null = null;
|
||||
|
||||
const isObject = (value: unknown): value is Record<string, unknown> =>
|
||||
typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||
|
||||
const normalizeThemeItem = (item: Record<string, unknown>): ThemeItem | null => {
|
||||
const name = item.name;
|
||||
const palettes = item.colorPalettes;
|
||||
if (typeof name !== 'string' || !isObject(palettes)) return null;
|
||||
|
||||
const colorPalettes: Record<string, string> = {};
|
||||
Object.entries(palettes).forEach(([key, value]) => {
|
||||
if (typeof value === 'string') {
|
||||
colorPalettes[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(colorPalettes).length === 0) return null;
|
||||
return { name, colorPalettes };
|
||||
};
|
||||
|
||||
const normalizeThemesConfig = (input: unknown): ThemesConfig => {
|
||||
if (!isObject(input)) {
|
||||
throw new Error('themes.json 必须为对象');
|
||||
}
|
||||
const rawItems = Array.isArray(input.items) ? input.items : [];
|
||||
const items = rawItems
|
||||
.filter((item): item is Record<string, unknown> => isObject(item))
|
||||
.map((item) => normalizeThemeItem(item))
|
||||
.filter((item): item is ThemeItem => Boolean(item));
|
||||
if (items.length === 0) {
|
||||
throw new Error('themes.json.items 不能为空');
|
||||
}
|
||||
return {
|
||||
default: typeof input.default === 'string' ? input.default : undefined,
|
||||
items,
|
||||
};
|
||||
};
|
||||
|
||||
const loadThemes = async (): Promise<ThemesConfig> => {
|
||||
if (cachedThemes) return cachedThemes;
|
||||
const response = await fetch(THEMES_PATH, { cache: 'no-store' });
|
||||
if (!response.ok) {
|
||||
throw new Error(`主题配置加载失败: ${response.status}`);
|
||||
}
|
||||
const raw = await response.json();
|
||||
cachedThemes = normalizeThemesConfig(raw);
|
||||
return cachedThemes;
|
||||
};
|
||||
|
||||
const resolveTheme = async (configObj: LuopanConfigInput): Promise<ThemeConfig> => {
|
||||
if (configObj.theme && !configObj.themeRef) {
|
||||
return configObj.theme;
|
||||
}
|
||||
const themes = await loadThemes();
|
||||
const themeName = configObj.themeRef ?? themes.default ?? themes.items[0].name;
|
||||
const matched = themes.items.find((item) => item.name === themeName);
|
||||
if (matched) {
|
||||
return { name: matched.name, colorPalettes: matched.colorPalettes };
|
||||
}
|
||||
if (configObj.theme) {
|
||||
return configObj.theme;
|
||||
}
|
||||
throw new Error(`未找到主题: ${themeName}`);
|
||||
};
|
||||
|
||||
/**
|
||||
* 罗盘逻辑 Hook
|
||||
* @param configPathOrObject 配置文件路径或配置对象
|
||||
* @param textRadialPositionRef 文字径向位置的响应式引用(可选)
|
||||
* @returns 罗盘相关的计算属性和方法
|
||||
*/
|
||||
type LuopanConfigSource = string | LuopanConfigInput;
|
||||
|
||||
export function useLuopan(
|
||||
configPathOrObject: string | LuopanConfig,
|
||||
configPathOrObject: LuopanConfigSource | Ref<LuopanConfigSource>,
|
||||
textRadialPositionRef?: Ref<TextRadialPosition>
|
||||
) {
|
||||
const config = ref<LuopanConfig | null>(null);
|
||||
@@ -62,6 +136,10 @@ export function useLuopan(
|
||||
() => textRadialPositionRef?.value ?? 'middle'
|
||||
);
|
||||
|
||||
const configSource = isRef(configPathOrObject)
|
||||
? configPathOrObject
|
||||
: ref(configPathOrObject);
|
||||
|
||||
const buildSectors = (configObj: LuopanConfig) => {
|
||||
const resolver = new ColorResolver(configObj.theme, configObj.background);
|
||||
const builder = new SectorBuilder(resolver, {
|
||||
@@ -79,27 +157,30 @@ export function useLuopan(
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
|
||||
let configObj: LuopanConfig;
|
||||
if (typeof configPathOrObject === 'string') {
|
||||
const jsonText = await fetch(configPathOrObject).then((res) => res.text());
|
||||
let configObj: LuopanConfigInput;
|
||||
const configInput = configSource.value;
|
||||
if (typeof configInput === 'string') {
|
||||
const jsonText = await fetch(configInput).then((res) => res.text());
|
||||
configObj = parseConfig(jsonText);
|
||||
} else {
|
||||
configObj = configPathOrObject;
|
||||
configObj = configInput;
|
||||
}
|
||||
|
||||
const resolvedTheme = await resolveTheme(configObj);
|
||||
const resolvedBackground = resolveThemeColor(
|
||||
configObj.theme,
|
||||
resolvedTheme,
|
||||
configObj.background,
|
||||
'#000000'
|
||||
);
|
||||
const resolvedStrokeColor = resolveThemeColor(
|
||||
configObj.theme,
|
||||
resolvedTheme,
|
||||
configObj.strokeColor,
|
||||
'#1f2937'
|
||||
);
|
||||
|
||||
const resolvedConfig: LuopanConfig = {
|
||||
...configObj,
|
||||
theme: resolvedTheme,
|
||||
background: resolvedBackground,
|
||||
strokeColor: resolvedStrokeColor,
|
||||
strokeWidth: typeof configObj.strokeWidth === 'number'
|
||||
@@ -139,6 +220,10 @@ export function useLuopan(
|
||||
}
|
||||
});
|
||||
|
||||
watch(configSource, () => {
|
||||
loadConfig();
|
||||
});
|
||||
|
||||
loadConfig();
|
||||
|
||||
const sectorLayers = computed(() =>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { LuopanConfig, ThemeConfig } from './types';
|
||||
import type { LuopanConfigInput, ThemeConfig } from './types';
|
||||
|
||||
const isObject = (value: unknown): value is Record<string, unknown> =>
|
||||
typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||
@@ -53,7 +53,7 @@ const normalizeTheme = (theme: Record<string, unknown>): ThemeConfig => {
|
||||
};
|
||||
};
|
||||
|
||||
export const parseConfig = (jsonText: string): LuopanConfig => {
|
||||
export const parseConfig = (jsonText: string): LuopanConfigInput => {
|
||||
const cleanText = stripJsonComments(jsonText);
|
||||
let parsed: unknown;
|
||||
|
||||
@@ -69,7 +69,9 @@ export const parseConfig = (jsonText: string): LuopanConfig => {
|
||||
const config = parsed as Record<string, unknown>;
|
||||
assertCondition(typeof config.name === 'string', 'name 为必填字符串');
|
||||
assertCondition(typeof config.background === 'string', 'background 为必填字符串');
|
||||
assertCondition(isObject(config.theme), 'theme 为必填对象');
|
||||
if (config.theme !== undefined) {
|
||||
assertCondition(isObject(config.theme), 'theme 必须为对象');
|
||||
}
|
||||
assertCondition(Array.isArray(config.layers), 'layers 为必填数组');
|
||||
|
||||
return {
|
||||
@@ -81,7 +83,8 @@ export const parseConfig = (jsonText: string): LuopanConfig => {
|
||||
strokeOpacity: typeof config.strokeOpacity === 'number' ? config.strokeOpacity : undefined,
|
||||
insetDistance: typeof config.insetDistance === 'number' ? config.insetDistance : undefined,
|
||||
outerRadius: typeof config.outerRadius === 'number' ? config.outerRadius : undefined,
|
||||
theme: normalizeTheme(config.theme),
|
||||
layers: config.layers as LuopanConfig['layers'],
|
||||
themeRef: typeof config.themeRef === 'string' ? config.themeRef : undefined,
|
||||
theme: isObject(config.theme) ? normalizeTheme(config.theme) : undefined,
|
||||
layers: config.layers as LuopanConfigInput['layers'],
|
||||
};
|
||||
};
|
||||
|
||||
@@ -6,7 +6,10 @@
|
||||
export type {
|
||||
Example,
|
||||
LuopanConfig,
|
||||
LuopanConfigInput,
|
||||
ThemeConfig,
|
||||
ThemeItem,
|
||||
ThemesConfig,
|
||||
CenterIconConfig,
|
||||
DegreeRingConfig,
|
||||
DegreeRingData,
|
||||
|
||||
@@ -8,14 +8,15 @@ export function splitMultiTextUnits(
|
||||
content: string,
|
||||
aStart: number,
|
||||
aEnd: number,
|
||||
svgIconPath: string = 'src/assets/icons/'
|
||||
svgIconPath: string = 'src/assets/icons/',
|
||||
unitRatios?: number[]
|
||||
): TextUnit[] {
|
||||
const parts = content.split('|').map((part) => part.trim());
|
||||
if (parts.length === 0 || (parts.length === 1 && parts[0] === '')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const ratios = getLayoutRatio(parts.length);
|
||||
const ratios = normalizeRatios(unitRatios, parts.length) ?? getLayoutRatio(parts.length);
|
||||
const totalAngle = aEnd - aStart;
|
||||
const units: TextUnit[] = [];
|
||||
const basePath = ensureTrailingSlash(svgIconPath);
|
||||
@@ -39,3 +40,14 @@ export function splitMultiTextUnits(
|
||||
|
||||
return units;
|
||||
}
|
||||
|
||||
const normalizeRatios = (
|
||||
ratios: number[] | undefined,
|
||||
count: number
|
||||
): number[] | null => {
|
||||
if (!ratios || ratios.length !== count) return null;
|
||||
if (ratios.some((value) => typeof value !== 'number' || value <= 0)) return null;
|
||||
const sum = ratios.reduce((acc, value) => acc + value, 0);
|
||||
if (sum <= 0) return null;
|
||||
return ratios.map((value) => value / sum);
|
||||
};
|
||||
|
||||
@@ -49,6 +49,13 @@ export class SectorBuilder {
|
||||
for (let i = 0; i < layer.divisions; i++) {
|
||||
const aStart = startAngle + i * angleStep;
|
||||
const aEnd = aStart + angleStep;
|
||||
// groupSplit=false 且扇区足够细时,对填充路径做轻微重叠,避免抗锯齿露底
|
||||
const angleOverlapDeg =
|
||||
layer.groupSplit === false && angleStep > 0 && angleStep <= 10
|
||||
? Math.min(0.2, angleStep * 0.02)
|
||||
: 0;
|
||||
const fillAStart = aStart - angleOverlapDeg;
|
||||
const fillAEnd = aEnd + angleOverlapDeg;
|
||||
const sectorConfig = layer.sectors?.[i];
|
||||
const rawContent = typeof sectorConfig?.content === 'string' ? sectorConfig.content.trim() : '';
|
||||
const isMultiText = rawContent.includes('|');
|
||||
@@ -65,8 +72,8 @@ export class SectorBuilder {
|
||||
? annularSectorInsetPath(
|
||||
layer.rInner,
|
||||
layer.rOuter,
|
||||
aStart,
|
||||
aEnd,
|
||||
fillAStart,
|
||||
fillAEnd,
|
||||
this.insetDistance
|
||||
)
|
||||
: undefined;
|
||||
@@ -102,7 +109,8 @@ export class SectorBuilder {
|
||||
layer.rInner,
|
||||
layer.rOuter,
|
||||
effectiveTextRadialPosition,
|
||||
sectorKey
|
||||
sectorKey,
|
||||
layer.unitRatios
|
||||
)
|
||||
: undefined;
|
||||
|
||||
@@ -130,7 +138,7 @@ export class SectorBuilder {
|
||||
fill: baseFillColor,
|
||||
textColor,
|
||||
label: isMultiText ? '' : rawContent,
|
||||
path: annularSectorPath(layer.rInner, layer.rOuter, aStart, aEnd),
|
||||
path: annularSectorPath(layer.rInner, layer.rOuter, fillAStart, fillAEnd),
|
||||
innerFillPath: normalizedInnerFillPath,
|
||||
innerFillColor,
|
||||
textPath,
|
||||
@@ -154,9 +162,10 @@ export class SectorBuilder {
|
||||
rInner: number,
|
||||
rOuter: number,
|
||||
textRadialPosition: TextRadialPosition,
|
||||
sectorKey: string
|
||||
sectorKey: string,
|
||||
unitRatios?: number[]
|
||||
): TextUnit[] {
|
||||
const units = splitMultiTextUnits(content, aStart, aEnd, this.svgIconPath);
|
||||
const units = splitMultiTextUnits(content, aStart, aEnd, this.svgIconPath, unitRatios);
|
||||
|
||||
return units.map((unit, index) => {
|
||||
const layout = this.computeTextLayout(
|
||||
@@ -230,9 +239,16 @@ export class SectorBuilder {
|
||||
if (!layer.num) return true;
|
||||
|
||||
const cycleLength = layer.num + (layer.interval ?? 0);
|
||||
const posInCycle = sectorIndex % cycleLength;
|
||||
if (cycleLength <= 0) return true;
|
||||
const patternOffset = layer.patternOffset ?? 1;
|
||||
const offsetBase = Number.isFinite(patternOffset) ? Math.trunc(patternOffset) - 1 : 0;
|
||||
const normalizedOffset =
|
||||
layer.divisions > 0 ? ((offsetBase % layer.divisions) + layer.divisions) % layer.divisions : 0;
|
||||
const posInCycle =
|
||||
((sectorIndex - normalizedOffset) % cycleLength + cycleLength) % cycleLength;
|
||||
|
||||
// 仅保留每个着色分组的末尾分割线。
|
||||
return posInCycle >= layer.num - 1;
|
||||
// 仅保留分组边界:着色组起点 + 间隔组起点(若存在间隔)。
|
||||
if ((layer.interval ?? 0) === 0) return posInCycle === 0;
|
||||
return posInCycle === 0 || posInCycle === layer.num;
|
||||
}
|
||||
}
|
||||
|
||||
23
src/types.ts
23
src/types.ts
@@ -22,7 +22,7 @@ export interface Example {
|
||||
/**
|
||||
* JSON 配置根对象
|
||||
*/
|
||||
export interface LuopanConfig {
|
||||
export interface LuopanConfigBase {
|
||||
name: string;
|
||||
description?: string;
|
||||
background: string;
|
||||
@@ -31,10 +31,17 @@ export interface LuopanConfig {
|
||||
strokeOpacity?: number;
|
||||
insetDistance?: number;
|
||||
outerRadius?: number;
|
||||
theme: ThemeConfig;
|
||||
themeRef?: string;
|
||||
theme?: ThemeConfig;
|
||||
layers: LayerConfig[];
|
||||
}
|
||||
|
||||
export interface LuopanConfig extends LuopanConfigBase {
|
||||
theme: ThemeConfig;
|
||||
}
|
||||
|
||||
export type LuopanConfigInput = LuopanConfigBase;
|
||||
|
||||
/**
|
||||
* 主题配置
|
||||
*/
|
||||
@@ -43,6 +50,16 @@ export interface ThemeConfig {
|
||||
colorPalettes: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface ThemeItem {
|
||||
name: string;
|
||||
colorPalettes: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface ThemesConfig {
|
||||
default?: string;
|
||||
items: ThemeItem[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 中心图标配置
|
||||
*/
|
||||
@@ -90,8 +107,10 @@ export interface SectorLayerConfig {
|
||||
startAngle?: number;
|
||||
colorRef?: string;
|
||||
innerFill?: 0 | 1;
|
||||
unitRatios?: number[];
|
||||
num?: number;
|
||||
interval?: number;
|
||||
patternOffset?: number;
|
||||
groupSplit?: boolean;
|
||||
sectors?: SectorConfig[];
|
||||
}
|
||||
|
||||
26
src/utils.ts
26
src/utils.ts
@@ -260,7 +260,7 @@ export function generateTextPath(
|
||||
}
|
||||
|
||||
// 不调整半径,保持在中线位置
|
||||
// 使用 dominant-baseline 属性来控制文字的垂直对齐
|
||||
// 使用 `dominant-baseline` 属性控制文字垂直对齐
|
||||
const adjustedRMid = rMid;
|
||||
|
||||
const a1 = normalizeDeg(aStartDeg);
|
||||
@@ -275,8 +275,8 @@ export function generateTextPath(
|
||||
|
||||
const largeArc = delta > 180 ? 1 : 0;
|
||||
|
||||
// 保持路径完整,不在这里应用 padding
|
||||
// padding 通过字体大小计算和 textPath 的 startOffset/text-anchor 来实现
|
||||
// 保持路径完整,不在这里应用内边距(`padding`)
|
||||
// 内边距(`padding`)通过字体大小计算以及 `textPath` 的 `startOffset`/`text-anchor` 实现
|
||||
|
||||
if (needReverse) {
|
||||
// 反向路径(从结束点到起始点),保持文字头朝外
|
||||
@@ -384,14 +384,14 @@ export function generateVerticalTextPath(
|
||||
const requiredPathLength =
|
||||
effectiveTextLength * TEXT_LAYOUT_CONFIG.CHAR_SPACING_RATIO * effectiveFontSize;
|
||||
|
||||
// 确保路径不超出扇区边界(考虑径向 padding)
|
||||
// 确保路径不超出扇区边界(考虑径向内边距 `padding`)
|
||||
const maxPathLength = radialHeight * TEXT_LAYOUT_CONFIG.RADIAL_PADDING_RATIO;
|
||||
const actualPathLength = Math.min(requiredPathLength, maxPathLength);
|
||||
|
||||
let finalStartR: number;
|
||||
let finalEndR: number;
|
||||
|
||||
// 对于从圆心开始的扇区(rInner=0),形心会偏向外侧
|
||||
// 对于从圆心开始的扇区(`rInner`=0),形心会偏向外侧
|
||||
// 需要特殊处理以防止溢出
|
||||
if (rInner === 0) {
|
||||
// 计算路径应该在哪里结束(从外圆向内)
|
||||
@@ -409,7 +409,7 @@ export function generateVerticalTextPath(
|
||||
finalEndR = rMid - halfPath;
|
||||
}
|
||||
} else {
|
||||
// 普通扇区:以 rMid 为中心
|
||||
// 普通扇区:以 `rMid` 为中心
|
||||
const halfPathLength = actualPathLength / 2;
|
||||
|
||||
// 确保不超出边界
|
||||
@@ -492,13 +492,13 @@ export function calculateSectorFontSize(
|
||||
const maxByHeight = availableHeight / (textLength * TEXT_LAYOUT_CONFIG.CHAR_SPACING_RATIO);
|
||||
|
||||
// 约束2:最内侧字符的弧长宽度(这是最严格的宽度限制)
|
||||
// 最内侧字符的中心位置大约在 rInner + fontSize/2 处
|
||||
// 最内侧字符的中心位置大约在 `rInner` + `fontSize`/2 处
|
||||
// 保守估计:假设字体大小约为径向宽度的一半
|
||||
const estimatedFontSize = radialWidth * 0.5;
|
||||
const innerMostRadius = rInner + estimatedFontSize / 2;
|
||||
const innerArcLength = (innerMostRadius * deltaDeg * Math.PI) / 180;
|
||||
|
||||
// 字符宽度约为 fontSize × 1.0(方块字)
|
||||
// 字符宽度约为 `fontSize` × 1.0(方块字)
|
||||
const availableArcLength = innerArcLength * TEXT_LAYOUT_CONFIG.TANGENT_PADDING_RATIO;
|
||||
const maxByWidth = availableArcLength / 1.0; // 单个字符宽度
|
||||
|
||||
@@ -515,11 +515,11 @@ export function calculateSectorFontSize(
|
||||
const maxByHeight = radialWidth * TEXT_LAYOUT_CONFIG.RADIAL_PADDING_RATIO;
|
||||
|
||||
// 2. 宽度约束:根据文字总宽度计算
|
||||
// 中文字符宽度 = fontSize(方块字)
|
||||
// 字符间距:约 0.1 * fontSize,总占用约 1.1 * fontSize
|
||||
// 中文字符宽度 = `fontSize`(方块字)
|
||||
// 字符间距:约 0.1 * `fontSize`,总占用约 1.1 * `fontSize`
|
||||
const availableArcLength = arcLength * TEXT_LAYOUT_CONFIG.TANGENT_PADDING_RATIO;
|
||||
|
||||
// 反推字体大小:fontSize = 可用弧长 / (字符数 × 1.1)
|
||||
// 反推字体大小:`fontSize` = 可用弧长 / (字符数 × 1.1)
|
||||
const maxByWidth = availableArcLength / (textLength * 1.1);
|
||||
|
||||
// 3. 取宽度和高度约束中较小的那个(更严格的限制)
|
||||
@@ -561,13 +561,13 @@ function calculateVerticalTextLength(
|
||||
// 计算径向可用高度
|
||||
const radialHeight = rOuter - rInner;
|
||||
|
||||
// 考虑上下padding,可用高度约为总高度的配置比例
|
||||
// 考虑上下内边距(`padding`),可用高度约为总高度的配置比例
|
||||
const availableHeight = radialHeight * TEXT_LAYOUT_CONFIG.RADIAL_PADDING_RATIO;
|
||||
|
||||
// 计算可以容纳的字符数
|
||||
const maxFittableChars = Math.floor(availableHeight / (fontSize * TEXT_LAYOUT_CONFIG.CHAR_SPACING_RATIO));
|
||||
|
||||
// 限制在 [MIN_CHARS, MAX_CHARS] 范围内
|
||||
// 限制在 [`MIN_CHARS`, `MAX_CHARS`] 范围内
|
||||
const charCount = Math.max(MIN_CHARS, Math.min(MAX_CHARS, maxFittableChars));
|
||||
|
||||
return charCount;
|
||||
|
||||
@@ -27,6 +27,16 @@ describe('colorResolver', () => {
|
||||
expect(map.has(5)).toBe(false);
|
||||
});
|
||||
|
||||
it('applyPatternColoring 应支持规律填色偏移', () => {
|
||||
const map = applyPatternColoring(6, '#111111', 2, 1, 2);
|
||||
expect(map.has(0)).toBe(false);
|
||||
expect(map.has(1)).toBe(true);
|
||||
expect(map.has(2)).toBe(true);
|
||||
expect(map.has(3)).toBe(false);
|
||||
expect(map.has(4)).toBe(true);
|
||||
expect(map.has(5)).toBe(true);
|
||||
});
|
||||
|
||||
it('resolveLayerColors 在提供 colorRef 且无 num 时覆盖全部扇区', () => {
|
||||
const resolver = new ColorResolver(theme, '#000000');
|
||||
const layer: SectorLayerConfig = {
|
||||
|
||||
@@ -22,13 +22,14 @@ describe('configParser', () => {
|
||||
|
||||
expect(config.name).toBe('demo');
|
||||
expect(config.layers.length).toBeGreaterThan(0);
|
||||
expect(config.theme.colorPalettes['木']).toBe('#43A047');
|
||||
expect(config.themeRef).toBe('五行配色主题');
|
||||
expect(config.theme).toBeUndefined();
|
||||
expect(config.layers.some((layer) => layer.type === 'centerIcon')).toBe(true);
|
||||
expect(config.layers.some((layer) => layer.type === 'degreeRing')).toBe(true);
|
||||
});
|
||||
|
||||
it('缺少必填字段时应抛错', () => {
|
||||
const raw = '{ "background": "#000", "theme": { "colorPalettes": {} }, "layers": [] }';
|
||||
const raw = '{ "background": "#000", "layers": [] }';
|
||||
expect(() => parseConfig(raw)).toThrow('name 为必填字符串');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,6 +20,16 @@ describe('multiTextParser', () => {
|
||||
expect(units[1].isSvg).toBe(false);
|
||||
});
|
||||
|
||||
it('应支持自定义 unitRatios', () => {
|
||||
const units = splitMultiTextUnits('甲|乙|丙', 0, 100, 'src/assets/icons', [0.2, 0.3, 0.5]);
|
||||
expect(units[0].aStart).toBe(0);
|
||||
expect(units[0].aEnd).toBe(20);
|
||||
expect(units[1].aStart).toBe(20);
|
||||
expect(units[1].aEnd).toBe(50);
|
||||
expect(units[2].aStart).toBe(50);
|
||||
expect(units[2].aEnd).toBe(100);
|
||||
});
|
||||
|
||||
it('空内容应返回空数组', () => {
|
||||
expect(splitMultiTextUnits('', 0, 60)).toEqual([]);
|
||||
});
|
||||
|
||||
@@ -61,6 +61,25 @@ describe('sectorBuilder', () => {
|
||||
expect(sectors[2].groupSplitVisible).toBe(true);
|
||||
});
|
||||
|
||||
it('groupSplit=false 时应受 patternOffset 影响', () => {
|
||||
const resolver = new ColorResolver(theme, '#000000');
|
||||
const builder = new SectorBuilder(resolver);
|
||||
const baseLayer: SectorLayerConfig = {
|
||||
divisions: 4,
|
||||
rInner: 0,
|
||||
rOuter: 10,
|
||||
num: 2,
|
||||
interval: 1,
|
||||
groupSplit: false,
|
||||
};
|
||||
|
||||
const baseSectors = builder.buildLayer(baseLayer, 0);
|
||||
const offsetSectors = builder.buildLayer({ ...baseLayer, patternOffset: 2 }, 0);
|
||||
|
||||
expect(baseSectors[1].groupSplitVisible).toBe(false);
|
||||
expect(offsetSectors[2].groupSplitVisible).toBe(false);
|
||||
});
|
||||
|
||||
it('应生成多文本单元并按角度分配', () => {
|
||||
const resolver = new ColorResolver(theme, '#000000');
|
||||
const builder = new SectorBuilder(resolver);
|
||||
|
||||
@@ -135,7 +135,7 @@ describe('annularSectorPath', () => {
|
||||
|
||||
it('应该在大角度时设置 large-arc-flag', () => {
|
||||
const path = annularSectorPath(50, 100, 0, 270);
|
||||
// 大角度应包含 large-arc-flag = 1
|
||||
// 大角度应包含 `large-arc-flag` = 1
|
||||
expect(path).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -72,6 +72,7 @@
|
||||
| innerFill | number | 否 | 内缩设置:0=不内缩,1=内缩1像素 | 1 |
|
||||
| num | number | 否 | 规律填色:连续着色的扇区数量,与interval配合使用 | 3 |
|
||||
| interval | number | 否 | 规律填色:着色后间隔的扇区数量,0表示无间隔 | 1 |
|
||||
| patternOffset | number | 否 | 规律填色起始偏移(扇区序号,从1开始),默认1 | 2 |
|
||||
| groupSplit | boolean | 否 | 是否显示同组扇区间分割线,false隐藏,默认true | false |
|
||||
| sectors | array | 否 | 扇区配置数组,可省略表示该层只有填色无内容 | 见下方sectors说明 |
|
||||
|
||||
@@ -100,7 +101,7 @@
|
||||
参数:
|
||||
startAngle表示第一个扇区的起始角度(以度为单位,0度为正北方向,顺时针增加)
|
||||
innerfill仅对num/interval定义的着色扇区生效(目前layer中的innerFill对所有sector生效,暂保持这个逻辑)
|
||||
-- start表示着色起始扇区(已废弃,统一从第1个扇区开始)
|
||||
patternOffset表示规律填色起始偏移(扇区序号,从1开始,默认1),未指定时与startAngle对齐,即从第一个扇区开始着色
|
||||
num表示连接几个单元着色
|
||||
interval表示中间间隔几个单元
|
||||
比如num=3,interval=1,意思是从第1个扇区开始着色,对1、2、3扇区着色colorref,4扇区全局背景,5、6、7着色colorref……
|
||||
@@ -123,7 +124,7 @@
|
||||
"startAngle": 0, -- 第一个扇区的起始角度(度,0度为正北方向,顺时针)
|
||||
"innerFill": 1, -- 着色区域的内缩设置
|
||||
"colorRef": "土", -- 着色使用的颜色引用
|
||||
-- "start": 1, -- 从第1个扇区开始着色(已废弃,统一从第1个扇区开始)
|
||||
-- "patternOffset": 2, -- 第一个着色单元偏移1个扇区(可选)
|
||||
"num": 3, -- 连续着色3个扇区
|
||||
"interval": 1, -- 着色后间隔1个扇区
|
||||
"sectors": [
|
||||
@@ -201,6 +202,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
注意:json中如配置了centerIcon,作为第一层。
|
||||
|
||||
### 360度刻度环配置参数:
|
||||
rinner
|
||||
router
|
||||
|
||||
Reference in New Issue
Block a user