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