增加测量窗口

This commit is contained in:
yuding
2025-12-23 11:31:16 +08:00
parent 7d522afb70
commit 4b5eb78bbb
15 changed files with 3846 additions and 2832 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

39
dist/index.d.ts vendored
View File

@@ -680,6 +680,14 @@ declare type Listener<T = any> = (payload: T) => void;
*/ */
declare type LocaleType = 'zh-CN' | 'en-US'; declare type LocaleType = 'zh-CN' | 'en-US';
/**
* 测量配置项(由组件内部维护默认值,并读取/写入缓存)
*/
declare interface MeasureConfig {
unit: MeasureUnit;
precision: MeasurePrecision;
}
/** /**
* 测量弹窗管理器 * 测量弹窗管理器
*/ */
@@ -687,6 +695,11 @@ declare class MeasureDialogManager extends BimComponent {
private dialogId; private dialogId;
private dialog; private dialog;
private panel; private panel;
/**
* 测量配置项(单位/精度)
* 说明MeasurePanel 会自行从缓存加载默认配置Manager 这里只做“对外读取/设置”的镜像。
*/
private config;
constructor(engine: BimEngine); constructor(engine: BimEngine);
init(): void; init(): void;
/** /**
@@ -708,6 +721,18 @@ declare class MeasureDialogManager extends BimComponent {
* @param result 测量结果;传 null 表示清空 * @param result 测量结果;传 null 表示清空
*/ */
setResult(result: MeasureResult | null): void; setResult(result: MeasureResult | null): void;
/**
* 获取测量配置(单位/精度)
* - 如果面板存在:返回面板当前配置
* - 否则:返回 Manager 缓存的最后一次配置(可能为 null
*/
getConfig(): MeasureConfig | null;
/**
* 设置测量配置(单位/精度)
* @param partial 部分更新
* @param persist 是否写入缓存(默认 true
*/
setConfig(partial: Partial<MeasureConfig>, persist?: boolean): void;
/** /**
* 删除全部(仅清空 UI真实测量清理逻辑后续再接 * 删除全部(仅清空 UI真实测量清理逻辑后续再接
*/ */
@@ -735,6 +760,15 @@ declare class MeasureDialogManager extends BimComponent {
*/ */
declare type MeasureMode = 'distance' | 'minDistance' | 'angle' | 'elevation' | 'volume' | 'laserDistance' | 'slope' | 'spaceVolume'; declare type MeasureMode = 'distance' | 'minDistance' | 'angle' | 'elevation' | 'volume' | 'laserDistance' | 'slope' | 'spaceVolume';
/**
* 精度(小数位数)
* - 0 -> 0
* - 1 -> 0.0
* - 2 -> 0.00
* - 3 -> 0.000
*/
declare type MeasurePrecision = 0 | 1 | 2 | 3;
/** /**
* 测量结果数据 * 测量结果数据
* *
@@ -763,6 +797,11 @@ declare interface MeasureResult {
xyz?: MeasureXYZ; xyz?: MeasureXYZ;
} }
/**
* 距离/标高等“长度类”单位
*/
declare type MeasureUnit = 'm' | 'cm' | 'mm' | 'km';
/** /**
* 3D 坐标(可选展示) * 3D 坐标(可选展示)
*/ */

View File

@@ -1192,3 +1192,5 @@ type ExpandDirection = 'up' | 'down' | 'left' | 'right';

View File

@@ -610,3 +610,5 @@ interface ModelLoadOptions {

View File

@@ -20,6 +20,17 @@
-`MeasureDialogManager` 创建并挂载到 `BimDialog` -`MeasureDialogManager` 创建并挂载到 `BimDialog`
- 外部业务SDK 使用者)不直接 import 组件类,统一通过 `engine.measure`Manager调用 - 外部业务SDK 使用者)不直接 import 组件类,统一通过 `engine.measure`Manager调用
### 1.3 配置项(单位/精度)与缓存策略(新增)
- **创建 `MeasurePanel` 不传入单位/精度**
- 默认配置由组件内部维护:
- `unit`: `'mm'`
- `precision`: `2`(即 `0.00`
- 组件初始化时会读取缓存(`localStorage`
- key`bim-engine:measure:config`
- 若缓存存在且合法,则使用缓存值覆盖默认配置
- 若缓存不存在/解析失败,则使用默认配置
- 用户在设置面板点击“保存设置”后,组件会写入缓存
--- ---
## 2. 组件类 API 文档 ## 2. 组件类 API 文档
@@ -76,7 +87,16 @@ constructor(options?: MeasurePanelOptions)
- 清空结果展示并触发 `onClearAll`(如果提供)。 - 清空结果展示并触发 `onClearAll`(如果提供)。
#### `openSettings(): void` #### `openSettings(): void`
- 触发 `onSettings`(如果提供),否则输出中文警告日志(仅预留接口)。 - 进入组件内部“设置面板”(单位/精度选择)。
- 同时触发 `onSettings`(如果提供,作为外部监听)。
#### `getConfig(): MeasureConfig`
- 获取当前测量配置(单位/精度)。
#### `setConfig(partial: Partial<MeasureConfig>, persist = false): void`
- 更新配置:
- `persist=false`:仅更新内存,不写缓存
- `persist=true`:更新并写入 `localStorage`
#### `setExpanded(expanded: boolean): void` #### `setExpanded(expanded: boolean): void`
- 展开/收起按钮区(收起时只显示前 4 个)。 - 展开/收起按钮区(收起时只显示前 4 个)。
@@ -112,6 +132,8 @@ constructor(options?: MeasurePanelOptions)
```html ```html
<div class="bim-measure-panel"> <div class="bim-measure-panel">
<!-- 主视图 -->
<div class="bim-measure-main">
<div class="bim-measure-tools"> <div class="bim-measure-tools">
<div class="bim-measure-tool-grid"> <div class="bim-measure-tool-grid">
<!-- 8 个按钮:收起时隐藏后 4 个 --> <!-- 8 个按钮:收起时隐藏后 4 个 -->
@@ -150,6 +172,37 @@ constructor(options?: MeasurePanelOptions)
<button class="bim-measure-clear-btn">删除全部</button> <button class="bim-measure-clear-btn">删除全部</button>
<button class="bim-measure-settings-btn">(齿轮 svg</button> <button class="bim-measure-settings-btn">(齿轮 svg</button>
</div> </div>
</div>
<!-- 设置视图(点击设置按钮进入) -->
<div class="bim-measure-settings">
<div class="bim-measure-settings-title">设置</div>
<div class="bim-measure-settings-row">
<div class="label">单位:</div>
<select class="bim-measure-settings-select">
<option value="m">m</option>
<option value="cm">cm</option>
<option value="mm">mm</option>
<option value="km">km</option>
</select>
</div>
<div class="bim-measure-settings-row">
<div class="label">精度:</div>
<select class="bim-measure-settings-select">
<option value="0">0</option>
<option value="1">0.0</option>
<option value="2">0.00</option>
<option value="3">0.000</option>
</select>
</div>
<div class="bim-measure-settings-actions">
<button class="bim-measure-settings-save">保存设置</button>
<button class="bim-measure-settings-cancel">取消</button>
</div>
</div>
</div> </div>
``` ```

View File

@@ -16,6 +16,100 @@
color: var(--bim-dialog-text-color, #ccc); color: var(--bim-dialog-text-color, #ccc);
} }
.bim-measure-settings {
display: none;
box-sizing: border-box;
color: var(--bim-dialog-text-color, #ccc);
}
.bim-measure-settings-title {
font-size: 14px;
font-weight: 600;
margin-bottom: 10px;
}
.bim-measure-settings-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
margin-bottom: 10px;
}
.bim-measure-settings-row .label {
color: var(--bim-measure-label-color, rgba(255, 255, 255, 0.70));
font-size: 13px;
flex: 0 0 auto;
}
.bim-measure-settings-select {
flex: 0 0 auto;
width: 120px;
height: 28px;
border-radius: 4px;
border: 1px solid var(--bim-measure-border, rgba(255, 255, 255, 0.12));
background: rgba(0, 0, 0, 0.12);
color: var(--bim-dialog-text-color, #ccc);
padding: 0 8px;
box-sizing: border-box;
outline: none;
}
.bim-measure-settings-hint {
font-size: 12px;
line-height: 1.4;
color: var(--bim-measure-label-color, rgba(255, 255, 255, 0.70));
margin-top: -4px;
margin-bottom: 8px;
}
.bim-measure-settings-actions {
margin-top: 14px;
display: flex;
justify-content: flex-start;
gap: 10px;
}
/* 注意demo 里有全局 button 样式,这里用 class 强制覆盖,避免被污染 */
.bim-measure-settings-save,
.bim-measure-settings-cancel {
flex: 0 0 auto !important;
width: auto;
min-width: 0;
height: 30px;
padding: 0 12px;
border-radius: 4px;
cursor: pointer;
box-sizing: border-box;
}
.bim-measure-settings-save {
border: none;
background: var(--bim-measure-primary, #0078d4);
color: #fff;
}
.bim-measure-settings-cancel {
border: 1px solid var(--bim-measure-border, rgba(255, 255, 255, 0.12));
background: transparent;
color: var(--bim-dialog-text-color, #ccc);
}
.bim-measure-settings-save:hover,
.bim-measure-settings-save:active,
.bim-measure-settings-save:focus,
.bim-measure-settings-cancel:hover,
.bim-measure-settings-cancel:active,
.bim-measure-settings-cancel:focus {
background: inherit;
outline: none;
}
/* 保存按钮 hover 用主题 hover 色(轻微反馈,不改变布局) */
.bim-measure-settings-save:hover {
background: var(--bim-measure-primary-hover, #0063b1);
}
/* 顶部:测量方式按钮区 */ /* 顶部:测量方式按钮区 */
.bim-measure-tools { .bim-measure-tools {
display: flex; display: flex;
@@ -139,6 +233,20 @@
word-break: break-word; word-break: break-word;
} }
/* 主数据:仅数值黄色,单位使用默认颜色 */
.bim-measure-main-number {
color: #ffd24a;
}
.bim-measure-main-number.is-laser-text {
/* 激光测距:不使用黄色,回到默认文字颜色 */
color: var(--bim-measure-value-color, rgba(255, 255, 255, 0.90));
}
.bim-measure-main-unit {
color: var(--bim-measure-value-color, rgba(255, 255, 255, 0.90));
}
.bim-measure-xyz { .bim-measure-xyz {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -149,6 +257,19 @@
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
} }
/* XYZ红/绿/蓝展示 */
.bim-measure-xyz-x {
color: #ff4d4f !important;
}
.bim-measure-xyz-y {
color: #52c41a !important;
}
.bim-measure-xyz-z {
color: #1677ff !important;
}
/* 底部:操作区(删除全部 / 设置) */ /* 底部:操作区(删除全部 / 设置) */
.bim-measure-footer { .bim-measure-footer {
margin-top: 12px; margin-top: 12px;

View File

@@ -3,7 +3,7 @@ import type { ThemeConfig } from '../../themes/types';
import { IBimComponent } from '../../types/component'; import { IBimComponent } from '../../types/component';
import { localeManager, t } from '../../services/locale'; import { localeManager, t } from '../../services/locale';
import { themeManager } from '../../services/theme'; import { themeManager } from '../../services/theme';
import type { MeasureMode, MeasurePanelOptions, MeasureResult } from './types'; import type { MeasureConfig, MeasureMode, MeasurePanelOptions, MeasurePrecision, MeasureResult, MeasureUnit } from './types';
/** /**
* 测量面板组件(只做 UI不实现真实测量 * 测量面板组件(只做 UI不实现真实测量
@@ -26,19 +26,53 @@ export class MeasurePanel implements IBimComponent {
private isExpanded: boolean; private isExpanded: boolean;
private result: MeasureResult | null = null; 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 // DOM 引用(便于局部更新,减少频繁 querySelector
private toolButtons: Map<MeasureMode, HTMLButtonElement> = new Map(); private toolButtons: Map<MeasureMode, HTMLButtonElement> = new Map();
private toggleBtn!: HTMLButtonElement; private toggleBtn!: HTMLButtonElement;
private toggleTextEl!: HTMLElement; private toggleTextEl!: HTMLElement;
private currentModeValueEl!: HTMLElement;
private mainValueValueEl!: HTMLElement; private mainValueValueEl!: HTMLElement;
private mainValueLabelEl!: HTMLElement; private mainValueLabelEl!: HTMLElement;
private mainNumberEl!: HTMLElement;
private mainUnitEl!: HTMLElement;
private xyzBoxEl!: HTMLElement;
private xyzXEl!: HTMLElement; private xyzXEl!: HTMLElement;
private xyzYEl!: HTMLElement; private xyzYEl!: HTMLElement;
private xyzZEl!: HTMLElement; private xyzZEl!: HTMLElement;
private clearBtn!: HTMLButtonElement; private clearBtn!: HTMLButtonElement;
private settingsBtn!: 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 unsubscribeLocale: (() => void) | null = null;
private unsubscribeTheme: (() => void) | null = null; private unsubscribeTheme: (() => void) | null = null;
@@ -52,6 +86,9 @@ export class MeasurePanel implements IBimComponent {
this.activeMode = options.defaultMode ?? 'distance'; this.activeMode = options.defaultMode ?? 'distance';
this.isExpanded = options.defaultExpanded ?? false; this.isExpanded = options.defaultExpanded ?? false;
// 读取配置:优先缓存,否则默认
this.config = this.loadConfigFromCache() ?? { ...MeasurePanel.DEFAULT_CONFIG };
this.element = this.createDom(); this.element = this.createDom();
} }
@@ -76,6 +113,7 @@ export class MeasurePanel implements IBimComponent {
// 初始渲染状态(按钮显隐、选中态、结果区) // 初始渲染状态(按钮显隐、选中态、结果区)
this.applyExpandedState(); this.applyExpandedState();
this.applyActiveModeState(); this.applyActiveModeState();
this.applyViewState();
this.renderResult(); this.renderResult();
} }
@@ -96,6 +134,9 @@ export class MeasurePanel implements IBimComponent {
// “删除全部”颜色:截图中偏绿色,这里用 primary 做一个合理映射 // “删除全部”颜色:截图中偏绿色,这里用 primary 做一个合理映射
style.setProperty('--bim-measure-danger', theme.primary ?? '#46d369'); 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.componentBackground ?? 'rgba(255, 255, 255, 0.06)'); 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-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)'); style.setProperty('--bim-measure-btn-active-bg', theme.componentActive ?? 'rgba(255, 255, 255, 0.14)');
@@ -125,10 +166,7 @@ export class MeasurePanel implements IBimComponent {
this.settingsBtn.title = t('measure.actions.settings'); this.settingsBtn.title = t('measure.actions.settings');
this.settingsBtn.setAttribute('aria-label', this.settingsBtn.title); this.settingsBtn.setAttribute('aria-label', this.settingsBtn.title);
// 4) 更新“当前方式”显示value // 4) 主值 label随模式变化
this.currentModeValueEl.textContent = t(this.getModeI18nKey(this.activeMode));
// 5) 主值 label随模式变化
this.mainValueLabelEl.textContent = t(this.getModeValueLabelI18nKey(this.activeMode)); this.mainValueLabelEl.textContent = t(this.getModeValueLabelI18nKey(this.activeMode));
// 6) XYZ label使用 key // 6) XYZ label使用 key
@@ -139,6 +177,10 @@ export class MeasurePanel implements IBimComponent {
const key = node.dataset.i18nKey; const key = node.dataset.i18nKey;
if (key) node.textContent = t(key); if (key) node.textContent = t(key);
}); });
// 7) 设置面板文本
this.saveSettingsBtn.textContent = t('measure.settings.save');
this.cancelSettingsBtn.textContent = t('measure.settings.cancel');
} }
/** /**
@@ -192,7 +234,6 @@ export class MeasurePanel implements IBimComponent {
// 切换方式后,主值 label 也需要更新 // 切换方式后,主值 label 也需要更新
this.mainValueLabelEl.textContent = t(this.getModeValueLabelI18nKey(this.activeMode)); this.mainValueLabelEl.textContent = t(this.getModeValueLabelI18nKey(this.activeMode));
this.currentModeValueEl.textContent = t(this.getModeI18nKey(this.activeMode));
// 通知外部(如果需要) // 通知外部(如果需要)
if (this.options.onModeChange) { if (this.options.onModeChange) {
@@ -201,6 +242,12 @@ export class MeasurePanel implements IBimComponent {
// 模式切换后,结果展示也应刷新(例如某些字段显示为 -- // 模式切换后,结果展示也应刷新(例如某些字段显示为 --
this.renderResult(); this.renderResult();
// 切换模式会影响结果区高度(例如 distance 显示 xyz其它不显示
// 复用 onExpandedChange 来通知外部重新计算 Dialog 高度(不额外扩展回调,保持接口简单)
if (this.options.onExpandedChange) {
this.options.onExpandedChange(this.isExpanded);
}
} }
/** /**
@@ -230,13 +277,44 @@ export class MeasurePanel implements IBimComponent {
* 打开设置(本次只预留方法/回调) * 打开设置(本次只预留方法/回调)
*/ */
public openSettings(): void { public openSettings(): void {
// 进入设置面板(组件内部逻辑)
this.enterSettingsView();
// 仍然保留回调(如果外部想监听)
if (this.options.onSettings) { if (this.options.onSettings) {
this.options.onSettings(); this.options.onSettings();
return; }
}
/**
* 获取当前测量配置
*/
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);
} }
// 兜底:避免无声失败,打印中文日志(符合项目规范 // 配置变化会影响数值显示(单位/精度
console.warn('[MeasurePanel] 未提供设置回调 onSettings当前仅预留接口。'); this.renderResult();
// 如果当前在设置面板,表单也需要同步
if (this.view === 'settings') {
this.syncSettingsFormFromConfig(next);
}
} }
/** /**
@@ -270,6 +348,10 @@ export class MeasurePanel implements IBimComponent {
const root = document.createElement('div'); const root = document.createElement('div');
root.className = 'bim-measure-panel'; root.className = 'bim-measure-panel';
// 主视图容器(默认显示)
this.mainViewEl = document.createElement('div');
this.mainViewEl.className = 'bim-measure-main';
// 顶部:工具按钮区 // 顶部:工具按钮区
const toolsBox = document.createElement('div'); const toolsBox = document.createElement('div');
toolsBox.className = 'bim-measure-tools'; toolsBox.className = 'bim-measure-tools';
@@ -355,25 +437,12 @@ export class MeasurePanel implements IBimComponent {
toggleBox.appendChild(this.toggleBtn); toggleBox.appendChild(this.toggleBtn);
toolsBox.appendChild(toggleBox); toolsBox.appendChild(toggleBox);
root.appendChild(toolsBox); this.mainViewEl.appendChild(toolsBox);
// 中部:结果区 // 中部:结果区
const resultBox = document.createElement('div'); const resultBox = document.createElement('div');
resultBox.className = 'bim-measure-result'; 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'); const mainValueRow = document.createElement('div');
mainValueRow.className = 'bim-measure-row'; mainValueRow.className = 'bim-measure-row';
@@ -383,6 +452,18 @@ export class MeasurePanel implements IBimComponent {
const mainValueValue = document.createElement('span'); const mainValueValue = document.createElement('span');
mainValueValue.className = 'value'; mainValueValue.className = 'value';
this.mainValueValueEl = mainValueValue; 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(mainValueLabel);
mainValueRow.appendChild(mainValueValue); mainValueRow.appendChild(mainValueValue);
resultBox.appendChild(mainValueRow); resultBox.appendChild(mainValueRow);
@@ -390,27 +471,28 @@ export class MeasurePanel implements IBimComponent {
// XYZ // XYZ
const xyzBox = document.createElement('div'); const xyzBox = document.createElement('div');
xyzBox.className = 'bim-measure-xyz'; xyzBox.className = 'bim-measure-xyz';
this.xyzBoxEl = xyzBox;
const makeXyzRow = (labelKey: string, valueElSetter: (el: HTMLElement) => void) => { const makeXyzRow = (labelKey: string, valueClassName: string, valueElSetter: (el: HTMLElement) => void) => {
const row = document.createElement('div'); const row = document.createElement('div');
row.className = 'bim-measure-row'; row.className = 'bim-measure-row';
const label = document.createElement('span'); const label = document.createElement('span');
label.className = 'label'; label.className = 'label';
label.dataset.i18nKey = labelKey; label.dataset.i18nKey = labelKey;
const value = document.createElement('span'); const value = document.createElement('span');
value.className = 'value'; value.className = `value ${valueClassName}`;
valueElSetter(value); valueElSetter(value);
row.appendChild(label); row.appendChild(label);
row.appendChild(value); row.appendChild(value);
return row; return row;
}; };
xyzBox.appendChild(makeXyzRow('measure.labels.x', (el) => (this.xyzXEl = el))); xyzBox.appendChild(makeXyzRow('measure.labels.x', 'bim-measure-xyz-x', (el) => (this.xyzXEl = el)));
xyzBox.appendChild(makeXyzRow('measure.labels.y', (el) => (this.xyzYEl = el))); xyzBox.appendChild(makeXyzRow('measure.labels.y', 'bim-measure-xyz-y', (el) => (this.xyzYEl = el)));
xyzBox.appendChild(makeXyzRow('measure.labels.z', (el) => (this.xyzZEl = el))); xyzBox.appendChild(makeXyzRow('measure.labels.z', 'bim-measure-xyz-z', (el) => (this.xyzZEl = el)));
resultBox.appendChild(xyzBox); resultBox.appendChild(xyzBox);
root.appendChild(resultBox); this.mainViewEl.appendChild(resultBox);
// 底部:删除全部 + 设置 // 底部:删除全部 + 设置
const footer = document.createElement('div'); const footer = document.createElement('div');
@@ -437,11 +519,230 @@ export class MeasurePanel implements IBimComponent {
footer.appendChild(this.clearBtn); footer.appendChild(this.clearBtn);
footer.appendChild(this.settingsBtn); footer.appendChild(this.settingsBtn);
root.appendChild(footer); this.mainViewEl.appendChild(footer);
// 设置视图容器(默认隐藏)
this.settingsViewEl = this.createSettingsDom();
root.appendChild(this.mainViewEl);
root.appendChild(this.settingsViewEl);
return root; 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 个按钮 * 应用“展开/收起”状态:默认只显示前 4 个按钮
*/ */
@@ -482,23 +783,53 @@ export class MeasurePanel implements IBimComponent {
* 渲染结果区(根据 activeMode 从 result 里取对应字段) * 渲染结果区(根据 activeMode 从 result 里取对应字段)
*/ */
private renderResult(): void { private renderResult(): void {
// 1) 主值 // 1) 根据模式决定结果区显示规则
const mainText = this.formatMainValue(this.activeMode, this.result); // 你给的规则:
this.mainValueValueEl.textContent = mainText; // - 距离:显示数值 + xyz
// - 最小距离:只显示数值
// - 角度:--°
// - 标高:--m固定 m
// - 体积:--mm³单位随设置变动即 unit³
// - 激光测距:不显示任何数值/xyz只显示“激光测距”文字
// - 坡度:--%
// - 空间体积:--mm³单位随设置变动即 unit³
// 2) XYZ // 1.1) 主行:默认显示 label + value数值/单位拆分)
const xyz = this.result?.xyz; // 激光测距:只显示文字,因此隐藏 label/单位
if (!xyz) { if (this.activeMode === 'laserDistance') {
this.xyzXEl.textContent = '--'; this.mainValueLabelEl.style.display = 'none';
this.xyzYEl.textContent = '--'; this.mainNumberEl.textContent = t(this.getModeI18nKey('laserDistance'));
this.xyzZEl.textContent = '--'; 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; return;
} }
// 为了可读性:这里不做 fancy formatter只做基础展示 // 非 distance隐藏 xyz
this.xyzXEl.textContent = this.formatNumber(xyz.x); this.xyzBoxEl.style.display = 'none';
this.xyzYEl.textContent = this.formatNumber(xyz.y);
this.xyzZEl.textContent = this.formatNumber(xyz.z);
} }
/** /**
@@ -515,53 +846,144 @@ export class MeasurePanel implements IBimComponent {
return `measure.labels.value.${mode}`; return `measure.labels.value.${mode}`;
} }
/** // 注意:旧的 formatMainValue/formatWithFixedUnit 已被 formatMainValueParts 替代,
* 将“当前模式”的主值格式化为文本 // 以支持“数值与单位分色显示”和“无数据时仍展示单位”。
* @param mode 当前模式
* @param result 当前结果
*/
private formatMainValue(mode: MeasureMode, result: MeasureResult | null): string {
if (!result) return '--';
// 根据不同 mode 读取对应字段并格式化单位 /**
// 单位文本也走国际化(可替换为英文/中文 * 基础数字格式化(按精度显示
switch (mode) { */
case 'distance': private formatNumberWithPrecision(value: number, precision: MeasurePrecision): string {
return this.formatWithUnit(result.distanceMm, 'measure.units.mm'); // 你要求精度可选0 / 0.0 / 0.00 / 0.000,因此这里不做 trim严格按 toFixed 输出
case 'minDistance': return value.toFixed(precision);
return this.formatWithUnit(result.minDistanceMm, 'measure.units.mm'); }
case 'angle':
return this.formatWithUnit(result.angleDeg, 'measure.units.deg'); // 注意:旧的 formatLengthWithConfig 已被 formatLengthParts 替代。
case 'elevation':
return this.formatWithUnit(result.elevationMm, 'measure.units.mm'); private convertMmToUnit(mm: number, unit: MeasureUnit): number {
case 'volume': switch (unit) {
return this.formatWithUnit(result.volumeM3, 'measure.units.m3'); case 'mm':
case 'laserDistance': return mm;
return this.formatWithUnit(result.laserDistanceMm, 'measure.units.mm'); case 'cm':
case 'slope': return mm / 10;
return this.formatWithUnit(result.slopePercent, 'measure.units.percent'); case 'm':
case 'spaceVolume': return mm / 1000;
return this.formatWithUnit(result.spaceVolumeM3, 'measure.units.m3'); case 'km':
return mm / 1_000_000;
default: default:
return '--'; 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 formatWithUnit(value: number | undefined, unitKey: string): string { private formatMainValueParts(mode: MeasureMode, result: MeasureResult | null): { numberText: string; unitText: string } {
if (value === null || value === undefined || Number.isNaN(value)) return '--'; if (mode === 'laserDistance') return { numberText: t(this.getModeI18nKey('laserDistance')), unitText: '' };
return `${this.formatNumber(value)} ${t(unitKey)}`;
// 没有数据:显示 `-- 单位`
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':
private formatNumber(value: number): string { case 'minDistance':
// 保留 3 位小数以内(简单策略:整数不带小数,非整数保留到 3 位) return { numberText: '--', unitText: t(this.getUnitI18nKey(this.config.unit)) };
if (Number.isInteger(value)) return String(value); case 'angle':
return value.toFixed(3).replace(/0+$/g, '').replace(/\.$/g, ''); 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 };
} }
} }

View File

@@ -23,6 +23,28 @@ export type MeasureMode =
| 'slope' // 坡度 | 'slope' // 坡度
| 'spaceVolume'; // 空间体积 | 'spaceVolume'; // 空间体积
/**
* 距离/标高等“长度类”单位
*/
export type MeasureUnit = 'm' | 'cm' | 'mm' | 'km';
/**
* 精度(小数位数)
* - 0 -> 0
* - 1 -> 0.0
* - 2 -> 0.00
* - 3 -> 0.000
*/
export type MeasurePrecision = 0 | 1 | 2 | 3;
/**
* 测量配置项(由组件内部维护默认值,并读取/写入缓存)
*/
export interface MeasureConfig {
unit: MeasureUnit;
precision: MeasurePrecision;
}
/** /**
* 3D 坐标(可选展示) * 3D 坐标(可选展示)
*/ */
@@ -83,6 +105,7 @@ export interface MeasurePanelOptions {
/** /**
* “设置”回调 * “设置”回调
* 说明:设置按钮点击时会先在组件内部进入设置面板,再触发该回调(如果存在)
*/ */
onSettings?: () => void; onSettings?: () => void;

View File

@@ -87,9 +87,20 @@ export const enUS: TranslationDictionary = {
}, },
units: { units: {
mm: 'mm', mm: 'mm',
cm: 'cm',
m: 'm',
km: 'km',
deg: '°', deg: '°',
m3: 'm³', m3: 'm³',
percent: '%', percent: '%',
},
settings: {
title: 'Settings',
unit: 'Unit:',
precision: 'Precision:',
hint: 'Distance, min distance and elevation use this unit by default; angle and volume use their own units.',
save: 'Save',
cancel: 'Cancel',
} }
} }
}; };

View File

@@ -106,10 +106,25 @@ export interface TranslationDictionary {
*/ */
units: { units: {
mm: string; mm: string;
cm: string;
m: string;
km: string;
deg: string; deg: string;
m3: string; m3: string;
percent: string; percent: string;
}; };
/**
* 设置面板(单位/精度)
*/
settings: {
title: string;
unit: string;
precision: string;
hint: string;
save: string;
cancel: string;
};
} }
} }

View File

@@ -87,9 +87,20 @@ export const zhCN: TranslationDictionary = {
}, },
units: { units: {
mm: 'mm', mm: 'mm',
cm: 'cm',
m: 'm',
km: 'km',
deg: '°', deg: '°',
m3: 'm³', m3: 'm³',
percent: '%', percent: '%',
},
settings: {
title: '设置',
unit: '单位:',
precision: '精度:',
hint: '距离、最小距离和标高默认使用该单位;角度和体积有各自默认单位。',
save: '保存设置',
cancel: '取消',
} }
} }
}; };

View File

@@ -2,7 +2,7 @@ import {BimComponent} from '../core/component';
import {BimEngine} from '../bim-engine'; import {BimEngine} from '../bim-engine';
import {BimDialog} from "../components/dialog"; import {BimDialog} from "../components/dialog";
import { MeasurePanel } from '../components/measure-panel'; import { MeasurePanel } from '../components/measure-panel';
import type { MeasureMode, MeasureResult } from '../components/measure-panel/types'; import type { MeasureConfig, MeasureMode, MeasureResult } from '../components/measure-panel/types';
/** /**
* 测量弹窗管理器 * 测量弹窗管理器
@@ -11,6 +11,11 @@ export class MeasureDialogManager extends BimComponent {
private dialogId = 'measure-dialog'; private dialogId = 'measure-dialog';
private dialog: BimDialog | null = null; private dialog: BimDialog | null = null;
private panel: MeasurePanel | null = null; private panel: MeasurePanel | null = null;
/**
* 测量配置项(单位/精度)
* 说明MeasurePanel 会自行从缓存加载默认配置Manager 这里只做“对外读取/设置”的镜像。
*/
private config: MeasureConfig | null = null;
constructor(engine: BimEngine) { constructor(engine: BimEngine) {
super(engine); super(engine);
@@ -63,6 +68,8 @@ export class MeasureDialogManager extends BimComponent {
} }
}); });
this.panel.init(); this.panel.init();
// 同步一次当前配置(由组件从缓存/默认加载)
this.config = this.panel.getConfig();
// 注意:你要求“组件本身不加边距”,因此在 Manager 这里用 wrapper 增加左右内边距 // 注意:你要求“组件本身不加边距”,因此在 Manager 这里用 wrapper 增加左右内边距
// 这样 MeasurePanel 可以保持通用性,避免在不同场景复用时产生多余 padding。 // 这样 MeasurePanel 可以保持通用性,避免在不同场景复用时产生多余 padding。
@@ -109,14 +116,56 @@ export class MeasureDialogManager extends BimComponent {
} }
/** /**
* 设置测量结果(由外部注入,仅用于显示 * 设置测量结果(推荐使用的新方法名
* 说明:内部直接调用 MeasurePanel.setResult()
* @param result 测量结果;传 null 表示清空 * @param result 测量结果;传 null 表示清空
*/ */
public setResult(result: MeasureResult | null): void { public setMeasureResult(result: MeasureResult | null): void {
if (!this.panel) return; // 按你的要求:仅当 panel 存在时才调用,不做缓存
if (!this.panel) {
return;
}
this.panel.setResult(result); this.panel.setResult(result);
} }
/**
* 获取测量配置(单位/精度)
* - 如果面板存在:返回面板当前配置
* - 否则:返回 Manager 缓存的最后一次配置(可能为 null
*/
public getConfig(): MeasureConfig | null {
if (this.panel) {
this.config = this.panel.getConfig();
}
return this.config ? { ...this.config } : null;
}
/**
* 设置测量配置(单位/精度)
* @param partial 部分更新
* @param persist 是否写入缓存(默认 true
*/
public setConfig(partial: Partial<MeasureConfig>, persist: boolean = true): void {
// 面板存在则直接设置面板;否则仅更新 Manager 缓存
if (this.panel) {
this.panel.setConfig(partial, persist);
this.config = this.panel.getConfig();
// 配置变化可能影响高度(比如设置面板显示/隐藏),安全起见做一次 fit
this.dialog?.fitHeight(false);
return;
}
// 面板未创建:只更新本地缓存
const prev = this.config;
const next: MeasureConfig = {
unit: partial.unit ?? prev?.unit ?? 'mm',
precision: partial.precision ?? prev?.precision ?? 2
};
this.config = next;
// 注意:缓存写入由 MeasurePanel 负责(你要求默认维护在组件里)
// 这里不写 localStorage避免重复逻辑。
}
/** /**
* 删除全部(仅清空 UI真实测量清理逻辑后续再接 * 删除全部(仅清空 UI真实测量清理逻辑后续再接
*/ */