---
title: Shell 应用外壳
description: 侧栏、命令面板、通知与看板组成的应用外壳
category: TemplateApp
status: beta
since: 0.6.0
tags: [template, shell, navigation, command-palette, dashboard]
syncStatus: reviewed
verified: false
---

## 用法

### 侧栏工作台
:::TuffDemoWrapper{demo="TemplateShellDemo" code-lang="vue" description="⌘K 打开命令面板，侧栏切换分区，铃铛展开通知。图中为示例数据。"}
---
code: |
  <script setup lang="ts">
  import type { CommandPaletteItem } from '@talex-touch/tuffex/command-palette'
  import { ref } from 'vue'
  import TemplateFrame from './TemplateFrame.vue'

  const section = ref('overview')
  const paletteOpen = ref(false)
  const trayOpen = ref(false)

  // ⌘K 绑在模板根节点上：window 上的 ⌘K 属于宿主页面自己的搜索
  function onKeydown(event: KeyboardEvent) {
    if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'k') {
      event.preventDefault()
      paletteOpen.value = true
    }
  }

  function run(command: CommandPaletteItem) {
    if (command.id.startsWith('go-'))
      section.value = command.id.slice(3)
  }
  </script>

  <template>
    <TemplateFrame title="Shell" :height="580" @enter="play">
      <div class="shell" @keydown="onKeydown">
        <TxSidebarNav
          class="shell__sidebar"
          :model-value="section"
          :items="navItems"
          :groups="navGroups"
          :workspace="{ name: 'Tuff Labs', description: '团队工作区 · 5 人' }"
          action-label="新建工作流"
          @update:model-value="section = $event"
          @action="createWorkflow"
        />
        <div class="shell__main">
          <header class="shell__topbar">
            <TxBreadcrumb :items="crumbs" @click="section = 'overview'" />
            <TxSearchInput v-model="filter" placeholder="筛选动态…" />
            <button type="button" aria-label="打开命令面板" @click="paletteOpen = true">
              <TxKbd>⌘K</TxKbd>
            </button>
            <TxIconButton icon="i-carbon-notification" :label="`通知，${unread} 条未读`" @click="trayOpen = !trayOpen" />
            <TxToastPanel :open="trayOpen" :stack="1">
              <!-- 最新一条通知 -->
            </TxToastPanel>
          </header>

          <div class="shell__overview">
            <TxStatCard
              v-for="stat in stats"
              :key="stat.key"
              :value="stat.value"
              :label="stat.label"
              :icon-class="stat.icon"
              :insight="stat.insight"
            />
            <TxTimeline>
              <TxTimelineItem v-for="row in feed" :key="row.id" :title="row.title" :time="row.time" :color="row.color">
                {{ row.body }}
              </TxTimelineItem>
            </TxTimeline>
            <TxChartScrubber :point-count="24" :active-index="hour" :rows="hourRows" @update:active-index="hour = $event">
              <TxSparkChart :series="queries" :active-index="hour" />
            </TxChartScrubber>
          </div>
        </div>
        <TxCommandPalette v-model="paletteOpen" :commands="commands" @select="run" />
      </div>
    </TemplateFrame>
  </template>

  <style scoped>
  .shell__overview {
    display: grid;
    grid-template-columns: minmax(0, 1.12fr) minmax(0, 1fr);
  }

  @container template (min-width: 960px) {
    .shell__overview { grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr) 288px; }
  }

  @container template (max-width: 639px) {
    .shell__sidebar { display: none; }
  }
  </style>
---
:::

### 顶栏控制台
导航收进顶部两行，宽度全部留给内容，适合分区少、常切换产品与工作区的控制台。
:::TuffDemoWrapper{demo="TemplateShellConsoleDemo" code-lang="vue" description="⌘K 打开命令面板；产品、工作区、通知与账户均为下拉菜单。图中为示例数据。"}
---
code: |
  <script setup lang="ts">
  import type { CommandPaletteItem } from '@talex-touch/tuffex/command-palette'
  import { ref } from 'vue'
  import TemplateFrame from './TemplateFrame.vue'

  const section = ref('overview')
  const detailId = ref<string | null>(null)
  const paletteOpen = ref(false)

  // 与侧栏风格一样：⌘K 绑在模板根节点上，不挂 window
  function onKeydown(event: KeyboardEvent) {
    if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'k') {
      event.preventDefault()
      paletteOpen.value = true
    }
  }

  // 下钻：切到「项目」页签并记下详情，面包屑据此多出一级
  function openProject(id: string) {
    section.value = 'projects'
    detailId.value = id
  }
  </script>

  <template>
    <TemplateFrame title="顶栏控制台" :height="580" @enter="play">
      <div class="console" @keydown="onKeydown">
        <header class="console__bar">
          <!-- 菜单面板最宽 360px：产品切换是面板里的两列应用格，方向键照常可用 -->
          <TxDropdownMenu :min-width="340">
            <template #trigger>
              <TxIconButton icon="i-carbon-app-switcher" label="切换产品" />
            </template>
            <div class="console__apps">
              <TxDropdownItem v-for="product in products" :key="product.id" @select="openProduct(product.id)">
                {{ product.name }}
              </TxDropdownItem>
            </div>
          </TxDropdownMenu>
          <TxDropdownMenu>
            <template #trigger>
              <button type="button" class="console__ws">{{ workspace.name }}</button>
            </template>
            <TxDropdownItem v-for="ws in workspaces" :key="ws.id" @select="switchWorkspace(ws.id)">
              {{ ws.name }}
            </TxDropdownItem>
            <TxDropdownSubmenu>
              数据区域
              <template #menu>
                <TxDropdownItem v-for="option in regions" :key="option.id" @select="region = option.id">
                  {{ option.name }}
                </TxDropdownItem>
              </template>
            </TxDropdownSubmenu>
          </TxDropdownMenu>
          <button type="button" class="console__search" @click="paletteOpen = true">
            搜索项目与命令… <TxKbd>⌘K</TxKbd>
          </button>
          <TxDropdownMenu placement="bottom-end" :min-width="320">
            <template #trigger>
              <TxCornerOverlay placement="top-right">
                <TxIconButton icon="i-carbon-notification" :label="`通知，${unread} 条未读`" />
                <template #overlay>
                  <TxBadge variant="error" :value="unread" />
                </template>
              </TxCornerOverlay>
            </template>
            <!-- 通知项……「全部标为已读」设 :close-on-select="false" -->
          </TxDropdownMenu>
        </header>
        <!-- 窄屏：左右两格本身就是按钮，菜单挪进抽屉 -->
        <TxNavBar class="console__navbar" :safe-area-top="false" @click-left="drawerOpen = true" @click-right="paletteOpen = true" />

        <TxTabs v-model="section" placement="top" indicator-variant="line" borderless :content-padding="0">
          <template #nav-right>
            <TxAvatarGroup :max="3" :size="24">
              <TxAvatar v-for="member in online" :key="member.id" :name="member.name" status="online" />
            </TxAvatarGroup>
            <TxTextMorph :text="`${online.length} 人在线`" />
          </template>
          <TxTabHeader>
            <TxBreadcrumb :items="crumbs" @click="onCrumb" />
            <TxSplitButton icon="i-carbon-add" @click="createProject('plugin')">
              新建项目
              <template #menu-icon>
                <i class="tx-split-button__menu-icon i-carbon-chevron-down" aria-hidden="true" />
                <span class="sr-only">选择项目类型</span>
              </template>
              <template #menu="{ close }">
                <!-- 插件 / 工作流 / 智能体 / 主题 -->
              </template>
            </TxSplitButton>
          </TxTabHeader>
          <TxTabItem name="overview">
            <template #name>概览</template>
            <div class="console__grid">
              <TxCard v-for="project in projects" :key="project.id" clickable @click="openProject(project.id)">
                <TxStatusBadge :text="project.statusText" :status="project.statusTone" size="sm" />
                <TxProgressBar v-if="project.status === 'building'" :percentage="project.progress" height="4px" />
              </TxCard>
            </div>
          </TxTabItem>
          <TxTabItem name="settings" :disabled="!isOwner">
            <template #name>设置</template>
          </TxTabItem>
        </TxTabs>

        <TxCommandPalette v-model="paletteOpen" :commands="commands" @select="run" />
      </div>
    </TemplateFrame>
  </template>

  <style scoped>
  .console__grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  }

  .console__navbar { display: none; }

  @container template (max-width: 639px) {
    .console__bar { display: none; }
    .console__navbar { display: block; }
  }
  </style>
---
:::

## 概述

团队工作区的后台外壳：切换分区只替换主区，外壳不动。

- ⌘K（Windows 为 Ctrl+K）绑在模板根节点并调用 `preventDefault()`，不会同时打开文档站的搜索。
- 侧栏切换分区；「新建工作流」在动态顶部插入一条记录。
- 铃铛展开最新通知；命令与账户操作的反馈进入同一托盘，3.2 秒后收起。
- 进入视口后自动演示一次，点击或按键即停止；减少动态效果时直接显示最终状态。
- 宽屏（≥ 960px）增加右栏；窄屏（< 640px）侧栏收进顶栏菜单。
- 顶栏控制台：产品、工作区、通知、账户均为下拉菜单，工作区菜单含「数据区域」子菜单；点项目卡进入详情，面包屑逐级返回。
- 顶栏控制台：窄屏（< 640px）时全局栏换成 TxNavBar，菜单移入抽屉。

## 自定义

- 导航数据就是 TxSidebarNav 的 `items`，窄屏菜单复用同一份。
- 快捷键绑在模板根节点，不挂 `window` / `document`；`search-hint` 只传多字符字形（如 `⌘K`）。
- 通知用受控的 TxToastPanel，不用全局 `toast()`。
- 分区较多时，把主区换成路由出口（`<RouterView>` / `<NuxtPage>`）。
- 顶栏控制台：TxDropdownMenu 面板最宽 360px；更宽的面板改用指定 `width` 的 TxPopover，并自行处理焦点。

## 相关组件

| 区域 | 组件 | 作用 |
|---|---|---|
| 侧栏 | [TxSidebarNav](/docs/dev/components/sidebar-nav) | 工作区切换、分组导航与主操作 |
| 顶栏 | [TxBreadcrumb](/docs/dev/components/breadcrumb)、[TxSearchInput](/docs/dev/components/search-input)、[TxKbd](/docs/dev/components/kbd)、[TxTooltip](/docs/dev/components/tooltip) | 当前位置、筛选、⌘K 入口 |
| 通知托盘 | [TxIconButton](/docs/dev/components/button)、[TxBadge](/docs/dev/components/badge)、[TxToastPanel](/docs/dev/components/toast-panel)、[TxIconChip](/docs/dev/components/icon-chip) | 铃铛、未读数与最新通知 |
| 账户菜单 | [TxDropdownMenu](/docs/dev/components/dropdown-menu)、[TxAvatar](/docs/dev/components/avatar) | 账户操作 |
| 命令面板 | [TxCommandPalette](/docs/dev/components/command-palette) | 跳转与全局命令 |
| 指标 | [TxStatCard](/docs/dev/components/stat-card)、[TxTextMorph](/docs/dev/components/text-morph) | 关键指标 |
| 动态 | [TxTimeline](/docs/dev/components/timeline)、[TxSearchEmpty](/docs/dev/components/search-empty) | 最近活动与无结果 |
| 查询趋势 | [TxSparkChart / TxChartScrubber](/docs/dev/components/spark-chart) | 按小时的查询量 |
| 右栏（宽屏） | [TxStatusBadge](/docs/dev/components/status-badge)、[TxAvatar](/docs/dev/components/avatar)、[TxButton](/docs/dev/components/button) | 在线成员、服务、设备 |
| 其他分区 | [TxEmptyState](/docs/dev/components/empty-state)、[TuffSwitch](/docs/dev/components/switch)、[TxTag](/docs/dev/components/tag) | 插件启停与空状态 |
| 全局栏（顶栏控制台） | [TxDropdownMenu / TxDropdownItem / TxDropdownSubmenu](/docs/dev/components/dropdown-menu)、[TxIconChip](/docs/dev/components/icon-chip)、[TxKbd](/docs/dev/components/kbd) | 产品、工作区与数据区域切换，搜索入口 |
| 通知与账户（顶栏控制台） | [TxCornerOverlay](/docs/dev/components/corner-overlay)、[TxBadge](/docs/dev/components/badge)、[TxDropdownMenu](/docs/dev/components/dropdown-menu)、[TxAvatar](/docs/dev/components/avatar)、[TxTooltip](/docs/dev/components/tooltip) | 未读角标、通知菜单与账户菜单 |
| 分区页签（顶栏控制台） | [TxTabs / TxTabItem](/docs/dev/components/tabs)、[TxAvatarGroup / TxAvatar](/docs/dev/components/avatar)、[TxTextMorph](/docs/dev/components/text-morph) | 分区页签与在线成员 |
| 页头（顶栏控制台） | [TxTabs](/docs/dev/components/tabs) 的 TxTabHeader、[TxBreadcrumb](/docs/dev/components/breadcrumb)、[TxButton / TxSplitButton](/docs/dev/components/button) | 面包屑下钻、按类型新建 |
| 项目（顶栏控制台） | [TxCard](/docs/dev/components/card)、[TxStatusBadge](/docs/dev/components/status-badge)、[TxProgressBar](/docs/dev/components/progress-bar)、[TxFilterChips](/docs/dev/components/filter-chips)、[TxSearchInput](/docs/dev/components/search-input)、[TxCardItem](/docs/dev/components/card-item) | 项目网格、筛选与详情 |
| 右栏（顶栏控制台，宽屏） | [TxCardItem](/docs/dev/components/card-item)、[TxSparkChart](/docs/dev/components/spark-chart) | 在线成员、构建与动态 |
| 窄屏（顶栏控制台） | [TxNavBar](/docs/dev/components/nav-bar)、[TxDrawer](/docs/dev/components/drawer) | 紧凑顶栏与抽屉菜单 |
