---
description: "一个盒子搜整站的所有东西。按 ⌘K 弹面板，键入几个字就命中跨资源的分组结果 —— 产品、订单、客户、快捷操作各是一组；↑↓ 移动、Enter 跳转、Esc 关闭，鼠标可以完全不动。资源多起来后，这是让\"我要跳去某个具体东西\"从翻侧栏变成一秒的形状"
---

# 全局搜索（⌘K 命令面板 + 跨资源检索）

一个盒子搜整站的所有东西。按 ⌘K 弹面板，键入几个字就命中跨资源的分组结果 —— 产品、订单、客户、快捷操作各是一组；↑↓ 移动、Enter 跳转、Esc 关闭，鼠标可以完全不动。资源多起来后，这是让"我要跳去某个具体东西"从翻侧栏变成一秒的形状。

<iframe src="/skills/dashboard/assets/demo-global-search.html"
        style="width:100%;height:640px;border:1px solid #8884;border-radius:10px"
        loading="lazy" title="形状 demo：全局搜索"></iframe>

试这几下：

- 按 **⌘K / Ctrl-K**（或右上角按钮）唤起面板 —— 输入框自动获焦
- 空关键字看到 **最近访问** 组（首次为空，选几条后 localStorage 保存 · 刷新还在）
- 键入 `陈` / `SO-` / `保温` —— 命中项按 **产品 / 订单 / 客户 / 操作** 四组分列，每组最多 5 条
- **↑↓** 移动高亮（跨组连续）、**Enter** 触发（资源类跳转 + 写入最近，操作类直接执行）、**Esc** 或点面板外关闭
- 「操作」组里的 **清空最近访问** 是红字的破坏性操作 —— 先弹确认弹窗

## 规矩

- **一个 SearchSource 一个契约**：`{ label, items, toResult }` 让面板在内存里按 `title/subtitle` 过滤并按 `label` 分组；也可以给 `search(query)` 走自定义匹配。demo 是 `includes` 子串匹配，真实版把父层 **debounce** 后的跨资源服务端结果喂回同一契约
- **组件自己过滤（`shouldFilter={false}`）**：为让 subtitle 也参与匹配、分组头显示精确条数；每组按 `limitPerGroup`（demo 5、真实版默认 6）截断，防某个资源刷屏挤走其他分组
- **键盘只在面板里活着**：焦点始终在输入框，↑↓ 只移动 `aria-activedescendant` 指向的高亮项，Enter 才 `navigate(href)` / 触发操作；破坏性操作先过确认弹窗
- **纯展示 + 导航**：面板不知道数据从哪来 —— sources 自己上游拉，命中项自带 `href`，选中即跳转并关闭。空关键字回显 **最近访问**：demo/真实版都存 localStorage，命中即推到最前、去重、Cap 长度

> 蓝本：`add-global-search.md`[^add-global-search]（open-dashboard @ `aa9815f`，MIT，Invariants 已消化进上面「规矩」）

??? abstract "demo 源码：`assets/demo-global-search.html`（自包含、未压缩）"

    ```html
    --8<-- "skills/dashboard/assets/demo-global-search.html"
    ```

[^add-global-search]: [`add-global-search.md`](https://github.com/ahpxex/open-dashboard/blob/aa9815f426e9520841c4aa15a341bfd9f866091f/.claude/skills/add-component/references/add-global-search.md)，2026-07-28 读。
