唯杰地图 logo唯杰地图
VJMAP
EN

自定义对话框

vjcad/references/quickref/custom-dialog.md 15.1 KB503 行
原文

创建自定义对话框和面板的指南。

对话框类型

类型 特点 适用场景 推荐基类
模态对话框 弹出后图形界面不能操作 复杂设置、数据输入、需要拾取点/实体 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() 恢复对话框