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 = { distance: ``, minDistance: ``, angle: ``, elevation: ``, volume: ``, laserDistance: ``, slope: ``, spaceVolume: `` }; /** * 测量面板组件(只做 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 = 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('[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, 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 = ` `; // 逐个创建按钮 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 = ` `; 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 = ` `; 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; 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 }; } }