update at 2026-01-22 18:43:01

This commit is contained in:
douboer
2026-01-22 18:43:01 +08:00
parent c23c71eabf
commit a930a99a50
23 changed files with 2082 additions and 1186 deletions

View File

@@ -10,18 +10,21 @@
- **内置逻辑**:颜色、文字、填充等逻辑写死在代码中
- **测试导向**:现有实现主要为测试工具函数正确性而设计
### 1.2 目标需求分析(基于 todolist.md 和 demo.json.conf
### 1.2 目标需求分析(基于 todolist.md 和 demo.json
说明:`public/*.json` 为实际加载配置,`.json.conf` 仅作解释说明,不参与加载。
**核心变化:**
1. **配置驱动**:从 JSON 配置文件完全定义罗盘结构
2. **复杂着色规则**:支持三级着色优先级(全局 → 层级规律填色 → 扇区独立)
3. **多文本单元**:扇区内容支持 `|` 分隔的多个文本单元,角度智能分配
4. **SVG 图标支持**:扇区内容可以是 SVG 文件
5. **中心图标**:支持可旋转的中心 SVG 图标
6. **360度刻度环**:支持多种刻度模式的度数环
7. **命名配色方案**:通过 theme.colorPalettes 定义可复用颜色
8. **规律填色机制**:通过 num + interval 实现周期性着
9. **同组分割线控制**groupSplit 参数控制组内分割线显示
2. **新增罗盘零改码**:新增罗盘只需在 `public/` 下增加 JSON 配置文件,无需修改代码
3. **复杂着色规则**:支持三级着色优先级(全局 → 层级规律填色 → 扇区独立)
4. **多文本单元**:扇区内容支持 `|` 分隔的多个文本单元,角度智能分配
5. **SVG 图标支持**:扇区内容可以是 SVG 文件
6. **中心图标**:作为 layer 类型,支持可旋转的中心 SVG 图标
7. **360度刻度环**:作为 layer 类型,支持多种刻度模式的度数环
8. **命名配色方案**:通过 theme.colorPalettes 定义可复用颜
9. **规律填色机制**:通过 num + interval 实现周期性着色
10. **同组分割线控制**groupSplit 参数控制组内分割线显示
---
@@ -95,18 +98,15 @@ JSON 文本 (带注释)
[sectorBuilder] 遍历 layers
对每个 layer:
1. 计算规律填色模式 (num, interval)
2. 生成所有扇区
3. 对每个扇区
- 应用颜色优先级
- 解析多文本单元
- 计算 SVG 路径
完整的 Sector 数组
[degreeRing] 生成刻度环
[centerIcon] 加载中心图标
- type=sectors
1. 计算规律填色模式 (num, interval)
2. 生成所有扇区
3. 对每个扇区:
- 应用颜色优先级
- 解析多文本单元
- 计算 SVG 路径
- type=degreeRing生成刻度环数据
- type=centerIcon加载中心图标
最终渲染数据
```
@@ -147,8 +147,6 @@ function applyPatternColoring(
}
```
CG: 角度分配还需结合扇区高度,就能计算出字的大小和布局
**多文本单元角度分配:**
```typescript
function splitMultiTextUnits(
@@ -192,9 +190,7 @@ export interface LuopanConfig {
description?: string;
background: string; // 全局背景色
theme: ThemeConfig;
centerIcon?: CenterIconConfig;
degreeRing?: DegreeRingConfig;
layers: LayerConfig[];
layers: LayerConfig[]; // 扇区层 + 中心图标层 + 刻度环层
}
/** 主题配置 */
@@ -228,7 +224,14 @@ export interface DegreeRingConfig {
}
/** 层配置 */
export interface LayerConfig {
export type LayerConfig =
| SectorLayerConfig
| CenterIconLayerConfig
| DegreeRingLayerConfig;
/** 普通扇区层配置 */
export interface SectorLayerConfig {
type?: 'sectors';
divisions: number;
rInner: number;
rOuter: number;
@@ -241,6 +244,18 @@ export interface LayerConfig {
sectors?: SectorConfig[];
}
/** 中心图标层配置 */
export interface CenterIconLayerConfig {
type: 'centerIcon';
centerIcon: CenterIconConfig;
}
/** 刻度环层配置 */
export interface DegreeRingLayerConfig {
type: 'degreeRing';
degreeRing: DegreeRingConfig;
}
/** 扇区配置 */
export interface SectorConfig {
content?: string; // 支持 "|" 分隔
@@ -256,7 +271,6 @@ export interface TextUnit {
isSvg: boolean; // 是否为 SVG 文件名
}
CG
/** 刻度线数据 */
export interface TickMark {
angle: number;
@@ -267,13 +281,12 @@ export interface TickMark {
label?: string; // 度数标签(仅主刻度)
}
CG
/** 扩展现有 Sector 接口 */
export interface Sector {
// ... 现有字段保持 ...
// 新增字段
textUnits?: TextUnit[]; // 多文本单元
textUnits?: TextUnit[]; // 多文本单元
groupSplitVisible?: boolean; // 是否显示与下一个扇区的分割线
isSvgContent?: boolean; // 内容是否为 SVG
svgPath?: string; // SVG 文件路径
@@ -284,7 +297,7 @@ export interface Sector {
```typescript
// 输入JSON 配置字符串
const jsonText = await fetch('/demo.json.conf').then(r => r.text());
const jsonText = await fetch('/demo.json').then(r => r.text());
// 步骤1解析配置
const config = parseConfig(jsonText);
@@ -294,18 +307,22 @@ const colorResolver = new ColorResolver(config.theme, config.background);
// 步骤3构建扇区
const sectorBuilder = new SectorBuilder(colorResolver);
const sectors = config.layers.flatMap((layer, layerIndex) =>
sectorBuilder.buildLayer(layer, layerIndex)
const sectors = config.layers.flatMap((layer, layerIndex) =>
layer.type === 'centerIcon' || layer.type === 'degreeRing'
? []
: sectorBuilder.buildLayer(layer, layerIndex)
);
// 步骤4生成刻度环
const degreeRingData = config.degreeRing
? buildDegreeRing(config.degreeRing)
// 步骤4生成刻度环(从 layers 中提取)
const degreeRingLayer = config.layers.find(layer => layer.type === 'degreeRing');
const degreeRingData = degreeRingLayer
? buildDegreeRing(degreeRingLayer.degreeRing)
: null;
// 步骤5加载中心图标
const centerIconData = config.centerIcon
? await loadCenterIcon(config.centerIcon)
// 步骤5加载中心图标(从 layers 中提取)
const centerIconLayer = config.layers.find(layer => layer.type === 'centerIcon');
const centerIconData = centerIconLayer
? await loadCenterIcon(centerIconLayer.centerIcon)
: null;
// 输出:完整渲染数据
@@ -332,10 +349,10 @@ const renderData = {
- JSON 解析
- 基础验证(必填字段检查)
3. ✅ 编写单元测试:`configParser.test.ts`
4. ✅ 测试用例:解析 `demo.json.conf`
4. ✅ 测试用例:解析 `demo.json`
**验收标准:**
- 能成功解析 `demo.json.conf` 为 LuopanConfig 对象
- 能成功解析 `demo.json` 为 LuopanConfig 对象
- 所有必填字段验证通过
- 测试覆盖率 > 80%
@@ -417,34 +434,15 @@ export class ColorResolver {
**核心代码:**
```typescript
export function parseMultiText(
// 仅保留名称,具体实现参考前文 splitMultiTextUnits
export function splitMultiTextUnits(
content: string,
aStart: number,
aEnd: number,
svgIconPath: string = 'src/assets/icons/'
): TextUnit[] {
const parts = content.split('|').map(s => s.trim());
const ratios = getLayoutRatio(parts.length);
const totalAngle = aEnd - aStart;
const units: TextUnit[] = [];
let currentAngle = aStart;
for (let i = 0; i < parts.length; i++) {
const unitAngle = totalAngle * ratios[i];
const isSvg = parts[i].endsWith('.svg');
units.push({
content: parts[i],
aStart: currentAngle,
aEnd: currentAngle + unitAngle,
isSvg,
});
currentAngle += unitAngle;
}
return units;
// 参考前面代码
return [];
}
```
@@ -503,7 +501,7 @@ export class SectorBuilder {
// 解析多文本单元
const content = sectorConfig?.content ?? '';
const textUnits = content.includes('|')
? parseMultiText(content, aStart, aEnd)
? splitMultiTextUnits(content, aStart, aEnd)
: [{ content, aStart, aEnd, isSvg: content.endsWith('.svg') }];
// 确定是否显示分割线
@@ -553,7 +551,7 @@ export class SectorBuilder {
```
**验收标准:**
- 完整解析 `demo.json.conf` 所有 layer
- 完整解析 `demo.json` 所有 layer
- 颜色优先级正确
- groupSplit 逻辑正确
- 多文本单元正确拆分
@@ -624,14 +622,17 @@ export function buildDegreeRing(config: DegreeRingConfig): {
ticks.push({ angle, type, length, startR, endR });
}
// 生成度数标签
// 生成度数标签(使用 textPath 保持方向一致)
const labels: DegreeLabel[] = [];
if (config.showDegree === 1) {
const r = (rInner + rOuter) / 2;
for (let angle = 0; angle < 360; angle += config.majorTick) {
labels.push({
angle,
text: angle.toString(),
r: (rInner + rOuter) / 2
r,
textPathId: `degree-label-${angle}`,
textPath: generateTextPath(r, r, angle - 4, angle + 4)
});
}
}
@@ -652,7 +653,7 @@ export function buildDegreeRing(config: DegreeRingConfig): {
**验收标准:**
- 刻度线长度正确major > minor > micro
- mode 模式正确inner, outer, both
- 度数标签位置准确
- 度数标签方向与扇区文字一致(自动翻转)
- 中心图标加载和旋转正常
---
@@ -711,17 +712,21 @@ export function useLuopan(configPathOrObject: string | LuopanConfig) {
// 构建扇区
const sectorBuilder = new SectorBuilder(colorResolver);
sectors.value = configObj.layers.flatMap((layer, i) =>
sectorBuilder.buildLayer(layer, i)
layer.type === 'centerIcon' || layer.type === 'degreeRing'
? []
: sectorBuilder.buildLayer(layer, i)
);
// 构建刻度环
if (configObj.degreeRing) {
degreeRing.value = buildDegreeRing(configObj.degreeRing);
// 构建刻度环(从 layers 中提取)
const degreeRingLayer = configObj.layers.find(layer => layer.type === 'degreeRing');
if (degreeRingLayer) {
degreeRing.value = buildDegreeRing(degreeRingLayer.degreeRing);
}
// 加载中心图标
if (configObj.centerIcon) {
centerIcon.value = await loadCenterIcon(configObj.centerIcon);
// 加载中心图标(从 layers 中提取)
const centerIconLayer = configObj.layers.find(layer => layer.type === 'centerIcon');
if (centerIconLayer) {
centerIcon.value = await loadCenterIcon(centerIconLayer.centerIcon);
}
} catch (e) {
@@ -807,12 +812,12 @@ export function useLuopan(configPathOrObject: string | LuopanConfig) {
<script setup lang="ts">
const { config, sectors, degreeRing, centerIcon, loading, error } =
useLuopan('/demo.json.conf');
useLuopan('/demo.json');
</script>
```
**验收标准:**
- 成功加载和渲染 `demo.json.conf`
- 成功加载和渲染 `demo.json`
- 所有 layer 正确显示
- 多文本单元正确拆分显示
- 刻度环和中心图标正常渲染
@@ -844,8 +849,8 @@ const { config, sectors, degreeRing, centerIcon, loading, error } =
**测试策略:**
```typescript
describe('完整流程测试', () => {
it('应正确解析和渲染 demo.json.conf', async () => {
const config = await loadConfig('/demo.json.conf');
it('应正确解析和渲染 demo.json', async () => {
const config = await loadConfig('/demo.json');
const sectors = buildAllSectors(config);
// 验证扇区数量
@@ -927,6 +932,7 @@ describe('完整流程测试', () => {
2. 每个 `TextUnit` 包含独立的 `aStart`, `aEnd`, `content`
3. 渲染时为每个单元生成独立的 `<textPath>` 元素
4. 使用 `getLayoutRatio()` 确保角度分配准确
5. 字体大小与布局按单元独立计算:每个 `TextUnit` 用自身 `aStart/aEnd` + `content.length` 调用 `calculateSectorFontSize`,并基于相同角度范围生成 `generateTextPath` / `generateVerticalTextPath`
**代码示例:**
```typescript
@@ -1091,7 +1097,7 @@ describe('ColorResolver', () => {
```typescript
describe('完整渲染流程', () => {
it('应从 JSON 配置生成完整罗盘', async () => {
const config = await loadConfig('/demo.json.conf');
const config = await loadConfig('/demo.json');
const { sectors, degreeRing, centerIcon } = buildLuopan(config);
// 验证总扇区数