对话框组件
大约 12 分钟
对话框组件
对话框组件用于在插件中与用户进行交互,支持输入、确认、提示等常见场景。
在线示例
便捷函数(推荐)
WebCAD 提供了一组便捷函数,可以快速创建常用的对话框,无需手动管理组件生命周期。
输入对话框
import { showInputDialog, showSelectDialog, showPrompt } from 'vjcad';
// 1. 完整配置的输入对话框
const result = await showInputDialog({
title: '图层名称',
label: '请输入新图层的名称',
placeholder: '例如: Layer1',
defaultValue: 'NewLayer',
required: true,
validator: (value) => {
if (value.length > 31) return '名称不能超过31个字符';
if (!/^[a-zA-Z0-9_-]+$/.test(value)) return '只能包含字母、数字、下划线和连字符';
return null; // 返回 null 表示验证通过
}
});
if (result.confirmed) {
console.log('用户输入:', result.value);
}
// 2. 下拉选择对话框
const selectResult = await showSelectDialog({
title: '选择线型',
label: '请选择线型',
options: [
{ value: 'Continuous', label: '实线' },
{ value: 'Dashed', label: '虚线' },
{ value: 'Dotted', label: '点线' },
{ value: 'DashDot', label: '点划线' }
],
defaultValue: 'Continuous'
});
// 3. 简单输入提示框
const name = await showPrompt('请输入名称', '默认值', '标题');
if (name !== null) {
console.log('用户输入:', name);
}确认对话框
import { showConfirm, showWarningConfirm, showInfo, showError } from 'vjcad';
// 1. 普通确认
const confirmed = await showConfirm('确定要删除选中的图元吗?', '删除确认');
if (confirmed) {
// 执行删除操作
}
// 2. 警告确认(黄色警告图标)
const proceed = await showWarningConfirm(
'此操作不可撤销,确定要继续吗?',
'警告'
);
// 3. 信息提示(只有确定按钮)
await showInfo('操作已完成', '提示');
// 4. 错误提示(红色错误图标)
await showError('保存失败,请检查文件权限', '错误');接口定义
InputDialogConfig
interface InputDialogConfig {
/** 对话框标题 */
title: string;
/** 输入框标签文本 */
label?: string;
/** 占位符文本 */
placeholder?: string;
/** 默认值 */
defaultValue?: string;
/** 确定按钮文本,默认"确定" */
confirmText?: string;
/** 取消按钮文本,默认"取消" */
cancelText?: string;
/** 验证函数,返回错误消息或 null 表示通过 */
validator?: (value: string) => string | null | undefined;
/** 描述文本(显示在输入框下方) */
description?: string;
/** 是否必填 */
required?: boolean;
/** 输入框类型,如 'text', 'password' */
type?: string;
}SelectDialogConfig
interface SelectDialogConfig {
/** 对话框标题 */
title: string;
/** 选择框标签文本 */
label?: string;
/** 选项列表 */
options: Array<{
value: string;
label: string;
disabled?: boolean;
}>;
/** 默认选中值 */
defaultValue?: string;
/** 确定按钮文本 */
confirmText?: string;
/** 取消按钮文本 */
cancelText?: string;
/** 描述文本 */
description?: string;
}InputDialogResult
interface InputDialogResult {
/** 是否点击了确定按钮 */
confirmed: boolean;
/** 用户输入/选择的值 */
value?: string;
}YesNoDialogConfig
class YesNoDialogConfig {
/** 对话框标题,默认 "WebCAD" */
title: string = "WebCAD";
/** 消息内容(支持 HTML) */
message: string = "";
/** 对话框类型:'info' | 'warning' | 'error' | 'confirm' */
type: 'info' | 'warning' | 'error' | 'confirm' = 'confirm';
/** 是按钮标题,默认 "是(Y)" */
yesTitle: string = "是(Y)";
/** 否按钮标题,默认 "否(N)" */
noTitle: string = "否(N)";
/** 是否显示取消按钮 */
showCancel: boolean = false;
/** 取消按钮标题 */
cancelTitle: string = "取消";
/** 是否只显示确认按钮(无否按钮) */
confirmOnly: boolean = false;
}对话框暂停/恢复(交互式选择)
在自定义对话框中,有时需要让用户暂时离开对话框去 CAD 界面上选择实体或拾取点。BaseDialogComponent 提供了 suspend() 和 resume() 方法来实现这一功能。
重要
对话框的 suspend()/resume() 功能必须在命令上下文中运行。如果直接调用对话框(不在命令中),点击 CAD 界面会触发默认命令行为。
API 说明
| 方法/属性 | 说明 |
|---|---|
suspend() | 暂停对话框,关闭模态状态,允许用户操作 CAD 界面 |
resume() | 恢复对话框,重新以模态方式打开,恢复到原位置 |
isSuspended | 属性,检查对话框是否处于暂停状态 |
基本用法
import {
Engine, LitElement, html, css,
PointInputOptions, SelectionInputOptions, InputStatusEnum, Point2D,
CommandRegistry, CommandDefinition, CommandOptions
} from 'vjcad';
// 1. 创建对话框组件
class MyInteractiveDialog extends LitElement {
baseDialog = null;
result = null;
selectedEntities = [];
pickedPoint = null;
async startDialog() {
Engine.dialog.appendChild(this);
await this.updateComplete;
this.baseDialog = this.renderRoot.querySelector('base-dialog');
await this.baseDialog?._startBaseDialog({
title: "交互式对话框",
renderTarget: this.renderRoot
});
this.remove();
return this.result;
}
// 选择实体
async selectEntities() {
// 暂停对话框
this.baseDialog?.suspend();
// 调用编辑器的选择接口
const options = new SelectionInputOptions();
const result = await Engine.editor.getSelections(options);
// 恢复对话框
this.baseDialog?.resume();
if (result.status === InputStatusEnum.OK && result.value?.length > 0) {
this.selectedEntities = result.value;
this.requestUpdate();
}
}
// 拾取点
async pickPoint() {
// 暂停对话框
this.baseDialog?.suspend();
// 调用编辑器的拾取点接口
const options = new PointInputOptions("指定点:");
const result = await Engine.editor.getPoint(options);
// 恢复对话框
this.baseDialog?.resume();
if (result.status === InputStatusEnum.OK && result.value) {
this.pickedPoint = result.value;
this.requestUpdate();
}
}
// 拾取点(带橡皮线)
async pickPointWithRubberband(basePoint) {
this.baseDialog?.suspend();
const options = new PointInputOptions("指定下一点:");
if (basePoint) {
options.useBasePoint = true;
options.basePoint = new Point2D(basePoint.x, basePoint.y);
}
const result = await Engine.editor.getPoint(options);
this.baseDialog?.resume();
return result;
}
okCallback() {
this.result = { entities: this.selectedEntities, point: this.pickedPoint };
this.baseDialog?.close();
}
cancelCallback() {
this.result = null;
this.baseDialog?.close();
}
render() {
return html`
<base-dialog>
<div class="container">
<div class="row">
<span>已选实体: ${this.selectedEntities.length} 个</span>
<button @click=${this.selectEntities}>选择</button>
</div>
<div class="row">
<span>坐标: ${this.pickedPoint ?
`(${this.pickedPoint.x.toFixed(2)}, ${this.pickedPoint.y.toFixed(2)})` :
'未指定'}</span>
<button @click=${this.pickPoint}>拾取</button>
</div>
<div class="button-bar">
<button @click=${this.cancelCallback}>取消</button>
<button @click=${this.okCallback}>确定</button>
</div>
</div>
</base-dialog>
`;
}
static styles = css`
/* 深色主题样式 */
base-dialog {
--dialog-header-bg: #2d2d30;
--dialog-header-color: #f0f0f0;
--dialog-header-border: rgba(255,255,255,0.1);
--dialog-contents-color: #2d2d30;
}
.container { padding: 16px; background: #2d2d30; color: #f0f0f0; }
.row { display: flex; align-items: center; gap: 8px; margin: 8px 0; }
.button-bar { display: flex; justify-content: flex-end; gap: 8px; margin-top: 16px; }
button { padding: 6px 12px; background: #0e639c; color: white; border: none; border-radius: 4px; cursor: pointer; }
button:hover { background: #1177bb; }
`;
}
customElements.define('my-interactive-dialog', MyInteractiveDialog);
// 2. 创建命令类(重要!)
class MyInteractiveCommand {
async main() {
const dialog = new MyInteractiveDialog();
const result = await dialog.startDialog();
if (result) {
console.log('选择的实体:', result.entities);
console.log('拾取的点:', result.point);
}
}
}
// 3. 注册命令
const cmdDef = new CommandDefinition(
'MYINTERACTIVE',
'交互式对话框示例',
MyInteractiveCommand,
new CommandOptions()
);
CommandRegistry.regist(cmdDef);
CommandRegistry.updated = true;
// 4. 执行命令
await Engine.editor.executerWithOp('MYINTERACTIVE');Vue 3 集成
也可以使用 Vue 3 来实现对话框内容,通过动态加载 Vue 3 库:
// 动态加载 Vue 3
const loadVue3 = () => {
return new Promise((resolve, reject) => {
if (window.Vue) {
resolve(window.Vue);
return;
}
const script = document.createElement('script');
script.src = '/js/vue@3.js';
script.onload = () => resolve(window.Vue);
document.head.appendChild(script);
});
};
const Vue = await loadVue3();
const { createApp, ref } = Vue;
// 创建对话框类
class VueDialog {
constructor() {
this.baseDialog = null;
this.vueApp = null;
this.result = null;
this.state = { entityCount: 0, point: null };
}
async selectEntities() {
if (this.baseDialog) this.baseDialog.suspend();
const result = await Engine.editor.getSelections(new SelectionInputOptions());
if (this.baseDialog) this.baseDialog.resume();
if (result.status === InputStatusEnum.OK && result.value) {
this.state.entityCount = result.value.length;
}
}
async startDialog() {
// 创建容器
const container = document.createElement('div');
container.innerHTML = '<base-dialog><div id="vue-root"></div></base-dialog>';
Engine.dialog.appendChild(container);
this.baseDialog = container.querySelector('base-dialog');
// 创建 Vue 应用
const dialogInstance = this;
this.vueApp = createApp({
setup() {
const entityCount = ref(0);
// 定时同步状态
setInterval(() => {
entityCount.value = dialogInstance.state.entityCount;
}, 100);
return { entityCount, selectEntities: () => dialogInstance.selectEntities() };
},
template: `
<div class="container">
<p>已选实体: {{ entityCount }} 个</p>
<button @click="selectEntities">选择</button>
</div>
`
});
this.vueApp.mount(container.querySelector('#vue-root'));
await this.baseDialog._startBaseDialog({
title: "Vue3 对话框",
renderTarget: container
});
this.vueApp.unmount();
container.remove();
return this.result;
}
}自定义对话框
如果便捷函数无法满足需求,可以继承 BaseDialogComponent 创建自定义对话框。
基本结构
import { LitElement, html, css } from 'vjcad';
import { BaseDialogComponent } from 'vjcad';
import { Engine } from 'vjcad';
class MyCustomDialog extends BaseDialogComponent {
// 定义结果
result: any = undefined;
// 定义属性
static properties = {
...BaseDialogComponent.properties,
myValue: { type: String }
};
declare myValue: string;
private baseDialog: any;
constructor() {
super();
this.myValue = '';
}
firstUpdated(): void {
super.firstUpdated();
this.baseDialog = this.shadowRoot?.querySelector('base-dialog');
}
// 确定按钮
onConfirm(): void {
this.result = this.myValue;
this.baseDialog?.close();
}
// 取消按钮
onCancel(): void {
this.result = undefined;
this.baseDialog?.close();
}
// 显示对话框
async show(): Promise<any> {
Engine.dialog.appendChild(this);
await this.waitUpdated();
await this.baseDialog?._startBaseDialog({
title: '我的对话框',
renderTarget: (this as any).renderRoot
});
this.remove();
return this.result;
}
render() {
return html`
<base-dialog>
<div id="container">
<div id="content">
<!-- 自定义内容 -->
<input
.value=${this.myValue}
@input=${(e: Event) => this.myValue = (e.target as HTMLInputElement).value}
/>
</div>
<div id="button-bar">
<dlg-button accept="true" @click=${this.onConfirm}>
确定
</dlg-button>
<dlg-button @click=${this.onCancel}>
取消
</dlg-button>
</div>
</div>
</base-dialog>
`;
}
static styles = css`
#container {
padding: 16px;
background-color: var(--dialog-contents-color, #f5f7fa);
min-width: 300px;
}
#content {
margin-bottom: 16px;
}
#button-bar {
display: flex;
justify-content: flex-end;
gap: 10px;
}
`;
}
// 注册自定义元素
customElements.define('my-custom-dialog', MyCustomDialog);使用自定义对话框
const dialog = new MyCustomDialog();
const result = await dialog.show();
if (result !== undefined) {
console.log('用户输入:', result);
}DlgButtonComponent
对话框按钮组件,用于在自定义对话框中创建标准样式的按钮。
HTML 标签
<dlg-button>普通按钮</dlg-button>
<dlg-button accept="true">主按钮</dlg-button>
<dlg-button disabled>禁用按钮</dlg-button>属性
| 属性 | 类型 | 说明 |
|---|---|---|
accept | boolean | 是否为主按钮(蓝色背景) |
disabled | boolean | 是否禁用 |
CSS 变量
对话框组件支持以下 CSS 变量进行主题定制:
/* 在 base-dialog 上设置 CSS 变量 */
base-dialog {
/* 标题栏背景色 */
--dialog-header-bg: #d0d0d0;
/* 标题栏文字颜色 */
--dialog-header-color: #000;
/* 标题栏底部边框颜色 */
--dialog-header-border: rgba(0,0,0,.05);
/* 内容区背景色 */
--dialog-contents-color: #f5f7fa;
/* 元素边框颜色 */
--dialog-ele-border-color: #d8d8d8;
}
/* 深色主题示例 */
base-dialog {
--dialog-header-bg: #2d2d30;
--dialog-header-color: #f0f0f0;
--dialog-header-border: rgba(255,255,255,0.1);
--dialog-contents-color: #2d2d30;
}键盘快捷键
| 快捷键 | 作用 |
|---|---|
Enter | 确认/是 |
Escape | 取消/否 |
Y | 是(YesNoDialog) |
N | 否(YesNoDialog) |
对话框/面板基类
WebCAD 提供了两个基类用于快速创建自定义对话框和面板:
| 基类 | 说明 | 适用场景 |
|---|---|---|
ModalDialogBase | 模态对话框基类 | 需要用户完成操作后才能继续的场景 |
ModelessPanelBase | 非模态面板基类 | 工具面板、属性面板等需要持续显示的场景 |
ModalDialogBase - 模态对话框基类
模态对话框会阻止用户操作 CAD 界面,直到用户关闭对话框。内置深色主题样式,支持 suspend()/resume() 暂停恢复功能。
基本用法
import { ModalDialogBase, html } from 'vjcad';
class MySettingsDialog extends ModalDialogBase<{ color: number; width: number }> {
// 设置对话框标题
static dialogTitle = "设置";
// 定义属性
static properties = {
...ModalDialogBase.properties,
color: { type: Number },
width: { type: Number },
};
color = 1;
width = 1;
// 实现 renderContent() 渲染对话框内容
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('my-settings-dialog', MySettingsDialog);
// 使用对话框
const dialog = new MySettingsDialog();
const result = await dialog.startDialog();
if (result) {
console.log('设置:', result.color, result.width);
}使用 suspend/resume 拾取点
import {
ModalDialogBase, html,
PointInputOptions, InputStatusEnum, Point2D, Engine
} from 'vjcad';
class DrawCircleDialog extends ModalDialogBase {
static dialogTitle = "画圆";
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 && result.value) {
this.centerPoint = { x: result.value.x, y: result.value.y };
}
}
// 拾取半径(带橡皮线)
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 && result.value) {
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>
<div class="row">
<span class="label">圆心:</span>
<span>${this.centerPoint ? `(${this.centerPoint.x.toFixed(2)}, ${this.centerPoint.y.toFixed(2)})` : '未指定'}</span>
<button class="btn" @click=${this.pickCenter}>拾取</button>
</div>
<div class="row">
<span class="label">半径:</span>
<input type="number" class="input" .value=${String(this.radius)}
@input=${(e) => this.radius = parseFloat(e.target.value)}>
<button class="btn" @click=${this.pickRadius}>从图拾取</button>
</div>
</div>
`;
}
confirm() {
if (!this.centerPoint) return;
this.result = { center: this.centerPoint, radius: this.radius };
this.close();
}
}禁用 Shadow DOM
当需要使用第三方库(如 x-spreadsheet)时,可以禁用 Shadow DOM:
import { ModalDialogBase, createNoShadowStyles, html } from 'vjcad';
class NoShadowDialog extends ModalDialogBase {
static dialogTitle = "使用第三方库";
// 禁用 Shadow DOM
useShadowDOM = false;
renderContent() {
return html`<div>自定义内容</div>`;
}
// 需要手动注入样式
render() {
return html`
<style>${createNoShadowStyles('no-shadow-dialog')}</style>
<base-dialog>
<div class="dialog-body">${this.renderContent()}</div>
<div class="dialog-footer">${this.renderFooter()}</div>
</base-dialog>
`;
}
}ModalDialogBase API
| 属性/方法 | 说明 |
|---|---|
static dialogTitle | 对话框标题 |
result | 对话框返回结果 |
useShadowDOM | 是否使用 Shadow DOM,默认 true |
startDialog(options?) | 显示对话框并等待结果 |
renderContent() | 抽象方法 - 渲染对话框内容 |
renderFooter() | 渲染底部按钮,默认为确定/取消 |
confirm() | 确定按钮回调,需设置 this.result |
cancel() | 取消按钮回调 |
close() | 关闭对话框 |
suspend() | 暂停对话框(用于拾取点/选择实体) |
resume() | 恢复对话框 |
isSuspended | 是否处于暂停状态 |
ModelessPanelBase - 非模态面板基类
非模态面板不会阻止用户操作 CAD 界面,适合工具面板、属性面板等需要持续显示的场景。内置深色主题样式和拖拽功能。
基本用法
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], [100, 50]);
line.setDefaults();
Engine.addEntities(line);
Engine.zoomExtents();
}
// 实现 renderContent() 渲染面板内容
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();
// 控制显示
panel.hide();
panel.toggle();
panel.destroy();带状态的面板
class PropertiesPanel extends ModelessPanelBase {
static panelTitle = "属性设置";
static panelWidth = "280px";
static properties = {
...ModelessPanelBase.properties,
currentColor: { type: Number },
lineWeight: { type: Number },
};
constructor() {
super();
this.currentColor = 7;
this.lineWeight = 1;
}
renderContent() {
return html`
<div>
<div class="row">
<span class="label">颜色:</span>
<input type="number" class="input"
.value=${String(this.currentColor)}
@input=${(e) => this.currentColor = parseInt(e.target.value)}>
</div>
<div class="row">
<span class="label">线宽:</span>
<input type="number" class="input"
.value=${String(this.lineWeight)}
@input=${(e) => this.lineWeight = parseInt(e.target.value)}>
</div>
</div>
`;
}
}动态标题
class EntityInfoPanel extends ModelessPanelBase {
static panelTitle = "实体信息";
entityCount = 0;
// 覆盖 onShow 在显示时刷新数据
onShow() {
super.onShow();
this.entityCount = Engine.getEntities().length;
}
// 覆盖 getPanelTitle 实现动态标题
getPanelTitle() {
return `实体信息 (${this.entityCount})`;
}
renderContent() {
return html`<div>共 ${this.entityCount} 个实体</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('面板可见');
}ModelessPanelBase 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 类
基类提供了一组内置的深色主题样式类:
| CSS 类 | 说明 |
|---|---|
.row | 行容器,flex 布局 |
.label | 标签文字 |
.input | 输入框样式 |
.select | 下拉选择框样式 |
.btn | 普通按钮 |
.btn-primary | 主按钮(蓝色) |
.section-title | 分组标题 |
.hint | 提示文字 |
.status | 状态文字 |