自定义对话框
大约 10 分钟
自定义对话框
创建自定义对话框和面板的完整指南。
对话框类型
WebCAD 支持两种对话框模式:
| 类型 | 特点 | 适用场景 |
|---|---|---|
| 模态对话框 | 弹出后图形界面不能操作 | 复杂设置、数据输入、需要拾取点/实体 |
| 非模态面板 | 图形界面仍可操作 | 工具面板、查找替换、实时预览 |
UI 风格规范
WebCAD 采用深色主题风格,建议使用以下配色:
/* 主要颜色 */
--bg-primary: #1e2530; /* 主背景 */
--bg-secondary: #0d1117; /* 次级背景(输入框、列表) */
--bg-header: #252d3a; /* 头部背景 */
--border-color: #3d4a5c; /* 边框颜色 */
--text-primary: #e8eaed; /* 主文字 */
--text-secondary: #9ca3af; /* 次级文字 */
--text-muted: #6b7280; /* 弱化文字 */
--accent-color: #58a6ff; /* 强调色 */
--btn-primary: #1a56db; /* 主按钮 */
--btn-hover: #1e40af; /* 按钮悬停 */通用样式模板
/* 容器 */
.dialog-container {
background: #1e2530;
border: 1px solid #3d4a5c;
border-radius: 8px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 13px;
color: #e8eaed;
}
/* 头部 */
.header {
padding: 12px 16px;
background: #252d3a;
border-bottom: 1px solid #3d4a5c;
border-radius: 8px 8px 0 0;
}
/* 输入框 */
.input {
background: #0d1117;
border: 1px solid #3d4a5c;
border-radius: 4px;
padding: 8px 12px;
color: #e8eaed;
font-size: 13px;
}
.input:focus {
outline: none;
border-color: #58a6ff;
}
/* 按钮 */
.btn {
background: #2d3748;
border: 1px solid #3d4a5c;
border-radius: 4px;
padding: 8px 16px;
color: #e8eaed;
cursor: pointer;
}
.btn:hover {
background: #3d4a5c;
}
.btn-primary {
background: #1a56db;
border-color: #1a56db;
}
.btn-primary:hover {
background: #1e40af;
}
/* 复选框 */
.checkbox input[type="checkbox"] {
width: 14px;
height: 14px;
accent-color: #58a6ff;
}模态对话框
使用 base-dialog 组件创建模态对话框。
基本结构
import { html, LitElement, Engine, type TemplateResult } from 'vjcad';
export class MyDialog extends LitElement {
private baseDialog!: any;
// 禁用 Shadow DOM(如需使用第三方库 CSS)
createRenderRoot() {
return this;
}
static properties = {
// 声明响应式属性
myValue: { type: String }
};
declare myValue: string;
constructor() {
super();
this.myValue = '';
}
async firstUpdated(): Promise<void> {
this.baseDialog = this.querySelector('base-dialog');
}
/**
* 启动对话框
*/
async startDialog(): Promise<void> {
// 添加到对话框容器
Engine.dialog!.appendChild(this);
await this.updateComplete;
// 启动对话框
await this.baseDialog?._startBaseDialog({
title: "我的对话框",
renderTarget: this.renderRoot
});
// 对话框关闭后清理
this.remove();
}
/**
* 关闭对话框
*/
private close(): void {
this.baseDialog?.close();
}
render() {
return html`
<style>
my-dialog {
display: block;
}
my-dialog #container {
width: 400px;
background: #1e2530;
color: #e8eaed;
padding: 16px;
}
/* ... 其他样式 ... */
</style>
<base-dialog>
<div id="container">
<div class="body">
<!-- 对话框内容 -->
<input type="text" .value=${this.myValue}
@input=${(e: Event) => this.myValue = (e.target as HTMLInputElement).value} />
</div>
<div class="buttons">
<button @click=${this.close}>取消</button>
<button class="btn-primary" @click=${this.onConfirm}>确定</button>
</div>
</div>
</base-dialog>
`;
}
private onConfirm(): void {
// 处理确认逻辑
this.close();
}
}
// 注册自定义元素
customElements.define('my-dialog', MyDialog);调用对话框
const dialog = new MyDialog();
await dialog.startDialog();模态对话框中拾取点和实体
模态对话框打开时,用户无法直接操作 CAD 界面。使用 suspend() 和 resume() 方法可以临时暂停对话框,允许用户在 CAD 界面进行拾取操作。
重要提示:对话框的
suspend()和resume()功能必须在命令上下文中使用!如果直接调用对话框(不在命令的
main()方法中),点击 CAD 界面会触发默认命令行为。正确用法:
- 创建命令类,在
main()方法中启动对话框- 使用
CommandRegistry.regist()注册命令- 使用
Engine.editor.executerWithOp('命令名')执行命令详见下方"完整示例"章节。
拾取点
import {
html, LitElement, Engine,
getPoint, getCorner,
PointInputOptions, CornerInputOptions,
InputStatusEnum, SelectionModeEnum
} from 'vjcad';
export class MyPickDialog extends LitElement {
private baseDialog!: any;
private selectedBounds: { minX: number; minY: number; maxX: number; maxY: number } | null = null;
/**
* 拾取矩形范围
*/
private async pickRange(): Promise<void> {
// 1. 暂停对话框,允许用户与 CAD 界面交互
this.baseDialog?.suspend();
try {
// 2. 获取第一个角点
const pointOptions = new PointInputOptions("指定范围的第一个角点:");
pointOptions.useOsnap = false; // 不使用对象捕捉
const point1Result = await getPoint(pointOptions);
if (point1Result.status !== InputStatusEnum.OK) {
return; // 用户取消
}
const firstPoint = point1Result.value;
// 3. 获取第二个角点(对角点),显示选择框
const cornerOptions = new CornerInputOptions("指定范围的对角点:", firstPoint);
cornerOptions.effect = SelectionModeEnum.Window; // 窗口选择效果
cornerOptions.useOsnap = false;
const cornerResult = await getCorner(cornerOptions);
if (cornerResult.status !== InputStatusEnum.OK) {
return; // 用户取消
}
const secondPoint = cornerResult.value;
// 4. 计算边界
this.selectedBounds = {
minX: Math.min(firstPoint.x, secondPoint.x),
minY: Math.min(firstPoint.y, secondPoint.y),
maxX: Math.max(firstPoint.x, secondPoint.x),
maxY: Math.max(firstPoint.y, secondPoint.y)
};
} finally {
// 5. 无论成功还是取消,都恢复对话框
this.baseDialog?.resume();
this.requestUpdate();
}
}
render() {
return html`
<base-dialog>
<div id="container">
<div class="row">
<span>选择范围:</span>
<button @click=${this.pickRange}>选择</button>
${this.selectedBounds ? html`
<span>已选择</span>
` : ''}
</div>
</div>
</base-dialog>
`;
}
}拾取实体
import { getSelections, SelectionInputOptions, InputStatusEnum } from 'vjcad';
export class MyEntityPickDialog extends LitElement {
private baseDialog!: any;
private selectedEntities: any[] = [];
/**
* 拾取实体
*/
private async pickEntities(): Promise<void> {
// 暂停对话框
this.baseDialog?.suspend();
try {
const options = new SelectionInputOptions("选择要处理的实体:");
const result = await getSelections(options);
if (result.status === InputStatusEnum.OK) {
this.selectedEntities = result.value;
}
} finally {
// 恢复对话框
this.baseDialog?.resume();
this.requestUpdate();
}
}
}关键方法
| 方法 | 说明 |
|---|---|
baseDialog.suspend() | 暂停对话框,允许 CAD 界面交互 |
baseDialog.resume() | 恢复对话框 |
非模态面板
非模态面板不阻止用户操作 CAD 界面,适合工具面板等场景。
基本结构
import { LitElement, html, css, Engine, type TemplateResult } from 'vjcad';
class MyPanel extends LitElement {
// 响应式属性
static properties = {
searchText: { type: String },
results: { type: Array }
};
declare searchText: string;
declare results: any[];
// 拖拽相关
private isDragging = false;
private dragStartX = 0;
private dragStartY = 0;
private panelStartLeft = 0;
private panelStartTop = 0;
// 样式定义
static styles = css`
:host {
position: fixed;
top: 100px;
right: 20px;
width: 360px;
background: #1e2530;
border: 1px solid #3d4a5c;
border-radius: 8px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 13px;
color: #e8eaed;
z-index: 100000;
display: none;
flex-direction: column;
user-select: none;
}
:host([visible]) {
display: flex;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background: #252d3a;
border-bottom: 1px solid #3d4a5c;
border-radius: 8px 8px 0 0;
cursor: grab;
}
.header:active {
cursor: grabbing;
}
.title {
font-weight: 600;
font-size: 14px;
}
.close-btn {
background: none;
border: none;
color: #9ca3af;
font-size: 20px;
cursor: pointer;
padding: 0 4px;
line-height: 1;
}
.close-btn:hover {
color: #fff;
}
.body {
padding: 16px;
}
.input {
width: 100%;
background: #0d1117;
border: 1px solid #3d4a5c;
border-radius: 4px;
padding: 8px 12px;
color: #e8eaed;
font-size: 13px;
outline: none;
}
.input:focus {
border-color: #58a6ff;
}
.btn {
background: #2d3748;
border: 1px solid #3d4a5c;
border-radius: 4px;
padding: 6px 12px;
color: #e8eaed;
font-size: 12px;
cursor: pointer;
}
.btn:hover {
background: #3d4a5c;
}
.btn-primary {
background: #1a56db;
border-color: #1a56db;
}
.btn-primary:hover {
background: #1e40af;
}
`;
constructor() {
super();
this.searchText = '';
this.results = [];
// 绑定拖拽事件
this.handleMouseMove = this.handleMouseMove.bind(this);
this.handleMouseUp = this.handleMouseUp.bind(this);
}
connectedCallback(): void {
super.connectedCallback();
document.addEventListener('mousemove', this.handleMouseMove);
document.addEventListener('mouseup', this.handleMouseUp);
}
disconnectedCallback(): void {
super.disconnectedCallback();
document.removeEventListener('mousemove', this.handleMouseMove);
document.removeEventListener('mouseup', this.handleMouseUp);
}
// 拖拽处理
private handleHeaderMouseDown(e: MouseEvent): void {
if ((e.target as HTMLElement).classList.contains('close-btn')) return;
this.isDragging = true;
this.dragStartX = e.clientX;
this.dragStartY = e.clientY;
const rect = this.getBoundingClientRect();
this.panelStartLeft = rect.left;
this.panelStartTop = rect.top;
}
private handleMouseMove(e: MouseEvent): void {
if (!this.isDragging) return;
const dx = e.clientX - this.dragStartX;
const dy = e.clientY - this.dragStartY;
this.style.left = `${this.panelStartLeft + dx}px`;
this.style.top = `${this.panelStartTop + dy}px`;
this.style.right = 'auto';
}
private handleMouseUp(): void {
this.isDragging = false;
}
// 显示/隐藏
show(): void {
this.setAttribute('visible', '');
// 聚焦到输入框
this.updateComplete.then(() => {
const input = this.shadowRoot?.querySelector('.input') as HTMLInputElement;
input?.focus();
});
}
hide(): void {
this.removeAttribute('visible');
}
destroy(): void {
this.remove();
}
render(): TemplateResult {
return html`
<div class="header" @mousedown=${this.handleHeaderMouseDown}>
<span class="title">我的面板</span>
<button class="close-btn" title="关闭" @click=${this.hide}>×</button>
</div>
<div class="body">
<input
type="text"
class="input"
placeholder="输入内容"
.value=${this.searchText}
@input=${(e: Event) => this.searchText = (e.target as HTMLInputElement).value}
>
<div class="buttons" style="margin-top: 12px; display: flex; gap: 8px;">
<button class="btn btn-primary" @click=${this.doSearch}>搜索</button>
</div>
</div>
`;
}
private doSearch(): void {
// 搜索逻辑
Engine.writeMessage(`<br/>搜索: ${this.searchText}`);
}
}
// 注册自定义元素
if (!customElements.get('my-panel')) {
customElements.define('my-panel', MyPanel);
}创建和使用面板
export interface MyPanelInterface {
show(): void;
hide(): void;
destroy(): void;
}
export function createMyPanel(): MyPanelInterface {
const panel = document.createElement('my-panel') as MyPanel;
document.body.appendChild(panel);
return {
show: () => panel.show(),
hide: () => panel.hide(),
destroy: () => panel.destroy()
};
}
// 使用
const panel = createMyPanel();
panel.show();
// ... 用户操作 ...
panel.hide();
panel.destroy();使用基类(推荐)
WebCAD 提供了 ModalDialogBase 和 ModelessPanelBase 两个基类,可以快速创建对话框和面板,无需手动处理样式、拖拽等细节。
ModalDialogBase - 模态对话框基类
内置深色主题样式和 suspend()/resume() 功能,只需实现 renderContent() 方法。
import { ModalDialogBase, html, Engine, CircleEnt } from 'vjcad';
class DrawCircleDialog extends ModalDialogBase<{ center: {x: number, y: number}, radius: number }> {
// 对话框标题
static dialogTitle = "画圆";
// 响应式属性
static properties = {
...ModalDialogBase.properties,
centerPoint: { type: Object },
radius: { type: Number },
};
centerPoint = null;
radius = 20;
// 拾取圆心
async pickCenter() {
this.suspend(); // 暂停对话框
const result = await Engine.editor.getPoint(new PointInputOptions("指定圆心:"));
this.resume(); // 恢复对话框
if (result.status === InputStatusEnum.OK) {
this.centerPoint = result.value;
}
}
// 必须实现:渲染对话框内容
renderContent() {
return html`
<div style="min-width: 300px;">
<div class="row">
<span class="label" style="width: 60px;">圆心:</span>
<span class="input" style="flex: 1;">${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="flex: 1;"
.value=${String(this.radius)}
@input=${(e) => this.radius = parseFloat(e.target.value)}>
</div>
</div>
`;
}
// 覆盖 confirm() 设置返回结果
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);
}
}
}ModalDialogBase API
| 属性/方法 | 说明 |
|---|---|
static dialogTitle | 对话框标题 |
result | 对话框返回结果 |
useShadowDOM | 是否使用 Shadow DOM,默认 true |
startDialog(options?) | 显示对话框并等待结果 |
renderContent() | 必须实现 - 渲染对话框内容 |
renderFooter() | 渲染底部按钮,默认为确定/取消 |
confirm() | 确定按钮回调,需设置 this.result |
cancel() | 取消按钮回调 |
close() | 关闭对话框 |
suspend() | 暂停对话框(用于拾取点/选择实体) |
resume() | 恢复对话框 |
ModelessPanelBase - 非模态面板基类
内置深色主题样式和拖拽功能,只需实现 renderContent() 方法。
import { ModelessPanelBase, html, Engine, LineEnt } from 'vjcad';
class DrawToolsPanel extends ModelessPanelBase {
// 面板配置
static panelTitle = "绘图工具";
static panelWidth = "200px";
static initialPosition = { top: '100px', right: '20px' };
drawLine() {
const line = new LineEnt([0, 0], [Math.random() * 100, Math.random() * 100]);
line.setDefaults();
Engine.addEntities(line);
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" @click=${() => Engine.zoomExtents()}>缩放全图</button>
</div>
`;
}
}
customElements.define('draw-tools-panel', DrawToolsPanel);
// 使用面板
const panel = new DrawToolsPanel();
document.body.appendChild(panel);
panel.show();使用 createPanel 工厂函数
import { createPanel } from 'vjcad';
const panelManager = createPanel(DrawToolsPanel, 'draw-tools-panel');
panelManager.show();
panelManager.hide();
panelManager.toggle();
panelManager.destroy();ModelessPanelBase API
| 属性/方法 | 说明 |
|---|---|
static panelTitle | 面板标题 |
static panelWidth | 面板宽度 |
static initialPosition | 初始位置,如 { top: '100px', right: '20px' } |
static maxHeight | 最大高度 |
renderContent() | 必须实现 - 渲染面板内容 |
getPanelTitle() | 获取标题(可覆盖实现动态标题) |
show() / hide() / toggle() | 显示控制 |
destroy() | 销毁面板 |
isVisible | 是否可见 |
onShow() / onHide() / onDestroy() | 生命周期钩子 |
内置 CSS 类
基类提供了一组内置的深色主题样式类,可直接在 renderContent() 中使用:
| CSS 类 | 说明 |
|---|---|
.row | 行容器,flex 布局 |
.label | 标签文字 |
.input | 输入框样式 |
.select | 下拉选择框样式 |
.btn | 普通按钮 |
.btn-primary | 主按钮(蓝色) |
.section-title | 分组标题 |
.hint | 提示文字 |
对比总结
| 特性 | 模态对话框 | 非模态面板 |
|---|---|---|
| 推荐基类 | ModalDialogBase | ModelessPanelBase |
| 组件基础 | base-dialog | 直接继承 LitElement |
| 挂载位置 | Engine.dialog!.appendChild() | document.body.appendChild() |
| CSS 定位 | 由 base-dialog 管理 | position: fixed |
| CAD 交互 | 需要 suspend()/resume() | 始终可交互 |
| 拖拽支持 | 内置 | 基类内置 |
| 显示控制 | startDialog()/close() | show()/hide()/toggle() |
| 适用场景 | 复杂表单、需要拾取 | 工具面板、实时操作 |
完整示例:命令上下文中的对话框
展示如何在命令上下文中正确使用 suspend()/resume() 进行点和实体拾取。
步骤概览
1. 创建对话框类(继承 LitElement)
2. 创建命令类(包含 main() 方法)
3. 注册命令(CommandRegistry.regist)
4. 执行命令(Engine.editor.executerWithOp)命令类示例
import {
CommandDefinition, CommandRegistry, CommandOptions,
Engine, writeMessage
} from 'vjcad';
// 命令类:在 main() 中启动对话框
class MyPickCommand {
async main() {
writeMessage("<br/>启动拾取对话框...");
// 创建并启动对话框
const dialog = new MyPickDialog();
const result = await dialog.startDialog();
// 处理结果
if (result) {
writeMessage(`<br/>选择了 ${result.entities.length} 个实体`);
if (result.distance !== null) {
writeMessage(`<br/>测量距离: ${result.distance.toFixed(4)}`);
}
} else {
writeMessage("<br/>用户取消了操作");
}
}
}
// 注册命令
const cmdDef = new CommandDefinition(
'MYPICKDIALOG', // 命令名称
'拾取对话框示例', // 命令描述
MyPickCommand, // 命令类
new CommandOptions() // 命令选项
);
CommandRegistry.regist(cmdDef);
// 执行命令(在命令上下文中运行)
await Engine.editor.executerWithOp('MYPICKDIALOG');对话框类中的拾取方法
// 在对话框类中实现拾取
async pickEntities() {
// 1. 暂停对话框
this.baseDialog?.suspend();
try {
// 2. 调用输入接口
const options = new SelectionInputOptions("选择实体:");
const result = await Engine.editor.getSelections(options);
// 3. 处理结果
if (result.status === InputStatusEnum.OK) {
this.selectedEntities = result.value;
}
} finally {
// 4. 恢复对话框(无论成功还是取消)
this.baseDialog?.resume();
this.requestUpdate();
}
}
async pickPoint() {
this.baseDialog?.suspend();
try {
const options = new PointInputOptions("指定点:");
// 可选:显示橡皮线
if (this.startPoint) {
options.useBasePoint = true;
options.basePoint = new Point2D(this.startPoint.x, this.startPoint.y);
}
const result = await Engine.editor.getPoint(options);
if (result.status === InputStatusEnum.OK) {
this.pickedPoint = result.value;
}
} finally {
this.baseDialog?.resume();
this.requestUpdate();
}
}关键要点
| 要点 | 说明 |
|---|---|
| 必须在命令上下文 | 直接调用对话框会导致点击触发默认命令 |
suspend() 暂停对话框 | 允许用户与 CAD 界面交互 |
resume() 恢复对话框 | 放在 finally 块中确保一定执行 |
requestUpdate() | 恢复后刷新界面显示新数据 |
Engine.editor.getPoint() | 在命令上下文中使用 Engine.editor 方法 |