【Unity UI 进阶】仿 Element UI 打造企业级 Unity UI 组件库(05)

【Unity UI 进阶】仿 Element UI 打造企业级 Unity UI 组件库(05)

环境与工具说明

项 说明
代码生成 本系列组件库代码由 Cursor(AI 编程助手)辅助生成与迭代,再结合工程内联调、重构落地
Unity 版本 2022.3.50f1c1(Unity 2022.3 LTS)
UI 框架 UGUI(UnityEngine.UI)
示例工程 EleUnityUI(仿 Element UI 企业级组件库)

建议使用 Unity 2022.3 LTS 同系列版本打开工程。本文实现均在 2022.3.50f1c1 下验证。上篇(04)讲完 Table 的选中、列显示与弹窗联动;本篇进入独立组件 EleTabs。


Tabs 标签页:图标、下划线指示、内容面板与 Table 内页签的区别

Element 的 Tabs 是「页面级分区 」:上面一排标签,下面一整块内容区,切换时只显示对应面板。EleUnityUI 的 EleTabs 按同一心智模型实现,并刻意与 Table 顶部的「筛选页签」区分开。

本篇覆盖:

  1. 图标 --- 配置 icon 键,走 EleUiIcons
  2. 下划线指示 --- 主题色横条跟随当前 Tab 滑动
  3. 内容面板 --- ContentHost/Panel_{tabCode},Prefab 里可继续塞 UI
  4. 与 Table 内页签的区别 --- 何时用 EleTabs,何时用 EleTable.showTabs

涉及文件:

能力 主要文件
运行时 Runtime/Tabs/EleTabs.cs
配置模型 Runtime/Tabs/EleTabsModels.cs
Config 资产 Runtime/Tabs/EleTabsConfigAsset.cs
示例生成 Editor/Tabs/EleTabsSampleGenerator.cs
图标 Runtime/Common/EleUiIcons.cs
Table 内页签(对比) Runtime/Tables/EleTable.Structure.cs、EleTableModels.cs → EleTableTabConfig

颜色继续走 设计变量 :Primary(激活文字 / 下划线 / 图标)、TextPrimary / TextRegular、White、BorderLighter。


一、整体结构(先建立全局图)

text 复制代码
EleTabs(根)
├── TabBar                    ← 标签栏(固定高度 tabBarHeight)
│   ├── TabRow                ← 横向排布 Tab 按钮
│   │   ├── Tab_user
│   │   ├── Tab_config
│   │   └── ...
│   ├── Divider               ← 底部分割线
│   └── Underline             ← 蓝色下划线指示器
└── ContentHost               ← 内容区(stretch 填满 TabBar 下方)
    ├── Panel_user            ← 仅当前 Tab 显示
    ├── Panel_config
    └── Panel_role

数据流:

text 复制代码
EleTabsConfig.tabs
    → BuildTabs() 生成按钮 + EnsurePanel()
    → SelectTab(tabCode)
        → ApplyActiveStyles()(文字/图标色 + 面板显隐)
        → MoveUnderline()(下划线对齐当前按钮)
        → OnTabChanged(tabCode, tabName)

业务侧最小接入:

csharp 复制代码
tabs.OnTabChanged += (code, name) =>
{
    Debug.Log($"切换到 {name} ({code})");
};

// 往某个面板里动态挂内容
var panel = tabs.GetPanel("config");

二、配置模型

1. 单项 EleTabItemConfig

csharp 复制代码
new EleTabItemConfig
{
    tabName = "配置管理",      // 显示文字
    tabCode = "config",        // 唯一编码 → 面板名 Panel_config
    icon = EleUiIcons.Settings,// 图标键,空=无图标
    active = true              // 初始选中(多项时取第一个 active)
}

2. 整体 EleTabsConfig

csharp 复制代码
public class EleTabsConfig
{
    public string title = "EleTabs";
    public EleTableTheme theme = EleTableTheme.ElementLight;
    public float tabBarHeight = 40f;
    public float iconSize = 14f;
    public float underlineHeight = 2f;
    public bool stretchPanels = true;
    public List<EleTabItemConfig> tabs = new List<EleTabItemConfig>();
}

示例预设(EleTabsConfig.CreateSample())包含四个 Tab:用户管理 / 配置管理 / 角色管理 / 定时任务补偿,分别对应 user / config / role / schedule 图标键。

3. 资源路径

资源 路径
Config Resources/Tabs/EleTabsSampleConfig.asset
Prefab Prefabs/Tabs/EleTabsSample.prefab

菜单:

text 复制代码
EleUnityUI → Samples → Tabs → Generate EleTabs Sample
EleUnityUI → Samples → Tabs → Spawn EleTabs Preview In Scene
GameObject → EleUnityUI → Tabs → Sample

三、图标

1. 配置方式

icon 填 字符串键 ,由 EleUiIcons.Get(icon) 取 Sprite。常用键:

键 常量 典型用途
user EleUiIcons.User 用户 / 账号
settings EleUiIcons.Settings 配置
role EleUiIcons.Role 角色权限
clock EleUiIcons.Clock 定时 / 任务

键不存在或 Sprite 为空时,不画图标,只显示文字,不会报错。

2. 实现步骤

  1. CreateTabButton 里判断 item.icon 是否为空
  2. EleUiIcons.Get 成功则创建 Image,preserveAspect = true
  3. 尺寸取 config.iconSize(默认 14)
  4. 与 Label 横向 HLayout,间距 6px,左右 padding 12
  5. 激活态:icon.color = Primary;未激活:TextRegular

3. 关键代码

csharp 复制代码
if (!string.IsNullOrEmpty(item.icon))
{
    var sp = EleUiIcons.Get(item.icon);
    if (sp != null)
    {
        iconImg = EleUiFactory.CreateImage("Icon", content, EleUiStyle.TextRegular);
        iconImg.sprite = sp;
        iconImg.preserveAspect = true;
        float s = Mathf.Max(12f, config.iconSize);
        EleUiFactory.AddFixedColumn(iconImg.gameObject, s, s);
    }
}

// ApplyActiveStyles
if (t.icon != null)
    t.icon.color = on ? EleUiStyle.Primary : EleUiStyle.TextRegular;

扩展新图标:在 EleUiIcons 增加常量 + Get() 分支 + 绘制函数即可,Tabs 侧不用改。


四、下划线指示器

Element Tabs 的标志性交互:底边主题色横条 滑到当前标签下方。EleTabs 用独立 Underline 节点实现,而不是给按钮加 Border(避免和 TabBar 底部分割线抢层级)。

1. 创建

BuildTabs 末尾:

csharp 复制代码
var underImg = EleUiFactory.CreateImage("Underline", tabBarRoot, EleUiStyle.Primary);
underImg.raycastTarget = false;
underline = underImg.rectTransform;
underline.anchorMin = new Vector2(0f, 0f);
underline.anchorMax = new Vector2(0f, 0f);
underline.pivot = new Vector2(0.5f, 0f);
underline.sizeDelta = new Vector2(48f, Mathf.Max(2f, config.underlineHeight));
underline.SetAsLastSibling();   // 盖在 Divider 上方

underlineHeight 默认 2px,工业风可略加厚。

2. 跟随当前 Tab:MoveUnderline

csharp 复制代码
void MoveUnderline(RectTransform tabBtn)
{
    Canvas.ForceUpdateCanvases();
    LayoutRebuilder.ForceRebuildLayoutImmediate(tabBarRoot);

    var world = tabBtn.TransformPoint(tabBtn.rect.center);
    var local = tabBarRoot.InverseTransformPoint(world);
    float width = Mathf.Max(32f, tabBtn.rect.width * 0.72f);
    underline.sizeDelta = new Vector2(width, Mathf.Max(2f, config.underlineHeight));
    underline.anchoredPosition = new Vector2(local.x, 0f);
}

要点:

  • 切换前先 ForceRebuildLayoutImmediate,避免 HLayout 未算完导致偏移
  • 宽度取按钮宽度的 72%,比整宽略短,更接近 Element 视觉
  • pivot 在底边中点,anchoredPosition.y = 0 贴在 TabBar 底部

3. 激活样式(文字)

csharp 复制代码
bool on = t.tabCode == currentTabCode;
var color = on ? EleUiStyle.Primary : EleUiStyle.TextPrimary;
if (t.label != null) t.label.color = color;

按钮本身用近乎透明的 Button 色块 + 轻微 Primary 悬停,高亮主要靠文字、图标和下划线,而不是整块背景变色。


五、内容面板 Panel_{tabCode}

1. 命名约定

每个 Tab 对应内容区子节点:

text 复制代码
ContentHost/Panel_{tabCode}

例如 tabCode = "config" → Panel_config。GetPanel("config") 就是取这个 RectTransform。

2. EnsurePanel:复用优先

csharp 复制代码
RectTransform EnsurePanel(EleTabItemConfig item)
{
    var name = $"Panel_{item.tabCode}";
    var existing = contentRoot.Find(name) as RectTransform;
    if (existing != null)
    {
        EleUiFactory.Stretch(existing);
        return existing;
    }

    var panel = EleUiFactory.CreateImage(name, contentRoot, EleUiStyle.White);
    EleUiFactory.Stretch(panel.rectTransform);
    // 首次创建时加占位 Label,Prefab 里可删
    return panel.rectTransform;
}

已存在的面板不会销毁 ,你在 Prefab 里手动加的子 UI(Table、表单、图表)会在 Rebuild 后保留。

3. 显隐逻辑

csharp 复制代码
void ApplyActiveStyles()
{
    foreach (var t in _tabs)
    {
        bool on = t.tabCode == currentTabCode;
        // ...文字 / 图标色...
        if (t.panel != null) t.panel.gameObject.SetActive(on);
    }
}

一次只显示一个 Panel_*,其余 SetActive(false)。不做 Addressables / 懒加载时,所有面板在层级里同时存在,只是隐藏------简单、适合企业后台体量。

4. Prefab 工作流(推荐)

  1. Generate EleTabs Sample 生成 Prefab
  2. 打开 Prefabs/Tabs/EleTabsSample.prefab
  3. 在 ContentHost/Panel_config 下拖入你的 UI(或挂脚本动态创建)
  4. 删掉占位 Placeholder 文本(可选)
  5. Play 时 Start → RebindButtons() 保留子节点,只重绑点击
csharp 复制代码
public RectTransform GetPanel(string tabCode)
{
    foreach (var t in _tabs)
        if (t.tabCode == tabCode) return t.panel;
    return null;
}

代码里往面板塞内容:

csharp 复制代码
var rt = tabs.GetPanel("user");
var table = Instantiate(tablePrefab, rt);
EleUiFactory.Stretch(table.GetComponent<RectTransform>());

5. 重建时清理策略

ClearTabBarChildrenKeepPanels:

  • 只清 TabBar 子节点(按钮、分割线、下划线会重建)
  • ContentHost 里:配置中仍存在的 Panel_{tabCode} 保留;配置删掉的 Tab 对应面板才销毁

这样改 tabs 列表后点「按 Config 重建 Tabs UI」,不会误删你辛苦摆好的面板内容。


六、与 Table 内筛选页签的区别

这是本篇最容易混淆的一点:名字都叫 Tab,职责完全不同。

1. 对照表

维度 EleTabs(独立组件) EleTable.showTabs(表内页签)
定位 页面 / 模块级分区 同一张表的数据 分类筛选
配置 EleTabsConfig → EleTabItemConfig(tabCode / tabName / icon) EleTableConfig.tabs → EleTableTabConfig(key / label)
内容区 Panel_{tabCode} 整块 UI 切换 没有独立面板,始终是同一张表
切换后果 显示/隐藏面板;OnTabChanged(code, name) _activeTabKey = key → 回到第 1 页 → RequestCurrentPage()
视觉 文字 + 图标 + 滑动下划线 小按钮块,TabActive / TabIdle 背景色
典型场景 「用户管理 | 角色管理 | 系统配置」三个子页 「全部 | 进行中 | 已归档」筛同一数据源
与查询栏 无关 常与 queryBar(关键字、状态下拉)叠加使用

Config 里已写明:

csharp 复制代码
[Header("页签(表内筛选 Tab,不是独立 EleTabs)")]
public bool showTabs;
public List<EleTableTabConfig> tabs;

2. Table 内页签实现(精简)

csharp 复制代码
void BuildTabs()
{
    tabsRoot.gameObject.SetActive(config.showTabs && config.tabs.Count > 0);
    foreach (var tab in config.tabs)
    {
        bool active = tab.key == _activeTabKey;
        var btn = EleUiFactory.CreateEleButton($"Tab_{tab.key}", tabsRoot, tab.label, ...);
        var key = tab.key;
        btn.onClick.AddListener(() =>
        {
            _activeTabKey = key;
            BuildTabs();              // 刷新按钮高亮
            OnTabChanged?.Invoke(key);
            _page = 1;
            RequestCurrentPage();     // 重新拉表数据
        });
    }
}

业务在 OnTabChanged 或 OnRequestPage 里根据 ActiveTabKey 过滤数据(Demo 里也可配合 statusFilter)。

3. 和「筛选」QueryBar 的关系

Table 还有一套 查询栏 (showQuery + EleTableQueryBar):

  • 页签 Tab:粗粒度分类(如业务线、单据状态组)
  • 查询栏:细粒度条件(关键字、日期、下拉枚举)
  • 状态筛选 showStatusFilters:历史 / 新增等行状态(走 statusFilter 字段)

三者可并存,但 都不会 创建 Panel_*------数据始终绑定到同一个 EleTable 实例。

4. 选型建议

text 复制代码
需要切换「整块不同 UI」     → EleTabs + 各 Panel 里放 Table / 表单 / 图表
需要切换「同一表格不同数据集」 → EleTable.showTabs + OnRequestPage 里按 key 过滤
需要临时搜几条               → queryBar,不必为此单独做 EleTabs

常见组合:外层 EleTabs 分模块,某个 Panel_* 里再放一张 EleTable(表内页签管该模块下的数据视图)。


七、API 与生命周期

API / 事件 说明
SetConfig(config, rebuild) 换配置并重建
Rebuild() / EditorRebuild() 编辑器按钮「按 Config 重建 Tabs UI」
SelectTab(tabCode, notify) 代码切换;notify=false 不抛事件
GetPanel(tabCode) 取内容面板
CurrentTabCode / CurrentTabName 当前选中
OnTabChanged (tabCode, tabName)

生命周期:

  1. Awake → ApplyTheme
  2. Start:若未 Build 则 Rebuild();若 Prefab 已构建则 RebindButtons() + 恢复 currentTabCode
  3. 初始 Tab:优先 currentTabCode 序列化值 → 否则第一个 active=true → 否则列表第一项

八、落地检查清单

  1. 每个 tabCode 唯一,且与 Panel_{tabCode} 一致
  2. 图标键能在 EleUiIcons.Get 里解析
  3. 切换 Tab 时下划线是否对齐、文字/图标是否变 Primary
  4. 非当前 Panel_* 是否 SetActive(false)
  5. Prefab 里手改面板内容后,Play 是否仍在(RebindButtons 路径)
  6. 从 Config 删掉某个 Tab 后,对应孤立面板是否被清理
  7. 不要 把「筛表」需求硬塞进 EleTabs------应使用 EleTable.showTabs

验证菜单:

text 复制代码
EleUnityUI → Samples → Tabs → Generate EleTabs Sample
EleUnityUI → Samples → Tabs → Spawn EleTabs Preview In Scene

九、小结

  1. EleTabs = 标签栏 + 下划线 + 多面板,配置驱动,Panel_{tabCode} 可在 Prefab 扩展
  2. 图标 走 EleUiIcons 键;激活态与下划线同色 Primary
  3. 下划线 独立节点 + 布局刷新后按按钮中心定位,宽度约为按钮 72%
  4. 重建保面板:只清 TabBar,ContentHost 按 tabCode 复用
  5. Table 内页签 是数据筛选,无 Panel_*,切换会 RequestCurrentPage------与 EleTabs 互补而非替代

系列回顾与下篇

篇 内容
03 Table 上:多行表头、拖宽、排序、分页
04 Table 下:选中、斑马纹、列显示、工具栏与弹窗
05 Tabs:图标、下划线、内容面板、与表内页签区别(本文)

06|NavMenu 导航条 :1~3 级菜单、maxDepth、顶栏与内容区联动,以及和 Tabs 组合成典型后台布局。

若你在 UGUI 里做过「Tab 内容被父级 Layout 挤没高度」或「下划线 Play 才偏一格」,欢迎留言------ContentHost 用手工锚点 stretch、切换前 ForceRebuildLayoutImmediate 都是为这类坑准备的。

相关推荐
传奇开心果编程1 小时前
【ArkUI进阶练中学】第12课:应用架构演进与遗留系统迁移
学习·ui·华为·harmonyos
海盗12342 小时前
微软技术日报 2026-10-04:26H2 三个已知问题确认,Blazor 补上智能体 UI
人工智能·microsoft·ui·机器人·aigc
java1234_小锋3 小时前
shadcn/ui 开源项目,专业打造专业UI
ui·开源
传奇开心果编程3 小时前
【ArkUI进阶练中学】第13课:元服务与卡片开发
学习·ui·华为·harmonyos
传奇开心果编程4 小时前
【ArkUI进阶练中学】第19课:AI安全与隐私治理
学习·ui·华为·harmonyos
WarPigs5 小时前
Unity对象不应该使用null传播的问题
unity
传奇开心果编程5 小时前
【ArkUI进阶练中学】第18课:Agent亲和架构与应用智能化改造
学习·ui·华为·harmonyos
每日出拳老爷子1 天前
【VR】【Unity】抓走转放系列21|一天搭完门和抽屉工具槽:小场景按这个次序拼装
unity·vr·驱动·排错·snap zone
传奇开心果编程1 天前
【ArkUI进阶练中学】第9课:分布式能力与多设备协同
学习·ui·华为·harmonyos
笨笨饿1 天前
#141_ Codex 本地 AI 代理工具链工作流重构:Headroom、Scrapling、Archify 统一接入
开发语言·人工智能·stm32·单片机·嵌入式硬件·ui·重构