# Menu + Breadcrumb + Tabs 导航工作台

这个示例演示一个常见后台/工作台骨架：左侧 `Menu` 作为主导航，顶部 `Breadcrumb` 显示当前位置，右侧 `Tabs` 动态承载已打开的业务页，底部 `Alert` 显示当前状态。

示例遵循 aardio 推荐的窗口写法：

- 窗口与宿主控件放在 `/*DSG{{*/ ... /*}}*/` 设计器区域内；
- 每个 AntdUI 主控件使用一个 aardio `custom` 宿主；
- 通过 `AntdUI.Menu(winform.menuHost)`、`AntdUI.Breadcrumb(winform.breadcrumbHost)`、`AntdUI.Tabs(winform.tabsHost)` 自然嵌入；
- 不把大量纯 .NET `SetBounds` 绝对布局当作普通窗口模板。

## 完整示例

```aardio
import win.ui;
/*DSG{{*/
var winform = win.form(text="AntdUI 导航工作台";right=1040;bottom=640)
winform.add(
menuHost={cls="custom";left=0;top=0;right=220;bottom=640;db=1;dl=1;dt=1;z=1};
breadcrumbHost={cls="custom";left=220;top=0;right=1040;bottom=48;dl=1;dr=1;dt=1;z=2};
tabsHost={cls="custom";left=220;top=48;right=1040;bottom=594;db=1;dl=1;dr=1;dt=1;z=3};
statusHost={cls="custom";left=220;top=594;right=1040;bottom=640;db=1;dl=1;dr=1;z=4}
)
/*}}*/

import dotNet.AntdUI;
import System.Windows.Forms;
import System.Drawing;

// 业务路由表：key 用于菜单、标签页、面包屑之间同步。
var routes = [
    {key="dashboard";section="首页";title="运营概览";desc="查看关键指标、待办任务与近期风险。"},
    {key="tickets";section="运维中心";title="工单列表";desc="按优先级跟踪设备巡检、维修、复核等工单。"},
    {key="devices";section="运维中心";title="设备台账";desc="管理设备基础信息、维保周期与在线状态。"},
    {key="reports";section="数据分析";title="运行报表";desc="汇总能耗、告警、工单处理时效等运营数据。"},
    {key="settings";section="系统配置";title="角色与权限";desc="维护角色、菜单权限与操作审计策略。"}
];

var menu = AntdUI.Menu(winform.menuHost);
menu.Mode = AntdUI.TMenuMode.Inline;
menu.Unique = true;
menu.Radius = 8;

var breadcrumb = AntdUI.Breadcrumb(winform.breadcrumbHost);
breadcrumb.Separator = "/";
breadcrumb.Gap = 8;

var tabs = AntdUI.Tabs(winform.tabsHost);
tabs.Type = AntdUI.TabType.Card;
tabs.EnablePageScrolling = true;
tabs.CloseDisposePage = true;

var status = AntdUI.Alert(winform.statusHost);
status.TextTitle = "就绪";
status.Text = "请选择左侧菜单打开业务页。";
status.Icon = AntdUI.TType.Info;
status.CloseIcon = false;

var allowMessage = false;

var findRoute = function(key){
    if(key === null) return null;
    for i,route in routes {
        if(route.key == key) return route;
    }
}

var findTabPage = function(key){
    if(key === null) return null;
    for(i=0;tabs.Pages.Count-1;1){
        var page = tabs.Pages.get_Item(i);
        if(page.Name == key) return page;
    }
}

var setBreadcrumb = function(route){
    breadcrumb.Items.Clear();

    var home = AntdUI.BreadcrumbItem();
    home.ID = "home";
    home.Text = "工作台";
    breadcrumb.Items.Add(home);

    var section = AntdUI.BreadcrumbItem();
    section.ID = route.section;
    section.Text = route.section;
    breadcrumb.Items.Add(section);

    var current = AntdUI.BreadcrumbItem();
    current.ID = route.key;
    current.Text = route.title;
    breadcrumb.Items.Add(current);
}

var createPage = function(route){
    var page = AntdUI.TabPage();
    page.Name = route.key;
    page.Text = route.title;
    page.Tag = route.key;
    page.AutoScroll = true;

    var header = AntdUI.Alert();
    header.Dock = System.Windows.Forms.DockStyle.Top;
    header.TextTitle = route.title;
    header.Text = route.desc;
    header.Icon = AntdUI.TType.Success;
    header.CloseIcon = false;
    page.Controls.Add(header);

    var label = AntdUI.Label();
    label.Dock = System.Windows.Forms.DockStyle.Fill;
    label.TextAlign = System.Drawing.ContentAlignment.MiddleCenter;
    label.TextMultiLine = true;
    label.Text = route.desc ++ '\r\n\r\n页面 key：' ++ route.key
        ++ '\r\n这里可以继续放置 Table、Form、Chart 或其他 AntdUI 控件。';
    page.Controls.Add(label);

    return page;
}

var openRoute = function(route){
    var page = findTabPage(route.key);
    if(!page){
        page = createPage(route);
        tabs.Pages.Add(page);
    }

    tabs.SelectTab(page);
    setBreadcrumb(route);

    status.TextTitle = "已打开";
    status.Text = route.section + " / " + route.title;

    // 静态 Message 建议以 BaseForm 作为 owner。
    // 由 AntdUI.Tabs(winform.tabsHost) 自动创建的父窗体就是 AntdUI.BaseForm。
    if(allowMessage){
        AntdUI.Message.open(
            AntdUI.Message.Config(tabs.Parent,"已打开：" + route.title,AntdUI.TType.Success)
        );
    }
}

var addLeaf = function(parent,route){
    var item = AntdUI.MenuItem(route.title);
    item.ID = route.key;
    item.IconSvg = "AppstoreOutlined";
    parent.Sub.Add(item);
}

var home = AntdUI.MenuItem("首页");
home.ID = "dashboard";
home.IconSvg = "HomeOutlined";
menu.Items.Add(home);

var ops = AntdUI.MenuItem("运维中心");
ops.ID = "ops";
ops.IconSvg = "ToolOutlined";
addLeaf(ops,routes[2]);
addLeaf(ops,routes[3]);
menu.Items.Add(ops);

var analysisMenu = AntdUI.MenuItem("数据分析");
analysisMenu.ID = "analysis";
analysisMenu.IconSvg = "BarChartOutlined";
addLeaf(analysisMenu,routes[4]);
menu.Items.Add(analysisMenu);

var sys = AntdUI.MenuItem("系统配置");
sys.ID = "sys";
sys.IconSvg = "SettingOutlined";
addLeaf(sys,routes[5]);
menu.Items.Add(sys);

menu.ItemClick = function(sender,e){
    var item = e.Item;
    if(!item) return;

    var route = findRoute(item.ID);
    if(route) openRoute(route);
}

tabs.SelectedIndexChanged = function(sender,e){
    var page = tabs.SelectedTab;
    if(!page) return;

    var route = findRoute(page.Name);
    if(route) setBreadcrumb(route);
}

breadcrumb.ItemClick = function(sender,e){
    if(e.Item && e.Item.ID == "home"){
        openRoute(routes[1]);
    }
}

openRoute(routes[1]);
winform.show();
allowMessage = true;
win.loopMessage();
```

## 关键模式

### 1. 用稳定 key 连接菜单、标签页和业务数据

示例里每个业务页都有唯一 `route.key`。菜单项的 `ID`、`TabPage.Name`、`TabPage.Tag` 都使用这个 key，便于查找已有标签页、防止重复打开。

```aardio
item.ID = route.key;
page.Name = route.key;
page.Tag = route.key;
```

如果 key 来自外部数据，建议只使用字母、数字、下划线等安全字符，避免把完整路径、中文标题或含特殊符号的字符串直接放进 `Control.Name`。

### 2. 打开页签前先查找已存在页面

`.NET` 集合索引通常从 0 开始，遍历 `tabs.Pages` 时不要套用 aardio 数组的 1 基习惯。

```aardio
for(i=0;tabs.Pages.Count-1;1){
    var page = tabs.Pages.get_Item(i);
    if(page.Name == key) return page;
}
```

### 3. 静态弹层 owner 使用 BaseForm

`AntdUI.Tabs(winform.tabsHost)` 会自动嵌入 `AntdUI.BaseForm`，因此 `tabs.Parent` 可作为 `Message` 的 owner：

```aardio
AntdUI.Message.open(
    AntdUI.Message.Config(tabs.Parent,"已打开：" + route.title,AntdUI.TType.Success)
);
```

不要把普通 aardio `custom` 宿主直接当成 `Message.Config` 的 owner。

### 4. 每个主控件用独立 custom 宿主

复杂业务窗口推荐在 aardio 设计器中拆出多个语义化宿主：

- `menuHost`：左侧导航菜单；
- `breadcrumbHost`：顶部面包屑；
- `tabsHost`：中间标签页内容区；
- `statusHost`：底部状态提示。

这样窗口缩放、DPI 缩放和控件锚定由 aardio 设计器负责，比在 .NET 容器内大量手写 `SetBounds` 更稳。

## 相关文档

- [Menu 导航菜单](../controls/navigation/menu.html)
- [Breadcrumb 面包屑](../controls/navigation/breadcrumb.html)
- [Tabs 标签页](../controls/data-display/tabs.html)
- [静态弹层组件](static-popup-components.html)
