# 对话框与消息

> 文档：`vjcad/doc/ui/dialogs.md`、`vjcad/doc/ui/controls.md`（message 部分）
> 示例：
> - `vjcad/example/ui/02message.js` — Toast
> - `vjcad/example/ui/04yesnodialog.js` — confirm / info / warning / error
> - `vjcad/example/ui/05inputdialog.js` — input / select / prompt
> - `vjcad/example/ui/10dialogsuspend.js` — suspend / resume
> - `vjcad/example/ui/12modaldialogbase.js` — `ModalDialogBase`
> - `vjcad/example/ui/13modelesspanelbase.js` — `ModelessPanelBase`

## 1. Toast 消息（message）

```js
message.info('操作成功');
message.success('保存成功');
message.warn('请注意');
message.error('操作失败');

// 配置式：duration=0 不自动关闭；同 key 会覆盖
message.info({ content: '正在保存…', duration: 0, key: 'saving' });
message.info({ content: '保存成功！',  duration: 3, key: 'saving' });
```

特性：

- 顶部居中、最多同时 3 条、超出滑出
- `duration` 单位秒（默认 3，0 表示常驻）
- 多参数自动空格拼接：`message.info('加载完成', 100, '条')`

`writeMessage('<br/>...')` 是把内容写到底部命令行，不是 Toast。

## 2. 便捷对话框

```js
// 输入
const r = await showInputDialog({
    title: '图层名称', label: '请输入新图层名',
    defaultValue: 'NewLayer', placeholder: '例如 Layer1',
    required: true,
    validator: v => v.length > 31 ? '不能超过31字符' : null
});
if (r.confirmed) console.log(r.value);

// 选择（下拉）
const s = await showSelectDialog({
    title: '选择线型', label: '请选择',
    options: [
        { value: 'Continuous', label: '实线' },
        { value: 'Hidden',     label: '虚线' }
    ],
    defaultValue: 'Continuous'
});

// 简易 prompt
const name = await showPrompt('请输入名称', '默认值', '标题');

// 确认 / 警告 / 信息 / 错误
const ok       = await showConfirm('确定删除？', '删除确认');
const proceed  = await showWarningConfirm('不可撤销，继续？', '警告');
await showInfo('操作完成');
await showError('保存失败：权限不足');
```

返回：`showInputDialog` → `InputDialogResult = { confirmed: boolean, value?: string }`（`value` 已 `trim()`）；`showPrompt` → `string | null`（取消返回 `null`；值已 trim，且内部 `required: true`，空输入不能确认）；确认类返回 `boolean`。

## 3. YesNoDialogConfig 完整字段

```ts
class YesNoDialogConfig {
    title       = 'WebCAD';
    message     = '';                                 // 支持 HTML
    type        = 'confirm';                          // 'info' | 'warning' | 'error' | 'confirm'
    yesTitle    = '是(Y)';
    noTitle     = '否(N)';
    showCancel  = false;
    cancelTitle = '取消';
    confirmOnly = false;                              // 只显示确定按钮
}
```

`InputDialogConfig` / `SelectDialogConfig` 见 `vjcad/doc/ui/dialogs.md` 完整字段定义。

## 4. 对话框中拾取点 / 选择实体（suspend / resume）

`BaseDialogComponent` 暴露 `suspend()` / `resume()`。**必须在命令上下文中弹出对话框** 才能让 CAD 接管交互。

```js
class InteractiveCommand {
    async main() {
        const dialog = new MyInteractiveDialog();
        const result = await dialog.startDialog();   // 见下方组件
        if (result) console.log(result);
    }
}
CommandRegistry.regist(new CommandDefinition('MYINTER', '交互', InteractiveCommand, new CommandOptions()));
await Engine.editor.executerWithOp('MYINTER');
```

组件中：

```js
async pickPoint() {
    this.baseDialog?.suspend();
    const opt = new PointInputOptions('指定点:');
    const r   = await Engine.editor.getPoint(opt);
    this.baseDialog?.resume();
    if (r.status === InputStatusEnum.OK) this.point = r.value;
}

async selectEntities() {
    this.baseDialog?.suspend();
    const r = await Engine.editor.getSelections(new SelectionInputOptions());
    this.baseDialog?.resume();
    if (r.status === InputStatusEnum.OK && r.value?.length) this.entities = r.value;
}
```

> 完整组件：`vjcad/example/ui/10dialogsuspend.js`，Vue3 版：`vjcad/example/ui/11dialogsuspend-vue3.js`

## 5. ModalDialogBase（模态对话框基类）

```js
class MySettingsDialog extends ModalDialogBase {
    static dialogTitle = '设置';
    static properties  = { ...ModalDialogBase.properties, color: { type: Number }, width: { type: Number } };
    color = 1; width = 1;

    renderContent() {
        return html`
            <div class="row"><span class="label">颜色:</span>
                <input type="number" class="input" .value=${String(this.color)}
                       @input=${e => this.color = parseInt(e.target.value)}></div>
            <div class="row"><span class="label">线宽:</span>
                <input type="number" class="input" .value=${String(this.width)}
                       @input=${e => this.width = parseInt(e.target.value)}></div>`;
    }

    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);
```

API（节选）：

| 方法 / 属性 | 说明 |
|---|---|
| `static dialogTitle` | 标题 |
| `result` | 你在 `confirm()` 里赋值的返回值 |
| `useShadowDOM` | 默认 `true`；用第三方库时设 `false` |
| `startDialog(options?)` | 显示并 await 结果 |
| `renderContent()` | **必须实现** |
| `renderFooter()` | 自定义底栏（默认确定/取消） |
| `confirm() / cancel() / close()` | 标准回调 |
| `suspend() / resume() / isSuspended` | 与第 4 节一致 |

## 6. ModelessPanelBase（非模态面板基类）

```js
class DrawToolsPanel extends ModelessPanelBase {
    static panelTitle      = '绘图工具';
    static panelWidth      = '200px';
    static initialPosition = { top: '100px', right: '20px' };

    drawLine() {
        const ln = new LineEnt([0, 0], [100, 50]);
        ln.setDefaults();
        Engine.addEntities(ln);
        Engine.zoomExtents();
    }

    renderContent() {
        return html`
            <button class="btn btn-primary" @click=${this.drawLine}>画直线</button>
            <button class="btn"             @click=${() => Engine.zoomExtents()}>缩放全图</button>`;
    }
}
customElements.define('draw-tools-panel', DrawToolsPanel);

const panel = new DrawToolsPanel();
document.body.appendChild(panel);
panel.show();      // panel.hide() / toggle() / destroy()
```

工厂：`createPanel(PanelClass, tagName)`：tagName 未注册时自动 `customElements.define`，然后**每次调用都 `createElement` 一个新面板并 append 到 `document.body`**，返回管理器（show / hide / toggle / destroy / isVisible）。它**不是单例**——要单例就把返回的管理器缓存在模块变量里，只调一次 `createPanel`。

## 7. 自定义 BaseDialogComponent

`ModalDialogBase` 已封装了 80% 场景；若要彻底自定义底栏、布局，可直接继承 `BaseDialogComponent`。骨架：

```ts
// BaseDialogComponent 本身就是 <base-dialog> 元素类；子类必须先注册成自定义元素，否则 new 会抛 Illegal constructor
class MyCustomDialog extends BaseDialogComponent {
    result: any = undefined;

    async show() {
        Engine.dialog.appendChild(this);
        await this.waitUpdated();
        await this.shadowRoot.querySelector('base-dialog')._startBaseDialog({
            title: '我的对话框', renderTarget: this.renderRoot
        });
        this.remove();
        return this.result;
    }

    render() {
        return html`<base-dialog>
            <div id="container">
                <input .value=${this.value} @input=${e => this.value = e.target.value}/>
                <div id="button-bar">
                    <dlg-button accept="true" @click=${this.onConfirm}>确定</dlg-button>
                    <dlg-button             @click=${this.onCancel}>取消</dlg-button>
                </div>
            </div>
        </base-dialog>`;
    }
}
if (!customElements.get('my-custom-dialog')) customElements.define('my-custom-dialog', MyCustomDialog);

const r = await new MyCustomDialog().show();
```

`<dlg-button accept="true">` 是主按钮；`disabled` 表示禁用。

## 8. 主题 CSS 变量

`<base-dialog>` 在 `:host` 上自带 `--dialog-header-bg / --dialog-header-color / --dialog-header-border / --dialog-contents-color / --dialog-ele-border-color` 默认值，而且每个对话框的 `<base-dialog>` 都在**该对话框自己的 shadow root 里**——写在页面全局 CSS 里的 `base-dialog {...}` 选不中它，**不会生效**。

- **只改自己的对话框**：把规则写进自己组件的 `static styles`（同一 shadow root 内的 `base-dialog` 选择器优先于它的 `:host` 默认值）。`ModalDialogBase / ModelessPanelBase` 已内置 `:host, base-dialog { --dialog-header-bg: var(--theme-color-dialog-header-bg, …) }` 这类映射：

```js
class MyDialog extends ModalDialogBase {
    static styles = [...ModalDialogBase.styles, css`
        base-dialog { --dialog-header-bg: #2d2d30; --dialog-header-color: #f0f0f0; }
    `];
}
```

- **整体换色**：`cadView.setThemeColors({ dialogHeaderBg, dialogBg, dialogBorder, dialogFont, dialogInputBg, font, ... })`（或 `Engine.setThemeColors`），写的是 `document.documentElement` 上的 `--theme-color-dialog-*` 等变量；基于 `ModalDialogBase / ModelessPanelBase`（样式里做了 `--dialog-*` → `--theme-color-dialog-*` 映射）的对话框会跟随，而颜色 / 线型等老式 picker 的 `<base-dialog>` 用固定默认色，不受影响。

## 9. 快捷键

| 键 | 作用 |
|---|---|
| `Enter` | 确认 / 是 |
| `Escape` | 取消 / 否 |
| `Y` / `N` | YesNoDialog 是 / 否 |

## 红旗

- `suspend / resume` 必须 **配对**，且 **在命令里**调用对话框，否则鼠标点画布会触发默认命令
- `ModalDialogBase.confirm()` 默认会自动 `close()`；如果你在 `confirm()` 里继续异步处理，记得校验失败时 **不要**调 `close()`
- `useShadowDOM = false` 后样式不再隔离，要手动注入 `createNoShadowStyles(tag)`
- `showInputDialog` 的 `validator` 是同步函数，不能 await；做异步校验请自己写组件
- 多次 `panel.show()` 会触发 `onShow` 多次；`createPanel(...)` 每调一次就新建一个面板实例（不是单例），想保持单例请自己缓存返回的管理器

## 参考（官方文档 MCP）

- 文档：`vjcad/doc/ui/dialogs.md`、`vjcad/doc/ui/controls.md`（message 部分）
- 示例：
- `vjcad/example/ui/02message.js` — Toast
- `vjcad/example/ui/04yesnodialog.js` — confirm / info / warning / error
- `vjcad/example/ui/05inputdialog.js` — input / select / prompt
- `vjcad/example/ui/10dialogsuspend.js` — suspend / resume
- `vjcad/example/ui/12modaldialogbase.js` — `ModalDialogBase`
- `vjcad/example/ui/13modelesspanelbase.js` — `ModelessPanelBase`
