增加测量窗口
This commit is contained in:
@@ -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 };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user