update at 2026-01-28 16:45:27

This commit is contained in:
douboer@gmail.com
2026-01-28 16:45:27 +08:00
parent 9fcc29782e
commit b54cc3f623
24 changed files with 39255 additions and 1490 deletions

View File

@@ -39,14 +39,38 @@
"startAngle": 0, "startAngle": 0,
"colorRef": "水", "colorRef": "水",
"sectors": [ "sectors": [
{ "content": "乾", "innerFill": 1 }, {
{ "content": "兑", "innerFill": 0 }, "content": "乾",
{ "content": "离", "innerFill": 1 }, "innerFill": 1
{ "content": "震", "innerFill": 0 }, },
{ "content": "巽", "innerFill": 1 }, {
{ "content": "坎", "innerFill": 0 }, "content": "兑",
{ "content": "艮", "innerFill": 1 }, "innerFill": 0
{ "content": "坤", "innerFill": 0 } },
{
"content": "离",
"innerFill": 1
},
{
"content": "震",
"innerFill": 0
},
{
"content": "巽",
"innerFill": 1
},
{
"content": "坎",
"innerFill": 0
},
{
"content": "艮",
"innerFill": 1
},
{
"content": "坤",
"innerFill": 0
}
] ]
}, },
{ {
@@ -58,18 +82,58 @@
"num": 3, "num": 3,
"interval": 1, "interval": 1,
"sectors": [ "sectors": [
{ "content": "子", "colorRef": "水", "innerFill": 1 }, {
{ "content": "丑" }, "content": "子",
{ "content": "寅", "colorRef": "木", "innerFill": 0 }, "colorRef": "水",
{ "content": "卯", "colorRef": "木", "innerFill": 1 }, "innerFill": 1
{ "content": "辰" }, },
{ "content": "巳", "colorRef": "火", "innerFill": 1 }, {
{ "content": "午", "colorRef": "火", "innerFill": 0 }, "content": "丑"
{ "content": "未", "innerFill": 1 }, },
{ "content": "申", "colorRef": "金", "innerFill": 0 }, {
{ "content": "酉", "colorRef": "金", "innerFill": 1 }, "content": "寅",
{ "content": "戌" }, "colorRef": "木",
{ "content": "亥", "innerFill": 0 } "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
}
] ]
}, },
{ {
@@ -78,30 +142,88 @@
"rOuter": 240, "rOuter": 240,
"startAngle": 0, "startAngle": 0,
"sectors": [ "sectors": [
{ "content": "甲乙|子|丙丁", "colorRef": "木", "innerFill": 1 }, {
{ "content": "丑" }, "content": "甲乙|子|丙丁",
{ "content": "戊己|寅|庚辛", "colorRef": "", "innerFill": 0 }, "colorRef": "",
{ "content": "卯" }, "innerFill": 1
{ "content": "壬癸|辰|甲乙", "innerFill": 1 }, },
{ "content": "巳" }, {
{ "content": "丙丁|午|戊己", "colorRef": "火", "innerFill": 0 }, "content": "丑"
{ "content": "未" }, },
{ "content": "庚辛|申|壬癸", "colorRef": "金", "innerFill": 1 }, {
{ "content": "酉" }, "content": "戊己|寅|庚辛",
{ "content": "甲乙|戌|丙丁", "innerFill": 0 }, "colorRef": "土",
{ "content": "亥" }, "innerFill": 0
{ "content": "戊己|子|庚辛" }, },
{ "content": "丑" }, {
{ "content": "壬癸|寅|甲乙" }, "content": "卯"
{ "content": "卯" }, },
{ "content": "丙丁|辰|戊己" }, {
{ "content": "巳" }, "content": "壬癸|辰|甲乙",
{ "content": "庚辛|午|壬癸" }, "innerFill": 1
{ "content": "未" }, },
{ "content": "甲乙|申|丙丁" }, {
{ "content": "" }, "content": ""
{ "content": "戊己|戌|庚辛" }, },
{ "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": "亥"
}
] ]
}, },
{ {
@@ -110,22 +232,81 @@
"rOuter": 270, "rOuter": 270,
"startAngle": 0, "startAngle": 0,
"sectors": [ "sectors": [
{ "content": "甲乙|丙丁|戊己|庚辛", "colorRef": "木", "innerFill": 1 }, {
{ "content": "壬癸|甲乙|丙丁|戊己", "colorRef": "土", "innerFill": 0 }, "content": "甲乙|丙丁|戊己|庚辛",
{ "content": "庚辛|壬癸|甲乙|丙丁", "innerFill": 1 }, "colorRef": "木",
{ "content": "戊己|庚辛|壬癸|甲乙", "colorRef": "火", "innerFill": 0 }, "innerFill": 1
{ "content": "丙丁|戊己|庚辛|壬癸", "colorRef": "金", "innerFill": 1 }, },
{ "content": "子丑|寅卯|辰巳|午未", "innerFill": 0 }, {
{ "content": "申酉|戌亥|子丑|寅卯", "colorRef": "木", "innerFill": 1 }, "content": "壬癸|甲乙|丙丁|戊己",
{ "content": "辰巳|午未|申酉|戌亥", "colorRef": "水", "innerFill": 0 }, "colorRef": "土",
{ "content": "甲乙|丙丁|戊己|庚辛", "innerFill": 1 }, "innerFill": 0
{ "content": "壬癸|甲乙|丙丁|戊己", "colorRef": "土", "innerFill": 0 }, },
{ "content": "庚辛|壬癸|甲乙|丙丁", "colorRef": "木", "innerFill": 1 }, {
{ "content": "戊己|庚辛|壬癸|甲乙", "innerFill": 0 }, "content": "庚辛|壬癸|甲乙|丙丁",
{ "content": "丙丁|戊己|庚辛|壬癸", "colorRef": "火", "innerFill": 1 }, "innerFill": 1
{ "content": "子丑|寅卯|辰巳|午未", "colorRef": "金", "innerFill": 0 }, },
{ "content": "申酉|戌亥|子丑|寅卯", "innerFill": 1 }, {
{ "content": "辰巳|午未|申酉|戌亥", "colorRef": "水", "innerFill": 0 } "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
}
] ]
}, },
{ {
@@ -141,20 +322,18 @@
}, },
{ {
"type": "degreeRing", "type": "degreeRing",
"degreeRing": { "rInner": 350,
"rInner": 350, "rOuter": 380,
"rOuter": 380, "showDegree": 1,
"showDegree": 1, "mode": "both",
"mode": "both", "opacity": 1,
"opacity": 1, "tickLength": 6,
"tickLength": 6, "tickLengthStep": 2,
"tickLengthStep": 2, "majorTick": 10,
"majorTick": 10, "minorTick": 5,
"minorTick": 5, "microTick": 1,
"microTick": 1, "tickColor": "#000000",
"tickColor": "#000000", "ringColor": "#000000"
"ringColor": "#000000"
}
} }
] ]
} }

View File

@@ -1,12 +1,12 @@
{ {
"name": "demo2", "name": "demo2",
"description": "luopan demo2 config with named color palettes", "description": "luopan demo2 config with named color palettes",
"background": "#000000", "background": "#000000",
"strokeWidth": 1, "strokeWidth": 1,
"strokeOpacity": 1, "strokeOpacity": 1,
"strokeColor": "白", "strokeColor": "白",
"insetDistance": 1, "insetDistance": 1,
"outerRadius": 500, "outerRadius": 500,
"themeRef": "五行弱", "themeRef": "五行弱",
"layers": [ "layers": [
{ {
@@ -18,21 +18,20 @@
} }
}, },
{ {
"divisions": 2, "divisions": 2,
"rInner": 60, "rInner": 60,
"rOuter": 90, "rOuter": 90,
"startAngle": 0, "startAngle": 0,
"sectors": [ "sectors": [
{ {
"content": "阴", "content": "阴",
"colorRef": "黑", "colorRef": "黑",
"innerFill": 1 "innerFill": 1
}, },
{ {
"content": "阳", "content": "阳",
"colorRef": "白", "colorRef": "白",
"innerFill": 0 "innerFill": 0
} }
] ]
}, },
@@ -42,14 +41,38 @@
"rOuter": 160, "rOuter": 160,
"startAngle": 0, "startAngle": 0,
"sectors": [ "sectors": [
{ "content": "乾", "innerFill": 1 }, {
{ "content": "兑", "innerFill": 0 }, "content": "乾",
{ "content": "离", "innerFill": 1 }, "innerFill": 1
{ "content": "震", "innerFill": 0 }, },
{ "content": "巽", "innerFill": 1 }, {
{ "content": "坎", "innerFill": 0 }, "content": "兑",
{ "content": "艮", "innerFill": 1 }, "innerFill": 0
{ "content": "坤", "innerFill": 0 } },
{
"content": "离",
"innerFill": 1
},
{
"content": "震",
"innerFill": 0
},
{
"content": "巽",
"innerFill": 1
},
{
"content": "坎",
"innerFill": 0
},
{
"content": "艮",
"innerFill": 1
},
{
"content": "坤",
"innerFill": 0
}
] ]
}, },
{ {
@@ -62,18 +85,58 @@
"num": 3, "num": 3,
"interval": 1, "interval": 1,
"sectors": [ "sectors": [
{ "content": "子", "colorRef": "水", "innerFill": 1 }, {
{ "content": "" }, "content": "",
{ "content": "寅", "colorRef": "木", "innerFill": 0 }, "colorRef": "水",
{ "content": "卯", "colorRef": "木", "innerFill": 1 }, "innerFill": 1
{ "content": "辰" }, },
{ "content": "巳", "colorRef": "火", "innerFill": 1 }, {
{ "content": "午", "colorRef": "火", "innerFill": 0 }, "content": "丑"
{ "content": "未", "innerFill": 1 }, },
{ "content": "申", "colorRef": "金", "innerFill": 0 }, {
{ "content": "酉", "colorRef": "金", "innerFill": 1 }, "content": "寅",
{ "content": "" }, "colorRef": "",
{ "content": "亥", "innerFill": 0 } "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
}
] ]
}, },
{ {
@@ -82,30 +145,88 @@
"rOuter": 240, "rOuter": 240,
"startAngle": 0, "startAngle": 0,
"sectors": [ "sectors": [
{ "content": "甲乙|子|丙丁", "colorRef": "木", "innerFill": 1 }, {
{ "content": "丑" }, "content": "甲乙|子|丙丁",
{ "content": "戊己|寅|庚辛", "colorRef": "", "innerFill": 0 }, "colorRef": "",
{ "content": "卯" }, "innerFill": 1
{ "content": "壬癸|辰|甲乙", "innerFill": 1 }, },
{ "content": "巳" }, {
{ "content": "丙丁|午|戊己", "colorRef": "火", "innerFill": 0 }, "content": "丑"
{ "content": "未" }, },
{ "content": "庚辛|申|壬癸", "colorRef": "金", "innerFill": 1 }, {
{ "content": "酉" }, "content": "戊己|寅|庚辛",
{ "content": "甲乙|戌|丙丁", "innerFill": 0 }, "colorRef": "土",
{ "content": "亥" }, "innerFill": 0
{ "content": "戊己|子|庚辛" }, },
{ "content": "丑" }, {
{ "content": "壬癸|寅|甲乙" }, "content": "卯"
{ "content": "卯" }, },
{ "content": "丙丁|辰|戊己" }, {
{ "content": "巳" }, "content": "壬癸|辰|甲乙",
{ "content": "庚辛|午|壬癸" }, "innerFill": 1
{ "content": "未" }, },
{ "content": "甲乙|申|丙丁" }, {
{ "content": "" }, "content": ""
{ "content": "戊己|戌|庚辛" }, },
{ "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": "亥"
}
] ]
}, },
{ {
@@ -114,22 +235,81 @@
"rOuter": 270, "rOuter": 270,
"startAngle": 0, "startAngle": 0,
"sectors": [ "sectors": [
{ "content": "甲乙|丙丁|戊己|庚辛", "colorRef": "木", "innerFill": 1 }, {
{ "content": "壬癸|甲乙|丙丁|戊己", "colorRef": "土", "innerFill": 0 }, "content": "甲乙|丙丁|戊己|庚辛",
{ "content": "庚辛|壬癸|甲乙|丙丁", "innerFill": 1 }, "colorRef": "木",
{ "content": "戊己|庚辛|壬癸|甲乙", "colorRef": "火", "innerFill": 0 }, "innerFill": 1
{ "content": "丙丁|戊己|庚辛|壬癸", "colorRef": "金", "innerFill": 1 }, },
{ "content": "子丑|寅卯|辰巳|午未", "innerFill": 0 }, {
{ "content": "申酉|戌亥|子丑|寅卯", "colorRef": "木", "innerFill": 1 }, "content": "壬癸|甲乙|丙丁|戊己",
{ "content": "辰巳|午未|申酉|戌亥", "colorRef": "水", "innerFill": 0 }, "colorRef": "土",
{ "content": "甲乙|丙丁|戊己|庚辛", "innerFill": 1 }, "innerFill": 0
{ "content": "壬癸|甲乙|丙丁|戊己", "colorRef": "土", "innerFill": 0 }, },
{ "content": "庚辛|壬癸|甲乙|丙丁", "colorRef": "木", "innerFill": 1 }, {
{ "content": "戊己|庚辛|壬癸|甲乙", "innerFill": 0 }, "content": "庚辛|壬癸|甲乙|丙丁",
{ "content": "丙丁|戊己|庚辛|壬癸", "colorRef": "火", "innerFill": 1 }, "innerFill": 1
{ "content": "子丑|寅卯|辰巳|午未", "colorRef": "金", "innerFill": 0 }, },
{ "content": "申酉|戌亥|子丑|寅卯", "innerFill": 1 }, {
{ "content": "辰巳|午未|申酉|戌亥", "colorRef": "水", "innerFill": 0 } "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
}
] ]
}, },
{ {
@@ -145,19 +325,17 @@
}, },
{ {
"type": "degreeRing", "type": "degreeRing",
"degreeRing": { "rInner": 450,
"rInner": 450, "rOuter": 500,
"rOuter": 500, "showDegree": 1,
"showDegree": 1, "mode": "both",
"mode": "both", "opacity": 0.3,
"opacity": 0.3, "tickLength": 6,
"tickLength": 6, "majorTick": 10,
"majorTick": 10, "minorTick": 5,
"minorTick": 5, "microTick": 1,
"microTick": 1, "tickColor": "#ffffff",
"tickColor": "#ffffff", "ringColor": "#ffffff"
"ringColor": "#ffffff"
}
} }
] ]
} }

View File

@@ -39,14 +39,38 @@
"startAngle": 0, "startAngle": 0,
"colorRef": "水", "colorRef": "水",
"sectors": [ "sectors": [
{ "content": "乾", "innerFill": 1 }, {
{ "content": "兑", "innerFill": 0 }, "content": "乾",
{ "content": "离", "innerFill": 1 }, "innerFill": 1
{ "content": "震", "innerFill": 0 }, },
{ "content": "巽", "innerFill": 1 }, {
{ "content": "坎", "innerFill": 0 }, "content": "兑",
{ "content": "艮", "innerFill": 1 }, "innerFill": 0
{ "content": "坤", "innerFill": 0 } },
{
"content": "离",
"innerFill": 1
},
{
"content": "震",
"innerFill": 0
},
{
"content": "巽",
"innerFill": 1
},
{
"content": "坎",
"innerFill": 0
},
{
"content": "艮",
"innerFill": 1
},
{
"content": "坤",
"innerFill": 0
}
] ]
}, },
{ {
@@ -58,18 +82,58 @@
"num": 3, "num": 3,
"interval": 1, "interval": 1,
"sectors": [ "sectors": [
{ "content": "子", "colorRef": "水", "innerFill": 1 }, {
{ "content": "丑" }, "content": "子",
{ "content": "寅", "colorRef": "木", "innerFill": 0 }, "colorRef": "水",
{ "content": "卯", "colorRef": "木", "innerFill": 1 }, "innerFill": 1
{ "content": "辰" }, },
{ "content": "巳", "colorRef": "火", "innerFill": 1 }, {
{ "content": "午", "colorRef": "火", "innerFill": 0 }, "content": "丑"
{ "content": "未", "innerFill": 1 }, },
{ "content": "申", "colorRef": "金", "innerFill": 0 }, {
{ "content": "酉", "colorRef": "金", "innerFill": 1 }, "content": "寅",
{ "content": "戌" }, "colorRef": "木",
{ "content": "亥", "innerFill": 0 } "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
}
] ]
}, },
{ {
@@ -78,30 +142,88 @@
"rOuter": 240, "rOuter": 240,
"startAngle": 0, "startAngle": 0,
"sectors": [ "sectors": [
{ "content": "甲乙|子|丙丁", "colorRef": "木", "innerFill": 1 }, {
{ "content": "丑" }, "content": "甲乙|子|丙丁",
{ "content": "戊己|寅|庚辛", "colorRef": "", "innerFill": 0 }, "colorRef": "",
{ "content": "卯" }, "innerFill": 1
{ "content": "壬癸|辰|甲乙", "innerFill": 1 }, },
{ "content": "巳" }, {
{ "content": "丙丁|午|戊己", "colorRef": "火", "innerFill": 0 }, "content": "丑"
{ "content": "未" }, },
{ "content": "庚辛|申|壬癸", "colorRef": "金", "innerFill": 1 }, {
{ "content": "酉" }, "content": "戊己|寅|庚辛",
{ "content": "甲乙|戌|丙丁", "innerFill": 0 }, "colorRef": "土",
{ "content": "亥" }, "innerFill": 0
{ "content": "戊己|子|庚辛" }, },
{ "content": "丑" }, {
{ "content": "壬癸|寅|甲乙" }, "content": "卯"
{ "content": "卯" }, },
{ "content": "丙丁|辰|戊己" }, {
{ "content": "巳" }, "content": "壬癸|辰|甲乙",
{ "content": "庚辛|午|壬癸" }, "innerFill": 1
{ "content": "未" }, },
{ "content": "甲乙|申|丙丁" }, {
{ "content": "" }, "content": ""
{ "content": "戊己|戌|庚辛" }, },
{ "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": "亥"
}
] ]
}, },
{ {
@@ -110,22 +232,81 @@
"rOuter": 270, "rOuter": 270,
"startAngle": 0, "startAngle": 0,
"sectors": [ "sectors": [
{ "content": "甲乙|丙丁|戊己|庚辛", "colorRef": "木", "innerFill": 1 }, {
{ "content": "壬癸|甲乙|丙丁|戊己", "colorRef": "土", "innerFill": 0 }, "content": "甲乙|丙丁|戊己|庚辛",
{ "content": "庚辛|壬癸|甲乙|丙丁", "innerFill": 1 }, "colorRef": "木",
{ "content": "戊己|庚辛|壬癸|甲乙", "colorRef": "火", "innerFill": 0 }, "innerFill": 1
{ "content": "丙丁|戊己|庚辛|壬癸", "colorRef": "金", "innerFill": 1 }, },
{ "content": "子丑|寅卯|辰巳|午未", "innerFill": 0 }, {
{ "content": "申酉|戌亥|子丑|寅卯", "colorRef": "木", "innerFill": 1 }, "content": "壬癸|甲乙|丙丁|戊己",
{ "content": "辰巳|午未|申酉|戌亥", "colorRef": "水", "innerFill": 0 }, "colorRef": "土",
{ "content": "甲乙|丙丁|戊己|庚辛", "innerFill": 1 }, "innerFill": 0
{ "content": "壬癸|甲乙|丙丁|戊己", "colorRef": "土", "innerFill": 0 }, },
{ "content": "庚辛|壬癸|甲乙|丙丁", "colorRef": "木", "innerFill": 1 }, {
{ "content": "戊己|庚辛|壬癸|甲乙", "innerFill": 0 }, "content": "庚辛|壬癸|甲乙|丙丁",
{ "content": "丙丁|戊己|庚辛|壬癸", "colorRef": "火", "innerFill": 1 }, "innerFill": 1
{ "content": "子丑|寅卯|辰巳|午未", "colorRef": "金", "innerFill": 0 }, },
{ "content": "申酉|戌亥|子丑|寅卯", "innerFill": 1 }, {
{ "content": "辰巳|午未|申酉|戌亥", "colorRef": "水", "innerFill": 0 } "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
}
] ]
}, },
{ {
@@ -141,20 +322,18 @@
}, },
{ {
"type": "degreeRing", "type": "degreeRing",
"degreeRing": { "rInner": 350,
"rInner": 350, "rOuter": 380,
"rOuter": 380, "showDegree": 1,
"showDegree": 1, "mode": "both",
"mode": "both", "opacity": 1,
"opacity": 1, "tickLength": 6,
"tickLength": 6, "tickLengthStep": 2,
"tickLengthStep": 2, "majorTick": 10,
"majorTick": 10, "minorTick": 5,
"minorTick": 5, "microTick": 1,
"microTick": 1, "tickColor": "#000000",
"tickColor": "#000000", "ringColor": "#000000"
"ringColor": "#000000"
}
} }
] ]
} }

File diff suppressed because one or more lines are too long

3766
public/fengshui10.json Normal file

File diff suppressed because it is too large Load Diff

3768
public/fengshui2.json Normal file

File diff suppressed because it is too large Load Diff

3768
public/fengshui3.json Normal file

File diff suppressed because it is too large Load Diff

3768
public/fengshui4.json Normal file

File diff suppressed because it is too large Load Diff

3768
public/fengshui5.json Normal file

File diff suppressed because it is too large Load Diff

3768
public/fengshui6.json Normal file

File diff suppressed because it is too large Load Diff

3768
public/fengshui7.json Normal file

File diff suppressed because it is too large Load Diff

3768
public/fengshui8.json Normal file

File diff suppressed because it is too large Load Diff

3768
public/fengshui9.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -20,6 +20,42 @@
{ {
"name": "fengshui", "name": "fengshui",
"path": "/fengshui.json" "path": "/fengshui.json"
},
{
"name": "fengshui2",
"path": "/fengshui2.json"
},
{
"name": "fengshui3",
"path": "/fengshui3.json"
},
{
"name": "fengshui4",
"path": "/fengshui4.json"
},
{
"name": "fengshui5",
"path": "/fengshui5.json"
},
{
"name": "fengshui6",
"path": "/fengshui6.json"
},
{
"name": "fengshui7",
"path": "/fengshui7.json"
},
{
"name": "fengshui8",
"path": "/fengshui8.json"
},
{
"name": "fengshui9",
"path": "/fengshui9.json"
},
{
"name": "fengshui10",
"path": "/fengshui10.json"
} }
] ]
} }

View File

@@ -38,14 +38,46 @@
"rOuter": 160, "rOuter": 160,
"startAngle": 0, "startAngle": 0,
"sectors": [ "sectors": [
{ "content": "乾", "colorRef": "火", "innerFill": 1 }, {
{ "content": "兑", "colorRef": "冷", "innerFill": 0 }, "content": "乾",
{ "content": "离", "colorRef": "火", "innerFill": 1 }, "colorRef": "火",
{ "content": "震", "colorRef": "木", "innerFill": 0 }, "innerFill": 1
{ "content": "巽", "colorRef": "木", "innerFill": 1 }, },
{ "content": "坎", "colorRef": "冷", "innerFill": 0 }, {
{ "content": "艮", "colorRef": "强", "innerFill": 1 }, "content": "兑",
{ "content": "坤", "colorRef": "", "innerFill": 0 } "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
}
] ]
}, },
{ {
@@ -57,18 +89,58 @@
"num": 3, "num": 3,
"interval": 1, "interval": 1,
"sectors": [ "sectors": [
{ "content": "子", "colorRef": "水", "innerFill": 1 }, {
{ "content": "丑" }, "content": "子",
{ "content": "寅", "colorRef": "木", "innerFill": 0 }, "colorRef": "水",
{ "content": "卯", "colorRef": "木", "innerFill": 1 }, "innerFill": 1
{ "content": "辰" }, },
{ "content": "巳", "colorRef": "火", "innerFill": 1 }, {
{ "content": "午", "colorRef": "火", "innerFill": 0 }, "content": "丑"
{ "content": "未", "innerFill": 1 }, },
{ "content": "申", "colorRef": "金", "innerFill": 0 }, {
{ "content": "酉", "colorRef": "金", "innerFill": 1 }, "content": "寅",
{ "content": "戌" }, "colorRef": "木",
{ "content": "亥", "innerFill": 0 } "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
}
] ]
}, },
{ {
@@ -80,30 +152,88 @@
"num": 2, "num": 2,
"interval": 1, "interval": 1,
"sectors": [ "sectors": [
{ "content": "甲乙|子|丙丁", "colorRef": "木", "innerFill": 1 }, {
{ "content": "丑" }, "content": "甲乙|子|丙丁",
{ "content": "戊己|寅|庚辛", "colorRef": "", "innerFill": 0 }, "colorRef": "",
{ "content": "卯" }, "innerFill": 1
{ "content": "壬癸|辰|甲乙", "innerFill": 1 }, },
{ "content": "巳" }, {
{ "content": "丙丁|午|戊己", "colorRef": "火", "innerFill": 0 }, "content": "丑"
{ "content": "未" }, },
{ "content": "庚辛|申|壬癸", "colorRef": "金", "innerFill": 1 }, {
{ "content": "酉" }, "content": "戊己|寅|庚辛",
{ "content": "甲乙|戌|丙丁", "innerFill": 0 }, "colorRef": "土",
{ "content": "亥" }, "innerFill": 0
{ "content": "戊己|子|庚辛" }, },
{ "content": "丑" }, {
{ "content": "壬癸|寅|甲乙" }, "content": "卯"
{ "content": "卯" }, },
{ "content": "丙丁|辰|戊己" }, {
{ "content": "巳" }, "content": "壬癸|辰|甲乙",
{ "content": "庚辛|午|壬癸" }, "innerFill": 1
{ "content": "未" }, },
{ "content": "甲乙|申|丙丁" }, {
{ "content": "" }, "content": ""
{ "content": "戊己|戌|庚辛" }, },
{ "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": "亥"
}
] ]
}, },
{ {
@@ -112,22 +242,81 @@
"rOuter": 270, "rOuter": 270,
"startAngle": 0, "startAngle": 0,
"sectors": [ "sectors": [
{ "content": "甲乙|丙丁|戊己|庚辛", "colorRef": "木", "innerFill": 1 }, {
{ "content": "壬癸|甲乙|丙丁|戊己", "colorRef": "土", "innerFill": 0 }, "content": "甲乙|丙丁|戊己|庚辛",
{ "content": "庚辛|壬癸|甲乙|丙丁", "innerFill": 1 }, "colorRef": "木",
{ "content": "戊己|庚辛|壬癸|甲乙", "colorRef": "火", "innerFill": 0 }, "innerFill": 1
{ "content": "丙丁|戊己|庚辛|壬癸", "colorRef": "金", "innerFill": 1 }, },
{ "content": "子丑|寅卯|辰巳|午未", "innerFill": 0 }, {
{ "content": "申酉|戌亥|子丑|寅卯", "colorRef": "木", "innerFill": 1 }, "content": "壬癸|甲乙|丙丁|戊己",
{ "content": "辰巳|午未|申酉|戌亥", "colorRef": "水", "innerFill": 0 }, "colorRef": "土",
{ "content": "甲乙|丙丁|戊己|庚辛", "innerFill": 1 }, "innerFill": 0
{ "content": "壬癸|甲乙|丙丁|戊己", "colorRef": "土", "innerFill": 0 }, },
{ "content": "庚辛|壬癸|甲乙|丙丁", "colorRef": "木", "innerFill": 1 }, {
{ "content": "戊己|庚辛|壬癸|甲乙", "innerFill": 0 }, "content": "庚辛|壬癸|甲乙|丙丁",
{ "content": "丙丁|戊己|庚辛|壬癸", "colorRef": "火", "innerFill": 1 }, "innerFill": 1
{ "content": "子丑|寅卯|辰巳|午未", "colorRef": "金", "innerFill": 0 }, },
{ "content": "申酉|戌亥|子丑|寅卯", "innerFill": 1 }, {
{ "content": "辰巳|午未|申酉|戌亥", "colorRef": "水", "innerFill": 0 } "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
}
] ]
}, },
{ {
@@ -143,20 +332,18 @@
}, },
{ {
"type": "degreeRing", "type": "degreeRing",
"degreeRing": { "rInner": 350,
"rInner": 350, "rOuter": 380,
"rOuter": 380, "showDegree": 1,
"showDegree": 1, "mode": "both",
"mode": "both", "opacity": 1,
"opacity": 1, "tickLength": 6,
"tickLength": 6, "tickLengthStep": 2,
"tickLengthStep": 2, "majorTick": 10,
"majorTick": 10, "minorTick": 5,
"minorTick": 5, "microTick": 1,
"microTick": 1, "tickColor": "#000000",
"tickColor": "#000000", "ringColor": "#000000"
"ringColor": "#000000"
}
} }
] ]
} }

View File

@@ -1,5 +1,20 @@
# 罗盘项目重构方案 # 罗盘项目重构方案
## 0. 重构方案提要(先看这一段)
**目标:** 从“硬编码示例”升级到“配置驱动”的罗盘渲染系统,并为大罗盘性能优化留出清晰路径。
**策略:** 先重构数据解析与渲染结构,再完善主题/多文本/刻度/中心图标,最后补齐测试与性能。
**落地路径:**
1) 解析层与类型统一(配置校验、主题、层级半径推导)。
2) 扇区构建器与颜色引擎含规律填色、groupSplit
3) UI 适配Luopan.vue、useLuopan 重构)。
4) 测试与性能优化(含大罗盘拖拽优化计划 10.4)。
**预期产出:**
- 新增/替换若干模块configParser / colorResolver / sectorBuilder / degreeRing / centerIcon
- `public/*.json` 配置即可新增罗盘,无需改代码。
- 统一主题与颜色规则,支持复杂布局与 SVG 图标。
--- ---
## 一、项目现状分析 ## 一、项目现状分析
@@ -103,6 +118,22 @@
- 新增 `public/themes.json` - 新增 `public/themes.json`
- 更新 `demo.json.conf` 说明 - 更新 `demo.json.conf` 说明
### 1.5 layerHeight 参数规则(新增)
**目标:** 支持在 layer 中以 `layerHeight` 定义厚度,并可自动推导 `rInner` / `rOuter`。
**规则:**
1. 若 layer 同时指定 `rInner` 和 `rOuter`,则忽略 `layerHeight`。
2. 若 layer 仅指定 `layerHeight`,且没有 `rInner` / `rOuter`
- 使用上一层的 `rOuter` 作为当前 `rInner`
- 当前 `rOuter = 上一层 rOuter + layerHeight`
3. 若 layer 未提供任何半径信息(`rInner` / `rOuter` / `layerHeight`),则视为配置错误(或按实现约定抛错/警告)。
**实现改动点:**
1. `types.ts`:为 sector layer 增加可选字段 `layerHeight?: number`
2. `configParser.ts`:新增“层半径归一化”步骤,按层顺序推导半径
3. 文档/示例:补充 `layerHeight` 的说明与示例用法
--- ---
## 二、整体架构设计 ## 二、整体架构设计

View File

@@ -42,6 +42,7 @@
<div <div
v-else v-else
class="svg-container" class="svg-container"
:class="{ 'is-dragging': isDragging }"
@wheel.prevent="handleWheel" @wheel.prevent="handleWheel"
@mousedown="handleMouseDown" @mousedown="handleMouseDown"
@mousemove="handleMouseMove" @mousemove="handleMouseMove"
@@ -53,252 +54,258 @@
:height="size" :height="size"
:viewBox="`${viewBoxMin} ${viewBoxMin} ${viewBoxSize} ${viewBoxSize}`" :viewBox="`${viewBoxMin} ${viewBoxMin} ${viewBoxSize} ${viewBoxSize}`"
class="svg" class="svg"
:style="{
transform: `scale(${scale}) translate(${panX}px, ${panY}px)`,
cursor: isDragging ? 'grabbing' : 'grab'
}"
> >
<!-- 背景 --> <g ref="viewport" class="viewport">
<circle <!-- 背景 -->
:r="outerMost || viewBoxSize / 2" <circle
:fill="config?.background || '#ffffff'" :r="outerMost || viewBoxSize / 2"
/> :fill="config?.background || '#ffffff'"
<g v-memo="[sectors]">
<!-- 扇区 -->
<g>
<path
v-for="s in sectors"
:key="s.key"
:d="s.path"
:fill="s.fill"
/> />
</g>
<!-- 内部填色区域 --> <g v-memo="[sectors]">
<g> <!-- 扇区 -->
<template v-for="s in sectors" :key="s.key + '-inner'"> <g>
<path <path
v-if="s.innerFillPath" v-for="s in sectors"
:d="s.innerFillPath" :key="s.key"
:fill="s.innerFillColor" :d="s.path"
fill-opacity="1" :fill="s.fill"
stroke="none" />
/> </g>
</template>
</g>
<!-- 定义文字路径 --> <!-- 内部填色区域 -->
<defs> <g>
<path <template v-for="s in sectors" :key="s.key + '-inner'">
v-for="s in sectors" <path
:key="s.textPathId" v-if="s.innerFillPath"
:id="s.textPathId" :d="s.innerFillPath"
:d="s.textPath" :fill="s.innerFillColor"
fill="none" fill-opacity="1"
/> stroke="none"
<template v-for="s in sectors" :key="s.key + '-units'"> />
<path </template>
v-for="unit in s.textUnits || []" </g>
:key="unit.textPathId"
:id="unit.textPathId" <!-- 定义文字路径 -->
:d="unit.textPath" <defs>
<path
v-for="s in sectors"
:key="s.textPathId"
:id="s.textPathId"
:d="s.textPath"
fill="none"
/>
<template v-for="s in sectors" :key="s.key + '-units'">
<path
v-for="unit in s.textUnits || []"
:key="unit.textPathId"
:id="unit.textPathId"
:d="unit.textPath"
fill="none"
/>
</template>
</defs>
<!-- 文字标签沿圆弧排列 -->
<g>
<template v-for="s in sectors" :key="s.key + '-text'">
<template v-if="s.textUnits">
<g v-for="unit in s.textUnits || []" :key="unit.textPathId">
<text
v-if="!unit.isSvg"
:font-size="unit.fontSize"
:fill="s.textColor"
:writing-mode="unit.isVertical ? 'tb' : undefined"
:glyph-orientation-vertical="unit.isVertical ? '0' : undefined"
:text-anchor="unit.isVertical ? 'middle' : undefined"
style="user-select: none"
>
<textPath
:href="'#' + unit.textPathId"
startOffset="50%"
text-anchor="middle"
dominant-baseline="central"
>
{{ unit.content }}
</textPath>
</text>
<image
v-else
:href="unit.svgPath"
:x="getUnitSvgBox(s, unit).x"
:y="getUnitSvgBox(s, unit).y"
:width="getUnitSvgBox(s, unit).size"
:height="getUnitSvgBox(s, unit).size"
:opacity="s.textColor ? 1 : 1"
/>
</g>
</template>
<template v-else>
<text
v-if="!s.isSvgContent"
:font-size="s.fontSize"
:fill="s.textColor"
:writing-mode="s.isVertical ? 'tb' : undefined"
:glyph-orientation-vertical="s.isVertical ? '0' : undefined"
:text-anchor="s.isVertical ? 'middle' : undefined"
style="user-select: none"
>
<textPath
:href="'#' + s.textPathId"
startOffset="50%"
text-anchor="middle"
dominant-baseline="central"
>
{{ s.label }}
</textPath>
</text>
<image
v-else
:href="s.svgPath"
:x="getSectorSvgBox(s).x"
:y="getSectorSvgBox(s).y"
:width="getSectorSvgBox(s).size"
:height="getSectorSvgBox(s).size"
/>
</template>
</template>
</g>
</g>
<!-- 扇区边界线 -->
<g
v-memo="[sectors, boundaryRings, strokeColor, strokeOpacity, strokeWidth]"
:stroke="strokeColor"
:stroke-opacity="strokeOpacity"
:stroke-width="strokeWidth"
>
<circle
v-for="r in boundaryRings"
:key="'ring-outline-' + r"
:r="r"
fill="none" fill="none"
/> />
</template> <template v-for="s in sectors" :key="'split-' + s.key">
</defs> <line
v-if="s.groupSplitVisible !== false"
<!-- 文字标签沿圆弧排列 --> :x1="toXY(s.aStart, s.rInner).x"
<g> :y1="toXY(s.aStart, s.rInner).y"
<template v-for="s in sectors" :key="s.key + '-text'"> :x2="toXY(s.aStart, s.rOuter).x"
<template v-if="s.textUnits"> :y2="toXY(s.aStart, s.rOuter).y"
<g v-for="unit in s.textUnits || []" :key="unit.textPathId">
<text
v-if="!unit.isSvg"
:font-size="unit.fontSize"
:fill="s.textColor"
:writing-mode="unit.isVertical ? 'tb' : undefined"
:glyph-orientation-vertical="unit.isVertical ? '0' : undefined"
:text-anchor="unit.isVertical ? 'middle' : undefined"
style="user-select: none"
>
<textPath
:href="'#' + unit.textPathId"
startOffset="50%"
text-anchor="middle"
dominant-baseline="central"
>
{{ unit.content }}
</textPath>
</text>
<image
v-else
:href="unit.svgPath"
:x="getUnitSvgBox(s, unit).x"
:y="getUnitSvgBox(s, unit).y"
:width="getUnitSvgBox(s, unit).size"
:height="getUnitSvgBox(s, unit).size"
:opacity="s.textColor ? 1 : 1"
/>
</g>
</template>
<template v-else>
<text
v-if="!s.isSvgContent"
:font-size="s.fontSize"
:fill="s.textColor"
:writing-mode="s.isVertical ? 'tb' : undefined"
:glyph-orientation-vertical="s.isVertical ? '0' : undefined"
:text-anchor="s.isVertical ? 'middle' : undefined"
style="user-select: none"
>
<textPath
:href="'#' + s.textPathId"
startOffset="50%"
text-anchor="middle"
dominant-baseline="central"
>
{{ s.label }}
</textPath>
</text>
<image
v-else
:href="s.svgPath"
:x="getSectorSvgBox(s).x"
:y="getSectorSvgBox(s).y"
:width="getSectorSvgBox(s).size"
:height="getSectorSvgBox(s).size"
/> />
</template> </template>
</g>
</template> <!-- 形心点仅辅助线开启时显示 -->
<g v-if="showGuides" v-memo="[sectors]">
<circle
v-for="s in sectors"
:key="s.key + '-center'"
:cx="s.cx"
:cy="s.cy"
r="2.2"
fill="#ef4444"
opacity="0.8"
/>
</g>
<!-- 辅助线圆环分度线 -->
<g v-if="showGuides" v-memo="[rings, anglesDeg, outerMost]" stroke="#111827" stroke-opacity="0.18">
<!-- 圆环 -->
<circle
v-for="r in rings"
:key="'ring-' + r"
:r="r"
fill="none"
:stroke-width="SECTOR_STROKE_WIDTH"
/>
<!-- 径向线 -->
<line
v-for="a in anglesDeg"
:key="'ang-' + a"
:x1="0"
:y1="0"
:x2="toXY(a, outerMost).x"
:y2="toXY(a, outerMost).y"
:stroke-width="SECTOR_STROKE_WIDTH"
/>
</g>
<!-- 刻度环 -->
<g v-if="degreeRing" v-memo="[degreeRing]">
<defs>
<path
v-for="label in degreeRing.labels || []"
:key="label.textPathId"
:id="label.textPathId"
:d="label.textPath"
fill="none"
/>
</defs>
<circle
v-if="degreeRing.background"
:r="(degreeRing.ring.rInner + degreeRing.ring.rOuter) / 2"
fill="none"
:stroke="degreeRing.background.color"
:stroke-opacity="degreeRing.background.opacity"
:stroke-width="degreeRing.ring.rOuter - degreeRing.ring.rInner"
/>
<circle
:r="degreeRing.ring.rOuter"
fill="none"
:stroke="degreeRing.ring.color"
:stroke-opacity="degreeRing.ring.opacity"
:stroke-width="SECTOR_STROKE_WIDTH"
/>
<circle
:r="degreeRing.ring.rInner"
fill="none"
:stroke="degreeRing.ring.color"
:stroke-opacity="degreeRing.ring.opacity"
:stroke-width="SECTOR_STROKE_WIDTH"
/>
<line
v-for="tick in degreeRing.ticks"
:key="'tick-' + tick.angle + '-' + tick.startR + '-' + tick.endR"
:x1="tick.x1"
:y1="tick.y1"
:x2="tick.x2"
:y2="tick.y2"
:stroke="degreeRing.tickColor"
:stroke-width="SECTOR_STROKE_WIDTH"
/>
<text
v-for="label in degreeRing.labels || []"
:key="'degree-' + label.angle"
:fill="degreeRing.tickColor"
:font-size="label.fontSize"
style="user-select: none"
>
<textPath
:href="'#' + label.textPathId"
startOffset="50%"
text-anchor="middle"
dominant-baseline="central"
>
{{ label.text }}
</textPath>
</text>
</g>
<!-- 中心图标 -->
<g v-if="centerIcon" v-memo="[centerIcon]">
<image
:href="centerIcon.svgPath"
:width="centerIcon.rIcon * 2"
:height="centerIcon.rIcon * 2"
:x="-centerIcon.rIcon"
:y="-centerIcon.rIcon"
:opacity="centerIcon.opacity"
:transform="`rotate(${centerIcon.rotation})`"
/>
</g>
</g> </g>
</g>
<!-- 扇区边界线 -->
<g
v-memo="[sectors, boundaryRings, strokeColor, strokeOpacity, strokeWidth]"
:stroke="strokeColor"
:stroke-opacity="strokeOpacity"
:stroke-width="strokeWidth"
>
<circle
v-for="r in boundaryRings"
:key="'ring-outline-' + r"
:r="r"
fill="none"
/>
<template v-for="s in sectors" :key="'split-' + s.key">
<line
v-if="s.groupSplitVisible !== false"
:x1="toXY(s.aStart, s.rInner).x"
:y1="toXY(s.aStart, s.rInner).y"
:x2="toXY(s.aStart, s.rOuter).x"
:y2="toXY(s.aStart, s.rOuter).y"
/>
</template>
</g>
<!-- 形心点仅辅助线开启时显示 -->
<g v-if="showGuides" v-memo="[sectors]">
<circle
v-for="s in sectors"
:key="s.key + '-center'"
:cx="s.cx"
:cy="s.cy"
r="2.2"
fill="#ef4444"
opacity="0.8"
/>
</g>
<!-- 辅助线圆环分度线 -->
<g v-if="showGuides" v-memo="[rings, anglesDeg, outerMost]" stroke="#111827" stroke-opacity="0.18">
<!-- 圆环 -->
<circle
v-for="r in rings"
:key="'ring-' + r"
:r="r"
fill="none"
:stroke-width="SECTOR_STROKE_WIDTH"
/>
<!-- 径向线 -->
<line
v-for="a in anglesDeg"
:key="'ang-' + a"
:x1="0"
:y1="0"
:x2="toXY(a, outerMost).x"
:y2="toXY(a, outerMost).y"
:stroke-width="SECTOR_STROKE_WIDTH"
/>
</g>
<!-- 刻度环 -->
<g v-if="degreeRing" v-memo="[degreeRing]">
<defs>
<path
v-for="label in degreeRing.labels || []"
:key="label.textPathId"
:id="label.textPathId"
:d="label.textPath"
fill="none"
/>
</defs>
<circle
:r="degreeRing.ring.rOuter"
fill="none"
:stroke="degreeRing.ring.color"
:stroke-opacity="degreeRing.ring.opacity"
:stroke-width="SECTOR_STROKE_WIDTH"
/>
<circle
:r="degreeRing.ring.rInner"
fill="none"
:stroke="degreeRing.ring.color"
:stroke-opacity="degreeRing.ring.opacity"
:stroke-width="SECTOR_STROKE_WIDTH"
/>
<line
v-for="tick in degreeRing.ticks"
:key="'tick-' + tick.angle + '-' + tick.startR + '-' + tick.endR"
:x1="tick.x1"
:y1="tick.y1"
:x2="tick.x2"
:y2="tick.y2"
:stroke="degreeRing.tickColor"
:stroke-width="SECTOR_STROKE_WIDTH"
/>
<text
v-for="label in degreeRing.labels || []"
:key="'degree-' + label.angle"
:fill="degreeRing.tickColor"
:font-size="label.fontSize"
style="user-select: none"
>
<textPath
:href="'#' + label.textPathId"
startOffset="50%"
text-anchor="middle"
dominant-baseline="central"
>
{{ label.text }}
</textPath>
</text>
</g>
<!-- 中心图标 -->
<g v-if="centerIcon" v-memo="[centerIcon]">
<image
:href="centerIcon.svgPath"
:width="centerIcon.rIcon * 2"
:height="centerIcon.rIcon * 2"
:x="-centerIcon.rIcon"
:y="-centerIcon.rIcon"
:opacity="centerIcon.opacity"
:transform="`rotate(${centerIcon.rotation})`"
/>
</g>
</svg> </svg>
</div> </div>
@@ -341,6 +348,7 @@ const textRadialPosition = ref<TextRadialPosition>(DEFAULT_TEXT_RADIAL_POSITION)
const scale = ref(1); const scale = ref(1);
const panX = ref(0); const panX = ref(0);
const panY = ref(0); const panY = ref(0);
const viewport = ref<SVGGElement | null>(null);
let nextScale = scale.value; let nextScale = scale.value;
let nextPanX = panX.value; let nextPanX = panX.value;
let nextPanY = panY.value; let nextPanY = panY.value;
@@ -421,6 +429,9 @@ const loadConfigList = async () => {
}; };
onMounted(loadConfigList); onMounted(loadConfigList);
onMounted(() => {
applyViewportTransform(scale.value, panX.value, panY.value);
});
watch(selectedConfigPath, (value, previous) => { watch(selectedConfigPath, (value, previous) => {
if (value === previous) return; if (value === previous) return;
@@ -472,6 +483,14 @@ const boundaryRings = computed(() => {
return Array.from(set); return Array.from(set);
}); });
const applyViewportTransform = (scaleValue: number, panXValue: number, panYValue: number) => {
if (!viewport.value) return;
viewport.value.setAttribute(
'transform',
`scale(${scaleValue}) translate(${panXValue}, ${panYValue})`
);
};
// 使用 `rAF` 合并缩放/拖拽更新,减少渲染频率 // 使用 `rAF` 合并缩放/拖拽更新,减少渲染频率
const scheduleTransform = () => { const scheduleTransform = () => {
if (rafId !== null) return; if (rafId !== null) return;
@@ -484,6 +503,7 @@ const scheduleTransform = () => {
scale.value = nextScale; scale.value = nextScale;
panX.value = nextPanX; panX.value = nextPanX;
panY.value = nextPanY; panY.value = nextPanY;
applyViewportTransform(nextScale, nextPanX, nextPanY);
rafId = null; rafId = null;
}); });
}; };
@@ -638,6 +658,12 @@ button.active {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
contain: paint;
cursor: grab;
}
.svg-container.is-dragging {
cursor: grabbing;
} }
.status { .status {
@@ -650,10 +676,13 @@ button.active {
.svg { .svg {
background: transparent; background: transparent;
transition: transform 0.1s ease-out;
user-select: none; user-select: none;
} }
.viewport {
will-change: transform;
}
.note { .note {
color: #6b7280; color: #6b7280;
font-size: 13px; font-size: 13px;

View File

@@ -16,8 +16,8 @@ import type {
ThemesConfig, ThemesConfig,
TextRadialPosition, TextRadialPosition,
} from '../types'; } from '../types';
import { polarToXY } from '../utils'; import { getTextColorForBackground, polarToXY } from '../utils';
import { parseConfig } from '../configParser'; import { normalizeLayerRadii, parseConfig } from '../configParser';
import { ColorResolver } from '../colorResolver'; import { ColorResolver } from '../colorResolver';
import { SectorBuilder } from '../sectorBuilder'; import { SectorBuilder } from '../sectorBuilder';
import { buildDegreeRing } from '../degreeRing'; import { buildDegreeRing } from '../degreeRing';
@@ -39,6 +39,29 @@ const resolveThemeColor = (
return theme.colorPalettes[value] ?? fallback; return theme.colorPalettes[value] ?? fallback;
}; };
const resolveDegreeRingColor = (
value: string | undefined,
theme: ThemeConfig,
background: string
) => {
if (typeof value === 'string' && value.trim().length > 0) {
return resolveThemeColor(theme, value, value);
}
const contrast = getTextColorForBackground(background);
return contrast === '#ffffff' ? '#ffffff' : '#000000';
};
const resolveDegreeRingBackground = (
value: string | undefined,
theme: ThemeConfig,
fallback: string
) => {
if (typeof value === 'string' && value.trim().length > 0) {
return resolveThemeColor(theme, value, fallback);
}
return fallback;
};
const findDegreeRingLayer = (layers: LayerConfig[]) => const findDegreeRingLayer = (layers: LayerConfig[]) =>
layers.find((layer) => layer.type === 'degreeRing'); layers.find((layer) => layer.type === 'degreeRing');
@@ -158,39 +181,45 @@ export function useLuopan(
error.value = null; error.value = null;
let configObj: LuopanConfigInput; let configObj: LuopanConfigInput;
let normalizedLayers: LuopanConfig['layers'];
const configInput = configSource.value; const configInput = configSource.value;
if (typeof configInput === 'string') { if (typeof configInput === 'string') {
const jsonText = await fetch(configInput).then((res) => res.text()); const jsonText = await fetch(configInput).then((res) => res.text());
configObj = parseConfig(jsonText); configObj = parseConfig(jsonText);
// parseConfig 已完成层归一化,避免重复处理
normalizedLayers = configObj.layers;
} else { } else {
configObj = configInput; configObj = configInput;
normalizedLayers = normalizeLayerRadii(configObj.layers);
} }
const resolvedTheme = await resolveTheme(configObj); const normalizedConfig = { ...configObj, layers: normalizedLayers };
const resolvedTheme = await resolveTheme(normalizedConfig);
const resolvedBackground = resolveThemeColor( const resolvedBackground = resolveThemeColor(
resolvedTheme, resolvedTheme,
configObj.background, normalizedConfig.background,
'#000000' '#000000'
); );
const resolvedStrokeColor = resolveThemeColor( const resolvedStrokeColor = resolveThemeColor(
resolvedTheme, resolvedTheme,
configObj.strokeColor, normalizedConfig.strokeColor,
'#1f2937' '#1f2937'
); );
const resolvedConfig: LuopanConfig = { const resolvedConfig: LuopanConfig = {
...configObj, ...normalizedConfig,
theme: resolvedTheme, theme: resolvedTheme,
background: resolvedBackground, background: resolvedBackground,
strokeColor: resolvedStrokeColor, strokeColor: resolvedStrokeColor,
strokeWidth: typeof configObj.strokeWidth === 'number' strokeWidth: typeof normalizedConfig.strokeWidth === 'number'
? configObj.strokeWidth ? normalizedConfig.strokeWidth
: undefined, : undefined,
strokeOpacity: typeof configObj.strokeOpacity === 'number' strokeOpacity: typeof normalizedConfig.strokeOpacity === 'number'
? configObj.strokeOpacity ? normalizedConfig.strokeOpacity
: undefined, : undefined,
insetDistance: typeof configObj.insetDistance === 'number' insetDistance: typeof normalizedConfig.insetDistance === 'number'
? configObj.insetDistance ? normalizedConfig.insetDistance
: undefined, : undefined,
}; };
@@ -198,15 +227,52 @@ export function useLuopan(
sectors.value = buildSectors(resolvedConfig); sectors.value = buildSectors(resolvedConfig);
const degreeRingLayer = findDegreeRingLayer(resolvedConfig.layers); const degreeRingLayer = findDegreeRingLayer(resolvedConfig.layers);
degreeRing.value = degreeRingLayer if (degreeRingLayer) {
? buildDegreeRing(degreeRingLayer.degreeRing) const ringBackground = resolveDegreeRingBackground(
: null; degreeRingLayer.degreeRing.colorRef,
resolvedConfig.theme,
resolvedConfig.background
);
const hasBackgroundRef =
typeof degreeRingLayer.degreeRing.colorRef === 'string' &&
degreeRingLayer.degreeRing.colorRef.trim().length > 0;
const tickColor = resolveDegreeRingColor(
degreeRingLayer.degreeRing.tickColor,
resolvedConfig.theme,
ringBackground
);
const ringColor = resolveDegreeRingColor(
degreeRingLayer.degreeRing.ringColor,
resolvedConfig.theme,
ringBackground
);
const ringData = buildDegreeRing({
...degreeRingLayer.degreeRing,
tickColor,
ringColor,
});
degreeRing.value = {
...ringData,
background: hasBackgroundRef
? {
color: ringBackground,
opacity: degreeRingLayer.degreeRing.opacity,
}
: undefined,
};
} else {
degreeRing.value = null;
}
const centerIconLayer = findCenterIconLayer(resolvedConfig.layers); const centerIconLayer = findCenterIconLayer(resolvedConfig.layers);
centerIcon.value = centerIconLayer centerIcon.value = centerIconLayer
? await loadCenterIcon(centerIconLayer.centerIcon) ? await loadCenterIcon(centerIconLayer.centerIcon)
: null; : null;
} catch (err) { } catch (err) {
console.error('加载罗盘配置失败', {
source: typeof configSource.value === 'string' ? configSource.value : 'inline config',
error: err,
});
error.value = err as Error; error.value = err as Error;
} finally { } finally {
loading.value = false; loading.value = false;

View File

@@ -1,4 +1,13 @@
import type { LuopanConfigInput, ThemeConfig } from './types'; import type {
DegreeRingLayerConfig,
DegreeRingLayerConfigInput,
LayerConfig,
LayerConfigInput,
LuopanConfigInput,
SectorLayerConfig,
SectorLayerConfigInput,
ThemeConfig,
} from './types';
const isObject = (value: unknown): value is Record<string, unknown> => const isObject = (value: unknown): value is Record<string, unknown> =>
typeof value === 'object' && value !== null && !Array.isArray(value); typeof value === 'object' && value !== null && !Array.isArray(value);
@@ -9,6 +18,9 @@ const assertCondition = (condition: boolean, message: string): void => {
} }
}; };
const isFiniteNumber = (value: unknown): value is number =>
typeof value === 'number' && Number.isFinite(value);
export const stripJsonComments = (input: string): string => { export const stripJsonComments = (input: string): string => {
let output = ''; let output = '';
let inString = false; let inString = false;
@@ -53,6 +65,175 @@ const normalizeTheme = (theme: Record<string, unknown>): ThemeConfig => {
}; };
}; };
const normalizeSectorLayer = (
layer: SectorLayerConfigInput,
index: number,
lastOuter: number | null
): { layer: SectorLayerConfig; nextOuter: number } => {
const rInnerRaw = layer.rInner;
const rOuterRaw = layer.rOuter;
const layerHeightRaw = layer.layerHeight;
const hasRInner = rInnerRaw !== undefined;
const hasROuter = rOuterRaw !== undefined;
if (hasRInner && !isFiniteNumber(rInnerRaw)) {
throw new Error(`layers[${index}].rInner 必须为数字`);
}
if (hasROuter && !isFiniteNumber(rOuterRaw)) {
throw new Error(`layers[${index}].rOuter 必须为数字`);
}
if (hasRInner || hasROuter) {
assertCondition(
hasRInner && hasROuter,
`layers[${index}] 必须同时提供 rInner 与 rOuter`
);
const rInner = rInnerRaw as number;
const rOuter = rOuterRaw as number;
assertCondition(rOuter > rInner, `layers[${index}] rOuter 必须大于 rInner`);
return {
layer: { ...layer, rInner, rOuter },
nextOuter: rOuter,
};
}
if (layerHeightRaw === undefined) {
throw new Error(`layers[${index}] 缺少 rInner/rOuter 或 layerHeight`);
}
if (!isFiniteNumber(layerHeightRaw)) {
throw new Error(`layers[${index}].layerHeight 必须为数字`);
}
assertCondition(
layerHeightRaw > 0,
`layers[${index}].layerHeight 必须大于 0`
);
assertCondition(
lastOuter !== null,
`layers[${index}] 使用 layerHeight 时必须有上一层 rOuter`
);
const rInner = lastOuter as number;
const rOuter = rInner + layerHeightRaw;
return {
layer: { ...layer, rInner, rOuter },
nextOuter: rOuter,
};
};
const normalizeDegreeRingLayer = (
layer: DegreeRingLayerConfigInput,
index: number,
lastOuter: number | null
): { layer: DegreeRingLayerConfig; nextOuter: number } => {
if (Object.prototype.hasOwnProperty.call(layer, 'degreeRing')) {
const keys = Object.keys(layer).join(', ');
console.error('检测到废弃的 degreeRing 嵌套配置', {
index,
keys,
layer,
});
throw new Error(
`layers[${index}].degreeRing 已废弃请改为扁平写法keys: ${keys}`
);
}
const { type: _type, layerHeight: layerHeightRaw, ...degreeRing } = layer;
const rInnerRaw = degreeRing.rInner;
const rOuterRaw = degreeRing.rOuter;
const hasRInner = rInnerRaw !== undefined;
const hasROuter = rOuterRaw !== undefined;
if (hasRInner && !isFiniteNumber(rInnerRaw)) {
throw new Error(`layers[${index}].rInner 必须为数字`);
}
if (hasROuter && !isFiniteNumber(rOuterRaw)) {
throw new Error(`layers[${index}].rOuter 必须为数字`);
}
if (hasRInner || hasROuter) {
assertCondition(
hasRInner && hasROuter,
`layers[${index}] 必须同时提供 rInner 与 rOuter`
);
const rInner = rInnerRaw as number;
const rOuter = rOuterRaw as number;
assertCondition(
rOuter > rInner,
`layers[${index}].rOuter 必须大于 rInner`
);
return {
layer: {
type: 'degreeRing',
layerHeight: layerHeightRaw,
degreeRing: { ...degreeRing, rInner, rOuter },
},
nextOuter: rOuter,
};
}
if (layerHeightRaw === undefined) {
throw new Error(`layers[${index}] 缺少 rInner/rOuter 或 layerHeight`);
}
if (!isFiniteNumber(layerHeightRaw)) {
throw new Error(`layers[${index}].layerHeight 必须为数字`);
}
assertCondition(
layerHeightRaw > 0,
`layers[${index}].layerHeight 必须大于 0`
);
assertCondition(
lastOuter !== null,
`layers[${index}] 使用 layerHeight 时必须有上一层 rOuter`
);
const rInner = lastOuter as number;
const rOuter = rInner + layerHeightRaw;
return {
layer: {
type: 'degreeRing',
layerHeight: layerHeightRaw,
degreeRing: { ...degreeRing, rInner, rOuter },
},
nextOuter: rOuter,
};
};
export const normalizeLayerRadii = (layers: LayerConfigInput[]): LayerConfig[] => {
const normalized: LayerConfig[] = [];
let lastOuter: number | null = null;
layers.forEach((layer, index) => {
assertCondition(isObject(layer), `layers[${index}] 必须为对象`);
const type = (layer as Record<string, unknown>).type;
if (type === 'centerIcon') {
normalized.push(layer as LayerConfig);
return;
}
if (type === 'degreeRing') {
const { layer: degreeRingLayer, nextOuter } = normalizeDegreeRingLayer(
layer as DegreeRingLayerConfigInput,
index,
lastOuter
);
normalized.push(degreeRingLayer);
lastOuter = nextOuter;
return;
}
const { layer: sectorLayer, nextOuter } = normalizeSectorLayer(
layer as SectorLayerConfigInput,
index,
lastOuter
);
normalized.push(sectorLayer);
lastOuter = nextOuter;
});
return normalized;
};
export const parseConfig = (jsonText: string): LuopanConfigInput => { export const parseConfig = (jsonText: string): LuopanConfigInput => {
const cleanText = stripJsonComments(jsonText); const cleanText = stripJsonComments(jsonText);
let parsed: unknown; let parsed: unknown;
@@ -73,6 +254,7 @@ export const parseConfig = (jsonText: string): LuopanConfigInput => {
assertCondition(isObject(config.theme), 'theme 必须为对象'); assertCondition(isObject(config.theme), 'theme 必须为对象');
} }
assertCondition(Array.isArray(config.layers), 'layers 为必填数组'); assertCondition(Array.isArray(config.layers), 'layers 为必填数组');
const normalizedLayers = normalizeLayerRadii(config.layers as LayerConfigInput[]);
return { return {
name: config.name, name: config.name,
@@ -85,6 +267,6 @@ export const parseConfig = (jsonText: string): LuopanConfigInput => {
outerRadius: typeof config.outerRadius === 'number' ? config.outerRadius : undefined, outerRadius: typeof config.outerRadius === 'number' ? config.outerRadius : undefined,
themeRef: typeof config.themeRef === 'string' ? config.themeRef : undefined, themeRef: typeof config.themeRef === 'string' ? config.themeRef : undefined,
theme: isObject(config.theme) ? normalizeTheme(config.theme) : undefined, theme: isObject(config.theme) ? normalizeTheme(config.theme) : undefined,
layers: config.layers as LuopanConfigInput['layers'], layers: normalizedLayers,
}; };
}; };

View File

@@ -15,6 +15,8 @@ export function buildDegreeRing(config: DegreeRingConfig): DegreeRingData {
const ticks: TickMark[] = []; const ticks: TickMark[] = [];
const { rInner, rOuter, mode } = config; const { rInner, rOuter, mode } = config;
const labelFontSize = 8; const labelFontSize = 8;
const tickColor = config.tickColor ?? '#000000';
const ringColor = config.ringColor ?? tickColor;
const majorTick = Math.max(1, config.majorTick); const majorTick = Math.max(1, config.majorTick);
const minorTick = Math.max(1, config.minorTick); const minorTick = Math.max(1, config.minorTick);
@@ -125,11 +127,11 @@ export function buildDegreeRing(config: DegreeRingConfig): DegreeRingData {
return { return {
ticks, ticks,
tickColor: config.tickColor, tickColor,
ring: { ring: {
rInner, rInner,
rOuter, rOuter,
color: config.ringColor, color: ringColor,
opacity: config.opacity, opacity: config.opacity,
}, },
labels: labels.length > 0 ? labels : undefined, labels: labels.length > 0 ? labels : undefined,

View File

@@ -62,10 +62,6 @@ export class SectorBuilder {
// 颜色优先级:扇区 > 规律填色 > 背景 // 颜色优先级:扇区 > 规律填色 > 背景
const fillColor = this.colorResolver.resolveSectorColor(layerColorMap, sectorConfig, i); const fillColor = this.colorResolver.resolveSectorColor(layerColorMap, sectorConfig, i);
const layerColor = layer.colorRef ? this.colorResolver.resolveColor(layer.colorRef) : undefined;
const sectorColor = sectorConfig?.colorRef
? this.colorResolver.resolveColor(sectorConfig.colorRef)
: undefined;
// 扇区的 `innerFill` 优先级高于层级的 `innerFill`。 // 扇区的 `innerFill` 优先级高于层级的 `innerFill`。
const innerFill = (sectorConfig?.innerFill ?? layer.innerFill ?? 0) === 1; const innerFill = (sectorConfig?.innerFill ?? layer.innerFill ?? 0) === 1;
const innerFillPath = innerFill const innerFillPath = innerFill
@@ -80,10 +76,10 @@ export class SectorBuilder {
const normalizedInnerFillPath = const normalizedInnerFillPath =
innerFillPath && innerFillPath.length > 0 ? innerFillPath : undefined; innerFillPath && innerFillPath.length > 0 ? innerFillPath : undefined;
const hasInnerFillPath = Boolean(normalizedInnerFillPath); const hasInnerFillPath = Boolean(normalizedInnerFillPath);
// `innerFill` 开启时:外圈保持白色,仅填充内缩块。 // `innerFill` 开启时:外圈保持白色,内缩块仍按规律填色
const baseFillColor = hasInnerFillPath ? '#ffffff' : fillColor; const baseFillColor = hasInnerFillPath ? '#ffffff' : fillColor;
const innerFillColor = hasInnerFillPath ? sectorColor ?? layerColor ?? fillColor : undefined; const innerFillColor = hasInnerFillPath ? fillColor : undefined;
const textBaseColor = hasInnerFillPath ? innerFillColor ?? fillColor : fillColor; const textBaseColor = fillColor;
const textColor = getTextColorForBackground(textBaseColor); const textColor = getTextColorForBackground(textBaseColor);
const sectorKey = `L${layerIndex}-P${i}`; const sectorKey = `L${layerIndex}-P${i}`;

View File

@@ -22,7 +22,7 @@ export interface Example {
/** /**
* JSON 配置根对象 * JSON 配置根对象
*/ */
export interface LuopanConfigBase { export interface LuopanConfigBase<TLayer = LayerConfig> {
name: string; name: string;
description?: string; description?: string;
background: string; background: string;
@@ -33,15 +33,13 @@ export interface LuopanConfigBase {
outerRadius?: number; outerRadius?: number;
themeRef?: string; themeRef?: string;
theme?: ThemeConfig; theme?: ThemeConfig;
layers: LayerConfig[]; layers: TLayer[];
} }
export interface LuopanConfig extends LuopanConfigBase { export interface LuopanConfig extends LuopanConfigBase<LayerConfig> {
theme: ThemeConfig; theme: ThemeConfig;
} }
export type LuopanConfigInput = LuopanConfigBase;
/** /**
* 主题配置 * 主题配置
*/ */
@@ -84,8 +82,15 @@ export interface DegreeRingConfig {
majorTick: number; majorTick: number;
minorTick: number; minorTick: number;
microTick: number; microTick: number;
tickColor: string; tickColor?: string;
ringColor: string; ringColor?: string;
colorRef?: string;
}
export interface DegreeRingConfigInput
extends Omit<DegreeRingConfig, 'rInner' | 'rOuter'> {
rInner?: number;
rOuter?: number;
} }
/** /**
@@ -99,11 +104,9 @@ export type LayerConfig =
/** /**
* 普通扇区层配置 * 普通扇区层配置
*/ */
export interface SectorLayerConfig { interface SectorLayerBase {
type?: 'sectors'; type?: 'sectors';
divisions: number; divisions: number;
rInner: number;
rOuter: number;
startAngle?: number; startAngle?: number;
colorRef?: string; colorRef?: string;
innerFill?: 0 | 1; innerFill?: 0 | 1;
@@ -115,6 +118,25 @@ export interface SectorLayerConfig {
sectors?: SectorConfig[]; sectors?: SectorConfig[];
} }
export interface SectorLayerConfig extends SectorLayerBase {
rInner: number;
rOuter: number;
layerHeight?: number;
}
export interface SectorLayerConfigInput extends SectorLayerBase {
rInner?: number;
rOuter?: number;
layerHeight?: number;
}
export type LayerConfigInput =
| SectorLayerConfigInput
| CenterIconLayerConfig
| DegreeRingLayerConfigInput;
export type LuopanConfigInput = LuopanConfigBase<LayerConfigInput>;
/** /**
* 中心图标层配置 * 中心图标层配置
*/ */
@@ -129,6 +151,13 @@ export interface CenterIconLayerConfig {
export interface DegreeRingLayerConfig { export interface DegreeRingLayerConfig {
type: 'degreeRing'; type: 'degreeRing';
degreeRing: DegreeRingConfig; degreeRing: DegreeRingConfig;
layerHeight?: number;
}
// 扁平化输入:刻度环字段直接挂在层上。
export interface DegreeRingLayerConfigInput extends DegreeRingConfigInput {
type: 'degreeRing';
layerHeight?: number;
} }
/** /**
@@ -190,6 +219,7 @@ export interface DegreeRingData {
ticks: TickMark[]; ticks: TickMark[];
tickColor: string; tickColor: string;
ring: { rInner: number; rOuter: number; color: string; opacity: number }; ring: { rInner: number; rOuter: number; color: string; opacity: number };
background?: { color: string; opacity: number };
labels?: DegreeLabel[]; labels?: DegreeLabel[];
} }

View File

@@ -5,11 +5,11 @@
import { describe, it, expect } from 'vitest'; import { describe, it, expect } from 'vitest';
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import Luopan from '../src/Luopan.vue'; import Luopan from '../src/Luopan.vue';
import type { LuopanConfig } from '../src/types'; import type { LuopanConfigInput } from '../src/types';
const flushPromises = () => new Promise((resolve) => setTimeout(resolve, 0)); const flushPromises = () => new Promise((resolve) => setTimeout(resolve, 0));
const baseConfig: LuopanConfig = { const baseConfig: LuopanConfigInput = {
name: '测试配置', name: '测试配置',
background: '#000000', background: '#000000',
theme: { theme: {
@@ -26,20 +26,18 @@ const baseConfig: LuopanConfig = {
}, },
{ {
type: 'degreeRing', type: 'degreeRing',
degreeRing: { rInner: 90,
rInner: 90, rOuter: 100,
rOuter: 100, showDegree: 1,
showDegree: 1, mode: 'both',
mode: 'both', opacity: 0.3,
opacity: 0.3, tickLength: 6,
tickLength: 6, tickLengthStep: 1,
tickLengthStep: 1, majorTick: 10,
majorTick: 10, minorTick: 5,
minorTick: 5, microTick: 1,
microTick: 1, tickColor: '#ffffff',
tickColor: '#ffffff', ringColor: '#ffffff',
ringColor: '#ffffff',
},
}, },
{ {
type: 'centerIcon', type: 'centerIcon',

View File

@@ -5,9 +5,9 @@
import { describe, it, expect } from 'vitest'; import { describe, it, expect } from 'vitest';
import { ref } from 'vue'; import { ref } from 'vue';
import { useLuopan } from '../src/composables/useLuopan'; import { useLuopan } from '../src/composables/useLuopan';
import type { LuopanConfig, TextRadialPosition } from '../src/types'; import type { LuopanConfigInput, TextRadialPosition } from '../src/types';
const createMockConfig = (): LuopanConfig => ({ const createMockConfig = (): LuopanConfigInput => ({
name: '测试配置', name: '测试配置',
background: '#000000', background: '#000000',
theme: { theme: {