自定义对话框
创建自定义对话框和面板的指南。
对话框类型
| 类型 | 特点 | 适用场景 | 推荐基类 |
|---|---|---|---|
| 模态对话框 | 弹出后图形界面不能操作 | 复杂设置、数据输入、需要拾取点/实体 | ModalDialogBase |
| 非模态面板 | 图形界面仍可操作 | 工具面板、查找替换、实时预览 | ModelessPanelBase |
ModalDialogBase - 模态对话框基类
内置深色主题样式,支持 suspend()/resume() 暂停恢复功能。
基本用法
import { ModalDialogBase, html } from 'vjcad';
class SettingsDialog extends ModalDialogBase<{ color: number; width: number }> {
static dialogTitle = "设置";
static properties = {
...ModalDialogBase.properties,
color: { type: Number },
width: { type: Number },
};
color = 1;
width = 1;
// 必须实现:渲染对话框内容
renderContent() {
return html`
<div style="min-width: 300px;">
<div class="row">
<span class="label" style="width: 80px;">颜色:</span>
<input type="number" class="input" style="flex: 1;"
.value=${String(this.color)}
@input=${(e) => this.color = parseInt(e.target.value)}>
</div>
<div class="row" style="margin-top: 12px;">
<span class="label" style="width: 80px;">线宽:</span>
<input type="number" class="input" style="flex: 1;"
.value=${String(this.width)}
@input=${(e) => this.width = parseInt(e.target.value)}>
</div>
</div>
`;
}
// 覆盖 confirm() 设置返回结果
confirm() {
this.result = { color: this.color, width: this.width };
this.close();
}
}
customElements.define('settings-dialog', SettingsDialog);
// 使用
const dialog = new SettingsDialog();
const result = await dialog.startDialog();
if (result) {
console.log('设置:', result.color, result.width);
}
使用 suspend/resume 拾取点
重要:
suspend()/resume()必须在命令上下文中使用!
import {
ModalDialogBase, html, Engine, CircleEnt,
PointInputOptions, InputStatusEnum, Point2D,
CommandDefinition, CommandRegistry, CommandOptions
} from 'vjcad';
class DrawCircleDialog extends ModalDialogBase<{ center: any, radius: number }> {
static dialogTitle = "画圆";
static properties = {
...ModalDialogBase.properties,
centerPoint: { type: Object },
radius: { type: Number },
};
centerPoint = null;
radius = 20;
// 拾取圆心
async pickCenter() {
this.suspend(); // 暂停对话框
const options = new PointInputOptions("指定圆心:");
const result = await Engine.editor.getPoint(options);
this.resume(); // 恢复对话框
if (result.status === InputStatusEnum.OK) {
this.centerPoint = result.value;
}
}
// 拾取半径(带橡皮线)
async pickRadius() {
if (!this.centerPoint) return;
this.suspend();
const options = new PointInputOptions("指定半径:");
options.useBasePoint = true;
options.basePoint = new Point2D(this.centerPoint.x, this.centerPoint.y);
const result = await Engine.editor.getPoint(options);
this.resume();
if (result.status === InputStatusEnum.OK) {
const dx = result.value.x - this.centerPoint.x;
const dy = result.value.y - this.centerPoint.y;
this.radius = Math.sqrt(dx * dx + dy * dy);
}
}
renderContent() {
return html`
<div style="min-width: 320px;">
<div class="section-title">圆参数</div>
<div class="row">
<span class="label" style="width: 60px;">圆心:</span>
<span class="input" style="flex: 1; background: #0d1117;">
${this.centerPoint ? `(${this.centerPoint.x.toFixed(2)}, ${this.centerPoint.y.toFixed(2)})` : '未指定'}
</span>
<button class="btn" @click=${this.pickCenter}>拾取</button>
</div>
<div class="row" style="margin-top: 12px;">
<span class="label" style="width: 60px;">半径:</span>
<input type="number" class="input" style="width: 80px;"
.value=${String(this.radius.toFixed(2))}
@input=${(e) => this.radius = parseFloat(e.target.value)}>
<button class="btn" @click=${this.pickRadius} ?disabled=${!this.centerPoint}>从图拾取</button>
</div>
</div>
`;
}
confirm() {
if (!this.centerPoint) return;
this.result = { center: this.centerPoint, radius: this.radius };
this.close();
}
}
customElements.define('draw-circle-dialog', DrawCircleDialog);
// 命令类
class DrawCircleCommand {
async main() {
const dialog = new DrawCircleDialog();
const result = await dialog.startDialog();
if (result) {
const circle = new CircleEnt([result.center.x, result.center.y], result.radius);
circle.setDefaults();
Engine.addEntities(circle);
Engine.zoomExtents();
}
}
}
// 注册并执行命令
CommandRegistry.regist(new CommandDefinition('DRAWCIRCLE', '画圆', DrawCircleCommand, new CommandOptions()));
await Engine.editor.executerWithOp('DRAWCIRCLE');
禁用 Shadow DOM
需要使用第三方库(如 x-spreadsheet)时:
import { ModalDialogBase, createNoShadowStyles, html } from 'vjcad';
class ThirdPartyDialog extends ModalDialogBase {
static dialogTitle = "使用第三方库";
useShadowDOM = false; // 禁用 Shadow DOM
renderContent() {
return html`<div id="spreadsheet-container"></div>`;
}
// 需要手动注入样式
render() {
return html`
<style>${createNoShadowStyles('third-party-dialog')}</style>
<base-dialog>
<div class="dialog-body">${this.renderContent()}</div>
<div class="dialog-footer">${this.renderFooter()}</div>
</base-dialog>
`;
}
}
API 参考
| 属性/方法 | 说明 |
|---|---|
static dialogTitle |
对话框标题 |
result |
对话框返回结果 |
useShadowDOM |
是否使用 Shadow DOM,默认 true |
startDialog(options?) |
显示对话框并等待结果 |
renderContent() |
必须实现 - 渲染对话框内容 |
renderFooter() |
渲染底部按钮,默认为确定/取消 |
confirm() |
确定按钮回调,需设置 this.result 后调用 this.close() |
cancel() |
取消按钮回调 |
close() |
关闭对话框 |
suspend() |
暂停对话框(用于拾取点/选择实体) |
resume() |
恢复对话框 |
isSuspended |
是否处于暂停状态 |
ModelessPanelBase - 非模态面板基类
内置深色主题样式和拖拽功能,不阻止用户操作 CAD 界面。
基本用法
import { ModelessPanelBase, html, Engine, LineEnt, CircleEnt } from 'vjcad';
class DrawToolsPanel extends ModelessPanelBase {
static panelTitle = "绘图工具";
static panelWidth = "200px";
static initialPosition = { top: '100px', right: '20px' };
drawLine() {
const line = new LineEnt(
[Math.random() * 100, Math.random() * 100],
[Math.random() * 100, Math.random() * 100]
);
line.setDefaults();
line.color = Math.floor(Math.random() * 7) + 1;
Engine.addEntities(line);
Engine.zoomExtents();
}
drawCircle() {
const circle = new CircleEnt(
[Math.random() * 100, Math.random() * 100],
Math.random() * 20 + 10
);
circle.setDefaults();
Engine.addEntities(circle);
Engine.zoomExtents();
}
// 必须实现:渲染面板内容
renderContent() {
return html`
<div style="display: flex; flex-direction: column; gap: 8px;">
<button class="btn btn-primary" @click=${this.drawLine}>画直线</button>
<button class="btn btn-primary" @click=${this.drawCircle}>画圆</button>
<div style="border-top: 1px solid #3d4a5c; margin: 8px 0;"></div>
<button class="btn" @click=${() => Engine.zoomExtents()}>缩放全图</button>
</div>
`;
}
}
customElements.define('draw-tools-panel', DrawToolsPanel);
// 使用
const panel = new DrawToolsPanel();
document.body.appendChild(panel);
panel.show();
带状态的面板
class PropertiesPanel extends ModelessPanelBase {
static panelTitle = "属性设置";
static panelWidth = "280px";
static initialPosition = { top: '100px', left: '20px' };
static properties = {
...ModelessPanelBase.properties,
currentColor: { type: Number },
lineWeight: { type: Number },
};
constructor() {
super();
this.currentColor = 7;
this.lineWeight = 1;
}
renderContent() {
return html`
<div>
<div class="section-title">常规属性</div>
<div class="row">
<span class="label" style="width: 60px;">颜色:</span>
<input type="number" class="input" style="flex: 1;"
.value=${String(this.currentColor)}
@input=${(e) => this.currentColor = parseInt(e.target.value)}>
</div>
<div class="row" style="margin-top: 8px;">
<span class="label" style="width: 60px;">线宽:</span>
<input type="number" class="input" style="flex: 1;"
.value=${String(this.lineWeight)}
@input=${(e) => this.lineWeight = parseInt(e.target.value)}>
</div>
<div style="margin-top: 16px;">
<button class="btn btn-primary" style="width: 100%;" @click=${this.applySettings}>
应用设置
</button>
</div>
</div>
`;
}
applySettings() {
console.log('应用设置:', this.currentColor, this.lineWeight);
}
}
动态标题
class EntityInfoPanel extends ModelessPanelBase {
static panelTitle = "实体信息";
entityCount = 0;
// 覆盖 onShow 在显示时刷新数据
onShow() {
super.onShow();
this.entityCount = Engine.getEntities(e => e.isAlive).length; // getEntities 含已删除实体
}
// 覆盖 getPanelTitle 实现动态标题
getPanelTitle() {
return `实体信息 (${this.entityCount})`;
}
renderContent() {
return html`
<div>
<div class="status">共 <strong style="color: #58a6ff;">${this.entityCount}</strong> 个实体</div>
<button class="btn" style="width: 100%; margin-top: 12px;" @click=${() => { this.entityCount = Engine.getEntities(e => e.isAlive).length; this.requestUpdate(); }}>
刷新
</button>
</div>
`;
}
}
使用 createPanel 工厂函数
import { createPanel } from 'vjcad';
const panelManager = createPanel(PropertiesPanel, 'properties-panel');
panelManager.show();
panelManager.hide();
panelManager.toggle();
panelManager.destroy();
if (panelManager.isVisible) {
console.log('面板可见');
}
API 参考
| 属性/方法 | 说明 |
|---|---|
static panelTitle |
面板标题 |
static panelWidth |
面板宽度,如 "360px" |
static initialPosition |
初始位置,如 { top: '100px', right: '20px' } |
static maxHeight |
最大高度,如 "80vh" |
renderContent() |
必须实现 - 渲染面板内容 |
getPanelTitle() |
获取标题(可覆盖实现动态标题) |
show() |
显示面板 |
hide() |
隐藏面板 |
toggle() |
切换显示状态 |
destroy() |
销毁面板 |
isVisible |
是否可见 |
onShow() |
面板显示时回调 |
onHide() |
面板隐藏时回调 |
onDestroy() |
面板销毁时回调 |
内置 CSS 类
基类提供深色主题样式类,可直接在 renderContent() 中使用:
| CSS 类 | 说明 |
|---|---|
.row |
行容器,flex 布局,align-items: center |
.label |
标签文字 |
.input |
输入框样式(深色背景) |
.select |
下拉选择框样式 |
.btn |
普通按钮 |
.btn-primary |
主按钮(蓝色) |
.section-title |
分组标题 |
.hint |
提示文字(灰色) |
.status |
状态文字 |
对比总结
| 特性 | ModalDialogBase | ModelessPanelBase |
|---|---|---|
| 挂载位置 | 自动挂载到 Engine.dialog |
document.body.appendChild() |
| CAD 交互 | 需要 suspend()/resume() |
始终可交互 |
| 拖拽支持 | 内置 | 内置 |
| 显示控制 | startDialog() 返回 Promise |
show()/hide()/toggle() |
| 返回值 | this.result |
无 |
| 适用场景 | 复杂表单、需要拾取 | 工具面板、实时操作 |
底层实现(高级)
如果基类不满足需求,可直接使用底层 base-dialog 组件或手动实现面板。
base-dialog CSS 变量
base-dialog {
--dialog-header-bg: #252d3a; /* 标题栏背景 */
--dialog-header-color: #e8eaed; /* 标题栏文字 */
--dialog-header-border: #3d4a5c; /* 标题栏边框 */
--dialog-contents-color: #1e2530; /* 内容区背景 */
}
手动创建模态对话框
import { html, LitElement, Engine } from 'vjcad';
class ManualDialog extends LitElement {
private baseDialog: any;
createRenderRoot() { return this; } // 禁用 Shadow DOM
async firstUpdated() {
this.baseDialog = this.querySelector('base-dialog');
}
async startDialog() {
Engine.dialog!.appendChild(this);
await this.updateComplete;
await this.baseDialog?._startBaseDialog({
title: "手动对话框",
renderTarget: this.renderRoot
});
this.remove();
}
render() {
return html`
<style>
manual-dialog, manual-dialog base-dialog {
--dialog-header-bg: #252d3a;
--dialog-header-color: #e8eaed;
}
</style>
<base-dialog>
<div style="padding: 16px; background: #1e2530; color: #e8eaed;">
内容
</div>
</base-dialog>
`;
}
}
关键方法
| 方法 | 说明 |
|---|---|
baseDialog._startBaseDialog({ title, renderTarget }) |
启动对话框 |
baseDialog.close() |
关闭对话框 |
baseDialog.suspend() |
暂停对话框 |
baseDialog.resume() |
恢复对话框 |