增加测量窗口

This commit is contained in:
yuding
2025-12-23 11:31:16 +08:00
parent 7d522afb70
commit 4b5eb78bbb
15 changed files with 3846 additions and 2832 deletions

View File

@@ -3,7 +3,7 @@ import type { ThemeConfig } from '../../themes/types';
import { IBimComponent } from '../../types/component';
import { localeManager, t } from '../../services/locale';
import { themeManager } from '../../services/theme';
import type { MeasureMode, MeasurePanelOptions, MeasureResult } from './types';
import type { MeasureConfig, MeasureMode, MeasurePanelOptions, MeasurePrecision, MeasureResult, MeasureUnit } from './types';
/**
* 测量面板组件(只做 UI不实现真实测量
@@ -26,19 +26,53 @@ export class MeasurePanel implements IBimComponent {
private isExpanded: boolean;
private result: MeasureResult | null = null;
/**
* 测量配置(单位/精度)
* 说明:
* - 你要求:创建 MeasurePanel 不传入单位和精度
* - 默认值维护在组件内部
* - 初始化时优先读取缓存localStorage否则使用默认值
*/
private config: MeasureConfig;
/** 设置面板的临时配置(用于“取消”回滚) */
private draftConfig: MeasureConfig | null = null;
/** 当前视图:主面板 / 设置面板 */
private view: 'main' | 'settings' = 'main';
/** 缓存 key默认全局 */
private static readonly CONFIG_CACHE_KEY = 'bim-engine:measure:config';
/** 默认配置(由组件内部维护) */
private static readonly DEFAULT_CONFIG: MeasureConfig = {
unit: 'mm',
precision: 2
};
// DOM 引用(便于局部更新,减少频繁 querySelector
private toolButtons: Map<MeasureMode, HTMLButtonElement> = new Map();
private toggleBtn!: HTMLButtonElement;
private toggleTextEl!: HTMLElement;
private currentModeValueEl!: HTMLElement;
private mainValueValueEl!: HTMLElement;
private mainValueLabelEl!: HTMLElement;
private mainNumberEl!: HTMLElement;
private mainUnitEl!: HTMLElement;
private xyzBoxEl!: HTMLElement;
private xyzXEl!: HTMLElement;
private xyzYEl!: HTMLElement;
private xyzZEl!: HTMLElement;
private clearBtn!: HTMLButtonElement;
private settingsBtn!: HTMLButtonElement;
// Settings DOM
private mainViewEl!: HTMLElement;
private settingsViewEl!: HTMLElement;
private unitSelectEl!: HTMLSelectElement;
private precisionSelectEl!: HTMLSelectElement;
private saveSettingsBtn!: HTMLButtonElement;
private cancelSettingsBtn!: HTMLButtonElement;
// 订阅清理
private unsubscribeLocale: (() => void) | null = null;
private unsubscribeTheme: (() => void) | null = null;
@@ -52,6 +86,9 @@ export class MeasurePanel implements IBimComponent {
this.activeMode = options.defaultMode ?? 'distance';
this.isExpanded = options.defaultExpanded ?? false;
// 读取配置:优先缓存,否则默认
this.config = this.loadConfigFromCache() ?? { ...MeasurePanel.DEFAULT_CONFIG };
this.element = this.createDom();
}
@@ -76,6 +113,7 @@ export class MeasurePanel implements IBimComponent {
// 初始渲染状态(按钮显隐、选中态、结果区)
this.applyExpandedState();
this.applyActiveModeState();
this.applyViewState();
this.renderResult();
}
@@ -96,6 +134,9 @@ export class MeasurePanel implements IBimComponent {
// “删除全部”颜色:截图中偏绿色,这里用 primary 做一个合理映射
style.setProperty('--bim-measure-danger', theme.primary ?? '#46d369');
// 设置面板“保存设置”按钮用主题色
style.setProperty('--bim-measure-primary', theme.primary ?? '#0078d4');
style.setProperty('--bim-measure-primary-hover', theme.primaryHover ?? '#0063b1');
style.setProperty('--bim-measure-btn-bg', theme.componentBackground ?? 'rgba(255, 255, 255, 0.06)');
style.setProperty('--bim-measure-btn-hover-bg', theme.componentHover ?? 'rgba(255, 255, 255, 0.10)');
style.setProperty('--bim-measure-btn-active-bg', theme.componentActive ?? 'rgba(255, 255, 255, 0.14)');
@@ -125,10 +166,7 @@ export class MeasurePanel implements IBimComponent {
this.settingsBtn.title = t('measure.actions.settings');
this.settingsBtn.setAttribute('aria-label', this.settingsBtn.title);
// 4) 更新“当前方式”显示value
this.currentModeValueEl.textContent = t(this.getModeI18nKey(this.activeMode));
// 5) 主值 label随模式变化
// 4) 主值 label随模式变化
this.mainValueLabelEl.textContent = t(this.getModeValueLabelI18nKey(this.activeMode));
// 6) XYZ label使用 key
@@ -139,6 +177,10 @@ export class MeasurePanel implements IBimComponent {
const key = node.dataset.i18nKey;
if (key) node.textContent = t(key);
});
// 7) 设置面板文本
this.saveSettingsBtn.textContent = t('measure.settings.save');
this.cancelSettingsBtn.textContent = t('measure.settings.cancel');
}
/**
@@ -192,7 +234,6 @@ export class MeasurePanel implements IBimComponent {
// 切换方式后,主值 label 也需要更新
this.mainValueLabelEl.textContent = t(this.getModeValueLabelI18nKey(this.activeMode));
this.currentModeValueEl.textContent = t(this.getModeI18nKey(this.activeMode));
// 通知外部(如果需要)
if (this.options.onModeChange) {
@@ -201,6 +242,12 @@ export class MeasurePanel implements IBimComponent {
// 模式切换后,结果展示也应刷新(例如某些字段显示为 --
this.renderResult();
// 切换模式会影响结果区高度(例如 distance 显示 xyz其它不显示
// 复用 onExpandedChange 来通知外部重新计算 Dialog 高度(不额外扩展回调,保持接口简单)
if (this.options.onExpandedChange) {
this.options.onExpandedChange(this.isExpanded);
}
}
/**
@@ -230,13 +277,44 @@ export class MeasurePanel implements IBimComponent {
* 打开设置(本次只预留方法/回调)
*/
public openSettings(): void {
// 进入设置面板(组件内部逻辑)
this.enterSettingsView();
// 仍然保留回调(如果外部想监听)
if (this.options.onSettings) {
this.options.onSettings();
return;
}
}
/**
* 获取当前测量配置
*/
public getConfig(): MeasureConfig {
return { ...this.config };
}
/**
* 设置测量配置(可选对外调用)
* @param partial 部分更新
* @param persist 是否写入缓存(默认 false
*/
public setConfig(partial: Partial<MeasureConfig>, persist: boolean = false): void {
const next: MeasureConfig = {
unit: partial.unit ?? this.config.unit,
precision: partial.precision ?? this.config.precision
};
this.config = next;
if (persist) {
this.saveConfigToCache(next);
}
// 兜底:避免无声失败,打印中文日志(符合项目规范
console.warn('[MeasurePanel] 未提供设置回调 onSettings当前仅预留接口。');
// 配置变化会影响数值显示(单位/精度
this.renderResult();
// 如果当前在设置面板,表单也需要同步
if (this.view === 'settings') {
this.syncSettingsFormFromConfig(next);
}
}
/**
@@ -270,6 +348,10 @@ export class MeasurePanel implements IBimComponent {
const root = document.createElement('div');
root.className = 'bim-measure-panel';
// 主视图容器(默认显示)
this.mainViewEl = document.createElement('div');
this.mainViewEl.className = 'bim-measure-main';
// 顶部:工具按钮区
const toolsBox = document.createElement('div');
toolsBox.className = 'bim-measure-tools';
@@ -355,25 +437,12 @@ export class MeasurePanel implements IBimComponent {
toggleBox.appendChild(this.toggleBtn);
toolsBox.appendChild(toggleBox);
root.appendChild(toolsBox);
this.mainViewEl.appendChild(toolsBox);
// 中部:结果区
const resultBox = document.createElement('div');
resultBox.className = 'bim-measure-result';
// 当前方式
const currentModeRow = document.createElement('div');
currentModeRow.className = 'bim-measure-row';
const currentModeLabel = document.createElement('span');
currentModeLabel.className = 'label';
currentModeLabel.dataset.i18nKey = 'measure.labels.currentMode';
const currentModeValue = document.createElement('span');
currentModeValue.className = 'value';
this.currentModeValueEl = currentModeValue;
currentModeRow.appendChild(currentModeLabel);
currentModeRow.appendChild(currentModeValue);
resultBox.appendChild(currentModeRow);
// 主结果值(随模式变化)
const mainValueRow = document.createElement('div');
mainValueRow.className = 'bim-measure-row';
@@ -383,6 +452,18 @@ export class MeasurePanel implements IBimComponent {
const mainValueValue = document.createElement('span');
mainValueValue.className = 'value';
this.mainValueValueEl = mainValueValue;
// 主值拆分:数值(黄色)+ 单位(普通色)
// 这样可以满足:
// 1) 只让“数据”变黄,单位不变色
// 2) 没有数据时展示 `-- 单位`
this.mainNumberEl = document.createElement('span');
this.mainNumberEl.className = 'bim-measure-main-number';
this.mainUnitEl = document.createElement('span');
this.mainUnitEl.className = 'bim-measure-main-unit';
this.mainValueValueEl.appendChild(this.mainNumberEl);
this.mainValueValueEl.appendChild(document.createTextNode(' '));
this.mainValueValueEl.appendChild(this.mainUnitEl);
mainValueRow.appendChild(mainValueLabel);
mainValueRow.appendChild(mainValueValue);
resultBox.appendChild(mainValueRow);
@@ -390,27 +471,28 @@ export class MeasurePanel implements IBimComponent {
// XYZ
const xyzBox = document.createElement('div');
xyzBox.className = 'bim-measure-xyz';
this.xyzBoxEl = xyzBox;
const makeXyzRow = (labelKey: string, valueElSetter: (el: HTMLElement) => void) => {
const makeXyzRow = (labelKey: string, valueClassName: string, valueElSetter: (el: HTMLElement) => void) => {
const row = document.createElement('div');
row.className = 'bim-measure-row';
const label = document.createElement('span');
label.className = 'label';
label.dataset.i18nKey = labelKey;
const value = document.createElement('span');
value.className = 'value';
value.className = `value ${valueClassName}`;
valueElSetter(value);
row.appendChild(label);
row.appendChild(value);
return row;
};
xyzBox.appendChild(makeXyzRow('measure.labels.x', (el) => (this.xyzXEl = el)));
xyzBox.appendChild(makeXyzRow('measure.labels.y', (el) => (this.xyzYEl = el)));
xyzBox.appendChild(makeXyzRow('measure.labels.z', (el) => (this.xyzZEl = el)));
xyzBox.appendChild(makeXyzRow('measure.labels.x', 'bim-measure-xyz-x', (el) => (this.xyzXEl = el)));
xyzBox.appendChild(makeXyzRow('measure.labels.y', 'bim-measure-xyz-y', (el) => (this.xyzYEl = el)));
xyzBox.appendChild(makeXyzRow('measure.labels.z', 'bim-measure-xyz-z', (el) => (this.xyzZEl = el)));
resultBox.appendChild(xyzBox);
root.appendChild(resultBox);
this.mainViewEl.appendChild(resultBox);
// 底部:删除全部 + 设置
const footer = document.createElement('div');
@@ -437,11 +519,230 @@ export class MeasurePanel implements IBimComponent {
footer.appendChild(this.clearBtn);
footer.appendChild(this.settingsBtn);
root.appendChild(footer);
this.mainViewEl.appendChild(footer);
// 设置视图容器(默认隐藏)
this.settingsViewEl = this.createSettingsDom();
root.appendChild(this.mainViewEl);
root.appendChild(this.settingsViewEl);
return root;
}
/**
* 创建“设置面板”DOM
*/
private createSettingsDom(): HTMLElement {
const box = document.createElement('div');
box.className = 'bim-measure-settings';
// 标题
const title = document.createElement('div');
title.className = 'bim-measure-settings-title';
title.dataset.i18nKey = 'measure.settings.title';
box.appendChild(title);
// 单位
const unitRow = document.createElement('div');
unitRow.className = 'bim-measure-settings-row';
const unitLabel = document.createElement('div');
unitLabel.className = 'label';
unitLabel.dataset.i18nKey = 'measure.settings.unit';
this.unitSelectEl = document.createElement('select');
this.unitSelectEl.className = 'bim-measure-settings-select';
this.unitSelectEl.appendChild(this.makeOption('m'));
this.unitSelectEl.appendChild(this.makeOption('cm'));
this.unitSelectEl.appendChild(this.makeOption('mm'));
this.unitSelectEl.appendChild(this.makeOption('km'));
unitRow.appendChild(unitLabel);
unitRow.appendChild(this.unitSelectEl);
box.appendChild(unitRow);
// 提示文本:你要求放在“单位”下面
const hint = document.createElement('div');
hint.className = 'bim-measure-settings-hint';
hint.dataset.i18nKey = 'measure.settings.hint';
box.appendChild(hint);
// 精度
const precisionRow = document.createElement('div');
precisionRow.className = 'bim-measure-settings-row';
const precisionLabel = document.createElement('div');
precisionLabel.className = 'label';
precisionLabel.dataset.i18nKey = 'measure.settings.precision';
this.precisionSelectEl = document.createElement('select');
this.precisionSelectEl.className = 'bim-measure-settings-select';
this.precisionSelectEl.appendChild(this.makePrecisionOption(0));
this.precisionSelectEl.appendChild(this.makePrecisionOption(1));
this.precisionSelectEl.appendChild(this.makePrecisionOption(2));
this.precisionSelectEl.appendChild(this.makePrecisionOption(3));
precisionRow.appendChild(precisionLabel);
precisionRow.appendChild(this.precisionSelectEl);
box.appendChild(precisionRow);
// 底部按钮
const actions = document.createElement('div');
actions.className = 'bim-measure-settings-actions';
this.saveSettingsBtn = document.createElement('button');
this.saveSettingsBtn.type = 'button';
this.saveSettingsBtn.className = 'bim-measure-settings-save';
this.saveSettingsBtn.addEventListener('click', () => {
this.saveSettings();
});
this.cancelSettingsBtn = document.createElement('button');
this.cancelSettingsBtn.type = 'button';
this.cancelSettingsBtn.className = 'bim-measure-settings-cancel';
this.cancelSettingsBtn.addEventListener('click', () => {
this.cancelSettings();
});
actions.appendChild(this.saveSettingsBtn);
actions.appendChild(this.cancelSettingsBtn);
box.appendChild(actions);
// 初次同步表单值
this.syncSettingsFormFromConfig(this.config);
return box;
}
private makeOption(unit: MeasureUnit): HTMLOptionElement {
const opt = document.createElement('option');
opt.value = unit;
// 选项显示内容:直接显示单位字符串
opt.textContent = unit;
return opt;
}
private makePrecisionOption(precision: MeasurePrecision): HTMLOptionElement {
const opt = document.createElement('option');
opt.value = String(precision);
// 显示0 / 0.0 / 0.00 / 0.000
opt.textContent = precision === 0 ? '0' : `0.${'0'.repeat(precision)}`;
return opt;
}
/**
* 进入设置视图:保存一份当前配置作为草稿基线
*/
private enterSettingsView(): void {
this.draftConfig = { ...this.config };
this.view = 'settings';
this.syncSettingsFormFromConfig(this.config);
this.applyViewState();
}
/**
* 保存设置:写入 config + 写缓存 + 返回主视图
*/
private saveSettings(): void {
const unit = (this.unitSelectEl.value as MeasureUnit) || this.config.unit;
const precision = (Number(this.precisionSelectEl.value) as MeasurePrecision);
const next: MeasureConfig = {
unit,
precision: this.isValidPrecision(precision) ? precision : this.config.precision
};
this.config = next;
this.saveConfigToCache(next);
this.draftConfig = null;
this.view = 'main';
this.applyViewState();
// 配置变化会影响显示
this.renderResult();
// 高度变化(设置面板 -> 主面板)也需要通知外部
if (this.options.onExpandedChange) {
this.options.onExpandedChange(this.isExpanded);
}
}
/**
* 取消设置:回滚到进入设置前的配置,并返回主视图
*/
private cancelSettings(): void {
if (this.draftConfig) {
this.config = { ...this.draftConfig };
}
this.draftConfig = null;
this.view = 'main';
this.applyViewState();
this.renderResult();
// 高度变化(设置面板 -> 主面板)也需要通知外部
if (this.options.onExpandedChange) {
this.options.onExpandedChange(this.isExpanded);
}
}
private syncSettingsFormFromConfig(config: MeasureConfig): void {
this.unitSelectEl.value = config.unit;
this.precisionSelectEl.value = String(config.precision);
}
private applyViewState(): void {
if (this.view === 'settings') {
this.mainViewEl.style.display = 'none';
// 注意CSS 里 `.bim-measure-settings { display: none; }` 是默认隐藏
// 因此这里必须显式设置为可见(否则会出现“进入设置页后什么都不显示”的问题)
this.settingsViewEl.style.display = 'block';
} else {
// 显式恢复主视图显示(避免外部样式干扰)
this.mainViewEl.style.display = 'block';
this.settingsViewEl.style.display = 'none';
}
}
/**
* 从缓存读取配置
* - 有缓存:返回解析后的配置
* - 无缓存/解析失败:返回 null
*/
private loadConfigFromCache(): MeasureConfig | null {
try {
const raw = localStorage.getItem(MeasurePanel.CONFIG_CACHE_KEY);
if (!raw) return null;
const parsed = JSON.parse(raw) as Partial<MeasureConfig>;
if (!parsed || typeof parsed !== 'object') return null;
const unit = parsed.unit;
const precision = parsed.precision;
if (!this.isValidUnit(unit) || !this.isValidPrecision(precision as number)) return null;
return {
unit,
precision: precision as MeasurePrecision
};
} catch (_e) {
// localStorage 可能被禁用或 JSON 格式不正确,直接忽略
return null;
}
}
/**
* 写入缓存localStorage
*/
private saveConfigToCache(config: MeasureConfig): void {
try {
localStorage.setItem(MeasurePanel.CONFIG_CACHE_KEY, JSON.stringify(config));
} catch (_e) {
// localStorage 可能被禁用:忽略即可,不影响功能
}
}
private isValidUnit(unit: any): unit is MeasureUnit {
return unit === 'm' || unit === 'cm' || unit === 'mm' || unit === 'km';
}
private isValidPrecision(precision: any): precision is MeasurePrecision {
return precision === 0 || precision === 1 || precision === 2 || precision === 3;
}
/**
* 应用“展开/收起”状态:默认只显示前 4 个按钮
*/
@@ -482,23 +783,53 @@ export class MeasurePanel implements IBimComponent {
* 渲染结果区(根据 activeMode 从 result 里取对应字段)
*/
private renderResult(): void {
// 1) 主值
const mainText = this.formatMainValue(this.activeMode, this.result);
this.mainValueValueEl.textContent = mainText;
// 1) 根据模式决定结果区显示规则
// 你给的规则:
// - 距离:显示数值 + xyz
// - 最小距离:只显示数值
// - 角度:--°
// - 标高:--m固定 m
// - 体积:--mm³单位随设置变动即 unit³
// - 激光测距:不显示任何数值/xyz只显示“激光测距”文字
// - 坡度:--%
// - 空间体积:--mm³单位随设置变动即 unit³
// 2) XYZ
const xyz = this.result?.xyz;
if (!xyz) {
this.xyzXEl.textContent = '--';
this.xyzYEl.textContent = '--';
this.xyzZEl.textContent = '--';
// 1.1) 主行:默认显示 label + value数值/单位拆分)
// 激光测距:只显示文字,因此隐藏 label/单位
if (this.activeMode === 'laserDistance') {
this.mainValueLabelEl.style.display = 'none';
this.mainNumberEl.textContent = t(this.getModeI18nKey('laserDistance'));
this.mainUnitEl.textContent = '';
// 激光测距:你要求不使用黄色主数据
this.mainNumberEl.classList.add('is-laser-text');
} else {
this.mainValueLabelEl.style.display = '';
this.mainValueLabelEl.textContent = t(this.getModeValueLabelI18nKey(this.activeMode));
const parts = this.formatMainValueParts(this.activeMode, this.result);
this.mainNumberEl.textContent = parts.numberText;
this.mainUnitEl.textContent = parts.unitText;
// 其它模式:恢复黄色主数据
this.mainNumberEl.classList.remove('is-laser-text');
}
// 1.2) XYZ只有“距离”需要展示
if (this.activeMode === 'distance') {
this.xyzBoxEl.style.display = '';
const xyz = this.result?.xyz;
if (!xyz) {
this.xyzXEl.textContent = '--';
this.xyzYEl.textContent = '--';
this.xyzZEl.textContent = '--';
return;
}
this.xyzXEl.textContent = this.formatNumberWithPrecision(xyz.x, this.config.precision);
this.xyzYEl.textContent = this.formatNumberWithPrecision(xyz.y, this.config.precision);
this.xyzZEl.textContent = this.formatNumberWithPrecision(xyz.z, this.config.precision);
return;
}
// 为了可读性:这里不做 fancy formatter只做基础展示
this.xyzXEl.textContent = this.formatNumber(xyz.x);
this.xyzYEl.textContent = this.formatNumber(xyz.y);
this.xyzZEl.textContent = this.formatNumber(xyz.z);
// 非 distance隐藏 xyz
this.xyzBoxEl.style.display = 'none';
}
/**
@@ -515,53 +846,144 @@ export class MeasurePanel implements IBimComponent {
return `measure.labels.value.${mode}`;
}
/**
* 将“当前模式”的主值格式化为文本
* @param mode 当前模式
* @param result 当前结果
*/
private formatMainValue(mode: MeasureMode, result: MeasureResult | null): string {
if (!result) return '--';
// 注意:旧的 formatMainValue/formatWithFixedUnit 已被 formatMainValueParts 替代,
// 以支持“数值与单位分色显示”和“无数据时仍展示单位”。
// 根据不同 mode 读取对应字段并格式化单位
// 单位文本也走国际化(可替换为英文/中文
switch (mode) {
case 'distance':
return this.formatWithUnit(result.distanceMm, 'measure.units.mm');
case 'minDistance':
return this.formatWithUnit(result.minDistanceMm, 'measure.units.mm');
case 'angle':
return this.formatWithUnit(result.angleDeg, 'measure.units.deg');
case 'elevation':
return this.formatWithUnit(result.elevationMm, 'measure.units.mm');
case 'volume':
return this.formatWithUnit(result.volumeM3, 'measure.units.m3');
case 'laserDistance':
return this.formatWithUnit(result.laserDistanceMm, 'measure.units.mm');
case 'slope':
return this.formatWithUnit(result.slopePercent, 'measure.units.percent');
case 'spaceVolume':
return this.formatWithUnit(result.spaceVolumeM3, 'measure.units.m3');
/**
* 基础数字格式化(按精度显示
*/
private formatNumberWithPrecision(value: number, precision: MeasurePrecision): string {
// 你要求精度可选0 / 0.0 / 0.00 / 0.000,因此这里不做 trim严格按 toFixed 输出
return value.toFixed(precision);
}
// 注意:旧的 formatLengthWithConfig 已被 formatLengthParts 替代。
private convertMmToUnit(mm: number, unit: MeasureUnit): number {
switch (unit) {
case 'mm':
return mm;
case 'cm':
return mm / 10;
case 'm':
return mm / 1000;
case 'km':
return mm / 1_000_000;
default:
return '--';
return mm;
}
}
private getUnitI18nKey(unit: MeasureUnit): string {
return `measure.units.${unit}`;
}
// 注意:旧的 formatElevationFixedMeters / formatVolumeWithConfig 已被 formatMainValueParts 替代。
private convertMm3ToUnit3(mm3: number, unit: MeasureUnit): number {
// 先把 mm³ -> 对应 unit³
// mm -> cm: /10因此 mm³ -> cm³: /1000
// mm -> m : /1000因此 mm³ -> m³ : /1e9
// mm -> km: /1e6因此 mm³ -> km³: /1e18
switch (unit) {
case 'mm':
return mm3;
case 'cm':
return mm3 / 1000;
case 'm':
return mm3 / 1_000_000_000;
case 'km':
return mm3 / 1_000_000_000_000_000_000;
default:
return mm3;
}
}
/**
* 格式化数值 + 单位(单位走国际化)
* 主数据拆分:返回 { 数值文本, 单位文本 }
* 规则:
* - 没数据时:必须展示 `-- 单位`(而不是只展示 `--`
* - 单位随模式变化:
* - 距离/最小距离:单位随设置变动
* - 角度:°
* - 标高:固定 m
* - 体积/空间体积:单位³(随设置变动)
* - 坡度:%
*/
private formatWithUnit(value: number | undefined, unitKey: string): string {
if (value === null || value === undefined || Number.isNaN(value)) return '--';
return `${this.formatNumber(value)} ${t(unitKey)}`;
private formatMainValueParts(mode: MeasureMode, result: MeasureResult | null): { numberText: string; unitText: string } {
if (mode === 'laserDistance') return { numberText: t(this.getModeI18nKey('laserDistance')), unitText: '' };
// 没有数据:显示 `-- 单位`
if (!result) {
return this.getEmptyValuePartsByMode(mode);
}
switch (mode) {
case 'distance':
return this.formatLengthParts(result.distanceMm);
case 'minDistance':
return this.formatLengthParts(result.minDistanceMm);
case 'angle':
return this.formatFixedUnitParts(result.angleDeg, t('measure.units.deg'));
case 'elevation':
// 标高固定 m外部注入值约定为 mm
return this.formatFixedUnitParts(
result.elevationMm === undefined ? undefined : result.elevationMm / 1000,
t('measure.units.m')
);
case 'volume':
return this.formatVolumeParts(result.volumeM3);
case 'slope':
return this.formatFixedUnitParts(result.slopePercent, t('measure.units.percent'));
case 'spaceVolume':
return this.formatVolumeParts(result.spaceVolumeM3);
default:
return { numberText: '--', unitText: '' };
}
}
/**
* 基础数字格式化(可读性优先)
*/
private formatNumber(value: number): string {
// 保留 3 位小数以内(简单策略:整数不带小数,非整数保留到 3 位)
if (Number.isInteger(value)) return String(value);
return value.toFixed(3).replace(/0+$/g, '').replace(/\.$/g, '');
private getEmptyValuePartsByMode(mode: MeasureMode): { numberText: string; unitText: string } {
switch (mode) {
case 'distance':
case 'minDistance':
return { numberText: '--', unitText: t(this.getUnitI18nKey(this.config.unit)) };
case 'angle':
return { numberText: '--', unitText: t('measure.units.deg') };
case 'elevation':
return { numberText: '--', unitText: t('measure.units.m') };
case 'volume':
case 'spaceVolume':
return { numberText: '--', unitText: `${this.config.unit}³` };
case 'slope':
return { numberText: '--', unitText: t('measure.units.percent') };
default:
return { numberText: '--', unitText: '' };
}
}
private formatFixedUnitParts(value: number | undefined, unitText: string): { numberText: string; unitText: string } {
if (value === null || value === undefined || Number.isNaN(value)) {
return { numberText: '--', unitText };
}
return { numberText: this.formatNumberWithPrecision(value, this.config.precision), unitText };
}
private formatLengthParts(valueMm: number | undefined): { numberText: string; unitText: string } {
const unitText = t(this.getUnitI18nKey(this.config.unit));
if (valueMm === null || valueMm === undefined || Number.isNaN(valueMm)) {
return { numberText: '--', unitText };
}
const converted = this.convertMmToUnit(valueMm, this.config.unit);
return { numberText: this.formatNumberWithPrecision(converted, this.config.precision), unitText };
}
private formatVolumeParts(valueMm3: number | undefined): { numberText: string; unitText: string } {
const unitText = `${this.config.unit}³`;
if (valueMm3 === null || valueMm3 === undefined || Number.isNaN(valueMm3)) {
return { numberText: '--', unitText };
}
const converted = this.convertMm3ToUnit3(valueMm3, this.config.unit);
return { numberText: this.formatNumberWithPrecision(converted, this.config.precision), unitText };
}
}