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
7.9 KiB
7.9 KiB
Phase 007 迁移规约(每个 agent 必读)
目标:grep -rn "naive-ui\|vueuc\|@css-render" packages/client/src 零命中,
且项目里只有 Arco 一个 UI 库。tests/client/no-naive-ui.test.ts 会卡住回归。
铁律
- 只换渲染层。 不改业务逻辑、不改 store、不改 API 调用、不改事件语义。 看到逻辑 bug 记下来但不要动手。
- 不引入新依赖。 Arco 已在
main.ts全量app.use(ArcoVue), 模板里直接写<a-button>等 kebab-case 标签即可,不要 import Arco 组件。 - Arco 不会对拼错的 prop 报错 —— 未声明的 prop 会作为普通 attribute 落到根元素上。
所以「弹层能打开但内容是空的」几乎总是 prop 写错了。改完每个弹层都要确认
prop 在
node_modules/@arco-design/web-vue/es/<组件>/的 props 定义里存在。 grep naive-ui匹配不到 CSS 里的类名。 必须另外用这个命令自查:这些是死样式,在 Arco 下静默失效。这是最容易漏、且测试测不出来的一类改动。grep -n "\.n-[a-z]\|:deep(\.n-" <你改过的每个文件>
组件映射(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) => ...",改写回调 |
类型导入
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-bodya-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),文案要放
contentprop。
红线
- 消息气泡 / 群聊气泡内部禁用任何 Arco 组件(流式性能),
由
tests/e2e/chat-rendering-smoke.spec.ts卡住 /chat-runSocket.IO 事件契约:事件名、字段名、字段类型、发射时机都不许改(只允许加可选字段)packages/server/src/services/hermes/agent-bridge/python/只读- 不要重排
<template>的结构来"顺便整理",只做等价替换,减少 diff 噪音
自查(每个 agent 收尾必做)
# 你负责的文件必须全部通过
grep -n "naive-ui\|\.n-[a-z]\|:deep(\.n-" <你的文件列表> # 期望零命中
npm run build # 期望成功
如果 build 报类型错误,说明有 naive 类型没解开(多半是 SelectOption /
UploadFileInfo / DropdownOption / TreeOption / DataTableColumns)。