增加测量窗口

This commit is contained in:
yuding
2025-12-22 18:48:38 +08:00
parent e1bb5558ff
commit 7d522afb70
25 changed files with 4625 additions and 2403 deletions

View File

@@ -0,0 +1,222 @@
/**
* 测量面板样式(只做 UI
*
* 设计目标:
* - 视觉尽量接近截图(深色半透明面板 + 图标按钮网格 + 结果区)
* - 主题颜色尽量使用 CSS 变量,保证可被 ThemeManager / Dialog 主题覆盖
*/
.bim-measure-panel {
width: 100%;
display: flex;
flex-direction: column;
box-sizing: border-box;
/* 面板内部颜色尽量复用 Dialog 的变量,保证整体一致 */
color: var(--bim-dialog-text-color, #ccc);
}
/* 顶部:测量方式按钮区 */
.bim-measure-tools {
display: flex;
flex-direction: column;
gap: 8px;
}
.bim-measure-tool-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 8px;
}
.bim-measure-tool-btn {
width: 100%;
height: 42px;
border-radius: 6px;
border: 1px solid var(--bim-measure-border, rgba(255, 255, 255, 0.12));
background: var(--bim-measure-btn-bg, rgba(255, 255, 255, 0.06));
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.15s ease, border-color 0.15s ease;
padding: 0;
box-sizing: border-box;
}
.bim-measure-tool-btn:hover {
background: var(--bim-measure-btn-hover-bg, rgba(255, 255, 255, 0.10));
}
.bim-measure-tool-btn.is-active {
border-color: var(--bim-measure-active-border, rgba(255, 255, 255, 0.30));
background: var(--bim-measure-btn-active-bg, rgba(255, 255, 255, 0.14));
}
.bim-measure-tool-icon {
width: 22px;
height: 22px;
display: inline-flex;
align-items: center;
justify-content: center;
color: var(--bim-measure-icon-color, #ddd);
}
.bim-measure-tool-icon svg {
width: 100%;
height: 100%;
fill: currentColor;
}
.bim-measure-toggle {
display: flex;
justify-content: flex-end;
}
.bim-measure-toggle-btn {
/* 你要求:更小,并带文字提示 */
height: 22px;
border-radius: 4px;
border: 1px solid var(--bim-measure-border, rgba(255, 255, 255, 0.12));
background: var(--bim-measure-btn-bg, rgba(255, 255, 255, 0.06));
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
transition: background-color 0.15s ease;
padding: 0 6px;
gap: 4px;
font-size: 12px;
line-height: 1;
}
.bim-measure-toggle-btn:hover {
background: var(--bim-measure-btn-hover-bg, rgba(255, 255, 255, 0.10));
}
.bim-measure-toggle-text {
color: var(--bim-measure-label-color, rgba(255, 255, 255, 0.70));
}
.bim-measure-toggle-icon svg {
width: 14px;
height: 14px;
fill: currentColor;
color: var(--bim-measure-icon-color, #ddd);
transition: transform 0.15s ease;
}
.bim-measure-toggle-btn.is-expanded .bim-measure-toggle-icon svg {
transform: rotate(180deg);
}
/* 中部:结果展示区 */
.bim-measure-result {
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid var(--bim-measure-divider, rgba(255, 255, 255, 0.10));
display: flex;
flex-direction: column;
gap: 10px;
}
.bim-measure-row {
display: flex;
align-items: baseline;
gap: 8px;
font-size: 13px;
line-height: 1.4;
}
.bim-measure-row .label {
color: var(--bim-measure-label-color, rgba(255, 255, 255, 0.70));
min-width: 84px;
}
.bim-measure-row .value {
color: var(--bim-measure-value-color, rgba(255, 255, 255, 0.90));
flex: 1;
word-break: break-word;
}
.bim-measure-xyz {
display: flex;
flex-direction: column;
gap: 6px;
}
.bim-measure-xyz .value {
font-variant-numeric: tabular-nums;
}
/* 底部:操作区(删除全部 / 设置) */
.bim-measure-footer {
margin-top: 12px;
padding-top: 10px;
border-top: 1px solid var(--bim-measure-divider, rgba(255, 255, 255, 0.10));
display: flex;
align-items: center;
/* 你要求:底部不要“占满”交互区域,按钮按自身尺寸布局 */
justify-content: flex-start;
gap: 10px;
}
.bim-measure-clear-btn {
background: transparent;
border: none;
color: var(--bim-measure-danger, white); /* 先用偏绿(接近截图),可由主题覆盖 */
cursor: pointer;
/* 缩小可点击区域:仅文字本身附近 */
padding: 0;
font-size: 13px;
/* 防止外部环境(如 demo给 button 设置 flex: 1 导致“各占一半” */
flex: 0 0 auto !important;
width: auto;
min-width: 0;
}
/* 你要求:删除按钮不需要 hover 效果 */
.bim-measure-clear-btn:hover,
.bim-measure-clear-btn:active,
.bim-measure-clear-btn:focus {
background: transparent;
border: none;
outline: none;
text-decoration: none;
}
.bim-measure-settings-btn {
/* 你要求:管理(设置)按钮去掉边框与 hover按钮按自身尺寸即可 */
width: 24px;
height: 24px;
border-radius: 4px;
border: none;
background: transparent;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0;
/* 右侧对齐,但不扩大可点击区域 */
margin-left: auto;
/* 防止外部环境(如 demo给 button 设置 flex: 1 导致“各占一半” */
flex: 0 0 auto !important;
}
/* 你要求:设置按钮不需要 hover 效果 */
.bim-measure-settings-btn:hover,
.bim-measure-settings-btn:active,
.bim-measure-settings-btn:focus {
background: transparent;
border: none;
outline: none;
}
.bim-measure-settings-btn svg {
width: 18px;
height: 18px;
fill: currentColor;
color: var(--bim-measure-icon-color, #ddd);
}

View 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, '');
}
}

View File

@@ -0,0 +1,96 @@
/**
* 测量面板 - 类型定义
*
* 注意:
* - 本次只实现 UI不实现真实测量逻辑拾取、画线、计算等
* - 这里的类型以“可读性优先”为原则,尽量直观、易扩展。
*/
/**
* 测量方式8 种)
*
* 说明:
* - id 采用英文驼峰/小写,便于程序内部使用;
* - 显示名称必须通过国际化 key 获取(见 locales
*/
export type MeasureMode =
| 'distance' // 距离
| 'minDistance' // 最小距离
| 'angle' // 角度
| 'elevation' // 标高
| 'volume' // 体积
| 'laserDistance' // 激光测距
| 'slope' // 坡度
| 'spaceVolume'; // 空间体积
/**
* 3D 坐标(可选展示)
*/
export interface MeasureXYZ {
x: number;
y: number;
z: number;
}
/**
* 测量结果数据
*
* 说明:
* - 真实测量未实现,因此结果由外部通过 setResult 传入。
* - 不同测量方式对应不同字段;未传入则 UI 显示 “--”。
*/
export interface MeasureResult {
/** 距离单位mm */
distanceMm?: number;
/** 最小距离单位mm */
minDistanceMm?: number;
/** 角度单位deg */
angleDeg?: number;
/** 标高单位mm */
elevationMm?: number;
/** 体积单位 */
volumeM3?: number;
/** 激光测距单位mm */
laserDistanceMm?: number;
/** 坡度(单位:% */
slopePercent?: number;
/** 空间体积单位 */
spaceVolumeM3?: number;
/** 可选:展示测量点/结果点坐标(单位由引擎侧定义,这里只负责显示) */
xyz?: MeasureXYZ;
}
/**
* MeasurePanel 组件配置
*/
export interface MeasurePanelOptions {
/** 默认测量方式(不传则默认 distance */
defaultMode?: MeasureMode;
/** 是否默认展开(不传则默认 false即展示前 4 个) */
defaultExpanded?: boolean;
/**
* 测量方式切换回调(只通知 UI 状态变化,不包含真实测量逻辑)
* @param mode 当前选中的测量方式
*/
onModeChange?: (mode: MeasureMode) => void;
/**
* “删除全部”回调
*/
onClearAll?: () => void;
/**
* “设置”回调
*/
onSettings?: () => void;
/**
* 展开/收起状态变更回调
* 说明:用于让外部(如 Dialog重新计算尺寸。
*/
onExpandedChange?: (expanded: boolean) => void;
}