增加测量窗口
This commit is contained in:
568
src/components/measure-panel/index.ts
Normal file
568
src/components/measure-panel/index.ts
Normal file
@@ -0,0 +1,568 @@
|
||||
import './index.css';
|
||||
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';
|
||||
|
||||
/**
|
||||
* 测量面板组件(只做 UI,不实现真实测量)
|
||||
*
|
||||
* 组件职责:
|
||||
* - 展示 8 种测量方式按钮(默认 4 个,可展开/收起)
|
||||
* - 维护当前选中的测量方式(current mode)
|
||||
* - 展示测量结果(由外部 setResult 注入)
|
||||
* - 提供 “删除全部 / 设置” 的 UI 与对外方法(暂不实现真实逻辑,仅回调/占位)
|
||||
*
|
||||
* 注意:
|
||||
* - 所有用户可见文本必须通过 t(key) 获取(国际化强制要求)
|
||||
* - 组件需要订阅主题/语言变更,并在 destroy 时清理订阅
|
||||
*/
|
||||
export class MeasurePanel implements IBimComponent {
|
||||
public element: HTMLElement;
|
||||
|
||||
private options: MeasurePanelOptions;
|
||||
private activeMode: MeasureMode;
|
||||
private isExpanded: boolean;
|
||||
private result: MeasureResult | null = null;
|
||||
|
||||
// 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 xyzXEl!: HTMLElement;
|
||||
private xyzYEl!: HTMLElement;
|
||||
private xyzZEl!: HTMLElement;
|
||||
private clearBtn!: HTMLButtonElement;
|
||||
private settingsBtn!: HTMLButtonElement;
|
||||
|
||||
// 订阅清理
|
||||
private unsubscribeLocale: (() => void) | null = null;
|
||||
private unsubscribeTheme: (() => void) | null = null;
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
* @param options 组件配置
|
||||
*/
|
||||
constructor(options: MeasurePanelOptions = {}) {
|
||||
this.options = options;
|
||||
this.activeMode = options.defaultMode ?? 'distance';
|
||||
this.isExpanded = options.defaultExpanded ?? false;
|
||||
|
||||
this.element = this.createDom();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化组件(实现 IBimComponent)
|
||||
*/
|
||||
public init(): void {
|
||||
// 订阅语言变更:更新所有文本/提示
|
||||
this.unsubscribeLocale = localeManager.subscribe(() => {
|
||||
this.setLocales();
|
||||
});
|
||||
|
||||
// 订阅主题变更:更新 CSS 变量(如需要)
|
||||
this.unsubscribeTheme = themeManager.subscribe((theme) => {
|
||||
this.setTheme(theme);
|
||||
});
|
||||
|
||||
// 初始应用
|
||||
this.setLocales();
|
||||
this.setTheme(themeManager.getTheme());
|
||||
|
||||
// 初始渲染状态(按钮显隐、选中态、结果区)
|
||||
this.applyExpandedState();
|
||||
this.applyActiveModeState();
|
||||
this.renderResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置主题(实现 IBimComponent)
|
||||
* @param theme 主题配置
|
||||
*/
|
||||
public setTheme(theme: ThemeConfig): void {
|
||||
// 为了可读性:这里显式写出映射,不做过度抽象
|
||||
const style = this.element.style;
|
||||
|
||||
// 这些变量不会强制覆盖外部(Dialog)已有变量,只做兜底
|
||||
style.setProperty('--bim-measure-border', theme.border ?? 'rgba(255, 255, 255, 0.12)');
|
||||
style.setProperty('--bim-measure-divider', theme.border ?? 'rgba(255, 255, 255, 0.10)');
|
||||
style.setProperty('--bim-measure-icon-color', theme.icon ?? '#ddd');
|
||||
style.setProperty('--bim-measure-label-color', theme.textSecondary ?? 'rgba(255, 255, 255, 0.70)');
|
||||
style.setProperty('--bim-measure-value-color', theme.textPrimary ?? 'rgba(255, 255, 255, 0.90)');
|
||||
|
||||
// “删除全部”颜色:截图中偏绿色,这里用 primary 做一个合理映射
|
||||
style.setProperty('--bim-measure-danger', theme.primary ?? '#46d369');
|
||||
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)');
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置语言(实现 IBimComponent)
|
||||
*/
|
||||
public setLocales(): void {
|
||||
// 1) 更新按钮 tooltip(图标占位时,tooltip 是主要的可读文本)
|
||||
for (const [mode, btn] of this.toolButtons.entries()) {
|
||||
btn.title = t(this.getModeI18nKey(mode));
|
||||
btn.setAttribute('aria-label', btn.title);
|
||||
}
|
||||
|
||||
// 2) 更新展开/收起按钮 tooltip
|
||||
this.toggleBtn.title = this.isExpanded ? t('measure.actions.collapse') : t('measure.actions.expand');
|
||||
this.toggleBtn.setAttribute('aria-label', this.toggleBtn.title);
|
||||
|
||||
// 2.1) 更新展开/收起按钮可见文本(你要求的“文字提示”)
|
||||
if (this.toggleTextEl) {
|
||||
this.toggleTextEl.textContent = this.toggleBtn.title;
|
||||
}
|
||||
|
||||
// 3) 更新底部按钮文本/tooltip
|
||||
this.clearBtn.textContent = t('measure.actions.clearAll');
|
||||
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(随模式变化)
|
||||
this.mainValueLabelEl.textContent = t(this.getModeValueLabelI18nKey(this.activeMode));
|
||||
|
||||
// 6) XYZ label(使用 key)
|
||||
// 这里 label 在 createDom 已经是固定文本节点,直接用 setText 更新更直观
|
||||
// 但为了减少 DOM 结构复杂度,我们把 label 写在 createDom 里,通过 data-key 更新
|
||||
const labelNodes = this.element.querySelectorAll<HTMLElement>('[data-i18n-key]');
|
||||
labelNodes.forEach((node) => {
|
||||
const key = node.dataset.i18nKey;
|
||||
if (key) node.textContent = t(key);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁组件(实现 IBimComponent)
|
||||
*/
|
||||
public destroy(): void {
|
||||
// 清理订阅
|
||||
if (this.unsubscribeLocale) {
|
||||
this.unsubscribeLocale();
|
||||
this.unsubscribeLocale = null;
|
||||
}
|
||||
if (this.unsubscribeTheme) {
|
||||
this.unsubscribeTheme();
|
||||
this.unsubscribeTheme = null;
|
||||
}
|
||||
|
||||
// 清理事件监听:由于本组件的监听都绑定在创建时的具体按钮上,
|
||||
// 且按钮会随 element 一起被 GC,这里不做逐个 removeEventListener(可读性优先)
|
||||
|
||||
// 移除 DOM
|
||||
this.element.remove();
|
||||
}
|
||||
|
||||
// ==========================
|
||||
// 对外 API(给 Manager / 外部业务调用)
|
||||
// ==========================
|
||||
|
||||
/**
|
||||
* 获取当前测量方式
|
||||
*/
|
||||
public getActiveMode(): MeasureMode {
|
||||
return this.activeMode;
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换测量方式(你要求的“切换类型的方法”)
|
||||
* @param mode 目标测量方式
|
||||
*/
|
||||
public switchMode(mode: MeasureMode): void {
|
||||
this.setActiveMode(mode);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置当前测量方式
|
||||
* @param mode 目标测量方式
|
||||
*/
|
||||
public setActiveMode(mode: MeasureMode): void {
|
||||
if (this.activeMode === mode) return;
|
||||
this.activeMode = mode;
|
||||
this.applyActiveModeState();
|
||||
|
||||
// 切换方式后,主值 label 也需要更新
|
||||
this.mainValueLabelEl.textContent = t(this.getModeValueLabelI18nKey(this.activeMode));
|
||||
this.currentModeValueEl.textContent = t(this.getModeI18nKey(this.activeMode));
|
||||
|
||||
// 通知外部(如果需要)
|
||||
if (this.options.onModeChange) {
|
||||
this.options.onModeChange(mode);
|
||||
}
|
||||
|
||||
// 模式切换后,结果展示也应刷新(例如某些字段显示为 --)
|
||||
this.renderResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置测量结果(由外部注入)
|
||||
* @param result 测量结果;传 null 表示清空
|
||||
*/
|
||||
public setResult(result: MeasureResult | null): void {
|
||||
this.result = result;
|
||||
this.renderResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除全部(只做 UI 状态清空 + 回调)
|
||||
*/
|
||||
public clearAll(): void {
|
||||
// 先清空结果显示
|
||||
this.result = null;
|
||||
this.renderResult();
|
||||
|
||||
// 通知外部
|
||||
if (this.options.onClearAll) {
|
||||
this.options.onClearAll();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开设置(本次只预留方法/回调)
|
||||
*/
|
||||
public openSettings(): void {
|
||||
if (this.options.onSettings) {
|
||||
this.options.onSettings();
|
||||
return;
|
||||
}
|
||||
|
||||
// 兜底:避免无声失败,打印中文日志(符合项目规范)
|
||||
console.warn('[MeasurePanel] 未提供设置回调 onSettings,当前仅预留接口。');
|
||||
}
|
||||
|
||||
/**
|
||||
* 展开 / 收起(可选对外调用)
|
||||
* @param expanded 是否展开
|
||||
*/
|
||||
public setExpanded(expanded: boolean): void {
|
||||
if (this.isExpanded === expanded) return;
|
||||
this.isExpanded = expanded;
|
||||
this.applyExpandedState();
|
||||
this.setLocales(); // 更新 tooltip(展开/收起)
|
||||
|
||||
// 通知外部:用于重新计算 Dialog 高度
|
||||
if (this.options.onExpandedChange) {
|
||||
this.options.onExpandedChange(this.isExpanded);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取是否展开
|
||||
*/
|
||||
public getExpanded(): boolean {
|
||||
return this.isExpanded;
|
||||
}
|
||||
|
||||
// ==========================
|
||||
// 内部实现
|
||||
// ==========================
|
||||
|
||||
private createDom(): HTMLElement {
|
||||
const root = document.createElement('div');
|
||||
root.className = 'bim-measure-panel';
|
||||
|
||||
// 顶部:工具按钮区
|
||||
const toolsBox = document.createElement('div');
|
||||
toolsBox.className = 'bim-measure-tools';
|
||||
|
||||
const grid = document.createElement('div');
|
||||
grid.className = 'bim-measure-tool-grid';
|
||||
|
||||
// 8 种测量方式(顺序严格按你给的)
|
||||
const modes: MeasureMode[] = [
|
||||
'distance',
|
||||
'minDistance',
|
||||
'angle',
|
||||
'elevation',
|
||||
'volume',
|
||||
'laserDistance',
|
||||
'slope',
|
||||
'spaceVolume'
|
||||
];
|
||||
|
||||
// 图标占位:统一用圆形(你要求的“圆形占位”)
|
||||
const circleIconSvg = `
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<circle cx="12" cy="12" r="9"></circle>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
// 逐个创建按钮
|
||||
for (let i = 0; i < modes.length; i++) {
|
||||
const mode = modes[i];
|
||||
const btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.className = 'bim-measure-tool-btn';
|
||||
btn.dataset.mode = mode;
|
||||
|
||||
// icon
|
||||
const icon = document.createElement('span');
|
||||
icon.className = 'bim-measure-tool-icon';
|
||||
icon.innerHTML = circleIconSvg;
|
||||
btn.appendChild(icon);
|
||||
|
||||
// 点击切换模式
|
||||
btn.addEventListener('click', () => {
|
||||
this.setActiveMode(mode);
|
||||
});
|
||||
|
||||
// 先不在这里设置 title/text(统一交给 setLocales)
|
||||
this.toolButtons.set(mode, btn);
|
||||
grid.appendChild(btn);
|
||||
}
|
||||
|
||||
toolsBox.appendChild(grid);
|
||||
|
||||
// 展开/收起按钮(箭头)
|
||||
const toggleBox = document.createElement('div');
|
||||
toggleBox.className = 'bim-measure-toggle';
|
||||
|
||||
this.toggleBtn = document.createElement('button');
|
||||
this.toggleBtn.type = 'button';
|
||||
this.toggleBtn.className = 'bim-measure-toggle-btn';
|
||||
// 展开/收起按钮:更小,并带文字提示(展开/收起)
|
||||
// 注意:文本内容由 setLocales() 统一更新,这里先放一个占位容器
|
||||
this.toggleTextEl = document.createElement('span');
|
||||
this.toggleTextEl.className = 'bim-measure-toggle-text';
|
||||
const toggleIconEl = document.createElement('span');
|
||||
toggleIconEl.className = 'bim-measure-toggle-icon';
|
||||
toggleIconEl.innerHTML = `
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M7 10l5 5 5-5z"></path>
|
||||
</svg>
|
||||
`;
|
||||
this.toggleBtn.appendChild(this.toggleTextEl);
|
||||
this.toggleBtn.appendChild(toggleIconEl);
|
||||
this.toggleBtn.addEventListener('click', () => {
|
||||
this.isExpanded = !this.isExpanded;
|
||||
this.applyExpandedState();
|
||||
this.setLocales(); // 更新 tooltip(展开/收起)
|
||||
|
||||
// 通知外部:用于重新计算 Dialog 高度
|
||||
if (this.options.onExpandedChange) {
|
||||
this.options.onExpandedChange(this.isExpanded);
|
||||
}
|
||||
});
|
||||
|
||||
toggleBox.appendChild(this.toggleBtn);
|
||||
toolsBox.appendChild(toggleBox);
|
||||
root.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';
|
||||
const mainValueLabel = document.createElement('span');
|
||||
mainValueLabel.className = 'label';
|
||||
this.mainValueLabelEl = mainValueLabel;
|
||||
const mainValueValue = document.createElement('span');
|
||||
mainValueValue.className = 'value';
|
||||
this.mainValueValueEl = mainValueValue;
|
||||
mainValueRow.appendChild(mainValueLabel);
|
||||
mainValueRow.appendChild(mainValueValue);
|
||||
resultBox.appendChild(mainValueRow);
|
||||
|
||||
// XYZ
|
||||
const xyzBox = document.createElement('div');
|
||||
xyzBox.className = 'bim-measure-xyz';
|
||||
|
||||
const makeXyzRow = (labelKey: 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';
|
||||
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)));
|
||||
resultBox.appendChild(xyzBox);
|
||||
|
||||
root.appendChild(resultBox);
|
||||
|
||||
// 底部:删除全部 + 设置
|
||||
const footer = document.createElement('div');
|
||||
footer.className = 'bim-measure-footer';
|
||||
|
||||
this.clearBtn = document.createElement('button');
|
||||
this.clearBtn.type = 'button';
|
||||
this.clearBtn.className = 'bim-measure-clear-btn';
|
||||
this.clearBtn.addEventListener('click', () => {
|
||||
this.clearAll();
|
||||
});
|
||||
|
||||
this.settingsBtn = document.createElement('button');
|
||||
this.settingsBtn.type = 'button';
|
||||
this.settingsBtn.className = 'bim-measure-settings-btn';
|
||||
this.settingsBtn.innerHTML = `
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M19.14 12.94c.04-.31.06-.63.06-.94s-.02-.63-.06-.94l2.03-1.58a.5.5 0 0 0 .12-.64l-1.92-3.32a.5.5 0 0 0-.6-.22l-2.39.96a7.27 7.27 0 0 0-1.63-.94l-.36-2.54A.5.5 0 0 0 13.9 1h-3.8a.5.5 0 0 0-.49.42l-.36 2.54c-.58.23-1.12.54-1.63.94l-2.39-.96a.5.5 0 0 0-.6.22L2.71 7.48a.5.5 0 0 0 .12.64l2.03 1.58c-.04.31-.06.63-.06.94s.02.63.06.94L2.83 14.52a.5.5 0 0 0-.12.64l1.92 3.32c.13.22.39.3.6.22l2.39-.96c.5.4 1.05.71 1.63.94l.36 2.54c.04.24.25.42.49.42h3.8c.24 0 .45-.18.49-.42l.36-2.54c.58-.23 1.12-.54 1.63-.94l2.39.96c.22.09.47 0 .6-.22l1.92-3.32a.5.5 0 0 0-.12-.64l-2.03-1.58zM12 15.5A3.5 3.5 0 1 1 12 8a3.5 3.5 0 0 1 0 7.5z"></path>
|
||||
</svg>
|
||||
`;
|
||||
this.settingsBtn.addEventListener('click', () => {
|
||||
this.openSettings();
|
||||
});
|
||||
|
||||
footer.appendChild(this.clearBtn);
|
||||
footer.appendChild(this.settingsBtn);
|
||||
root.appendChild(footer);
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用“展开/收起”状态:默认只显示前 4 个按钮
|
||||
*/
|
||||
private applyExpandedState(): void {
|
||||
let index = 0;
|
||||
for (const btn of this.toolButtons.values()) {
|
||||
// 默认展示前四个,其余根据展开状态显示/隐藏
|
||||
if (index >= 4) {
|
||||
btn.style.display = this.isExpanded ? '' : 'none';
|
||||
} else {
|
||||
btn.style.display = '';
|
||||
}
|
||||
index++;
|
||||
}
|
||||
|
||||
// toggle 样式(旋转箭头)
|
||||
if (this.isExpanded) {
|
||||
this.toggleBtn.classList.add('is-expanded');
|
||||
} else {
|
||||
this.toggleBtn.classList.remove('is-expanded');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用“当前选中按钮”样式
|
||||
*/
|
||||
private applyActiveModeState(): void {
|
||||
for (const [mode, btn] of this.toolButtons.entries()) {
|
||||
if (mode === this.activeMode) {
|
||||
btn.classList.add('is-active');
|
||||
} else {
|
||||
btn.classList.remove('is-active');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染结果区(根据 activeMode 从 result 里取对应字段)
|
||||
*/
|
||||
private renderResult(): void {
|
||||
// 1) 主值
|
||||
const mainText = this.formatMainValue(this.activeMode, this.result);
|
||||
this.mainValueValueEl.textContent = mainText;
|
||||
|
||||
// 2) XYZ
|
||||
const xyz = this.result?.xyz;
|
||||
if (!xyz) {
|
||||
this.xyzXEl.textContent = '--';
|
||||
this.xyzYEl.textContent = '--';
|
||||
this.xyzZEl.textContent = '--';
|
||||
return;
|
||||
}
|
||||
|
||||
// 为了可读性:这里不做 fancy formatter,只做基础展示
|
||||
this.xyzXEl.textContent = this.formatNumber(xyz.x);
|
||||
this.xyzYEl.textContent = this.formatNumber(xyz.y);
|
||||
this.xyzZEl.textContent = this.formatNumber(xyz.z);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取模式名称的国际化 key
|
||||
*/
|
||||
private getModeI18nKey(mode: MeasureMode): string {
|
||||
return `measure.modes.${mode}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取“主值 label”的国际化 key(随模式变化)
|
||||
*/
|
||||
private getModeValueLabelI18nKey(mode: MeasureMode): string {
|
||||
return `measure.labels.value.${mode}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将“当前模式”的主值格式化为文本
|
||||
* @param mode 当前模式
|
||||
* @param result 当前结果
|
||||
*/
|
||||
private formatMainValue(mode: MeasureMode, result: MeasureResult | null): string {
|
||||
if (!result) return '--';
|
||||
|
||||
// 根据不同 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');
|
||||
default:
|
||||
return '--';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化数值 + 单位(单位走国际化)
|
||||
*/
|
||||
private formatWithUnit(value: number | undefined, unitKey: string): string {
|
||||
if (value === null || value === undefined || Number.isNaN(value)) return '--';
|
||||
return `${this.formatNumber(value)} ${t(unitKey)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 基础数字格式化(可读性优先)
|
||||
*/
|
||||
private formatNumber(value: number): string {
|
||||
// 保留 3 位小数以内(简单策略:整数不带小数,非整数保留到 3 位)
|
||||
if (Number.isInteger(value)) return String(value);
|
||||
return value.toFixed(3).replace(/0+$/g, '').replace(/\.$/g, '');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user