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
102 lines
7.9 KiB
Markdown
102 lines
7.9 KiB
Markdown
# 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`)。
|