- ThemeConfig 接口扩展至 60+ 语义化属性 - 新增深浅主题预设 (glassPill overrides) - button-group 支持 glass-pill 样式变体 - 默认主题改为浅色 - 移除 toolbar 容器硬编码定位 - 统一组件 CSS 变量命名规范 - 暂时隐藏下拉箭头
1016 lines
41 KiB
TypeScript
1016 lines
41 KiB
TypeScript
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 { MeasureConfig, MeasureMode, MeasurePanelOptions, MeasurePrecision, MeasureResult, MeasureUnit } from './types';
|
||
|
||
/**
|
||
* 测量方式图标(SVG)
|
||
*
|
||
* 说明:
|
||
* - 你上传的 SVG 原文件放在 `src/assets/icons/` 目录
|
||
* - 原始 SVG 含 defs/clipPath/style/背景 rect,直接内联时容易出现渲染/裁剪异常(尤其多个图标同时出现)
|
||
* - 这里把图标“瘦身”为纯 path,并统一使用 currentColor,确保稳定渲染
|
||
*/
|
||
const MEASURE_MODE_ICON_SVGS: Record<MeasureMode, string> = {
|
||
distance: `<svg viewBox="0 0 32 32" aria-hidden="true"><g transform="translate(0 4.197)"><path fill="currentColor" d="M29.692,3.03,27.55.919a.529.529,0,0,1-.014-.756A.549.549,0,0,1,28.3.15l.014.013,3.067,3.023a.529.529,0,0,1,0,.756L28.317,6.966a.549.549,0,0,1-.767.013.529.529,0,0,1-.014-.756l.014-.013L29.692,4.1H2.31L4.452,6.21a.528.528,0,0,1,.013.756.547.547,0,0,1-.766.013l-.014-.013L.616,3.942a.531.531,0,0,1,0-.756L3.685.163a.548.548,0,0,1,.767.014.528.528,0,0,1,0,.742L2.31,3.03ZM24.136,15.055H23.051V18H21.966v-2.94H20.882V18H19.8v-2.94H18.712V18H17.627v-2.94H16.543v5.078H15.458V15.055H14.373V18H13.288v-2.94H12.2V18H11.119v-2.94H10.034V18H8.949v-2.94H7.865V18H6.78v-2.94H5.7v5.078H4.61V15.055H1.9a.27.27,0,0,0-.272.268v6.413A.269.269,0,0,0,1.9,22H30.1a.268.268,0,0,0,.271-.267V15.323a.269.269,0,0,0-.271-.268H27.39v5.078H26.305V15.055H25.221V18H24.136Zm5.966-1.6A1.884,1.884,0,0,1,32,15.323v6.413a1.885,1.885,0,0,1-1.9,1.871H1.9A1.885,1.885,0,0,1,0,21.736V15.323a1.885,1.885,0,0,1,1.9-1.871Z"/></g></svg>`,
|
||
minDistance: `<svg viewBox="0 0 32 32" aria-hidden="true"><path fill="currentColor" d="M-5.839,24.8H-34.16A1.875,1.875,0,0,1-36,22.933V16.52a1.887,1.887,0,0,1,1.9-1.871H-5.9A1.887,1.887,0,0,1-4,16.52v6.412A1.875,1.875,0,0,1-5.839,24.8ZM-34.1,16.252a.27.27,0,0,0-.272.268v6.412a.27.27,0,0,0,.272.267H-5.9a.269.269,0,0,0,.271-.267V16.52a.27.27,0,0,0-.271-.268H-8.61V21.33H-9.695V16.252h-1.085v2.939h-1.085V16.252h-1.085v2.939h-1.085V16.252h-1.084v2.939H-16.2V16.252h-1.085v2.939h-1.085V16.252h-1.084V21.33h-1.084V16.252h-1.085v2.939h-1.085V16.252H-23.8v2.939h-1.085V16.252h-1.085v2.939h-1.085V16.252h-1.084v2.939H-29.22V16.252H-30.3V21.33H-31.39V16.252Z" transform="translate(36 2)"/><path fill="currentColor" d="M23.716,7.947V4.875c0-.8-.232-1.085-.765-1.085a1.573,1.573,0,0,0-1.133.585V7.947H20.4V2.75h1.163l.1.687H21.7a2.547,2.547,0,0,1,1.763-.817c1.172,0,1.676.78,1.676,2.089V7.947Zm-7.26,0V2.62h1.58V7.947Zm-3.8,0V4.875c0-.8-.243-1.085-.76-1.085a1.606,1.606,0,0,0-1.049.585V7.947H9.421V4.875c0-.8-.243-1.085-.758-1.085a1.608,1.608,0,0,0-1.05.585V7.947H6.194V2.75H7.36l.1.7H7.5A2.326,2.326,0,0,1,9.169,2.62a1.486,1.486,0,0,1,1.5.91A2.445,2.445,0,0,1,12.4,2.62c1.156,0,1.691.78,1.691,2.089V7.947Zm3.8-6.849a.79.79,0,0,1,1.58,0,.79.79,0,0,1-1.58,0Z" transform="translate(0.333 3.053)"/></svg>`,
|
||
angle: `<svg viewBox="0 0 32 32" aria-hidden="true"><path fill="currentColor" d="M39.587,50.766h13.7a1,1,0,0,1,0,2H23.171a1,1,0,0,1,0-2h1.418l6.582-7.006v-.006a.517.517,0,0,1,.14-.357.456.456,0,0,1,.337-.144l12.1-12.876a.451.451,0,0,1,.665,0,.524.524,0,0,1,0,.708L32.883,43.355a8.3,8.3,0,0,1,6.7,7.411Zm-.949,0a7.254,7.254,0,0,0-6.611-6.5l-6.108,6.5Z" transform="translate(-22.229 -26.489)"/></svg>`,
|
||
elevation: `<svg viewBox="0 0 32 32" aria-hidden="true"><path fill="currentColor" d="M84.131,193.119a1.056,1.056,0,0,1,1.116.982v7.857a1.056,1.056,0,0,1-1.116.982H54.367a1.056,1.056,0,0,1-1.116-.982V194.1a1.056,1.056,0,0,1,1.116-.982Zm-1.116,1.964H55.483v5.893H83.015Zm1.116-13.749a1.064,1.064,0,0,1,1.114.935,1.032,1.032,0,0,1-1.007,1.025l-.107,0H71.2l-7.858,6.914a1.227,1.227,0,0,1-1.578,0l-8.185-7.2-.018-.016-.032-.031.049.047a1.107,1.107,0,0,1-.092-.092l-.011-.014a.869.869,0,0,1-.182-.857l0-.008L53.31,182l.012-.029.02-.045.019-.035a1.1,1.1,0,0,1,.891-.552h.007q.053,0,.107,0ZM68.043,183.3H57.06l5.492,4.831Z" transform="translate(-53.247 -176.136)"/></svg>`,
|
||
volume: `<svg viewBox="0 0 32 32" aria-hidden="true"><path fill="currentColor" d="M94.74,86.658V71.189a.371.371,0,0,1,.2-.329l13.869-7.22a.371.371,0,0,1,.344,0l13.053,6.891h0l.819.431a.371.371,0,0,1,.2.328v15.3a.371.371,0,0,1-.2.328l-13.872,7.255a.371.371,0,0,1-.342,0L94.94,86.987a.371.371,0,0,1-.2-.329Zm2.119-.837,11.2,5.8a.024.024,0,0,0,.035-.022V79.483a.371.371,0,0,0-.2-.328l-11.2-5.909a.024.024,0,0,0-.035.021V85.492A.371.371,0,0,0,96.859,85.821Zm13.151-6.459v12a.12.12,0,0,0,.176.106L114,89.474l3.334-1.745,3.771-1.978a.371.371,0,0,0,.2-.328V73.5a.193.193,0,0,0-.284-.171l-10.812,5.708A.371.371,0,0,0,110.01,79.362ZM97.925,71.725l10.839,5.72a.371.371,0,0,0,.346,0L119.8,71.808a.214.214,0,0,0,0-.378l-10.649-5.621a.371.371,0,0,0-.344,0L97.925,71.47A.144.144,0,0,0,97.925,71.725Z" transform="translate(-92.982 -62.907)"/></svg>`,
|
||
laserDistance: `<svg viewBox="0 0 32 32" aria-hidden="true"><g transform="translate(0 -1.293)"><path fill="currentColor" d="M0,1.293v31.96H32V1.293ZM30.97,32.182H1.03V2.323H30.97Z"/><path fill="currentColor" d="M160.026,291.9l1.6,1.6,7.305-7.305-7.305-7.305-1.6,1.6,4.794,4.566h-6.392v2.283h6.392Zm-5.251,0-4.566-4.566h6.164v-2.283H150.21l4.566-4.566-1.37-1.6L146.1,286.19l7.305,7.305Z" transform="translate(-141.535 -268.917)"/></g></svg>`,
|
||
slope: `<svg viewBox="0 0 32 32" aria-hidden="true"><path fill="currentColor" d="M202.1,188.337l2.629-2.191-8.447-3.106,1.533,8.871,2.629-2.194,9.341,11.209,1.656-1.379Zm-13.726-.435a1.075,1.075,0,0,0-1.07-.341,1.057,1.057,0,0,0-.5.277l-5.11,4.08a1.08,1.08,0,0,0-.406.84l-.007,17.386a1.079,1.079,0,0,0,1.077,1.077L205.7,211.2a1.078,1.078,0,0,0,.822-1.774Zm-4.934,21.164.007-15.788,3.968-3.171,15.974,18.941Z" transform="translate(-180.36 -181.131)"/></svg>`,
|
||
spaceVolume: `<svg viewBox="0 0 32 32" aria-hidden="true"><g transform="translate(-106.35 -97.661)"><path fill="currentColor" d="M125.977,128.829l13.076-7.363v-13.6l-13.076,6.8Zm-3.126-15.655a.565.565,0,0,1-.258-.064L109.3,106.323a.567.567,0,0,1-.011-1L122.578,98a.567.567,0,0,1,.55,0l13.288,7.325a.567.567,0,0,1-.011,1l-13.292,6.79A.63.63,0,0,1,122.851,113.174ZM110.773,105.8l12.078,6.172,12.078-6.172-12.078-6.657Z" transform="translate(-1.922)"/><path fill="currentColor" d="M120.649,322.52a.58.58,0,0,1-.262-.064l-13.08-6.8a.573.573,0,0,1-.307-.5V301a.566.566,0,0,1,.273-.486.573.573,0,0,1,.558-.019l13.076,6.8a.573.573,0,0,1,.307.5v14.161a.57.57,0,0,1-.565.569Zm-12.511-7.708,11.942,6.206V308.136l-11.942-6.206Zm15.917,9.408a.585.585,0,0,1-.288-.076.567.567,0,0,1-.281-.489V309.49a.562.562,0,0,1,.307-.5l13.076-6.8a.573.573,0,0,1,.558.019.562.562,0,0,1,.273.486v13.6a.568.568,0,0,1-.288.493l-13.076,7.359A.557.557,0,0,1,124.055,324.22Zm.569-14.385V322.68l11.942-6.722V303.629Z" transform="translate(0 -194.822)"/></g></svg>`
|
||
};
|
||
|
||
/**
|
||
* 测量面板组件(只做 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;
|
||
|
||
/**
|
||
* 测量配置(单位/精度)
|
||
* 说明:
|
||
* - 你要求:创建 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 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;
|
||
|
||
/**
|
||
* 构造函数
|
||
* @param options 组件配置
|
||
*/
|
||
constructor(options: MeasurePanelOptions = {}) {
|
||
this.options = options;
|
||
this.activeMode = options.defaultMode ?? 'distance';
|
||
this.isExpanded = options.defaultExpanded ?? false;
|
||
|
||
// 读取配置:优先缓存,否则默认
|
||
this.config = this.loadConfigFromCache() ?? { ...MeasurePanel.DEFAULT_CONFIG };
|
||
|
||
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.applyViewState();
|
||
this.renderResult();
|
||
|
||
// 触发初始测量模式的回调(让外部知道默认激活了哪个模式)
|
||
if (this.options.onModeChange) {
|
||
this.options.onModeChange(this.activeMode);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 设置主题(实现 IBimComponent)
|
||
* @param theme 主题配置
|
||
*/
|
||
public setTheme(theme: ThemeConfig): void {
|
||
// 为了可读性:这里显式写出映射,不做过度抽象
|
||
const style = this.element.style;
|
||
|
||
// 这些变量不会强制覆盖外部(Dialog)已有变量,只做兜底
|
||
style.setProperty('--bim-measure-border', theme.borderDefault ?? 'rgba(255, 255, 255, 0.12)');
|
||
style.setProperty('--bim-measure-divider', theme.borderDefault ?? 'rgba(255, 255, 255, 0.10)');
|
||
style.setProperty('--bim-measure-icon-color', theme.iconDefault ?? '#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-primary', theme.primary ?? '#0078d4');
|
||
style.setProperty('--bim-measure-primary-hover', theme.primaryHover ?? '#0063b1');
|
||
style.setProperty('--bim-measure-btn-bg', theme.componentBg ?? 'rgba(255, 255, 255, 0.06)');
|
||
style.setProperty('--bim-measure-btn-hover-bg', theme.componentBgHover ?? 'rgba(255, 255, 255, 0.10)');
|
||
style.setProperty('--bim-measure-btn-active-bg', theme.componentBgActive ?? '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) 主值 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);
|
||
});
|
||
|
||
// 7) 设置面板文本
|
||
this.saveSettingsBtn.textContent = t('measure.settings.save');
|
||
this.cancelSettingsBtn.textContent = t('measure.settings.cancel');
|
||
}
|
||
|
||
/**
|
||
* 销毁组件(实现 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));
|
||
|
||
// 通知外部(如果需要)
|
||
if (this.options.onModeChange) {
|
||
this.options.onModeChange(mode);
|
||
}
|
||
|
||
// 模式切换后,结果展示也应刷新(例如某些字段显示为 --)
|
||
this.renderResult();
|
||
|
||
// 切换模式会影响结果区高度(例如 distance 显示 xyz,其它不显示)
|
||
// 复用 onExpandedChange 来通知外部重新计算 Dialog 高度(不额外扩展回调,保持接口简单)
|
||
if (this.options.onExpandedChange) {
|
||
this.options.onExpandedChange(this.isExpanded);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 设置测量结果(由外部注入)
|
||
* @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 {
|
||
// 进入设置面板(组件内部逻辑)
|
||
this.enterSettingsView();
|
||
|
||
// 仍然保留回调(如果外部想监听)
|
||
if (this.options.onSettings) {
|
||
this.options.onSettings();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取当前测量配置
|
||
*/
|
||
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);
|
||
}
|
||
|
||
// 配置变化会影响数值显示(单位/精度)
|
||
this.renderResult();
|
||
|
||
// 如果当前在设置面板,表单也需要同步
|
||
if (this.view === 'settings') {
|
||
this.syncSettingsFormFromConfig(next);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 展开 / 收起(可选对外调用)
|
||
* @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';
|
||
|
||
// 主视图容器(默认显示)
|
||
this.mainViewEl = document.createElement('div');
|
||
this.mainViewEl.className = 'bim-measure-main';
|
||
|
||
// 顶部:工具按钮区
|
||
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'
|
||
];
|
||
|
||
// 图标:优先使用你上传的 SVG 文件内容(已内联到 MEASURE_MODE_ICON_SVGS)
|
||
// 兜底:如果某个 mode 没有配置图标,则使用圆形占位(防止页面空白)
|
||
const fallbackCircleIconSvg = `
|
||
<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 = MEASURE_MODE_ICON_SVGS[mode] || fallbackCircleIconSvg;
|
||
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);
|
||
this.mainViewEl.appendChild(toolsBox);
|
||
|
||
// 中部:结果区
|
||
const resultBox = document.createElement('div');
|
||
resultBox.className = 'bim-measure-result';
|
||
|
||
// 主结果值(随模式变化)
|
||
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;
|
||
|
||
// 主值拆分:数值(黄色)+ 单位(普通色)
|
||
// 这样可以满足:
|
||
// 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);
|
||
|
||
// XYZ
|
||
const xyzBox = document.createElement('div');
|
||
xyzBox.className = 'bim-measure-xyz';
|
||
this.xyzBoxEl = xyzBox;
|
||
|
||
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 ${valueClassName}`;
|
||
valueElSetter(value);
|
||
row.appendChild(label);
|
||
row.appendChild(value);
|
||
return row;
|
||
};
|
||
|
||
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);
|
||
|
||
this.mainViewEl.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);
|
||
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 个按钮
|
||
*/
|
||
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) 根据模式决定结果区显示规则
|
||
// 你给的规则:
|
||
// - 距离:显示数值 + xyz
|
||
// - 最小距离:只显示数值
|
||
// - 角度:--°
|
||
// - 标高:--m(固定 m)
|
||
// - 体积:--mm³(单位随设置变动,即 unit³)
|
||
// - 激光测距:不显示任何数值/xyz,只显示“激光测距”文字
|
||
// - 坡度:--%
|
||
// - 空间体积:--mm³(单位随设置变动,即 unit³)
|
||
|
||
// 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;
|
||
}
|
||
|
||
// 非 distance:隐藏 xyz
|
||
this.xyzBoxEl.style.display = 'none';
|
||
}
|
||
|
||
/**
|
||
* 获取模式名称的国际化 key
|
||
*/
|
||
private getModeI18nKey(mode: MeasureMode): string {
|
||
return `measure.modes.${mode}`;
|
||
}
|
||
|
||
/**
|
||
* 获取“主值 label”的国际化 key(随模式变化)
|
||
*/
|
||
private getModeValueLabelI18nKey(mode: MeasureMode): string {
|
||
return `measure.labels.value.${mode}`;
|
||
}
|
||
|
||
// 注意:旧的 formatMainValue/formatWithFixedUnit 已被 formatMainValueParts 替代,
|
||
// 以支持“数值与单位分色显示”和“无数据时仍展示单位”。
|
||
|
||
/**
|
||
* 基础数字格式化(按精度显示)
|
||
*/
|
||
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 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 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 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 };
|
||
}
|
||
}
|
||
|
||
|