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

@@ -2,87 +2,142 @@
* 罗盘业务逻辑组合函数
*/
import { computed, type Ref, type ComputedRef } from 'vue';
import type { Example, Sector, TextRadialPosition } from '../types';
import {
polarToXY,
generateSectorData,
} from '../utils';
import { computed, ref, readonly, watch, type Ref } from 'vue';
import type {
CenterIconData,
DegreeRingData,
LayerConfig,
LuopanConfig,
Sector,
SectorLayerConfig,
TextRadialPosition,
} from '../types';
import { polarToXY } from '../utils';
import { parseConfig } from '../configParser';
import { ColorResolver } from '../colorResolver';
import { SectorBuilder } from '../sectorBuilder';
import { buildDegreeRing } from '../degreeRing';
import { loadCenterIcon } from '../centerIcon';
const isSectorLayer = (layer: LayerConfig): layer is SectorLayerConfig =>
layer.type !== 'centerIcon' && layer.type !== 'degreeRing';
const findDegreeRingLayer = (layers: LayerConfig[]) =>
layers.find((layer) => layer.type === 'degreeRing');
const findCenterIconLayer = (layers: LayerConfig[]) =>
layers.find((layer) => layer.type === 'centerIcon');
/**
* 罗盘逻辑 Hook
* @param exampleRef 当前示例的响应式引用
* @param textRadialPositionRef 文字径向位置的响应式引用
* @param configPathOrObject 配置文件路径或配置对象
* @param textRadialPositionRef 文字径向位置的响应式引用(可选)
* @returns 罗盘相关的计算属性和方法
*/
export function useLuopan(
exampleRef: Ref<Example>,
textRadialPositionRef: Ref<TextRadialPosition>
configPathOrObject: string | LuopanConfig,
textRadialPositionRef?: Ref<TextRadialPosition>
) {
/**
* 角度分割点列表
*/
const anglesDeg = computed(() => exampleRef.value.angles);
const config = ref<LuopanConfig | null>(null);
const sectors = ref<Sector[]>([]);
const degreeRing = ref<DegreeRingData | null>(null);
const centerIcon = ref<CenterIconData | null>(null);
const loading = ref(false);
const error = ref<Error | null>(null);
/**
* 圆环半径列表
*/
const rings = computed(() => exampleRef.value.radii);
const textRadialPosition = computed(
() => textRadialPositionRef?.value ?? 'middle'
);
/**
* 最外层半径
*/
const outerMost = computed(() => {
const radii = exampleRef.value.radii;
return radii[radii.length - 1];
});
const buildSectors = (configObj: LuopanConfig) => {
const resolver = new ColorResolver(configObj.theme, configObj.background);
const builder = new SectorBuilder(resolver, {
textRadialPosition: textRadialPosition.value,
});
const sectorLayers = configObj.layers.filter(isSectorLayer);
return sectorLayers.flatMap((layer, index) => builder.buildLayer(layer, index));
};
/**
* 生成所有扇区数据
*/
const sectors = computed<Sector[]>(() => {
const res: Sector[] = [];
const A = exampleRef.value.angles;
const R = exampleRef.value.radii;
const loadConfig = async () => {
try {
loading.value = true;
error.value = null;
const layerCount = R.length;
const pieCount = A.length - 1;
for (let j = 0; j < layerCount; j++) {
const rInner = j === 0 ? 0 : R[j - 1];
const rOuter = R[j];
for (let i = 0; i < pieCount; i++) {
const aStart = A[i];
const aEnd = A[i + 1];
const sector = generateSectorData({
layerIndex: j,
pieIndex: i,
rInner,
rOuter,
aStart,
aEnd,
textRadialPosition: textRadialPositionRef.value,
});
res.push(sector);
let configObj: LuopanConfig;
if (typeof configPathOrObject === 'string') {
const jsonText = await fetch(configPathOrObject).then((res) => res.text());
configObj = parseConfig(jsonText);
} else {
configObj = configPathOrObject;
}
config.value = configObj;
sectors.value = buildSectors(configObj);
const degreeRingLayer = findDegreeRingLayer(configObj.layers);
degreeRing.value = degreeRingLayer
? buildDegreeRing(degreeRingLayer.degreeRing)
: null;
const centerIconLayer = findCenterIconLayer(configObj.layers);
centerIcon.value = centerIconLayer
? await loadCenterIcon(centerIconLayer.centerIcon)
: null;
} catch (err) {
error.value = err as Error;
} finally {
loading.value = false;
}
};
// 文字位置切换后仅重建扇区
watch(textRadialPosition, () => {
if (config.value) {
sectors.value = buildSectors(config.value);
}
return res;
});
/**
* 极坐标转 XY暴露给模板使用
*/
const toXY = polarToXY;
loadConfig();
const sectorLayers = computed(() =>
config.value ? config.value.layers.filter(isSectorLayer) : []
);
const rings = computed(() => sectorLayers.value.map((layer) => layer.rOuter));
const anglesDeg = computed(() => {
const firstLayer = sectorLayers.value[0];
if (!firstLayer || firstLayer.divisions <= 0) return [];
const step = 360 / firstLayer.divisions;
const start = firstLayer.startAngle ?? 0;
return Array.from({ length: firstLayer.divisions + 1 }, (_, i) => start + i * step);
});
const outerMost = computed(() => {
if (!config.value) return 0;
if (typeof config.value.outerRadius === 'number') return config.value.outerRadius;
const radii = sectorLayers.value.map((layer) => layer.rOuter);
const degreeRingLayer = findDegreeRingLayer(config.value.layers);
if (degreeRingLayer) {
radii.push(degreeRingLayer.degreeRing.rOuter);
}
return radii.length > 0 ? Math.max(...radii) : 0;
});
return {
config: readonly(config),
sectors: readonly(sectors),
degreeRing: readonly(degreeRing),
centerIcon: readonly(centerIcon),
anglesDeg,
rings,
outerMost,
sectors,
toXY,
toXY: polarToXY,
loading: readonly(loading),
error: readonly(error),
reload: loadConfig,
};
}