569 lines
19 KiB
TypeScript
569 lines
19 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 { 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, '');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|