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

7.9 KiB
Raw Blame History

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 里的类名。 必须另外用这个命令自查:
    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) => ...",改写回调

类型导入

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 收尾必做)

# 你负责的文件必须全部通过
grep -n "naive-ui\|\.n-[a-z]\|:deep(\.n-" <你的文件列表>   # 期望零命中
npm run build                                              # 期望成功

如果 build 报类型错误,说明有 naive 类型没解开(多半是 SelectOption / UploadFileInfo / DropdownOption / TreeOption / DataTableColumns)。