# 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)`, 模板里直接写 `` 等 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` → ``。**注意是 `"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` | `` | 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/` 只读 - 不要重排 `