Files
hermes-ui/docs/arco-mapping.md
T
toor c3eeea4187 fix(chat): repair Arco migration fallout in the chat page
The Phase 006 walkthrough surfaced four issues the automated checks could
not see. Dead naive-ui class names left behind by the migration stopped
matching, tooltips that carried their label as trailing default-slot text
leaked that text into the flex row, the workspace panel squeezed the chat
column until the input toolbar wrapped, and the Escape fallback still
looked for naive-ui's modal mask.

Tooltips now pass their label through the content prop, the tool panel
reserves a minimum width for the chat column, and Escape finds the visible
Arco modal and clicks its close button.
2026-10-18 03:11:35 +08:00

96 lines
8.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Naive UI → Arco Design Vue 组件映射表
Phase 005 起,新代码与迁移代码一律使用 Arco Design Vue。本文表是迁移的唯一映射依据:
**先查表,再改代码**。表中没有的组件,先在此表补一行再动手。
- 全局注册:`main.ts` 全量 `app.use(ArcoVue)`,不做按需引入
- 主题:`body[arco-theme='dark']`(见 `styles/arco-theme.ts` 与 `docs/ui-baseline.md`)
- hooks:`useMessage` / `useDialog` / `useNotification` 适配层在 `packages/client/src/composables/`,
签名冻结,**不得**在页面里直接 import Arco 的 `Message` / `Modal` / `Notification`
## Provider / hooks
| Naive | Arco | 备注 |
|---|---|---|
| `NConfigProvider` | 直接用 `body[arco-theme]` | 不需要组件 |
| `NMessageProvider` / `useMessage` | `composables/useMessage.ts` | 适配层,签名冻结 |
| `NDialogProvider` / `useDialog` | `composables/useDialog.ts` | `positiveText→okText`、`onPositiveClick→onBeforeOk`(resolve false / reject → 保持打开)由适配层转换 |
| `NNotificationProvider` / `useNotification` | `composables/useNotification.ts` | `description→content` 由适配层转换 |
## 基础组件(按当前仓库用量排序)
| Naive | Arco | 用量 | 备注 |
|---|---|---|---|
| `NButton` | `a-button` | 732 | `block`→`long`、`size="tiny"`→`size="mini"`、`type="warning"`→`type="primary" status="warning"`、`quaternary`/`secondary`→`type="secondary"`(Arco `status` 只有 `normal\|success\|warning\|danger`,没有 `secondary`) |
| `NInput` | `a-input` / `a-textarea` | 160 | `type="textarea"` 用独立 `a-textarea`;`a-input` 关闭了 attrs 透传,`autofocus` 无效,需 `ref` + `focus()` |
| `NModal` | `a-modal` | 138 | `v-model:show`→`v-model:visible`;`preset="dialog"` 无对应,信息型弹窗用 `:footer="false"`(`footer` 为 Boolean prop,默认 true) |
| `NFormItem` | `a-form-item` | 136 | 校验规则 API 不同(`rules` 结构、`required` 表达式)需适配 |
| `NSelect` | `a-select` | 97 | `options` 的 `label/value` 语义一致;`consistent-menu-width` 等 prop 无对应 |
| `NSpin` | `a-spin` | 86 | `show`→`loading` |
| `NTooltip` | `a-tooltip` | 76 | `trigger="hover"` 默认一致;`placement`→`position`;**触发元素走默认插槽**,naive 的 `<template #trigger>` 必须拆掉 |
| `NPopconfirm` / `NTooltip` 的 `#trigger` 槽 | Arco 默认插槽 | — | Arco 的 `a-tooltip` / `a-popconfirm` / `a-dropdown` 都用默认插槽承载触发元素,naive 的 `<template #trigger>` 写法会静默丢内容 |
| Arco Trigger 嵌套 | 不可嵌套,无论谁包谁 | — | `a-tooltip` / `a-dropdown` / `a-popconfirm` 内部都用 `mergeFirstChild` 找首个元素节点并各自接管事件;嵌套时两个 popup 容器互相踩,菜单**点不开**(`.arco-trigger-popup-wrapper` 卡在过渡中间态、选项数为 0)。同一触发元素要同时有说明文案和下拉时,说明文案改用原生 `:title`,不要套 `a-tooltip` |
| `NTag` | `a-tag` | 57 | `bordered` prop 存在(默认 true);`size="small"` 一致;naive `type="info"`→`color="arcoblue"` 等内建色(`red/orange/green/arcoblue/...`),自定义色用 `color` 传 CSS 颜色 |
| `NSwitch` | `a-switch` | 53 | `value`/`v-model` 一致 |
| `NSpace` | `a-space` | 51 | `size` 语义一致(数字/预设名) |
| `NInputNumber` | `a-input-number` | 43 | `min`/`max`/`precision` 一致 |
| `NPopconfirm` | `a-popconfirm` | 36 | `positive-text`→`ok-text`;`@positive-click`→`@ok` |
| `NForm` | `a-form` | 32 | 校验规则 API 不同,需适配 |
| `NTabPane` | `a-tab-pane` | 30 | `NTabs/NTabPane` → `a-tabs/a-tab-pane` |
| `NDrawer` / `NDrawerContent` | `a-drawer` | 27 | Arco 无独立 content 组件;`footer` slot 用 `a-drawer` 的 `footer` |
| `NDropdown` | `a-dropdown` | 25 | **Arco 没有 `droplist` prop**:菜单内容必须写进 `#content` 插槽、用 `a-doption` / `a-dsubmenu` 声明。传 `:droplist` 会被静默忽略,弹层能打开但 `<ul class="arco-dropdown-list">` 是空的;子菜单用 `a-dsubmenu` + 自己的 `#content` 插槽 |
| `NDescriptions` / `NDescriptionsItem` | `a-descriptions` / `a-descriptions-item` | 19 | 基本一致 |
| `NAlert` | `a-alert` | 19 | `type` 一致;`show-icon` 一致 |
| `NRadioButton` / `NRadioGroup` / `NRadio` | `a-radio`(`type="radio-button"`)/ `a-radio-group` | 32 | Arco radio-button 是 `a-radio` 的 `type` prop |
| `NCheckbox` / `NCheckboxGroup` | `a-checkbox` / `a-checkbox-group` | 17 | `options` 数组语义一致 |
| `NEmpty` | `a-empty` | 12 | 基本一致 |
| `NCollapse` / `NCollapseItem` | `a-collapse` / `a-collapse-item` | 24 | `arrow` 语义不同 |
| `NUpload` | `a-upload` | 9 | `file-list` 结构不同,需适配 |
| `NText` | `<a-typography>` / 原生 `span` | 8 | 多数场景直接用原生元素 + class |
| `NTabs` | `a-tabs` | 6 | 见 NTabPane 行 |
| `NBreadcrumb` / `NBreadcrumbItem` | `a-breadcrumb` / `a-breadcrumb-item` | 8 | 基本一致 |
| `NDataTable` | `a-table` | 4 | **列定义 API 差异最大**(`columns`/`render`/`slots`),逐例处理 |
| `NSlider` | `a-slider` | 3 | 基本一致 |
| `NScrollbar` | `a-scrollbar` | 3 | Arco 全局可用,无需组件包裹注册 |
| `NPopselect` | `a-dropdown` | 3 | 无直接对应。实测 `a-dropdown` + `:droplist`(`key`/`content`)可完整替代 |
| `NPopover` | `a-popover` | 3 | `trigger` 语义有差异 |
| `NIcon` | `<icon-*>`(`@arco-design/web-vue/es/icon`) | 3 | Arco 图标是独立组件,需 import |
| `NBadge` | `a-badge` | 3 | `value`→`count` |
| `NTree` | `a-tree` | 2 | 结构差异大,逐例处理 |
| `NCard` | `a-card` | — | 当前仓库未直接使用;若出现按此映射 |
| `NProgress` | `a-progress` | — | 当前仓库未直接使用;若出现按此映射 |
| `NLayout` / `NLayoutSider` | `a-layout` / `a-layout-sider` | — | 当前仓库未使用(侧栏是手写 div),保持手写或按需引入 |
## 迁移批次(Phase 006/007 使用)
- Phase 006:聊天页(`components/hermes/chat/`)——消息气泡内部**不用** Arco 组件(性能红线)
- Phase 007:其余视图 + `stores/` + `composables/` 批量替换 + 删除 naive-ui 依赖
## 例外
- 聊天虚拟滚动区不用 `a-scrollbar` 包裹。`VirtualMessageList.vue` 直接用
`vue-virtual-scroller` 的 `DynamicScroller` 独占滚动(原本也没有 `NScrollbar`),
额外包一层滚动容器会破坏高度测量与 sticky 定位。
- 测试里 Arco 组件的 stub 必须用 Arco 内部组件名(`Dropdown` / `Modal` / `Spin`),
而不是模板里的 `a-*` 标签名:`app.use(ArcoVue)` 之后组件已全局注册,VTU 的
`stubs` 只按内部名匹配,用 `ADropdown` 会静默不生效。
- `ChatInput.vue` 的推理强度按钮、输入设置按钮放弃 `a-tooltip`,改用原生 `:title`。
这两个按钮必须挂 `a-dropdown`,而 Trigger 不能嵌套(见上表同名行),因此失去
Arco 的悬浮气泡;文案与 `aria-label` 都保留,信息量不变。
- Arco(和 Vue 本身)不会对拼错的 prop 报错,未声明的 prop 会作为普通 attribute
落到根元素上。所以「弹层能打开、但内容是空的」几乎总是 prop 写错而不是数据为空。
排查手法:`el.__vueParentComponent.props` 看目标组件到底认不认这个 prop,
再看 `el.outerHTML` 确认渲染出来的骨架。回归由
`tests/e2e/chat-input-dropdown.spec.ts` 卡住。
- **迁移时最容易漏的是 CSS 里的 naive 类名**,因为 `grep naive-ui` 匹配不到
`:deep(.n-button__content)` 这种写法。聊天页原本有 35 处这类死样式,在 Arco 下
全部不生效,必须逐条改写成 Arco 类名。注意三处结构差异:
- `a-button` **没有** content 包裹层(naive 有 `.n-button__content`),
`gap` / `min-width` 要直接写在 button 上
- `a-modal` 没有 `body-wrapper` 分层,用 `.arco-modal` / `.arco-modal-body`
- `a-drawer` 同理,没有 `content` / `body-content-wrapper` 分层
- `useKeyboard.ts` 里查 `.n-modal-mask` 的 Esc 兜底属于跨组件的全局约定,迁移
组件时最容易漏掉。改弹窗库时必须同步 grep `.n-modal|.n-drawer|.n-base-` 这类
**字符串字面量**里的类名,而不只是组件 import。回归由
`tests/client/session-search.test.ts` 的 Esc 用例卡住。