Files
hermes-ui/docs/arco-migration-rules.md
T
toor 836d0228d2 Phase 007: migrate every remaining view from naive-ui to Arco Design Vue
The project now has exactly one UI library. Every naive-ui component in
packages/client/src is replaced by its Arco equivalent across 24 views and
roughly 80 components, and naive-ui is dropped from package.json along with
its transitive vueuc / @css-render / vdirs / seemly dependencies.

The migration is rendering-layer only. No store logic, no API contract, and
no /chat-run payload was touched.

Three classes of leftover were invisible to `grep naive-ui` and are now
covered by tests/client/no-naive-ui.test.ts, which asserts all three:
  - dead .n-* class names in style blocks
  - dead --n-* custom properties
  - naive-ui in package.json

Notable behavioural traps handled along the way, recorded in
docs/arco-migration-rules.md:
  - a-input-password defaults to masked; passing default-visibility="false"
    would have exposed API keys in clear text
  - a-upload fires a real XHR unless auto-upload is false
  - nested Trigger components fight over the first element child, so the
    group-chat settings dropdown was flattened
  - Arco renders tooltip default-slot text inline, which was silently
    collapsing the chat and group-chat toolbars
2026-10-18 22:57:25 +08:00

102 lines
7.9 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.
# Phase 007 迁移规约(每个 agent 必读)
目标:`grep -rn "naive-ui\|vueuc\|@css-render" packages/client/src` 零命中,
且项目里只有 Arco 一个 UI 库。`tests/client/no-naive-ui.test.ts` 会卡住回归。
## 铁律
1. **只换渲染层。** 不改业务逻辑、不改 store、不改 API 调用、不改事件语义。
看到逻辑 bug 记下来但不要动手。
2. **不引入新依赖。** Arco 已在 `main.ts` 全量 `app.use(ArcoVue)`,
模板里直接写 `<a-button>` 等 kebab-case 标签即可,**不要** import Arco 组件。
3. **Arco 不会对拼错的 prop 报错** —— 未声明的 prop 会作为普通 attribute 落到根元素上。
所以「弹层能打开但内容是空的」几乎总是 prop 写错了。改完每个弹层都要确认
prop 在 `node_modules/@arco-design/web-vue/es/<组件>/` 的 props 定义里存在。
4. **`grep naive-ui` 匹配不到 CSS 里的类名。** 必须另外用这个命令自查:
```bash
grep -n "\.n-[a-z]\|:deep(\.n-" <你改过的每个文件>
```
这些是死样式,在 Arco 下静默失效。**这是最容易漏、且测试测不出来的一类改动。**
## 组件映射(Arco 全局已注册,直接用 kebab-case 标签)
| Naive | Arco | 必须改的差异 |
|---|---|---|
| `NButton` | `a-button` | `block`→`long`;`size="tiny"`→`size="mini"`;`quaternary`/`secondary`→`type="text"` / `type="secondary"`;`type="warning"`→`type="primary" status="warning"`;`circle`/`round` 形状用 `shape` prop |
| `NInput` | `a-input` | `type="textarea"` → 独立 `a-textarea`;`a-input` 不透传 attrs,`autofocus` 无效,要用 `ref` + `focus()` |
| `NInputPassword` / `show-password-on` | `a-input-password` | **不要传 `default-visibility`**。它的默认值 `true` 表示"处于可见状态",而 `input-password.js:77` 是 `type: mergedVisible ? "password" : "text"` —— 所以默认就是**遮蔽**显示。传 `:default-visibility="false"` 反而会让 API Key **明文显示**。不传即可 |
| `NModal` | `a-modal` | `v-model:show`→`v-model:visible`;`preset="dialog"`/`preset="card"` 无对应 → `:footer="false"`;**`#action` 插槽要改名 `#footer`**(Arco 只有 `#footer`);`positive-text`→`ok-text`;`@positive-click`→`@ok` |
| `NDrawer`/`NDrawerContent` | `a-drawer` | Arco 没有独立 content 组件,`NDrawerContent` 的 `:title` 直接给 `a-drawer`;`v-model:show`→`v-model:visible`;`placement` 保留 |
| `NForm` | `a-form` | **本仓库 `:rules` 全仓零使用,校验都是手写 `if`,直接替换即可,不需要写规则适配** |
| `NFormItem` | `a-form-item` | `label-placement="top"`→`layout="vertical"`;`NFormItem` 没有 `feedback` prop,错误文案自己渲染一个元素 |
| `NSelect` | `a-select` | `options` 的 `{label,value,disabled}` 语义一致;`consistent-menu-width` 等无对应,删掉 |
| `NSpin` | `a-spin` | `show`→`loading` |
| `NTooltip` | `a-tooltip` | `placement`→`position`;**`#trigger` 模板插槽必须拆掉**,触发元素放默认插槽 |
| `NPopconfirm` | `a-popconfirm` | `positive-text`→`ok-text`;`@positive-click`→`@ok`;**`#trigger` 必须改默认插槽** |
| `NDropdown` | `a-dropdown` | **Arco 没有 `droplist` prop**,菜单必须写进 `#content` 插槽用 `a-doption`;`@select` 的 key 结构一致 |
| `NTag` | `a-tag` | `type="info"`→`color="arcoblue"` 等内建色;`bordered` 存在;**naive 的 `round` 没有对应 prop**(Arco `a-tag` 只有 `color/size/bordered/visible/loading/closable/checkable/checked/nowrap`),传 `shape="round"` 会静默失效,要圆角就自己写 `border-radius: 999px` |
| `NSwitch` | `a-switch` | `value`/`v-model:value` 一致 |
| `NSpace` | `a-space` | `size` 语义一致 |
| `NInputNumber` | `a-input-number` | 一致 |
| `NAlert` | `a-alert` | `type` 一致 |
| `NEmpty` | `a-empty` | 一致 |
| `NTabs`/`NTabPane` | `a-tabs`/`a-tab-pane` | `NTabPane` 的 `name`+`:tab` → `a-tab-pane` 的 `key`+`title`;`animated` 无对应,删;`v-model:value`→`v-model:active-key`,`@update:value`→`@change` |
| `NCollapse`/`NCollapseItem` | `a-collapse`/`a-collapse-item` | `arrow` 语义不同 |
| `NRadioGroup`/`NRadio`/`NRadioButton` | `a-radio-group`/`a-radio` | `NRadioButton` → `<a-radio type="button">`。**注意是 `"button"` 不是 `"radio-button"`**(`radio.js:115` 判 `mergedType === "button"`,写错会静默失效) |
| `NCheckbox`/`NCheckboxGroup` | `a-checkbox`/`a-checkbox-group` | `options` 一致 |
| `NUpload` | `a-upload` | `file-list` 结构不同;`UploadFileInfo` 类型是 naive 专有 → 改成原生 `File[]` 或 Arco 的 `FileItem`;`directory-dnd` 无对应,删 |
| `NDataTable` | `a-table` | **列定义 API 差异最大**:`columns` 的 `title/key/render/width/minWidth/ellipsis` 要逐个对;naive 的 `render(row)` 返回 vnode,Arco 用 `#columns` 插槽或同样的 `render` |
| `NBreadcrumb`/`NBreadcrumbItem` | `a-breadcrumb`/`a-breadcrumb-item` | 一致 |
| `NDescriptions`/`NDescriptionsItem` | `a-descriptions`/`a-descriptions-item` | 一致 |
| `NPopover` | `a-popover` | `placement`→`position` |
| `NBadge` | `a-badge` | `value`→`count` |
| `NScrollbar` | `a-scrollbar` | Arco 的 `a-scrollbar` 需要显式 `height`,用 `max-height` 会高度塌陷 |
| `NIcon` | `<icon-xxx>` | Arco 图标是独立组件,`import { IconXxx } from '@arco-design/web-vue/es/icon'` |
| `NText` | 原生 `span` / `a-typography` | 多数场景直接换原生元素 + 原有 class |
| `NTree` | `a-tree` | **懒加载语义不同**:naive `:on-load="fn"` 直接改 `node.children`;Arco 是 `@load="(node, done) => ..."`,改写回调 |
## 类型导入
```ts
import type { SelectOption } from 'naive-ui' // 删
export interface X extends SelectOption {} // 改成本地 interface:
// { label: string; value: string; disabled?: boolean }
```
## CSS 类名对照(`:deep()` 里也要改)
`.n-button`→`.arco-btn` / `.n-input`→`.arco-input` / `.n-input__*`→`.arco-input-inner`
/ `.n-switch`→`.arco-switch` / `.n-tag`→`.arco-tag` / `.n-spin-container`→`.arco-spin-container`
/ `.n-spin-content`→`.arco-spin` / `.n-collapse`→`.arco-collapse`
/ `.n-drawer-body-content-wrapper`→`.arco-drawer-body` / `.n-modal-body-wrapper`→`.arco-modal`
/ `.n-data-table*`→`.arco-table*`
**结构差异(Arco 没有的 naive 嵌套层)**:
- `a-button` **没有** content 包裹层 → `gap`/`min-width` 直接写在 button 上
- `a-modal` 没有 `body-wrapper` 分层 → 用 `.arco-modal` / `.arco-modal-body`
- `a-drawer` 没有 `content` / `body-content-wrapper` 分层 → 用 `.arco-drawer` / `.arco-drawer-body`
- Arco **Trigger 不能嵌套**:`a-tooltip`/`a-dropdown`/`a-popconfirm` 内部都用
`mergeFirstChild` 接管首个元素的��件,互相嵌套时菜单**点不开**。
同一元素既要 tooltip 又要 dropdown 时,tooltip 改用原生 `:title`。
- **tooltip/dropdown 的默认插槽必须只含一个元素**。尾随的纯文本会被当可见内容
渲染进布局流(变成 0 宽的 flex item),文案要放 `content` prop。
## 红线
- 消息气泡 / 群聊气泡内部**禁用任何 Arco 组件**(流式性能),
由 `tests/e2e/chat-rendering-smoke.spec.ts` 卡住
- `/chat-run` Socket.IO 事件契约:事件名、字段名、字段类型、发射时机都不许改(只允许加可选字段)
- `packages/server/src/services/hermes/agent-bridge/python/` 只读
- 不要重排 `<template>` 的结构来"顺便整理",只做等价替换,减少 diff 噪音
## 自查(每个 agent 收尾必做)
```bash
# 你负责的文件必须全部通过
grep -n "naive-ui\|\.n-[a-z]\|:deep(\.n-" <你的文件列表> # 期望零命中
npm run build # 期望成功
```
如果 build 报类型错误,说明有 naive 类型没解开(多半是 `SelectOption` /
`UploadFileInfo` / `DropdownOption` / `TreeOption` / `DataTableColumns`)。